diff --git a/.cargo/config.toml b/.cargo/config.toml index aceda7e..9549db1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,22 +2,13 @@ target = "wasm32-unknown-emscripten" target-dir = "target/workers" +# worker-build --emscripten supplies these (and the link settings) through +# RUSTFLAGS when building with the `worker/experimental_tokio` feature; they are repeated +# here so `cargo check` sees the same cfgs. [target.wasm32-unknown-emscripten] rustflags = [ - # Static linking and unwind semantics are required by the hosted runtime. "-Crelocation-model=static", + "-Cllvm-args=-wasm-use-legacy-eh=false", "--cfg=tokio_unstable", - "-Clink-arg=-sWASM_BINDGEN=auto", - "-Clink-arg=-sEXPORT_ES6", - # Generated host glue uses Workers' Node compatibility APIs. - "-Clink-arg=-sENVIRONMENT=node", - "-Clink-arg=-sNODERAWSOCKETS", - "-Clink-arg=-sNODERAWFS", - "-Clink-arg=-sJSPI", - "-Clink-arg=-sASSERTIONS=0", - "-Clink-arg=-sSTACK_SIZE=8MB", - "-Clink-arg=-sALLOW_MEMORY_GROWTH=1", - # Each Durable Object creates its own wasm instance and mounted filesystem. - "-Clink-arg=-sMODULARIZE=1", - "-Clink-arg=-sEXPORT_NAME=createPumpkinModule", + "--cfg=wasm_bindgen_unstable_tokio", ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb185c..0f3dc68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ concurrency: jobs: smoke: - runs-on: macos-14 - timeout-minutes: 75 + runs-on: ubuntu-latest + timeout-minutes: 90 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -25,18 +25,15 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.13' - - name: Install Emscripten backend - run: brew install emscripten - uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git - key: minecraft-cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }} - - name: Setup pinned sources and toolchain + ~/.cache/worker-build + key: minecraft-toolchain-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml', 'scripts/setup.sh') }} + - name: Setup pinned sources, toolchain and Worker dependencies run: bash scripts/setup.sh - - name: Install Worker dependencies - run: npm ci - name: Worker TCP, shared world, and SQLite restart run: | set -o pipefail diff --git a/.gitignore b/.gitignore index 4a53fb4..cca1d50 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.work/ /target/ **/target/ +/build/ node_modules/ .wrangler/ /worker/env.d.ts diff --git a/AGENTS.md b/AGENTS.md index a5418ab..7ae1c9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,25 +5,28 @@ Read [README.md](README.md), [architecture](docs/architecture.md), and ## Project layout -- `src/`: Pumpkin entry points and configuration. The hosted Tokio bridge lives in workers-rs. -- `worker/`: TCP ingress, Durable Object lifecycle, and SQLite filesystem. -- `tests/`: protocol clients, integration tests, and isolated filesystem fixtures. -- `scripts/setup.sh`: provision pinned sources and build the toolchain. +- `src/`: the Worker: TCP ingress, the Durable Object, the SQLite filesystem + mount (`js/mount.js`), Pumpkin configuration, and the Emscripten JS library + (`workerd.js`). +- `tests/`: protocol clients and the integration test. +- `scripts/setup.sh`: provision the patched Pumpkin checkout, worker-build (which + provisions Emscripten and wasm-bindgen) and npm dependencies. - `scripts/{build,serve,test}.sh`: build, run, and validate the Workers server. -Use Node 24+ (26 recommended) and the pinned Rust nightly. Run `npm test` after +Use Node 24+ (26 recommended) and the pinned Rust toolchain. Run `npm test` after build/runtime changes; it must print `PUMPKIN-DO-SQLITE-RESTART-OK` after verifying -player and chunk restoration. Keep test fixtures out of the production bundle. +player and chunk restoration. Scripts bind to loopback and fail if their ports are occupied. Do not kill another process to free a port. ## Reproducibility and data -Changes to patched checkouts (Pumpkin, wasm-bindgen, workers-rs, wasm-streams) must -be reflected in `patches/`. Keep unpatched checkouts unmodified; update pins for -upstream changes. Preserve the unified ticker and cooperative scheduler unless -the task requires a runtime change. Do not reintroduce old Tokio/libc networking -patches. +Changes to the patched Pumpkin checkout must be reflected in `patches/` +(`node_modules` patches are applied by `npm install`). Update pins for +upstream changes; workers-rs, tokio and mio are git dependencies in Cargo.toml. Preserve the unified ticker and cooperative scheduler unless +the task requires a runtime change. Keep the event-loop model: exports +return promises and nothing blocks or suspends; do not reintroduce JSPI, old +Tokio/libc networking patches, or a JS-side driver. Worlds under `.data/` are user data; do not remove or copy them into commits. Playable databases live in `.data/workers/server/`; tests use `.data/probes/`. diff --git a/Cargo.lock b/Cargo.lock index 30dfaff..7e9344b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.44" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +checksum = "ef217a77a86a6e3dab9a5b3c81dc445b603fe743a90c1cb10a2f2144628d8cfa" dependencies = [ "compression-codecs", "compression-core", @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -107,9 +107,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "shlex", @@ -195,9 +195,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures", @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.39" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +checksum = "257c7085cbb71be72d8fb97edff08b03d86d5d9f2222b9cc34a6bec87093bc10" dependencies = [ "compression-core", "flate2", @@ -305,9 +305,9 @@ checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -339,9 +339,9 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -352,18 +352,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -371,27 +371,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-bigint" @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "const-oid", "pem-rfc7468", @@ -532,13 +532,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -579,9 +579,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" @@ -654,9 +654,9 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" @@ -686,9 +686,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -701,9 +701,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -711,15 +711,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -728,38 +728,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -807,9 +807,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -902,9 +902,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -921,9 +921,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "subtle", "typenum", @@ -932,9 +932,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -991,9 +991,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1005,9 +1005,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1018,9 +1018,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1032,16 +1032,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1052,15 +1053,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1107,9 +1108,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1126,9 +1127,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.1" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "itertools" @@ -1196,7 +1197,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -1212,12 +1215,13 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.189" +source = "git+https://github.com/rust-lang/libc?branch=libc-0.2#b29beb056cb7cea51dd1b9e68342e30a956951a9" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -1230,9 +1234,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lz4-java-wrc" @@ -1309,8 +1313,8 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" -source = "git+https://github.com/guybedford/mio?rev=a62c9e46833fc255c9217ab9aa362c6221ed4401#a62c9e46833fc255c9217ab9aa362c6221ed4401" +version = "1.2.3" +source = "git+https://github.com/guybedford/mio?tag=1.2.3-cf.emscripten#0788dbb67883cb6191503af999471def54e3fb77" dependencies = [ "libc", "wasi", @@ -1569,9 +1573,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -1842,9 +1846,13 @@ dependencies = [ "js-sys", "pumpkin", "pumpkin-config", + "pumpkin-world", "rayon", + "serde_json", "tokio", "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "worker", ] @@ -1950,9 +1958,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1967,11 +1975,11 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "futures-core", "h2", @@ -2014,7 +2022,8 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.14" +version = "0.17.16000" +source = "git+https://github.com/guybedford/ring?branch=emscripten#a1930ce8d9b8006e5060eda14cacf7cf6f7401c1" dependencies = [ "cc", "cfg-if", @@ -2069,9 +2078,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -2096,9 +2105,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] @@ -2132,9 +2141,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2414,14 +2423,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" version = "0.6.5" -source = "git+https://github.com/rust-lang/socket2?rev=239dd83a4ced08e514d2c38942aab99791119f0d#239dd83a4ced08e514d2c38942aab99791119f0d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys", @@ -2535,18 +2545,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -2564,9 +2574,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -2596,9 +2606,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -2606,7 +2616,8 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" +source = "git+https://github.com/guybedford/tokio?tag=1.53.1-cf.emscripten#d56f1eff2c3fd72a946e1e536a3b93e32b002a80" dependencies = [ "bytes", "libc", @@ -2619,11 +2630,12 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" +source = "git+https://github.com/guybedford/tokio?tag=1.53.1-cf.emscripten#d56f1eff2c3fd72a946e1e536a3b93e32b002a80" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -2653,9 +2665,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.5+spec-1.1.0" +version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ "serde_core", "serde_spanned", @@ -2848,9 +2860,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom", "js-sys", @@ -2892,7 +2904,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -2903,15 +2917,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" dependencies = [ "js-sys", + "tokio", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2919,25 +2938,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" -version = "0.6.0" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ce9bdac782bbca70d6ac48a3ba18b873c848a706d6dc3bd9a03c1b96f04693" dependencies = [ "futures-util", "js-sys", @@ -2948,7 +2973,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" dependencies = [ "js-sys", "wasm-bindgen", @@ -3007,7 +3034,9 @@ dependencies = [ [[package]] name = "worker" -version = "0.8.5" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057bce8ec66c80b7e270d884c312e2a4ff60eb997fd02936656106c06a964e5" dependencies = [ "async-trait", "bytes", @@ -3036,7 +3065,9 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.8.5" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d7389670b55aab89760be149007aeff773cdad7b650d807f7fb556d04bb304" dependencies = [ "async-trait", "proc-macro2", @@ -3050,7 +3081,9 @@ dependencies = [ [[package]] name = "worker-sys" -version = "0.8.5" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a440092e7ae7c5feb5507fbdc6aee0cc241d132757c83e1729766a24cddff82" dependencies = [ "cfg-if", "js-sys", @@ -3060,9 +3093,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xxhash-rust" @@ -3122,9 +3155,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -3133,9 +3166,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -3144,13 +3177,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ce459aa..37719e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,45 +12,37 @@ autobins = false exclude = [".work"] resolver = "2" -[features] -test-fixtures = [] - -[[bin]] -name = "filesystem-probe" -path = "tests/fixtures/filesystem.rs" -required-features = ["test-fixtures"] - [[bin]] name = "pumpkin-do" path = "src/main.rs" [dependencies] -wasm-bindgen = { path = ".work/wasm-bindgen" } -js-sys = { path = ".work/wasm-bindgen/crates/js-sys" } +worker = { version = "0.8.7", features = ["experimental_tokio"] } +wasm-bindgen = "0.2.129" +wasm-bindgen-futures = "0.4.79" +js-sys = "0.3.106" +web-sys = { version = "0.3.106", features = ["AbortController", "AbortSignal", "ReadableStream", "StreamPipeOptions", "WritableStream", "console"] } tokio = { version = "1", default-features = false, features = ["rt", "macros", "sync", "time", "net", "io-util"] } pumpkin = { path = ".work/pumpkin/crates/pumpkin", default-features = false, features = ["single-threaded"] } pumpkin-config = { path = ".work/pumpkin/crates/pumpkin-config", default-features = false } +pumpkin-world = { path = ".work/pumpkin/crates/pumpkin-world", default-features = false, features = ["single-threaded"] } rayon = "1.12" -worker = { path = ".work/workers-rs/worker", default-features = false } +serde_json = "1" +# wasm32-unknown-emscripten support pending upstream releases. [patch.crates-io] -js-sys = { path = ".work/wasm-bindgen/crates/js-sys" } -wasm-bindgen = { path = ".work/wasm-bindgen" } -wasm-bindgen-futures = { path = ".work/wasm-bindgen/crates/futures" } -wasm-bindgen-macro-support = { path = ".work/wasm-bindgen/crates/macro-support" } -wasm-bindgen-shared = { path = ".work/wasm-bindgen/crates/shared" } -web-sys = { path = ".work/wasm-bindgen/crates/web-sys" } -wasm-streams = { path = ".work/wasm-streams" } -libc = { path = ".work/libc" } -mio = { git = "https://github.com/guybedford/mio", rev = "a62c9e46833fc255c9217ab9aa362c6221ed4401" } -ring = { path = ".work/ring" } -socket2 = { git = "https://github.com/rust-lang/socket2", rev = "239dd83a4ced08e514d2c38942aab99791119f0d" } -tokio = { path = ".work/tokio/tokio" } -tokio-macros = { path = ".work/tokio/tokio-macros" } - +# LocalEventLoop: the host event loop drives Tokio (tokio-rs/tokio#8484). +tokio = { git = "https://github.com/guybedford/tokio", tag = "1.53.1-cf.emscripten" } +tokio-macros = { git = "https://github.com/guybedford/tokio", tag = "1.53.1-cf.emscripten" } +# tokio-rs/mio#1969; its emscripten epoll bindings come from libc's unreleased libc-0.2 branch. +mio = { git = "https://github.com/guybedford/mio", tag = "1.2.3-cf.emscripten" } +libc = { git = "https://github.com/rust-lang/libc", branch = "libc-0.2" } +# getrandom-backed SystemRandom on emscripten. +ring = { git = "https://github.com/guybedford/ring", branch = "emscripten" } + +# mio's own libc source, unified with the crates.io users above. [patch."https://github.com/guybedford/libc"] -libc = { path = ".work/libc" } - +libc = { git = "https://github.com/rust-lang/libc", branch = "libc-0.2" } [profile.release] opt-level = "s" diff --git a/README.md b/README.md index 3fc6595..30a8ea2 100644 --- a/README.md +++ b/README.md @@ -13,40 +13,41 @@ Minecraft → Workers TCP ingress → MinecraftWorld → Pumpkin + SQLite ## Try it -The supported build host is macOS with Homebrew. Install Git, -[rustup](https://rustup.rs/), Python 3.11+, and Node 24+ (26 recommended). -From a checkout of this repository: +Linux and macOS build hosts are supported. Install Git, +[rustup](https://rustup.rs/), Python 3.11+, and Node 24+ (26 recommended). From a checkout of this repository: ```sh -brew install emscripten -npm ci bash scripts/setup.sh npm run dev ``` -Setup installs the pinned Rust toolchain, fetches dependency sources, and builds -the matching wasm-bindgen CLI. `npm run dev` compiles Pumpkin and starts Wrangler. -The first build takes several minutes; later builds use Cargo's cache. +Setup installs the pinned Rust toolchain, fetches and patches Pumpkin under +`.work/`, and installs worker-build, which provisions Emscripten and +wasm-bindgen itself. `npm run dev` compiles Pumpkin with +`worker-build --emscripten` and starts Wrangler. The first build takes several +minutes; later builds use Cargo's cache. Connect **Minecraft Java 26.2** to **`localhost:25565`**. Status is available at **http://localhost:8787/**. -The example uses offline mode, with encryption and compression disabled. Local +The example uses offline mode with encryption disabled and fast packet compression. Local listeners bind to loopback. `WORLD_NAME` in [wrangler.jsonc](wrangler.jsonc) selects -the Durable Object; server settings are in [src/config.rs](src/config.rs). +the Durable Object; [gameplay settings](docs/configuration.md) such as view +distance and the player limit are Worker variables. -World data lives under `.data/workers/server/`. After the last connection closes, -the server saves and stops. The next connection restores the same world. Wait for -status to report `phase: "idle"` and a checkpoint timestamp before stopping -Wrangler; changes held in memory can be lost if the process is terminated early. +World data lives under `.data/workers/server/`, written through to the Durable +Object's SQLite storage as Pumpkin saves. After the last player leaves, the +server saves and keeps a short reconnect window before stopping; the next +connection starts it on the same world. Server-list pings do not start the +runtime. Wait for status to report `phase: "idle"` before stopping Wrangler so +the final save completes. ## Build and contribute ```sh -npm run build # Compile the server without starting Wrangler +npm run build # Compile the Worker without starting Wrangler npm test # Two-player gameplay, a persistent block edit, and restart npm run test:scheduler -npm run typecheck ``` The tests use separate data under `.data/probes/`. They verify that two clients @@ -57,24 +58,26 @@ See [development](docs/development.md), [architecture](docs/architecture.md), an ## Deploy -After setup, build the runtime and deploy the Worker: +After setup, deploy the Worker; Wrangler builds the Rust runtime first: ```sh -npm run build npx wrangler deploy ``` -Deployment uses standard SQLite-backed Durable Objects with Workers TCP ingress. +The Durable Object is declared under `exports` in `wrangler.jsonc` as a +SQLite-backed class. Workers TCP ingress maps a public IP and port to the +Worker's `connect` handler; that mapping is provisioned separately and is tied +to the Worker's name, so deploy under the mapped name. Players connect to the +mapped `ip:port`. The listener in `wrangler.jsonc` is for local development. See [memory usage](docs/memory-reduction.md) for the optimizations and measurements. -Provision the public TCP endpoint separately and route it to this Worker's -`connect` handler. The listener in `wrangler.jsonc` is for local development. ## Credits and license Built on [Pumpkin](https://github.com/Pumpkin-MC/Pumpkin), -[Guy Bedford's Rust/Emscripten work](https://github.com/guybedford), -[workers-rs](https://github.com/cloudflare/workers-rs), Thomas Rubini's TCP ingress -work, and [worker-fs-mount](https://github.com/danlapid/worker-fs-mount). +[workers-rs](https://github.com/cloudflare/workers-rs), +[Tokio](https://github.com/tokio-rs/tokio), [Emscripten](https://emscripten.org/), +[wasm-bindgen](https://github.com/wasm-bindgen/wasm-bindgen), and +[Guy Bedford's Rust/Emscripten work](https://github.com/guybedford). Licensed under [GPL-3.0-only](LICENSE), consistent with Pumpkin. The [original MIT notice](LICENSES/rust-workers-minecraft-MIT.txt) is retained for diff --git a/build.rs b/build.rs index 413100c..a6139d6 100644 --- a/build.rs +++ b/build.rs @@ -1,17 +1,22 @@ use std::{env, path::PathBuf}; +// worker-build supplies the common emscripten link settings; these are the +// application's own. fn main() { - println!("cargo::rerun-if-changed=worker/fs-library.js"); + println!("cargo::rerun-if-changed=src/workerd.js"); if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("emscripten") { return; } - // Keep growth headroom bounded after generation's temporary allocations. - println!("cargo::rustc-link-arg-bins=-sMEMORY_GROWTH_LINEAR_STEP=2097152"); - let library = - PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("worker/fs-library.js"); - println!( - "cargo::rustc-link-arg-bins=-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=[\"$workerFilesystem\"]" - ); - println!("cargo::rustc-link-arg-bins=--js-library"); + for arg in [ + "-sNODERAWFS", + "-sSTACK_SIZE=8MB", + // Keep growth headroom bounded after generation's temporary allocations. + "-sMEMORY_GROWTH_LINEAR_STEP=2097152", + "-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=[\"$workerdFs\"]", + "--js-library", + ] { + println!("cargo::rustc-link-arg-bins={arg}"); + } + let library = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("src/workerd.js"); println!("cargo::rustc-link-arg-bins={}", library.display()); } diff --git a/docs/architecture.md b/docs/architecture.md index e41d442..0103beb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,18 +2,57 @@ ## Runtime and connections -The Worker forwards TCP streams to a named Durable Object using the platform's -stream-piping API. The object lazily creates an Emscripten module and passes a -workers-rs `Socket` to Pumpkin's injected connection entry point. - -Each object owns separate wasm memory, Rust statics, filesystem descriptors, and -one hosted Tokio runtime, supplied by workers-rs. Pumpkin's `single-threaded` feature selects cooperative -inline chunk generation and the async ticker. The same scheduler loop serves -native builds, which dispatch generation onto Rayon. Save batches await queue -capacity; shutdown drains results with an elapsed-time timeout before the final -flush. Bounded CPU tasks use -regular Tokio tasks because the pinned fork's public `spawn_blocking` still -requires OS threads. JSPI supplies stack suspension, not threads. +The whole Worker is Rust: `worker-build --emscripten`, with the `worker` crate's +`experimental_tokio` feature, links the `pumpkin-do` bin for `wasm32-unknown-emscripten` +through emcc and emits the module Wrangler serves. The entrypoint's +`#[event(connect)]` handler reads a bounded Minecraft handshake over the +`worker::Socket` (`src/handshake.rs`): server-list requests are answered from +the object's metadata (`serverList`) without starting a server, and gameplay +connections are forwarded to the named `MinecraftWorld` object (declared under +`exports` in `wrangler.jsonc` and reached through `cloudflare:workers`' +`exports`) with its `connect`, writing the consumed bytes ahead and copying both directions +with Tokio. `fetch` (`#[event(fetch)]`) serves status. + +`MinecraftWorld` is a `#[durable_object(connect)]`; its constructor mounts +`state.storage().as_raw()` and the server root awaits `Storage::sync`. The +macro exports the object's handlers with `experimental_tokio` (ambient): the +future runs on the thread's Tokio `LocalEventLoop` (tokio-rs/tokio#8484), the +current-thread scheduler and drivers with the host event loop as their wait, +and the export returns the Promise of its outcome. Nothing blocks or suspends. +`status` and `serverList` are plain `#[wasm_bindgen]` RPC methods. The first +`connect` after idle runs the server lifetime: it starts Pumpkin on the mounted +world, awaits the server's return after its final save, awaits `storage.sync()`, +and settles the connection's promise. Later connections and `status` calls enter +the same instance as ordinary calls while that future is parked. A connection +arriving while the server is starting or stopping waits for the next phase +change and then retries, so a client that connects as the previous server +saves and stops starts the next one. The runtime's epoll and timer waits register +with the host through Emscripten's Node backend (emscripten-core/emscripten#27547), +so a readiness or timer callback resumes the scheduler on the host loop. + +Pumpkin binds its stock `TcpListener` on port 25565. Emscripten's Node socket +backend implements that with `net.BoundSocket`/`net.Server`, which workerd scopes +to the Durable Object's own port table. The object's `connect` routes each +inbound socket to that listener with `Socket::handle_as_node_connection`; the +accepted connection surfaces +through epoll readiness and Pumpkin's normal accept loop, reporting the bound +address and an unspecified peer. + +One wasm instance serves the isolate, so one object hosts one server at a time; +`WORLD_NAME` selects it. When the last connection closes, the server root saves +players, level data and dirty chunks, then keeps the server running for the +`IDLE_TIMEOUT_SECONDS` reconnect window (`idle_deadline` in status); a new +connection cancels the shutdown. When the window expires the root calls +`stop_server`, Pumpkin performs its final save and returns, and `reset_stop` +clears the process-wide stop state so the next connection can start a fresh +server in the same instance. Operator settings ([configuration](configuration.md)) +are read from the environment in the object's constructor. + +Pumpkin's `single-threaded` feature runs chunk generation as Tokio tasks +and selects the async ticker. The same scheduler loop serves native builds, which +dispatch generation onto Rayon. Save batches await queue capacity; shutdown +drains results with an elapsed-time timeout before the final flush. Bounded CPU +tasks use regular Tokio tasks because `spawn_blocking` requires OS threads. Structure templates store palette indices and share their block arrays between the placement and query APIs. Block-entity NBT is shared until a placement needs @@ -23,41 +62,22 @@ neighborhood compact without changing its dependency rules. Emscripten grows memory in 2 MiB increments, configured by `build.rs`, while retaining the 8 MiB stack. See [memory measurements](memory-reduction.md). -Emscripten factory mode (`MODULARIZE=1`) isolates module instances. Wrangler -provides the compiled wasm through `instantiateWasm`; the generated host glue uses -Workers' Node compatibility APIs. The SDK's standalone-loader initialization and -recovery hook is disabled for this embedding. - -## Filesystem and checkpoints - -The host mounts `LocalDOFilesystem(ctx.storage)` from durable-object-fs at `/data`. -Emscripten's `NODERAWFS` routes Rust filesystem calls through worker-fs-mount's -synchronous Node API, including descriptors, positional I/O, rename, and sync. -World metadata, chunks, entities, players, and configuration use this mount. -Logging goes to the host console. - -Each wasm instance retains its own mount context across requests and Tokio -callbacks using `AsyncLocalStorage.snapshot()`. Descriptors opened at startup -therefore remain valid until that runtime shuts down, without exposing one world's -mount to another object. - -The Emscripten compatibility library in `worker/fs-library.js` supplies the mounted Node -filesystem to the factory: the generated loader's runtime `createRequire` calls -cannot use Wrangler's build-time aliases. It also initializes file flags from -public `fs.constants` and implements synchronous `fd_sync`. Paths resolve against -the wasm instance's working directory (initially `/data`), without changing the -isolate-wide Node process directory. - -The upstream `entries` table stores metadata, and `file_pages` stores contents in -64 KiB rows. Descriptor writes update only affected pages. Truncation supports -sparse files, and file writes and native rename use SQLite transactions. Whole-file -reads still require a buffer large enough for the result. - -After the final connection leaves, Pumpkin saves and shuts down, and the object -awaits `storage.sync()`. New connections wait for this checkpoint, then create a -fresh runtime from the stored files. Checkpoint failures remain visible in status. -This preserves issued writes; terminating a server with active clients can still -lose changes in Pumpkin's in-memory caches. +## Filesystem + +The world is a SQLite-backed filesystem: the object constructor mounts +`durable-object-fs`'s `LocalDOFilesystem(storage)` at `/data` through +`worker-fs-mount`, whose `node:fs` implementation routes that prefix to the +object's storage and every other path (stdio, `/tmp`) to workerd's own +filesystem. `src/workerd.js` rebinds Emscripten's `NODERAWFS` to it, resolves +relative paths against Emscripten's working directory (`/data`, distinct from +the isolate's `process.cwd()`), and disables the VFS's own permission checks +since the host enforces them. Every Pumpkin write is therefore durable as it +happens, under the object's transaction semantics; after the final save the +server awaits `storage.sync()` before the last connection is reported closed, +and status shows that time. + +Logging goes to the process's stdout and stderr through `NODERAWFS`, which +Wrangler relays as `stdout:` and `stderr:` lines. ## Development interfaces @@ -65,35 +85,26 @@ The supplied configuration listens on loopback TCP port 25565 and HTTP port 8787 The world name comes from `WORLD_NAME` in `wrangler.jsonc`. - `GET /health`: Worker readiness. -- `GET /`: phase, connection count, runtime statistics, and last checkpoint time. +- `GET /`: phase, connection count, runtime statistics, the runtime start + count, last shutdown save time, reconnect deadline, and effective settings. Runtime statistics include Wasm capacity and allocator in-use/free/arena bytes. Allocator counters include allocation metadata and unused container capacity; Wasm capacity additionally includes static data, stack, and growth headroom. -The public status path reads no filesystem internals and remains available when -startup fails. Checkpointing is internal to the connection lifecycle. The -`worker/pumpkin.ts` facade binds exports to the module context once, so the -Durable Object deals with `connect`, `stop`, and `status` methods. - Local databases live in `.data/workers/server/`. Integration tests use separate -storage under `.data/probes/` and restart Wrangler to verify restoration. The echo -and large-file test fixture has its own Worker configuration and wasm binary; it -is excluded from the application bundle. - -Current workerd versions can report `Network connection lost` when TCP clients -disconnect. Rust panics, Rust error logs, and runtime crashes fail the integration -test. +storage under `.data/probes/` and restart Wrangler to verify restoration. Rust +panics, Rust error logs, and runtime crashes fail the integration test. ## Build constraints -- Use the pinned Rust nightly and matching wasm-bindgen CLI from setup. -- Keep static relocation, unwind semantics, the 8 MiB stack, and memory growth. -- Pass Emscripten link settings through rustc so they do not affect C compilation. -- Keep the compatibility initializer linked even when Rust does not call `fd_sync`; - `build.rs` configures this and tracks the library as a build input. +- Use the pinned Rust toolchain and the worker-build from setup, which + provisions Emscripten and the wasm-bindgen CLI. +- Keep static relocation, exnref exception handling on both the C and Rust + sides, the 8 MiB stack, and memory growth. worker-build supplies the common + codegen and link settings (including `--cfg tokio_unstable`; `LocalEventLoop` + is unstable API); `build.rs` adds the application's own: `NODERAWFS`, the + stack size, the growth step, and the JS library. - Prefix Rust exports to avoid collisions with libc and Emscripten symbols. -- The synchronous SQLite mount supplies `fd_sync`; checkpoint completion separately - awaits Durable Object storage synchronization. [Dependency pins and patch maintenance](dependencies.md) diff --git a/docs/configuration-results.json b/docs/configuration-results.json new file mode 100644 index 0000000..1e88cab --- /dev/null +++ b/docs/configuration-results.json @@ -0,0 +1,174 @@ +{ + "date": "2026-09-12", + "environment": "macOS arm64, Node 26.4.0, Wrangler 4.129.0, pinned Rust/Emscripten", + "scope": "Single local runs with synthetic movement. Wasm capacity excludes JS memory. Samples may miss transient peaks.", + "runs": [ + { + "label": "size-v2-two", + "settings": { + "VIEW_DISTANCE": "2", + "SIMULATION_DISTANCE": "2", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "seed": "1789195700909824175", + "steps": 32, + "wasm_sha256": "4288be9cd44fe68e57bb358e8d041b886b4b43a8f30719477a76c27df97fa0c6", + "join_ms": 5174.268208, + "peak_wasm_mib": 107.75, + "received_mb": 1.5, + "exploration_tps": 18.28, + "status_p95_ms": 246.2 + }, + { + "label": "size-v3-compressed", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "seed": "1789195700909824175", + "steps": 32, + "wasm_sha256": "4288be9cd44fe68e57bb358e8d041b886b4b43a8f30719477a76c27df97fa0c6", + "join_ms": 6301.3658749999995, + "peak_wasm_mib": 121.75, + "received_mb": 2.16, + "exploration_tps": 16.7, + "status_p95_ms": 301.1 + }, + { + "label": "size-v3-four", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "seed": "1789195700909824175", + "steps": 32, + "wasm_sha256": "4288be9cd44fe68e57bb358e8d041b886b4b43a8f30719477a76c27df97fa0c6", + "join_ms": 6185.213540999999, + "peak_wasm_mib": 159.75, + "received_mb": 4.33, + "exploration_tps": 15.2, + "status_p95_ms": 1200.8 + }, + { + "label": "size-v3-uncompressed", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "-1", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "seed": "1789195700909824175", + "steps": 32, + "wasm_sha256": "4288be9cd44fe68e57bb358e8d041b886b4b43a8f30719477a76c27df97fa0c6", + "join_ms": 6294.287249999999, + "peak_wasm_mib": 123.75, + "received_mb": 19.62, + "exploration_tps": 16.5, + "status_p95_ms": 380.9 + }, + { + "label": "size-v4-two", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "seed": "1789195700909824175", + "steps": 32, + "wasm_sha256": "4288be9cd44fe68e57bb358e8d041b886b4b43a8f30719477a76c27df97fa0c6", + "join_ms": 7427.388416, + "peak_wasm_mib": 129.75, + "received_mb": 2.76, + "exploration_tps": 16.68, + "status_p95_ms": 354.3 + } + ], + "optimization_experiment": { + "opt_level": 3, + "result": "Build failed: wasm-metadce received SIGBUS in the pinned Binaryen toolchain." + }, + "final_preset_validation": { + "wasm_sha256": "b182e11d18a2694f6cf72e0444576873f498aef184aee7672baf14cd58efc74e", + "settings": { + "VIEW_DISTANCE": "2", + "SIMULATION_DISTANCE": "2", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "seed": "1789195700909824175", + "players": 2, + "steps": 32, + "join_ms": 4923.233542, + "peak_wasm_mib": 107.81, + "received_mb": 1.48, + "exploration_tps": 17.78, + "status_p95_ms": 228.9 + }, + "extended_validation": [ + { + "label": "default-long", + "failure": null, + "seed": "1789195700909824175", + "steps": 128, + "settings": { + "VIEW_DISTANCE": "2", + "SIMULATION_DISTANCE": "2", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_mib": 125.75, + "peak_allocated_heap_mib": 88.47, + "received_mb": 4.18, + "exploration_tps": 18.3, + "status_p95_ms": 249.9 + }, + { + "label": "default-second-seed", + "failure": null, + "seed": "1789200079352125165", + "steps": 32, + "settings": { + "VIEW_DISTANCE": "2", + "SIMULATION_DISTANCE": "2", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_mib": 113.75, + "peak_allocated_heap_mib": 81.76, + "received_mb": 1.89, + "exploration_tps": 16.91, + "status_p95_ms": 204.5 + } + ] +} diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..430eb6d --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,136 @@ +# Server configuration + +Gameplay settings live in `wrangler.jsonc` under `vars`. Use string values. +Changing these settings restarts the local Worker, but does not require rebuilding +Rust. The object reads and validates them in its constructor; invalid settings +fail the next connection and appear in the HTTP status response's `failure` field. + +| Variable | Default | Meaning | +| --- | --- | --- | +| `WORLD_NAME` | `world` | Selects the Durable Object and its separate saved world. | +| `WORLD_SEED` | Unset | Seed for a new world; an existing world keeps its saved seed. | +| `VIEW_DISTANCE` | `4` | Maximum client view distance, from 2 to 32 chunks. | +| `SIMULATION_DISTANCE` | `3` | Simulation radius, from 2 to the configured view distance. | +| `MAX_PLAYERS` | `20` | Login limit, from 1 to 1,000; capacity depends on the world and workload. | +| `COMPRESSION_THRESHOLD` | `512` | Compress packets at or above this byte count; `-1` disables compression. | +| `COMPRESSION_LEVEL` | `1` | Compression level, from 0 to 9; 1 favors speed. | +| `IDLE_TIMEOUT_SECONDS` | `10` | Reconnect window after the last player leaves, from 0 to 30 seconds; 0 stops immediately. | +| `MOTD` | `Minecraft on Cloudflare Workers` | Description displayed in the Minecraft server list. | + +The accepted ranges describe valid protocol settings, not a supported capacity +for every world. Chunk generation, separate player locations, and world content +can substantially increase memory usage. A larger view distance does not imply +a larger simulation distance is needed. + +The default view is now 4 with simulation distance 3. Pumpkin's +[memory optimizations](memory-reduction.md#exploration-comparison-september-12-2026) +made room for the larger view: a two-player exploration probe commanding 512 +blocks of travel each completed at 113.81 MiB of Wasm capacity. That is one +workload, not an unrestricted exploration guarantee. + +To try a larger view while measuring your world's memory usage: + +```sh +npm run dev -- --var VIEW_DISTANCE:5 --var SIMULATION_DISTANCE:3 +``` + +For packet debugging without compression: + +```sh +npm run dev -- --var COMPRESSION_THRESHOLD:-1 +``` + +The server still runs at 20 ticks per second in Survival with Normal difficulty. +It uses offline authentication; the example's listeners bind to loopback. + +## Status and reconnects + +Server-list requests are answered before forwarding a gameplay connection to +Pumpkin. They report the configured description and player limit, and the current +player count, without creating a Wasm instance. Status requests have a bounded +packet size and a five-second handshake/ping deadline. + +World writes go straight to the object's SQLite storage through the mounted +filesystem. After the final gameplay connection closes, the server saves players, +level data and dirty chunks, then keeps running for the reconnect window. A new +connection cancels the pending shutdown. When the window expires, it performs its +final save, awaits `storage.sync()`, and shuts down. + +The HTTP status endpoint reports `runtime_starts`, `saved_at` (the last completed +shutdown save), `idle_deadline`, the effective `settings`, and runtime memory/tick +statistics. During the reconnect window, `phase` is `running`, `connections` is +zero, and `idle_deadline` is set. Wait for `phase: "idle"` before stopping +Wrangler so the final shutdown also completes. + +Changing configuration reloads the Worker. Wait for players to leave and the +world to become idle before changing settings on a world you care about. + +## Measuring a configuration + +The experience probe starts an isolated world with a fixed seed. It measures +initial view delivery, stationary play, and synthetic movement in different +directions, recording traffic, Wasm capacity, allocator usage, status latency, +and observed tick rate. It writes `experience.json` under `.data/probes/`. + +```sh +npm run test:experience -- --view 3 --simulation 3 --players 2 --compression=512 +npm run test:experience -- --view 4 --simulation 3 --players 2 --compression=512 +npm run test:experience -- --view 3 --simulation 3 --players 2 --compression=-1 +``` + +Use the same seed, workload, and build when comparing settings. These are local +synthetic probes, not player-capacity guarantees. Wasm capacity excludes +JavaScript allocations, and sampling can miss short-lived peaks. + +Tests can use separate loopback ports while the playable server is running: + +```sh +TEST_TCP_PORT=25566 TEST_HTTP_PORT=8788 TEST_INSPECTOR_PORT=9230 npm test +TEST_TCP_PORT=25566 TEST_HTTP_PORT=8788 TEST_INSPECTOR_PORT=9230 npm run test:experience -- --view 4 +``` + +`TEST_INSPECTOR_PORT` separates the probe's debugger listener from another local +Wrangler instance. The probes fail if their chosen ports are occupied and only +stop processes they started. Playable world databases are never used by the probes. + +## Local comparison (September 12, 2026) + +This historical sweep predates the palette, light, carving-mask, and pathfinder +optimizations. It used the same size-optimized build, seed, and 32 movement +steps per player. Each step commanded four blocks of movement, with players +moving in different directions. Results are single runs: + +| View / simulation | Players | Compression | Initial views | Peak Wasm capacity | Received traffic | +| --- | ---: | --- | ---: | ---: | ---: | +| 3 / 3 | 2 | Off | 6.29 s | 123.75 MiB | 19.62 MB | +| 3 / 3 | 2 | Level 1 | 6.30 s | 121.75 MiB | 2.16 MB | +| 4 / 3 | 2 | Level 1 | 7.43 s | 129.75 MiB | 2.76 MB | +| 3 / 3 | 4 | Level 1 | 6.19 s | 159.75 MiB | 4.33 MB | +| 2 / 2 | 2 | Level 1 | 5.17 s | 107.75 MiB | 1.50 MB | + +Compression reduced traffic by about 89% in the comparable view-distance-3 +runs, with essentially unchanged initial load time. The larger view and +four-player runs left insufficient headroom for a standard Workers isolate; +Wasm capacity does not include JavaScript memory. The temporary 2/2 preset was +rerun after adding the acknowledged checkpoint and reached 107.81 MiB. + +`opt-level=3` failed to build: the pinned Binaryen `wasm-metadce` process exited +with SIGBUS. The working `opt-level="s"` profile is retained; no speed improvement +is claimed for the failed experiment. + +[Machine-readable measurements](configuration-results.json) include the settings, +Wasm hashes, sampled status latency, and observed tick rate. A run interrupted +by a development-server reload was excluded. The early-checkpoint change was +validated separately with 179 native world tests and the full restart suite, +including terminating the test Worker before the reconnect deadline. + +A longer run commanded 512 blocks of travel per player and reached **125.75 MiB +of Wasm capacity**, despite the smaller preset. A separate seed reached 113.75 +MiB. Both completed, but the long run leaves very little space for JavaScript +inside the standard isolate memory budget. These controls reduce demand; they +do not establish unrestricted exploration or a general two-player capacity +bound. The subsequent [memory work](memory-reduction.md#exploration-comparison-september-12-2026) +reduced allocation at the same gameplay settings and replaced that temporary +preset with the current 4/3 defaults. The original measurements remain in +`configuration-results.json`; the newer [exploration results](memory-exploration-results.json) +record the optimized builds and unsuccessful attempts separately. diff --git a/docs/dependencies.md b/docs/dependencies.md index 6dbb09c..25d88d8 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -1,23 +1,21 @@ # Dependency sources and pins -A clean checkout is self-contained after `bash scripts/setup.sh`. Dependency sources -are ignored under `.work/`. Both Cargo and npm dependency graphs are locked. +A clean checkout is self-contained after `bash scripts/setup.sh`. The patched +Pumpkin checkout and the worker-build CLI live under `.work/` (ignored); +worker-build provisions its own Emscripten and wasm-bindgen CLI under +`~/.cache/worker-build`. Both Cargo and npm dependency graphs are locked. -## Direct checkouts +## Pinned sources -| Component | Source / branch | Commit | Local changes or purpose | +| Component | Source / ref | Commit or tag | Local changes or purpose | | --- | --- | --- | --- | -| emscripten | [cf](https://github.com/guybedford/emscripten) | `21166256c4c4d73d39b3685c8973d7cbe427ce8c` | Fork frontend: epoll, JSPI, wasm-bindgen post-link | -| wasm-bindgen | [emscripten-non-identifier-names](https://github.com/guybedford/wasm-bindgen) | `4b69f3b3ba4212c857be6854f77fa5aec8b62871` | Closure-finalizer string escaping | -| tokio | [emscripten-layering](https://github.com/guybedford/tokio) | `7c1d4977c510866775ed6164b58b2218a6a2955b` | HostedRuntime and normal Tokio TCP/UDP APIs; unmodified | -| libc | [libc-0.2-emscripten](https://github.com/guybedford/libc) | `4091fe0b0dc5f9c1a27bed75be1ff02bb27e756d` | Emscripten epoll and socket support; unmodified | -| ring | [emscripten](https://github.com/guybedford/ring) | `6671f7cfbb13f249b571ffa6326275a8596e0ca2` | Portable Emscripten crypto backend; unmodified | -| pumpkin | [master](https://github.com/Pumpkin-MC/Pumpkin) | `b5b9b9d7010e793806a83c495af223c67e1d35ee` | Headless embedding, shared async scheduler, compact templates/generation chunks, and build-script fixes | -| workers-rs | [connect-bindings](https://github.com/ThomasRubini/workers-rs) | `7db011ec97658a5d907f3e3102028ce86c044f19` | TCP ingress PR #1041; pinned ABI, Emscripten Tokio promise adapter, host-owned initialization, and socket shutdown flushing | -| wasm-streams | [v0.6.0](https://github.com/MattiasBuelens/wasm-streams) | `35665f7b1da830b5ac51b4c6c3ff13f5c1a09ccb` | Build only the Rust library; its standalone `cdylib` is incompatible with static Emscripten linking | - -Patches are relative to the pinned commits above. Setup checks reverse application -before applying a patch and refuses to repin a modified checkout. +| pumpkin | [master](https://github.com/Pumpkin-MC/Pumpkin) | `b5b9b9d7010e793806a83c495af223c67e1d35ee` | Checkout under `.work/pumpkin`: headless embedding, shared async scheduler, restartable stop signal, compact templates/generation chunks, and build-script fixes | +| tokio | [guybedford/tokio](https://github.com/guybedford/tokio) | tag `1.53.1-cf.emscripten` | `LocalEventLoop` (tokio-rs/tokio#8484) and `net` over epoll on Emscripten; a `[patch.crates-io]` git dependency; unmodified | +| emscripten | [guybedford/emscripten](https://github.com/guybedford/emscripten) | tag `6.0.10-cf.emscripten` | The 6.0.10 release plus emscripten-core/emscripten#27547 (epoll listeners on the host loop) and #27742 (async DNS lookup). worker-build installs emsdk 6.0.10 and applies these as its bundled patches | + +The Pumpkin patches are relative to the pinned commit above. Setup checks +reverse application before applying a patch and refuses to repin a modified +checkout. Pumpkin has two patches: `pumpkin-emscripten.patch` for embedding/runtime support, followed by `pumpkin-memory.patch` for compact templates and generation chunks. @@ -26,26 +24,37 @@ commit. ## Cargo-managed forks -| Crate | Source | Commit | +| Crate | Source | Purpose | | --- | --- | --- | -| mio | https://github.com/guybedford/mio | `a62c9e46833fc255c9217ab9aa362c6221ed4401` | -| socket2 | https://github.com/rust-lang/socket2 | `239dd83a4ced08e514d2c38942aab99791119f0d` | +| mio | https://github.com/guybedford/mio tag `1.2.3-cf.emscripten` | Emscripten epoll selector (tokio-rs/mio#1969) | +| libc | https://github.com/rust-lang/libc branch `libc-0.2` | Emscripten epoll bindings, unreleased | +| ring | https://github.com/guybedford/ring branch `emscripten` | getrandom-backed `SystemRandom` on Emscripten | -The root Cargo.toml applies these overrides and the local checkouts. Cargo.lock -pins the full application graph. `toolchain/wasm-bindgen.Cargo.lock` separately pins -the host CLI graph, since that upstream workspace does not commit a lockfile. +The root Cargo.toml applies these overrides and the local checkouts; the +wasm-bindgen crates (0.2.129, with `#[wasm_bindgen(experimental_tokio)]`) and +wasm-streams 0.7 come from crates.io. Cargo.lock pins the full application +graph, including the branch commits. ## Host tools -- Rust: `nightly-2026-07-20`, target `wasm32-unknown-emscripten`; setup installs both through rustup. +- Rust: `beta` channel (1.99; `OwnedFd::try_clone` on Emscripten), target + `wasm32-unknown-emscripten`; setup installs both through rustup. rustc needs a + larger compile-thread stack for pumpkin-data's generated tables; the scripts + set `RUST_MIN_STACK`. +- worker-build 0.8.7 (cloudflare/workers-rs#1061 released): installed by setup + with `cargo install`, matching the `worker` crate version in Cargo.toml, into + `.work/bin/`. It provisions emsdk 6.0.10 with + its bundled Emscripten patches and the matching wasm-bindgen CLI under + `~/.cache/worker-build`, drives cargo and emcc with the common link settings, + wraps the exports into the entrypoint and Durable Object classes, and emits + `build/`. `EMSCRIPTEN`/`EMSDK` select a local toolchain instead. - Node: 24+; 26 recommended and selected in CI. Used for build tools and tests. -- Python: 3.11+ (Emscripten scripts and TOML parsing). -- Backend: Homebrew `emscripten`, detected with `brew --prefix emscripten`. - Homebrew backend versions are external prerequisites, not pinned source files. - -Setup deliberately uses the fork frontend with Homebrew's LLVM/binaryen backend; -emsdk is not needed. It writes a machine-local `.emscripten_cf`, builds wasm-bindgen -explicitly for the host target, and places the CLI in `.work/bin/`. +- Python: 3.11+ (Emscripten scripts, emsdk and TOML parsing). +- workerd 1.20260925.1 (Wrangler 4.141.0's bundled version, also pinned directly + in `package.json`); 1.20260918.1 was the first release with `net.Server` inbound routing into + Durable Objects (`handleAsNodeConnection`, cloudflare/workerd#7306, #7313) and + the `node:fs` fixes for positional buffer I/O (#7368), `O_TRUNC` (#7369), + `O_CREAT` (#7393), and rename over an existing path (#7394). ## Updating a patch @@ -54,14 +63,12 @@ upstream base from the repository root. Write it under `.work/` to preserve the commit description at the top of the maintained patch: ```sh +git -C .work/pumpkin add -N crates/pumpkin/src/net/bedrock/nethernet_stub.rs git -C .work/pumpkin diff b5b9b9d7010e793806a83c495af223c67e1d35ee -- \ . ':(exclude)Cargo.lock' ':(exclude)crates/pumpkin-world/src/generation' \ > .work/pumpkin-emscripten.diff git -C .work/pumpkin diff b5b9b9d7010e793806a83c495af223c67e1d35ee -- \ crates/pumpkin-world/src/generation > .work/pumpkin-memory.diff -git -C .work/wasm-bindgen diff 4b69f3b3ba4212c857be6854f77fa5aec8b62871 -- crates/cli-support/src/js/mod.rs > .work/wasm-bindgen-emscripten-closures.diff -git -C .work/workers-rs diff 7db011ec97658a5d907f3e3102028ce86c044f19 > .work/workers-rs-emscripten-toolchain.diff -git -C .work/wasm-streams diff 35665f7b1da830b5ac51b4c6c3ff13f5c1a09ccb > .work/wasm-streams-rlib.diff ``` Replace the corresponding patch's contents from its first `diff --git` line onward @@ -72,14 +79,14 @@ The Pumpkin path filters reflect the current separation: all memory-patch files are under `crates/pumpkin-world/src/generation/`. Adjust the filters if that scope changes, keeping platform support and memory optimizations in their own patches. -`git diff` omits untracked files, including files created by existing patches. -Preserve those added-file diffs when regenerating (for example, -`nethernet_stub.rs` in the compatibility patch). If you add a file in a dependency -checkout, include it explicitly and verify application on a fresh copy of the base. -Run `bash scripts/test.sh` after runtime changes. Keep unpatched checkouts unmodified. -Setup refuses to repin a modified checkout rather than discarding local work. +`git diff` omits untracked files, including files created by existing patches; +`add -N` above includes `nethernet_stub.rs`. If you add a file in a dependency +checkout, include it the same way and verify application on a fresh copy of the +base. Run `bash scripts/test.sh` after runtime changes. Keep unpatched checkouts +unmodified. Setup refuses to repin a modified checkout rather than discarding +local work. -For source/patch validation without rebuilding the toolchain: +For source/patch validation without provisioning the toolchain: ```sh bash scripts/setup.sh --sources-only @@ -87,11 +94,14 @@ bash scripts/setup.sh --sources-only ## JavaScript dependencies -`package-lock.json` pins Wrangler 4.129.0, worker-fs-mount 0.2.0, -durable-object-fs 1.0.0, TypeScript, and their dependencies. The npm override for -durable-object-fs replaces its published `workspace:*` peer dependency with the -selected worker-fs-mount version. Generated runtime/binding types are recreated by -`npm run types` and are not committed. +`package-lock.json` pins Wrangler 4.141.0, workerd 1.20260925.1, `worker-fs-mount` 0.2.0 and +`durable-object-fs` 1.0.0 (the SQLite filesystem mount, imported by +`src/js/mount.js` and bundled by worker-build). `npm install` applies +`patches/worker-fs-mount-open-mode.patch` to the installed `worker-fs-mount` +(numeric open modes masked to their permission bits, as Node does; pending +upstream). worker-build generates +`build/index.js`, which wraps the exports into the entrypoint and derives the +Durable Object class from `DurableObject` for RPC. After moving a checkout with cached build output, run `cargo clean` before rebuilding. Generated data can contain absolute paths. This leaves databases under `.data/` intact. diff --git a/docs/development.md b/docs/development.md index f27b4bb..8ccc8f4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,30 +1,37 @@ # Development Follow the [README setup instructions](../README.md#try-it) to install the pinned -Rust toolchain, provision dependency sources, and build the matching wasm-bindgen CLI. +Rust toolchain, provision the patched Pumpkin checkout and install worker-build. The initial Pumpkin compilation takes several minutes. Later builds use Cargo's -cache. `build.rs` owns the Emscripten library arguments and tracks changes to the -compatibility library. +cache. worker-build owns the common codegen and link settings; `build.rs` adds +the application's own and the JS library (`src/workerd.js`), tracking changes to +it. `.cargo/config.toml` repeats the codegen cfgs so `cargo check` sees them. +Wrangler runs `worker-build --emscripten --release` as its build command +and serves `build/index.js`. ```sh -npm run build # Compile the production server -npm run typecheck # Regenerate Worker types and check TypeScript +npm run build # Compile the production Worker npm test # Full Wrangler integration suite -npm run test:scheduler # Native queue/backpressure and shutdown regressions +npm run test:scheduler # Native scheduler, chunk-queue and shutdown regressions npm run test:memory # Two-player memory probe on a fresh, fixed-seed world +npm run test:experience # Configuration and movement/traffic probe ``` -Tests fail if their loopback ports (25565 and 8787) are occupied. They start and +Tests fail if their loopback ports (25565 and 8787 by default) are occupied; set +`TEST_TCP_PORT` and `TEST_HTTP_PORT` to use others. They start and stop only their own Wrangler process groups. Their databases and logs live in `.data/probes/`; playable worlds are separate under `.data/workers/server/`. -`npm test` must finish with `PUMPKIN-DO-SQLITE-RESTART-OK`. It checks filesystem -isolation, synchronous descriptors, large files, failed-startup status, two-player -gameplay, a real block edit, and player/block restoration after a restart. +`npm test` must finish with `PUMPKIN-DO-SQLITE-RESTART-OK`. It runs the unit +tests (`tests/*.test.mjs`: protocol framing), then +two-player gameplay with compression, server-list pings that must not start the +runtime, the player limit, a real block edit, a reconnect during the idle window, +and player/block restoration after a restart from the SQLite-mounted world. -The memory probe waits for both clients to receive 81 chunks, observes 30 seconds -of stationary play, then checkpoints. Each run creates a fresh isolated world -with a fixed seed; use the same seed when comparing builds: +The memory probe waits for both clients to receive their configured view, observes 30 seconds +of stationary play, then stops the server. Each run creates a fresh isolated world +with a fixed seed; use the same seed when comparing builds. See +[configuration](configuration.md) for the exploration probe: ```sh npm run test:memory -- --seed 1789200079352125165 @@ -32,7 +39,7 @@ npm run test:memory -- --seed 1789200079352125165 The probe writes `memory.json` under `.data/probes/`, recording the seed, Wasm and host-glue hashes, observed capacity, and sampled allocator usage. Sampling ends -before checkpointing and can miss short-lived allocations. These numbers exclude +before the stop and can miss short-lived allocations. These numbers exclude JavaScript memory; use an inspector snapshot to check the combined footprint. The test harness sets `WORLD_SEED` to known terrain with a stable spawn block. diff --git a/docs/memory-exploration-results.json b/docs/memory-exploration-results.json new file mode 100644 index 0000000..9347c41 --- /dev/null +++ b/docs/memory-exploration-results.json @@ -0,0 +1,417 @@ +{ + "recorded": "2026-09-12", + "environment": { + "node": "26", + "wrangler": "4.129.0", + "workerd": "1.20260903.1", + "host": "macOS arm64" + }, + "workload": { + "blocks_commanded_per_step": 4, + "step_interval_ms": 500, + "stationary_before_ms": 5000, + "settling_after_ms": 5000, + "directions": "Players move in separate cardinal directions." + }, + "scope": "Local synthetic runs. Movement commands do not verify actual terrain traversal. Wasm capacity excludes JavaScript; samples can miss transient peaks. Failed attempts retain partial measurements and are not comparable to completed runs.", + "runs": [ + { + "label": "baseline-v3-four-long-isolated", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_bytes": 47972352, + "peak_heap_bytes": 1167696, + "join_ms": null, + "exploration_tps": null, + "failure": "Error: ProbeA: server closed the connection", + "completed": false + }, + { + "label": "baseline-v3-four-long-recheck", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_bytes": 115081216, + "peak_heap_bytes": 82226312, + "join_ms": 6340.188582999999, + "exploration_tps": 11.36957727176025, + "failure": "AssertionError [ERR_ASSERTION]: Wrangler exited", + "completed": false, + "diagnostic": "esbuild reported: all goroutines are asleep - deadlock" + }, + { + "label": "baseline-v3-four-long", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_bytes": 47972352, + "peak_heap_bytes": 1167680, + "join_ms": null, + "exploration_tps": null, + "failure": "Error: ProbeA: server closed the connection", + "completed": false + }, + { + "label": "baseline-v3-four-repeat", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_bytes": 167510016, + "peak_heap_bytes": 135631840, + "join_ms": 7865.266250000001, + "exploration_tps": 15.257301744867481, + "failure": null, + "completed": true + }, + { + "label": "baseline-v3-four", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "5cc604276208e8d5ce72e4e05b9c45343a33f0974487c86f76ebf69e1b77b63f", + "peak_wasm_bytes": 169607168, + "peak_heap_bytes": 136446024, + "join_ms": 6950.489290999999, + "exploration_tps": 15.284518065214796, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-startup-recheck", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 75300864, + "peak_heap_bytes": 41347192, + "join_ms": 6282.028833, + "exploration_tps": null, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v3-four-long-isolated", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 167575552, + "peak_heap_bytes": 124633688, + "join_ms": 6664.49625, + "exploration_tps": 14.729428031607982, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v3-four-long", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 152895488, + "peak_heap_bytes": 114727672, + "join_ms": 6314.068791000001, + "exploration_tps": 15.02771301167962, + "failure": "Error: ProbeD: disconnected during Play", + "completed": false + }, + { + "label": "optimized-v2-v3-four", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 117243904, + "peak_heap_bytes": 85542872, + "join_ms": 6258.018834, + "exploration_tps": 15.41217864693511, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v4-four", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 131923968, + "peak_heap_bytes": 98050136, + "join_ms": 7551.753375, + "exploration_tps": 15.109774055309819, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v4-two-inspector-isolated", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 119341056, + "peak_heap_bytes": 79627424, + "join_ms": 8522.887792, + "exploration_tps": 16.911632990999518, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v4-two-long-recheck", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 48037888, + "peak_heap_bytes": 1167808, + "join_ms": null, + "exploration_tps": null, + "failure": "Error: ProbeA: server closed the connection", + "completed": false + }, + { + "label": "optimized-v2-v4-two-long", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 48037888, + "peak_heap_bytes": 1167640, + "join_ms": null, + "exploration_tps": null, + "failure": "Error: read ECONNRESET", + "completed": false + }, + { + "label": "optimized-v2-v4-two-seed2-isolated", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 32, + "seed": "1789200079352125165", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 104660992, + "peak_heap_bytes": 71445384, + "join_ms": 7133.787125000001, + "exploration_tps": 16.824789895965726, + "failure": null, + "completed": true + }, + { + "label": "optimized-v2-v4-two-seed2-recheck", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 32, + "seed": "1789200079352125165", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 48037888, + "peak_heap_bytes": 1167632, + "join_ms": null, + "exploration_tps": null, + "failure": "Error: ProbeA: server closed the connection", + "completed": false, + "diagnostic": "esbuild reported: all goroutines are asleep - deadlock" + }, + { + "label": "optimized-v2-v4-two-seed2", + "build": "optimized", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "2", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 2, + "steps": 32, + "seed": "1789200079352125165", + "wasm_sha256": "c0e6347eed76026976853fe0fce2f5ee18f18296caa6ee8680d8c6203f4a3b44", + "peak_wasm_bytes": 104660992, + "peak_heap_bytes": 71498208, + "join_ms": 7088.9845000000005, + "exploration_tps": 14.3798795236709, + "failure": null, + "completed": true + }, + { + "label": "optimized-v3-four", + "build": "intermediate-before-light-compaction", + "settings": { + "VIEW_DISTANCE": "3", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "7c8a2496d31e03dfc9f40344085a8cf40609dc0dbea4c4202f5d8224a8edc5d6", + "peak_wasm_bytes": 140312576, + "peak_heap_bytes": 106684768, + "join_ms": 6691.6392909999995, + "exploration_tps": 14.701649679242797, + "failure": null, + "completed": true + }, + { + "label": "optimized-v4-four", + "build": "intermediate-before-light-compaction", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "4", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 32, + "seed": "1789195700909824175", + "wasm_sha256": "7c8a2496d31e03dfc9f40344085a8cf40609dc0dbea4c4202f5d8224a8edc5d6", + "peak_wasm_bytes": 152895488, + "peak_heap_bytes": 118868144, + "join_ms": 8154.795958999999, + "exploration_tps": 14.824272313788612, + "failure": null, + "completed": true + } + ] +} diff --git a/docs/memory-reduction.md b/docs/memory-reduction.md index e969183..14f09e0 100644 --- a/docs/memory-reduction.md +++ b/docs/memory-reduction.md @@ -2,9 +2,10 @@ The optimized server runs on standard SQLite-backed Durable Objects. -The memory optimizations are maintained in -[`pumpkin-memory.patch`](../patches/pumpkin-memory.patch), separately from the -Emscripten embedding patch. Setup applies both to the pinned Pumpkin revision. +Storage and cache optimizations are maintained in +[`pumpkin-memory.patch`](../patches/pumpkin-memory.patch). Scheduler and network +queue changes live in the embedding patch alongside their runtime integration. +Setup applies both to the pinned Pumpkin revision. Structure templates share indexed block arrays and store block-state descriptions once per palette. Block-entity NBT is shared until placement needs a mutable copy. @@ -12,7 +13,142 @@ Generation chunks retain uniform or indexed sections; a temporary dense buffer keeps the active noise pass efficient. Emscripten grows memory in 2 MiB increments while retaining the 8 MiB stack. -## Recorded comparison +Exploration profiling added four further changes: + +- Pack small chunk-palette indices into 1, 2, 4, or 8 bits instead of one byte + each. Palette growth still falls back to dense storage when needed; Minecraft's + separate network and disk packing is unchanged. +- Share uniform initialized light arrays and detach on writes. Compact arrays + after initial light propagation, when many have become uniform. Initialized + light remains distinct from missing light, preserving packet masks and saves. +- Allocate carving-mask words only as carving reaches them, retaining every bit + and the original dimension bounds. +- Let pathfinder heaps grow when searching. Each idle navigator previously + reserved 1,024 nodes before using any of them; search limits are unchanged. + +`LightContainer::Full` now holds `Arc<[u8]>` instead of `Box<[u8]>`. This changes +the Rust enum's public payload type and needs consideration when upstreaming; +the serialized light bytes remain unchanged. + +## Exploration comparison (September 12, 2026) + +A matched comparison used four clients, view/simulation distance 3, seed +`1789195700909824175`, and level-1 compression above 512 bytes. Each client sent +32 four-block movement steps in a different direction, spaced 500 ms apart, +with five seconds stationary before and after movement. The baseline already +included the earlier template and generation-chunk optimizations. These runs +used Node 26, Wrangler 4.129.0, and workerd 1.20260903.1 on macOS arm64. + +| Measurement | Baseline | Baseline repeat | Optimized | +| --- | ---: | ---: | ---: | +| Peak sampled Wasm capacity | 161.75 MiB | 159.75 MiB | 111.81 MiB | +| Peak sampled allocated heap | 130.13 MiB | 129.35 MiB | 81.58 MiB | +| Time to receive initial views | 6.95 s | 7.87 s | 6.26 s | +| Observed exploration ticks/second | 15.28 | 15.26 | 15.41 | + +At identical gameplay settings, Wasm capacity fell about **30%** and allocated +heap about **37%**. Tick performance was similar; these individual runs do not +establish a CPU speedup. + +An allocator walk of the baseline's Wasm memory found 31.55 MiB in 4 KiB palette +buffers, 27.61 MiB in 2 KiB light buffers (about 17.6 MiB held uniform values), +7.04 MiB in carving masks, and 8.44 MiB in idle pathfinder buffers. Heap +backreferences identified the latter as vectors with length 1 and capacity 1,024. +These measurements guided the changes rather than reducing gameplay settings. + +Additional optimized runs checked the larger view and longer movement: + +| View / simulation | Players | Movement steps each | Seed | Peak Wasm | Result | +| --- | ---: | ---: | --- | ---: | --- | +| 4 / 3 | 2 | 128 | Same | 113.81 MiB | Passed | +| 4 / 3 | 2 | 32 | `1789200079352125165` | 99.81 MiB | Passed | +| 4 / 3 | 4 | 32 | Same | 125.81 MiB | Passed | +| 3 / 3 | 4 | 128 | Same | 159.81 MiB | Passed on retry | + +The configuration initially chosen after these measurements used view 4/simulation +3 with a two-player limit. The current limit is 20; that capacity has not been +load-tested. The long four-player +result is still too large for the standard isolate budget, and even the +two-player results do not establish a general capacity bound. Movement counts +describe commands sent by synthetic clients, not verified terrain traversal. +Wasm capacity excludes JavaScript allocations; sampling can miss transient peaks. + +Some attempts failed: the first long optimized four-player run disconnected a +client, and several baseline and optimized attempts failed during startup or +initial view delivery. One baseline log also reported an esbuild deadlock. +Later runs used a separate inspector port and completed, but the cause of the +failures was not established. Failed attempts are retained in the +[measurement report](memory-exploration-results.json), with intermediate builds +identified separately; their partial peaks are not comparable to completed runs. + +The final source passed 186 native world tests, 101 protocol tests, and the +pathfinder heap growth/ordering test. The full integration suite passed at the +new 4/3 defaults, including a shared block edit and restoration of the player +position and edited chunk after restarting Wrangler. + +## Cache lifetime and outgoing data (September 22, 2026) + +This pass kept view distance 4, simulation distance 3, and the configured player +limit of 20. Four clients each commanded 128 four-block movement steps in +different directions. The baseline includes the earlier memory optimizations +and the matching LLVM 24/Binaryen 131 backend. + +| Measurement | Baseline | Baseline repeat | Optimized | +| --- | ---: | ---: | ---: | +| Peak sampled Wasm capacity | 173.81 MiB | 175.88 MiB | 155.94 MiB | +| Peak sampled allocated heap | 129.86 MiB | 130.98 MiB | 113.19 MiB | +| Initial views | 8.63 s | 8.82 s | 7.49 s | +| Observed exploration ticks/second | 14.31 | 14.17 | 14.92 | + +That is approximately **10–11% less Wasm capacity** and **13–14% less allocated +heap** at the same settings. The optimized run received 1,876 distinct chunks +across the clients, compared with 1,277 in the baseline repeat. The lower memory +use did not come from delivering less terrain. Timing results remain individual +observations, not a statistical CPU benchmark. + +With **20 nearby clients** and no movement, the optimized build peaked at +**83.94 MiB** of Wasm capacity versus **91.69 MiB** in both baseline runs. All +clients received their initial 117 chunks. Initial view delivery took 9.24 s +optimized and 8.64–8.73 s baseline. A second seed's optimized four-client long +run completed at 161.94 MiB; its baseline attempts failed, so it is not a paired +comparison. These results do not establish a 20-player exploration capacity. + +The changes address retained memory and large bursts of outgoing data: + +- Release large generation-graph arrays once no queued or running task can hold + a key. Clear stale holder keys before reusing the graph. +- After the structure-start cache passes 2,048 entries, periodically remove + entries that active generation no longer references. Keep shared collector + identity intact, including concurrent cache misses. +- Limit pooled density-buffer payloads to 4 MiB per thread while retaining + reusable buffers that fit. +- Give each Java client's queued chunk payloads a 2 MiB byte budget. Reserve + capacity for a whole chunk batch before marking chunks sent. When capacity is + unavailable, chunks stay pending for retry. A single larger chunk can occupy + the whole budget so valid packets still make progress. +- Trim serialized packet capacity and release raw packet data after writing its + frame. Keep completion acknowledgements until the writer flushes. Asynchronous + resends serialize small groups and share the same batch-ordering guard. + +The [complete report](memory-retention-results.json) includes unsuccessful +attempts and intermediate builds. Some heap captures caused client timeouts; +stopped-world dumps were excluded. Several baseline and candidate runs ended +with Wrangler proxy errors. Their cause was not established. An additional +dependency-pruning experiment was left out because it showed no clear benefit +over the selected implementation. + +Native validation covered 191 world tests and six packet/chunk tests, including +atomic batch reservation, retrying unsent chunks, collector identity, and pool +accounting. The integration suite verifies player and edited-chunk restoration. + +To repeat the workloads with the current build: + +```sh +TEST_TCP_PORT=25566 TEST_HTTP_PORT=8788 TEST_INSPECTOR_PORT=9241 npm run test:experience -- --players 4 --max-players 20 --view 4 --simulation 3 --steps 128 +TEST_TCP_PORT=25566 TEST_HTTP_PORT=8788 TEST_INSPECTOR_PORT=9241 npm run test:experience -- --players 20 --max-players 20 --view 4 --simulation 3 --steps 0 +``` + +## Earlier stationary comparison (September 5, 2026) A local comparison on September 5, 2026 used the same world seed, two nearby Java 26.2 clients, and view/simulation distance 3. Both clients received 81 chunks, @@ -46,7 +182,7 @@ Status reports `wasm_memory_bytes`, `heap_allocated_bytes`, `heap_free_bytes`, a containers. Wasm capacity also includes static data, the stack, and growth headroom. Use a DevTools heap snapshot to account for JavaScript allocations. -The optimization was validated with 114 native generation tests, including +The earlier optimization was validated with 114 native generation tests, including structure/template round trips, and the full integration suite's shared block edit and player/block restoration. Future changes must continue to pass `npm test` with `PUMPKIN-DO-SQLITE-RESTART-OK`. diff --git a/docs/memory-retention-results.json b/docs/memory-retention-results.json new file mode 100644 index 0000000..981fcc7 --- /dev/null +++ b/docs/memory-retention-results.json @@ -0,0 +1,487 @@ +{ + "recorded_local_date": "2026-09-22", + "environment": { + "node": "v26.4.0", + "platform": "darwin", + "arch": "arm64", + "wrangler": "4.129.0", + "workerd": "1.20260903.1", + "custom_workerd": false + }, + "backend": "emsdk 6.0.6 / LLVM 24 / Binaryen 131", + "selected_wasm_sha256": "3b5e4411faa7b230116fdfabe0bde4e280c94355a7cc329f6299424e99f45dd3", + "workload": { + "view_distance": 4, + "simulation_distance": 3, + "configured_player_limit": 20, + "blocks_commanded_per_step": 4, + "step_interval_ms": 500, + "stationary_before_ms": 5000, + "settling_after_ms": 5000 + }, + "scope": "Single local synthetic runs. Wasm capacity excludes JavaScript and sampling can miss transient peaks. Chunk counts measure received terrain packets, not verified player traversal. Nearby twenty-client runs do not establish twenty-player exploration capacity. Failed attempts retain partial measurements and are excluded from successful comparisons. Sampling ends before checkpoint/shutdown; a completed run also finishes its checkpoint.", + "profiling_notes": [ + "Initial heap captures caused client timeouts or found a stopped world; those final dumps are excluded from gameplay comparisons.", + "A valid nearby allocator walk identified about 5 MiB retained in generation graph edge storage and 3.49 MiB of reusable density buffers by wasm32 layouts and pointer backreferences.", + "Several baseline and candidate attempts ended with Wrangler proxy/connection failures; the underlying cause was not established. Later probes used unique Worker names.", + "The extra completed-stage dependency-pruning experiment was left out: the simpler selected implementation had lower observed memory use in the successful long comparison." + ], + "runs": [ + { + "label": "baseline-long", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": true, + "failure": null, + "peak_wasm_bytes": 182255616, + "peak_heap_bytes": 136163472, + "join_ms": 8631.871042, + "exploration_tps": 14.30899306879655, + "chunks_received": 1273, + "received_wire_bytes": 8728023 + }, + { + "label": "baseline-long-repeat", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": true, + "failure": null, + "peak_wasm_bytes": 184418304, + "peak_heap_bytes": 137346400, + "join_ms": 8823.353667, + "exploration_tps": 14.167904180156748, + "chunks_received": 1277, + "received_wire_bytes": 8853334 + }, + { + "label": "baseline-long-seed2", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": false, + "failure": "AssertionError [ERR_ASSERTION]: HTTP 500: Error: Network connection lost.", + "peak_wasm_bytes": 135987200, + "peak_heap_bytes": 103214704, + "join_ms": 7156.3352079999995, + "exploration_tps": 13.40691957914532, + "chunks_received": 699, + "received_wire_bytes": 5658443 + }, + { + "label": "baseline-long-seed2-recheck", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": false, + "failure": "Error: ProbeA: server closed the connection", + "peak_wasm_bytes": 47906816, + "peak_heap_bytes": 1167672, + "join_ms": null, + "exploration_tps": null, + "chunks_received": 0, + "received_wire_bytes": 0 + }, + { + "label": "baseline-long-seed2-unique", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": false, + "failure": "Error: ProbeD: server closed the connection", + "peak_wasm_bytes": 47906816, + "peak_heap_bytes": 1167744, + "join_ms": null, + "exploration_tps": null, + "chunks_received": 115, + "received_wire_bytes": 1303815 + }, + { + "label": "baseline-nearby20", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 20, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": true, + "failure": null, + "peak_wasm_bytes": 96141312, + "peak_heap_bytes": 54240944, + "join_ms": 8731.120708, + "exploration_tps": null, + "chunks_received": 2340, + "received_wire_bytes": 16224372 + }, + { + "label": "baseline-nearby20-repeat", + "build": "baseline", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 20, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "498196d6fe8b5857b6db9474eae6b7938b60909434139f395252582217e8cafc", + "completed": true, + "failure": null, + "peak_wasm_bytes": 96141312, + "peak_heap_bytes": 54756112, + "join_ms": 8639.415500000001, + "exploration_tps": null, + "chunks_received": 2340, + "received_wire_bytes": 16523340 + }, + { + "label": "candidate-v1-long", + "build": "intermediate-cache-and-resend-changes", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "238886c03f50c79fc240aab7b17aeffbf9bbfbdc09cee4b8597868dd6e33fa41", + "completed": true, + "failure": null, + "peak_wasm_bytes": 182190080, + "peak_heap_bytes": 133187688, + "join_ms": 7977.920416999999, + "exploration_tps": 14.209825772457, + "chunks_received": 1669, + "received_wire_bytes": 11489670 + }, + { + "label": "candidate-v1-nearby20", + "build": "intermediate-cache-and-resend-changes", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 20, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "238886c03f50c79fc240aab7b17aeffbf9bbfbdc09cee4b8597868dd6e33fa41", + "completed": true, + "failure": null, + "peak_wasm_bytes": 87818240, + "peak_heap_bytes": 48901464, + "join_ms": 9162.850625000001, + "exploration_tps": null, + "chunks_received": 2340, + "received_wire_bytes": 16253325 + }, + { + "label": "candidate-v2-long", + "build": "selected-optimization", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "3b5e4411faa7b230116fdfabe0bde4e280c94355a7cc329f6299424e99f45dd3", + "completed": true, + "failure": null, + "peak_wasm_bytes": 163512320, + "peak_heap_bytes": 118689336, + "join_ms": 7485.766084000001, + "exploration_tps": 14.920846126541553, + "chunks_received": 1876, + "received_wire_bytes": 13268900 + }, + { + "label": "candidate-v3-long", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": false, + "failure": "AssertionError [ERR_ASSERTION]: HTTP 500: Error: Network connection lost.", + "peak_wasm_bytes": 153026560, + "peak_heap_bytes": 118278944, + "join_ms": 8116.79725, + "exploration_tps": 15.347543187969494, + "chunks_received": 1066, + "received_wire_bytes": 7254643 + }, + { + "label": "candidate-v3-long-recheck", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": false, + "failure": "Error: ProbeB: server closed the connection", + "peak_wasm_bytes": 48168960, + "peak_heap_bytes": 1167704, + "join_ms": null, + "exploration_tps": null, + "chunks_received": 9, + "received_wire_bytes": 174527 + }, + { + "label": "candidate-v3-long-seed2", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": false, + "failure": "AssertionError [ERR_ASSERTION]: Wrangler exited", + "peak_wasm_bytes": 165609472, + "peak_heap_bytes": 122126696, + "join_ms": 8757.433041, + "exploration_tps": 14.93088013020327, + "chunks_received": 1230, + "received_wire_bytes": 10066807 + }, + { + "label": "candidate-v3-long-seed2-recheck", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": false, + "failure": "Error: ProbeA: server closed the connection", + "peak_wasm_bytes": 48168960, + "peak_heap_bytes": 1167856, + "join_ms": null, + "exploration_tps": null, + "chunks_received": 0, + "received_wire_bytes": 0 + }, + { + "label": "candidate-v3-long-seed2-unique", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": true, + "failure": null, + "peak_wasm_bytes": 169803776, + "peak_heap_bytes": 123455824, + "join_ms": 7019.83625, + "exploration_tps": 15.179562150849492, + "chunks_received": 1833, + "received_wire_bytes": 14175975 + }, + { + "label": "candidate-v3-long-unique", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789195700909824175", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": true, + "failure": null, + "peak_wasm_bytes": 169803776, + "peak_heap_bytes": 122208224, + "join_ms": 7683.319583999999, + "exploration_tps": 14.523260021964266, + "chunks_received": 1686, + "received_wire_bytes": 11749454 + }, + { + "label": "candidate-v3-nearby20", + "build": "rejected-additional-dependency-pruning", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 20, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "5678c4547905980256f3bd20ef6e8c673b3899e3fd6f14aaf39d983284eb41a0", + "completed": true, + "failure": null, + "peak_wasm_bytes": 88014848, + "peak_heap_bytes": 52300336, + "join_ms": 9841.464042, + "exploration_tps": null, + "chunks_received": 2340, + "received_wire_bytes": 16432368 + }, + { + "label": "final-long-seed2", + "build": "selected-optimization", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 4, + "steps": 128, + "seed": "1789200079352125165", + "wasm_sha256": "3b5e4411faa7b230116fdfabe0bde4e280c94355a7cc329f6299424e99f45dd3", + "completed": true, + "failure": null, + "peak_wasm_bytes": 169803776, + "peak_heap_bytes": 122074488, + "join_ms": 7275.794417000001, + "exploration_tps": 14.470819241483792, + "chunks_received": 1319, + "received_wire_bytes": 10389366 + }, + { + "label": "final-nearby20", + "build": "selected-optimization", + "settings": { + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "0" + }, + "players": 20, + "steps": 0, + "seed": "1789195700909824175", + "wasm_sha256": "3b5e4411faa7b230116fdfabe0bde4e280c94355a7cc329f6299424e99f45dd3", + "completed": true, + "failure": null, + "peak_wasm_bytes": 88014848, + "peak_heap_bytes": 48976504, + "join_ms": 9236.968666, + "exploration_tps": null, + "chunks_received": 2340, + "received_wire_bytes": 16117981 + } + ] +} diff --git a/package-lock.json b/package-lock.json index 5c4e381..22ee585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,15 +5,14 @@ "packages": { "": { "name": "rust-workers-minecraft", + "hasInstallScript": true, "dependencies": { "durable-object-fs": "1.0.0", "worker-fs-mount": "0.2.0" }, "devDependencies": { - "@cloudflare/workers-types": "5.20260905.1", - "@types/node": "26.4.1", - "typescript": "~5.9.3", - "wrangler": "4.129.0" + "workerd": "1.20260925.1", + "wrangler": "4.141.0" }, "engines": { "node": ">=24" @@ -30,9 +29,9 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", - "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { @@ -46,9 +45,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260903.1.tgz", - "integrity": "sha512-FG+4mGxAXhKiL/1temH42alevIkumtYXNidhTa//3yULpzux6APw5UNVocI/vCQ1yG1YOEZRfbVy8lyuipM9MQ==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260925.1.tgz", + "integrity": "sha512-GFDjFo5jfmg98iiy73DOZjHRV+lPWaeGeJ6MbtUIVyostkh9L69KliaAWVytBHCSp36o3NNZOQ0CN8VrqO3+8A==", "cpu": [ "x64" ], @@ -63,9 +62,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260903.1.tgz", - "integrity": "sha512-o241VefnjG8eG+kGap5CjgV4zOT5UaAmS3OR1VZCpNj7vkXGxvp9KftKvtQgcCsIqJKaKx+7Xd7xq0L1DjkfPQ==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260925.1.tgz", + "integrity": "sha512-b3BLoP9NkF6b2QOQFs8wFfOt9XSH3jVtacdGn9lcvkUCdPq7F6CfDlleHqyQEnDfZ32lH5jgMRTdaMHUPf/XbA==", "cpu": [ "arm64" ], @@ -80,9 +79,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260903.1.tgz", - "integrity": "sha512-/VEvvtQ/XKf6HlBbg6FbvpwcpfYUcx6Fv6RkASY8DyEmUyuJ8rc7Qxil83ClRFoBzz/GY0BV94UZ6F+wers/hg==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260925.1.tgz", + "integrity": "sha512-LczXd6uEMqEmBxBJPFwplRIwKmUHqj5t8hilIWOtbX5i0TFp7/iMYBXbAiIhvZnev+8yiTyA7uWBBjDvfPh5Ug==", "cpu": [ "x64" ], @@ -97,9 +96,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260903.1.tgz", - "integrity": "sha512-OWhihGC6KoTXF4u2C1AonfpgXeM4/7p/1IXuALqXESmFUpLLP5gZhRzjSk/gWW+mrCZDfSrvnjifl+lRqselbA==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260925.1.tgz", + "integrity": "sha512-hziOugQbLuva85w8z9bGJbEtABKzSip653x0NgvxgP/Q3n1TpRqRyIJRBufbSUZRkJlfH2se2nQaTx2OU0TT3g==", "cpu": [ "arm64" ], @@ -114,9 +113,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260903.1.tgz", - "integrity": "sha512-soPMF9/aMHlHKK7M0vq5HrRRicPbnZO1F6ZZ7JWN8EltxWJU5L7CEq7CxjO878DzyPx7gYvqBFy3SVgdZKZjMQ==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260925.1.tgz", + "integrity": "sha512-mG0JYuF6HVCgXSsNeB5os8Vtdprk5jHPysviKNNMHJSLA3Q1djFjSmK4+pnQR/zOAgzNo0HJ+Tpcz+tZj1Q7fQ==", "cpu": [ "x64" ], @@ -130,13 +129,6 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workers-types": { - "version": "5.20260905.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260905.1.tgz", - "integrity": "sha512-ebDpVTgrnee245IRBTazyfjfmZas+HJehZyVQkKmUozsBN8RzvXXMI0XjjcT2rQjw+0USewfEGGLYiHO+TL0Sg==", - "dev": true, - "license": "MIT OR Apache-2.0" - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -614,9 +606,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", - "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -633,13 +625,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.1" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", - "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -656,13 +648,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.1" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", - "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -670,7 +662,7 @@ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -680,9 +672,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", - "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -697,9 +689,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", - "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -714,9 +706,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", - "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -734,9 +726,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", - "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -754,9 +746,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", - "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -774,9 +766,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", - "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -794,9 +786,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", - "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -814,9 +806,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", - "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -834,9 +826,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", - "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -854,9 +846,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", - "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -874,9 +866,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", - "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -896,13 +888,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.1" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", - "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -922,13 +914,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.1" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", - "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -948,13 +940,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.1" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", - "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -974,13 +966,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.1" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", - "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -1000,13 +992,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.1" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", - "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -1026,13 +1018,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.1" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", - "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -1052,13 +1044,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", - "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -1078,18 +1070,18 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", - "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -1099,9 +1091,9 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", - "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], @@ -1109,7 +1101,7 @@ "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -1119,9 +1111,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", - "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -1139,9 +1131,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", - "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -1159,9 +1151,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", - "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -1255,16 +1247,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/@types/node": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", - "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1386,16 +1368,16 @@ } }, "node_modules/miniflare": { - "version": "5.20260903.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260903.0-alpha.tgz", - "integrity": "sha512-VCZIFxOqFXeibRBJWwTlppqbv2lkeO10IX3QBRGK9j1QiMAfH/OSsCvbjp1MslBMhVZmy2VTRlVaVnsKnbDp6A==", + "version": "5.20260925.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260925.0-alpha.tgz", + "integrity": "sha512-ly2ygNOfuouMiPcyTNEUW6f3+zgndfC45e1nQZ2LSciOSFfrih8V7ciaZ01I/64Ccp3F6fWraxh5VuHPsP3AAw==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.35.2", + "sharp": "0.35.4", "undici": "7.29.0", - "workerd": "1.20260903.1", + "workerd": "1.20260925.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -1431,15 +1413,15 @@ } }, "node_modules/sharp": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", - "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "semver": "^7.8.5" }, "engines": { "node": ">=20.9.0" @@ -1448,31 +1430,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.2", - "@img/sharp-darwin-x64": "0.35.2", - "@img/sharp-freebsd-wasm32": "0.35.2", - "@img/sharp-libvips-darwin-arm64": "1.3.1", - "@img/sharp-libvips-darwin-x64": "1.3.1", - "@img/sharp-libvips-linux-arm": "1.3.1", - "@img/sharp-libvips-linux-arm64": "1.3.1", - "@img/sharp-libvips-linux-ppc64": "1.3.1", - "@img/sharp-libvips-linux-riscv64": "1.3.1", - "@img/sharp-libvips-linux-s390x": "1.3.1", - "@img/sharp-libvips-linux-x64": "1.3.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", - "@img/sharp-libvips-linuxmusl-x64": "1.3.1", - "@img/sharp-linux-arm": "0.35.2", - "@img/sharp-linux-arm64": "0.35.2", - "@img/sharp-linux-ppc64": "0.35.2", - "@img/sharp-linux-riscv64": "0.35.2", - "@img/sharp-linux-s390x": "0.35.2", - "@img/sharp-linux-x64": "0.35.2", - "@img/sharp-linuxmusl-arm64": "0.35.2", - "@img/sharp-linuxmusl-x64": "0.35.2", - "@img/sharp-webcontainers-wasm32": "0.35.2", - "@img/sharp-win32-arm64": "0.35.2", - "@img/sharp-win32-ia32": "0.35.2", - "@img/sharp-win32-x64": "0.35.2" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/supports-color": { @@ -1496,20 +1483,6 @@ "license": "0BSD", "optional": true }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -1520,13 +1493,6 @@ "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -1547,9 +1513,9 @@ } }, "node_modules/workerd": { - "version": "1.20260903.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260903.1.tgz", - "integrity": "sha512-xJzt2RnCy7ulOULmZy/4JLbEPg1uisp9lVoOUsEz+UVhDsTmrSQ0rBXZMGcXuhr2HCGKs89bg8nnbbzvBSX1Ig==", + "version": "1.20260925.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260925.1.tgz", + "integrity": "sha512-cl7Mrvhm+xEC9brMBIWIHyfYl7y5dGH3CjQog6CglWhgRwAq+/b+2gHBXNQb4hsjLXQVCuCYExJRYCjEaZmAlA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1560,28 +1526,28 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260903.1", - "@cloudflare/workerd-darwin-arm64": "1.20260903.1", - "@cloudflare/workerd-linux-64": "1.20260903.1", - "@cloudflare/workerd-linux-arm64": "1.20260903.1", - "@cloudflare/workerd-windows-64": "1.20260903.1" + "@cloudflare/workerd-darwin-64": "1.20260925.1", + "@cloudflare/workerd-darwin-arm64": "1.20260925.1", + "@cloudflare/workerd-linux-64": "1.20260925.1", + "@cloudflare/workerd-linux-arm64": "1.20260925.1", + "@cloudflare/workerd-windows-64": "1.20260925.1" } }, "node_modules/wrangler": { - "version": "4.129.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.129.0.tgz", - "integrity": "sha512-PGPvs9UPoFrwxT0VogpESSZGvZIctAuTK3wGsLLPHtHsSgS85kNdvtpa2d14UzG8gwLWD64XUGFPGG9tOXG9VQ==", + "version": "4.141.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.141.0.tgz", + "integrity": "sha512-8+LRZEcynBMfVYgO4N378z5XyuajaILt2IzsdWRLCHrRWz7mmGWv4igdVjqbRQMJtE0S5bSmyz0W2XpA6UM/ow==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", - "@cloudflare/unenv-preset": "2.16.1", + "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20260903.0-alpha", + "miniflare": "5.20260925.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260903.1" + "workerd": "1.20260925.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -1595,7 +1561,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260903.1" + "@cloudflare/workers-types": "^5.20260925.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index eac72cc..d45b133 100644 --- a/package.json +++ b/package.json @@ -10,23 +10,21 @@ "worker-fs-mount": "0.2.0" }, "devDependencies": { - "@cloudflare/workers-types": "5.20260905.1", - "@types/node": "26.4.1", - "typescript": "~5.9.3", - "wrangler": "4.129.0" - }, - "overrides": { - "durable-object-fs": { - "worker-fs-mount": "$worker-fs-mount" - } + "workerd": "1.20260925.1", + "wrangler": "4.141.0" }, "scripts": { + "postinstall": "node scripts/patch-packages.mjs", "build": "bash scripts/build.sh", "dev": "bash scripts/serve.sh", "test": "bash scripts/test.sh", "test:memory": "npm run build && node tests/memory.mjs", - "types": "wrangler types worker/env.d.ts --strict-vars=false", - "typecheck": "npm run types && tsc --noEmit", - "test:scheduler": "bash scripts/test-scheduler.sh" + "test:scheduler": "bash scripts/test-scheduler.sh", + "test:experience": "npm run build && node tests/experience.mjs" + }, + "overrides": { + "durable-object-fs": { + "worker-fs-mount": "$worker-fs-mount" + } } } diff --git a/patches/README.md b/patches/README.md index 945c45b..17ee30a 100644 --- a/patches/README.md +++ b/patches/README.md @@ -9,14 +9,13 @@ diff below it. Preserve the description when regenerating the diff. | Patch | Checkout | Purpose | | --- | --- | --- | -| `pumpkin-emscripten.patch` | `.work/pumpkin` | Headless optional subsystems, single-threaded runtime and blocking-task helper, shared async chunk scheduler with backpressure and timed draining, unified ticker, and platform guards | -| `pumpkin-memory.patch` | `.work/pumpkin` | Shared indexed structure templates, immutable block-entity NBT, paletted generation chunks, and a temporary dense noise buffer | -| `wasm-bindgen-emscripten-closures.patch` | `.work/wasm-bindgen` | Escape generated closure-finalizer postsets as JavaScript strings | -| `workers-rs-emscripten-toolchain.patch` | `.work/workers-rs` | Match the pinned wasm-bindgen ABI, provide the Emscripten Tokio promise adapter, leave initialization/recovery to the host, and flush pending writes before socket shutdown | -| `wasm-streams-rlib.patch` | `.work/wasm-streams` | Omit the standalone cdylib when embedding the stream adapter in Emscripten | +| `pumpkin-emscripten.patch` | `.work/pumpkin` | Headless optional subsystems, single-threaded runtime and blocking-task helper, shared async chunk scheduler with backpressure, timed draining and idle graph release, byte-bounded outgoing chunk batches, unified ticker, restartable stop signal, and platform guards | +| `worker-fs-mount-open-mode.patch` | `node_modules/worker-fs-mount` (via `npm install`) | Mask numeric `openSync`/`fchmodSync` modes to `0o7777` like Node, so Emscripten's `S_IFREG \| perms` creates files | +| `pumpkin-memory.patch` | `.work/pumpkin` | Shared indexed structure templates, immutable block-entity NBT, paletted generation chunks, a temporary dense noise buffer, packed palettes with copy-on-write light arrays, on-demand carving masks, bounded density pools, and pruned structure caches | -`pumpkin-emscripten.patch` includes the injected-stream connection entry point used -by the DO. Setup applies it before `pumpkin-memory.patch`. The two patches use the +Patches whose paths start with `node_modules/` are applied by +`scripts/patch-packages.mjs` from npm's `postinstall`; the rest by setup. +Setup applies `pumpkin-emscripten.patch` before `pumpkin-memory.patch`. The two patches use the same pinned base and currently modify separate files, so each can also be applied and checked independently. diff --git a/patches/pumpkin-emscripten.patch b/patches/pumpkin-emscripten.patch index ce284ef..f711f73 100644 --- a/patches/pumpkin-emscripten.patch +++ b/patches/pumpkin-emscripten.patch @@ -1,17 +1,21 @@ Subject: [PATCH] Support headless Pumpkin on a cooperative runtime -Embedding Pumpkin in a Durable Object requires accepting host-provided -streams and running without OS worker threads or native plugin runtimes. -Add an injected-stream connection entry point and optional subsystems for -NetherNet, plugin loading, the console, and system crash reporting. Native -builds retain these subsystems and thread-backed execution by default. +Embedding Pumpkin in a Durable Object requires running without OS worker +threads or native plugin runtimes. Add optional subsystems for NetherNet, +plugin loading, the console, and system crash reporting. Native builds +retain these subsystems and thread-backed execution by default. Share asynchronous ticker and chunk-scheduler loops between native and -single-threaded builds. Route Rayon and blocking jobs through helpers that -can run on the current thread, await scheduler notifications and results, -and track cooperative tasks through shutdown. Match the Tokio requirement -to the hosted-runtime fork and guard unsupported Emscripten system calls -and synchronous HTTP lookups. +single-threaded builds. Run Rayon, chunk-generation and blocking jobs as +Tokio tasks on single-threaded builds so each takes its own turn of the +event loop, await scheduler notifications and results, and track +cooperative tasks through shutdown. Drop the Tokio features that +need OS threads and signals from single-threaded builds and guard +unsupported Emscripten system calls and synchronous HTTP lookups. + +Make the process-wide stop token replaceable behind a StopSignal wrapper +and add reset_stop, so an embedding can start another server in the same +process after one has stopped. Reserve save-queue capacity before transferring chunks or marking writes pending, and widen pending-write counters to usize. Drain late generation @@ -23,11 +27,17 @@ paths do not invalidate Cargo's cache on every build. The application Cargo.lock supplies the embedded dependency resolution; Pumpkin's lockfile is excluded from this patch. +Reserve outgoing chunk batches by bytes and queue slots before marking them +sent. Keep backpressured chunks pending, release framed packet data promptly, +and preserve batch ordering for asynchronous resends. Release idle generation +graph allocations once no task holds a key. Keep imports and helpers behind +the same feature gates as their consumers so headless builds compile cleanly. + Base-commit: b5b9b9d7010e793806a83c495af223c67e1d35ee --- diff --git a/Cargo.toml b/Cargo.toml -index 143ba8a36..57314ca35 100644 +index 143ba8a3..57314ca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ strip = false @@ -40,7 +50,7 @@ index 143ba8a36..57314ca35 100644 thiserror = { version = "2.0", default-features = false } diff --git a/crates/pumpkin-util/Cargo.toml b/crates/pumpkin-util/Cargo.toml -index f4fb121fb..0a4a30761 100644 +index f4fb121f..0a4a3076 100644 --- a/crates/pumpkin-util/Cargo.toml +++ b/crates/pumpkin-util/Cargo.toml @@ -6,11 +6,13 @@ rust-version.workspace = true @@ -67,25 +77,23 @@ index f4fb121fb..0a4a30761 100644 [[bench]] diff --git a/crates/pumpkin-util/src/lib.rs b/crates/pumpkin-util/src/lib.rs -index 5119797bc..a1ae947a2 100644 +index 5119797b..35d78184 100644 --- a/crates/pumpkin-util/src/lib.rs +++ b/crates/pumpkin-util/src/lib.rs -@@ -310,3 +310,42 @@ impl TryFrom for Hand { +@@ -310,3 +310,39 @@ impl TryFrom for Hand { } } } + +/// Spawn a fire-and-forget CPU task. +/// -+/// On normal targets this hands the closure to rayon's global thread pool. On -+/// `wasm32-unknown-emscripten` there are no OS threads and rayon has no worker to -+/// run an injected job (a bare `rayon::spawn` would never execute, so any code that -+/// then `.await`s the result over a channel would deadlock). There we run the -+/// closure inline on the calling thread instead. ++/// On normal targets this hands the closure to rayon's global thread pool. ++/// Single-threaded builds have no rayon workers (a bare `rayon::spawn` would never ++/// run), so the closure becomes a Tokio task and runs on a later turn of the loop. +pub fn rayon_spawn(f: F) { + #[cfg(feature = "single-threaded")] + { -+ f(); ++ tokio::spawn(async move { f() }); + } + #[cfg(not(feature = "single-threaded"))] + { @@ -98,7 +106,6 @@ index 5119797bc..a1ae947a2 100644 +/// Single-threaded builds run the closure in a regular Tokio task, preserving +/// panic reporting through `JoinError` without creating a blocking-pool thread. +/// The closure itself must finish without waiting for another task on this thread. -+/// JSPI does not make the public Tokio `spawn_blocking` API threadless. +pub fn spawn_blocking(f: F) -> tokio::task::JoinHandle +where + F: FnOnce() -> R + Send + 'static, @@ -114,7 +121,7 @@ index 5119797bc..a1ae947a2 100644 + } +} diff --git a/crates/pumpkin-world/Cargo.toml b/crates/pumpkin-world/Cargo.toml -index f1675a8f2..82c219179 100644 +index f1675a8f..82c21917 100644 --- a/crates/pumpkin-world/Cargo.toml +++ b/crates/pumpkin-world/Cargo.toml @@ -17,11 +17,9 @@ bytes.workspace = true @@ -145,7 +152,7 @@ index f1675a8f2..82c219179 100644 tempfile.workspace = true criterion = { workspace = true, features = ["html_reports", "async_tokio"] } diff --git a/crates/pumpkin-world/src/chunk/format/anvil.rs b/crates/pumpkin-world/src/chunk/format/anvil.rs -index 38de745f9..60cedf53d 100644 +index 38de745f..60cedf53 100644 --- a/crates/pumpkin-world/src/chunk/format/anvil.rs +++ b/crates/pumpkin-world/src/chunk/format/anvil.rs @@ -798,7 +798,7 @@ impl ChunkSerializer for AnvilChunkFile< @@ -168,7 +175,7 @@ index 38de745f9..60cedf53d 100644 }); }); diff --git a/crates/pumpkin-world/src/chunk/format/linear.rs b/crates/pumpkin-world/src/chunk/format/linear.rs -index 3927c66fc..fd76019bf 100644 +index 3927c66f..fd76019b 100644 --- a/crates/pumpkin-world/src/chunk/format/linear.rs +++ b/crates/pumpkin-world/src/chunk/format/linear.rs @@ -599,7 +599,7 @@ impl ChunkSerializer for LinearV2File @@ -191,7 +198,7 @@ index 3927c66fc..fd76019bf 100644 }); }); diff --git a/crates/pumpkin-world/src/chunk/format/pump.rs b/crates/pumpkin-world/src/chunk/format/pump.rs -index f1c44adc7..b5a8dd1d7 100644 +index f1c44adc..b5a8dd1d 100644 --- a/crates/pumpkin-world/src/chunk/format/pump.rs +++ b/crates/pumpkin-world/src/chunk/format/pump.rs @@ -147,7 +147,7 @@ where @@ -214,7 +221,7 @@ index f1c44adc7..b5a8dd1d7 100644 }); }); diff --git a/crates/pumpkin-world/src/chunk/io/mod.rs b/crates/pumpkin-world/src/chunk/io/mod.rs -index c4daf5bb5..78271c344 100644 +index c4daf5bb..78271c34 100644 --- a/crates/pumpkin-world/src/chunk/io/mod.rs +++ b/crates/pumpkin-world/src/chunk/io/mod.rs @@ -13,7 +13,7 @@ where @@ -227,7 +234,7 @@ index c4daf5bb5..78271c344 100644 /// The result of loading a chunk data. diff --git a/crates/pumpkin-world/src/chunk_system/channel.rs b/crates/pumpkin-world/src/chunk_system/channel.rs -index 190d578fa..c138b1f50 100644 +index 190d578f..c138b1f5 100644 --- a/crates/pumpkin-world/src/chunk_system/channel.rs +++ b/crates/pumpkin-world/src/chunk_system/channel.rs @@ -14,6 +14,7 @@ pub type LevelChange = ( @@ -281,7 +288,7 @@ index 190d578fa..c138b1f50 100644 } } diff --git a/crates/pumpkin-world/src/chunk_system/mod.rs b/crates/pumpkin-world/src/chunk_system/mod.rs -index f670ad37b..b9d9a8671 100644 +index f670ad37..b9d9a867 100644 --- a/crates/pumpkin-world/src/chunk_system/mod.rs +++ b/crates/pumpkin-world/src/chunk_system/mod.rs @@ -13,7 +13,7 @@ pub type HashSetType = rustc_hash::FxHashSet; @@ -294,7 +301,7 @@ index f670ad37b..b9d9a8671 100644 )>; diff --git a/crates/pumpkin-world/src/chunk_system/schedule.rs b/crates/pumpkin-world/src/chunk_system/schedule.rs -index 145e15de8..2942a1d31 100644 +index 145e15de..eca8ecde 100644 --- a/crates/pumpkin-world/src/chunk_system/schedule.rs +++ b/crates/pumpkin-world/src/chunk_system/schedule.rs @@ -69,13 +69,14 @@ pub struct GenerationSchedule { @@ -473,18 +480,36 @@ index 145e15de8..2942a1d31 100644 let mut chunks = Vec::with_capacity(self.chunk_map.len()); for (pos, holder) in &mut self.chunk_map { -@@ -969,9 +987,7 @@ impl GenerationSchedule { +@@ -969,9 +987,26 @@ impl GenerationSchedule { } drop(data); - if let Err(e) = self.io_write.blocking_send(chunks) { - error!("Failed to send chunks to io write thread: {:?}", e); -- } + permit.send(chunks); ++ } ++ ++ /// SlotMap retains its largest backing arrays after tasks finish. When no ++ /// task can hold a key, release those arrays and remove all stale holder keys. ++ fn release_idle_graph(&mut self) { ++ if self.running_task_count != 0 ++ || !self.graph.nodes.is_empty() ++ || !self.queue.is_empty() ++ || !self.waiting_for_chunks.is_empty() ++ || (self.graph.nodes.capacity() < 4096 && self.graph.edges.capacity() < 8192) ++ { ++ return; ++ } ++ for holder in self.chunk_map.values_mut() { ++ holder.tasks.fill(NodeKey::null()); ++ holder.occupied = NodeKey::null(); ++ holder.occupied_by = EdgeKey::null(); + } ++ self.graph = DAG::default(); } fn drop_node(&mut self, node: NodeKey) { -@@ -1254,25 +1270,22 @@ impl GenerationSchedule { +@@ -1254,25 +1289,22 @@ impl GenerationSchedule { } #[expect(clippy::too_many_lines)] @@ -517,7 +542,7 @@ index 145e15de8..2942a1d31 100644 break; } -@@ -1287,7 +1300,7 @@ impl GenerationSchedule { +@@ -1287,7 +1319,7 @@ impl GenerationSchedule { // is what puts stale dependency holders into the queue in the first place. if self.last_unload.elapsed() >= std::time::Duration::from_secs(1) { self.garbage_collect_dependencies(); @@ -526,7 +551,7 @@ index 145e15de8..2942a1d31 100644 self.last_unload = std::time::Instant::now(); } -@@ -1308,7 +1321,7 @@ impl GenerationSchedule { +@@ -1308,7 +1340,7 @@ impl GenerationSchedule { if level.shut_down_chunk_system.load(Relaxed) { self.queue.push(task); info!("Shutdown detected during task processing, saving chunks..."); @@ -535,7 +560,7 @@ index 145e15de8..2942a1d31 100644 break 'out2; } -@@ -1400,11 +1413,12 @@ impl GenerationSchedule { +@@ -1400,11 +1432,12 @@ impl GenerationSchedule { if io_batch.len() >= 16 && self .io_read @@ -550,7 +575,7 @@ index 145e15de8..2942a1d31 100644 break 'out2; } } else { -@@ -1412,11 +1426,12 @@ impl GenerationSchedule { +@@ -1412,11 +1445,12 @@ impl GenerationSchedule { if !io_batch.is_empty() && self .io_read @@ -565,7 +590,7 @@ index 145e15de8..2942a1d31 100644 break 'out2; } -@@ -1552,12 +1567,19 @@ impl GenerationSchedule { +@@ -1552,12 +1586,16 @@ impl GenerationSchedule { let send_chunk = self.send_chunk.clone(); let level = level.clone(); @@ -580,14 +605,11 @@ index 145e15de8..2942a1d31 100644 + #[cfg(not(feature = "single-threaded"))] + self.generation_pool.spawn(generate); + #[cfg(feature = "single-threaded")] -+ { -+ generate(); -+ tokio::task::yield_now().await; -+ } ++ tokio::spawn(async move { generate() }); } } } -@@ -1566,82 +1588,39 @@ impl GenerationSchedule { +@@ -1566,82 +1604,40 @@ impl GenerationSchedule { if !io_batch.is_empty() && self .io_read @@ -656,11 +678,12 @@ index 145e15de8..2942a1d31 100644 - if self.resort_work(self.send_level.wait_and_get(level)) { - self.garbage_collect_dependencies(); - } - } +- } - if self.queue_dirty { - self.sort_queue(); - self.queue_dirty = false; -- } ++ self.release_idle_graph(); + } - } else if self.running_task_count >= self.max_in_flight { - // Queue has tasks, but we are at maximum in-flight capacity. - // Wait for an in-flight worker to finish instead of busy-spinning. @@ -675,12 +698,12 @@ index 145e15de8..2942a1d31 100644 - if self.resort_work(self.send_level.get()) { - self.garbage_collect_dependencies(); - } -- } + } - Err(crossbeam::channel::RecvTimeoutError::Timeout) => { - if self.resort_work(self.send_level.get()) { - self.garbage_collect_dependencies(); - } - } +- } - Err(crossbeam::channel::RecvTimeoutError::Disconnected) => break, + () = self.send_level.changed() => {} + () = tokio::time::sleep(Duration::from_millis(50)) => {} @@ -693,7 +716,7 @@ index 145e15de8..2942a1d31 100644 } } } -@@ -1649,24 +1628,10 @@ impl GenerationSchedule { +@@ -1649,24 +1645,10 @@ impl GenerationSchedule { "schedule: waiting for {} generation tasks to finish", self.running_task_count ); @@ -722,7 +745,7 @@ index 145e15de8..2942a1d31 100644 if self.running_task_count > 0 { warn!( -@@ -1716,6 +1681,15 @@ impl GenerationSchedule { +@@ -1716,6 +1698,15 @@ impl GenerationSchedule { self.graph.edges.clear(); } @@ -738,7 +761,7 @@ index 145e15de8..2942a1d31 100644 fn debug_check(&self) -> bool { if !self.graph.nodes.is_empty() { for (key, value) in &self.graph.nodes { -@@ -1746,3 +1720,123 @@ impl GenerationSchedule { +@@ -1746,3 +1737,160 @@ impl GenerationSchedule { true } } @@ -803,6 +826,43 @@ index 145e15de8..2942a1d31 100644 + ) + } + ++ #[test] ++ fn idle_graph_release_clears_stale_keys_and_never_drops_live_tasks() { ++ let (mut scheduler, _receiver) = scheduler(); ++ let pos = ChunkPos::new(0, 0); ++ let node = scheduler ++ .graph ++ .nodes ++ .insert(Node::new(pos, StagedChunkEnum::Noise)); ++ scheduler.graph.nodes.reserve(8192); ++ scheduler.graph.edges.reserve(16384); ++ let edge = scheduler ++ .graph ++ .edges ++ .insert(super::super::dag::Edge::new(node, EdgeKey::null())); ++ let mut holder = ChunkHolder { ++ chunk: Some(chunk(StagedChunkEnum::Biomes)), ++ occupied_by: edge, ++ ..ChunkHolder::default() ++ }; ++ holder.tasks[StagedChunkEnum::Noise as usize] = node; ++ scheduler.chunk_map.insert(pos, holder); ++ scheduler.release_idle_graph(); ++ assert!(scheduler.graph.nodes.contains_key(node)); ++ scheduler.graph.nodes.remove(node); ++ scheduler.running_task_count = 1; ++ scheduler.release_idle_graph(); ++ assert!(scheduler.graph.edges.contains_key(edge)); ++ scheduler.running_task_count = 0; ++ scheduler.release_idle_graph(); ++ assert!(scheduler.graph.nodes.capacity() < 4096); ++ assert!(scheduler.graph.edges.is_empty()); ++ let holder = &scheduler.chunk_map[&pos]; ++ assert!(holder.tasks.iter().all(Key::is_null)); ++ assert!(holder.occupied_by.is_null()); ++ assert!(holder.chunk.is_some()); ++ } ++ + #[tokio::test] + async fn full_save_queue_retains_the_batch_until_capacity_is_available() { + let (mut scheduler, mut receiver) = scheduler(); @@ -863,7 +923,7 @@ index 145e15de8..2942a1d31 100644 + } +} diff --git a/crates/pumpkin-world/src/chunk_system/worker_logic.rs b/crates/pumpkin-world/src/chunk_system/worker_logic.rs -index b50ac05c9..281fa41f0 100644 +index b50ac05c..281fa41f 100644 --- a/crates/pumpkin-world/src/chunk_system/worker_logic.rs +++ b/crates/pumpkin-world/src/chunk_system/worker_logic.rs @@ -90,7 +90,7 @@ fn process_loaded_chunk(chunk: Arc, level: &Level) -> C @@ -876,10 +936,31 @@ index b50ac05c9..281fa41f0 100644 lock: IOLock, ) { diff --git a/crates/pumpkin-world/src/level.rs b/crates/pumpkin-world/src/level.rs -index fbc69749a..533b09d21 100644 +index fbc69749..52fe29fb 100644 --- a/crates/pumpkin-world/src/level.rs +++ b/crates/pumpkin-world/src/level.rs -@@ -316,7 +316,7 @@ impl Level { +@@ -28,15 +28,19 @@ use pumpkin_util::math::{position::BlockPos, vector2::Vector2}; + use pumpkin_util::world_seed::Seed; + use rustc_hash::FxHashSet; + use std::sync::{Arc, Mutex, Weak}; ++#[cfg(not(feature = "single-threaded"))] + use std::time::Duration; + use std::{ + path::PathBuf, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, + thread, + }; ++#[cfg(not(feature = "single-threaded"))] + use tokio::time::timeout; + use tokio_util::sync::CancellationToken; +-use tracing::{debug, error, info, trace, warn}; ++#[cfg(not(feature = "single-threaded"))] ++use tracing::warn; ++use tracing::{debug, error, info, trace}; + // use tokio::runtime::Handle; + use tokio::{ + select, +@@ -316,7 +320,7 @@ impl Level { pub fn spawn_entity_generation(self: &Arc, pos: Vector2) { let level = self.clone(); @@ -888,7 +969,7 @@ index fbc69749a..533b09d21 100644 let arc_chunk = Arc::new(ChunkEntityData { x: pos.x, z: pos.y, -@@ -354,43 +354,46 @@ impl Level { +@@ -354,43 +358,46 @@ impl Level { self.tasks.close(); self.chunk_system_tasks.close(); @@ -969,7 +1050,7 @@ index fbc69749a..533b09d21 100644 } diff --git a/crates/pumpkin-world/src/world_info/anvil.rs b/crates/pumpkin-world/src/world_info/anvil.rs -index ebbcb49b6..46975c6c5 100644 +index ebbcb49b..46975c6c 100644 --- a/crates/pumpkin-world/src/world_info/anvil.rs +++ b/crates/pumpkin-world/src/world_info/anvil.rs @@ -425,6 +425,11 @@ impl WorldInfoWriter for AnvilLevelInfo { @@ -985,7 +1066,7 @@ index ebbcb49b6..46975c6c5 100644 let path = level_folder.join(LEVEL_DAT_FILE_NAME); let path_new = level_folder.join("level.dat_new"); diff --git a/crates/pumpkin/Cargo.toml b/crates/pumpkin/Cargo.toml -index 6ade6bfcb..c9230c03a 100644 +index 6ade6bfc..c9230c03 100644 --- a/crates/pumpkin/Cargo.toml +++ b/crates/pumpkin/Cargo.toml @@ -17,6 +17,14 @@ LegalCopyright = "Copyright © 2026 Aleksander Medvedev" @@ -1109,7 +1190,7 @@ index 6ade6bfcb..c9230c03a 100644 [lints] diff --git a/crates/pumpkin/build.rs b/crates/pumpkin/build.rs -index 5314180a1..a6accb815 100644 +index 5314180a..a6accb81 100644 --- a/crates/pumpkin/build.rs +++ b/crates/pumpkin/build.rs @@ -23,8 +23,21 @@ fn main() { @@ -1137,7 +1218,7 @@ index 5314180a1..a6accb815 100644 println!("cargo::rustc-env=GIT_HASH_FULL={git_hash_full}"); } diff --git a/crates/pumpkin/src/command/commands/pumpkin.rs b/crates/pumpkin/src/command/commands/pumpkin.rs -index 0045511a9..9586e5dc5 100644 +index 0045511a..326c0c64 100644 --- a/crates/pumpkin/src/command/commands/pumpkin.rs +++ b/crates/pumpkin/src/command/commands/pumpkin.rs @@ -56,6 +56,7 @@ fn fetch_all_contributors_cached() -> Vec { @@ -1158,7 +1239,63 @@ index 0045511a9..9586e5dc5 100644 if !contributors.is_empty() { if let Ok(mut guard) = CONTRIBUTORS_CACHE.lock() { -@@ -251,6 +255,7 @@ fn fetch_donators_hover_cached() -> TextComponent { +@@ -80,6 +84,7 @@ fn fetch_all_contributors_cached() -> Vec { + contributors + } + ++#[cfg(not(feature = "single-threaded"))] + async fn fetch_all_contributors() -> Vec { + let client = pumpkin_util::client_builder() + .user_agent("Pumpkin-MC") +@@ -121,6 +126,7 @@ async fn fetch_all_contributors() -> Vec { + all_contributors + } + ++#[cfg(not(feature = "single-threaded"))] + fn extract_next_url(header: &str) -> Option { + header + .split(',') +@@ -132,6 +138,7 @@ fn extract_next_url(header: &str) -> Option { + }) + } + ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct Donator { + name: String, +@@ -139,6 +146,7 @@ struct Donator { + tier_slug: String, + } + ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct DonatorResponse { + current: Vec, +@@ -146,6 +154,7 @@ struct DonatorResponse { + past: Vec, + } + ++#[cfg(not(feature = "single-threaded"))] + fn tier_priority(tier_slug: &str, tier_name: &str) -> u32 { + let s = format!("{tier_slug} {tier_name}").to_lowercase(); + +@@ -174,6 +183,7 @@ fn tier_priority(tier_slug: &str, tier_name: &str) -> u32 { + } + } + ++#[cfg(not(feature = "single-threaded"))] + fn tier_color(tier_slug: &str, tier_name: &str) -> NamedColor { + let s = format!("{tier_slug} {tier_name}").to_lowercase(); + +@@ -194,6 +204,7 @@ fn tier_color(tier_slug: &str, tier_name: &str) -> NamedColor { + } + } + ++#[cfg(not(feature = "single-threaded"))] + async fn fetch_donators_hover() -> TextComponent { + let url = "https://market.pumpkinmc.org/api/v1/rest/donators"; + let client = pumpkin_util::client_builder() +@@ -251,6 +262,7 @@ fn fetch_donators_hover_cached() -> TextComponent { return cache.data.clone(); } @@ -1166,7 +1303,7 @@ index 0045511a9..9586e5dc5 100644 let donators = tokio::runtime::Handle::try_current().map_or_else( |_| { tokio::runtime::Runtime::new().map_or_else( -@@ -260,6 +265,8 @@ fn fetch_donators_hover_cached() -> TextComponent { +@@ -260,6 +272,8 @@ fn fetch_donators_hover_cached() -> TextComponent { }, |handle| tokio::task::block_in_place(|| handle.block_on(fetch_donators_hover())), ); @@ -1176,18 +1313,20 @@ index 0045511a9..9586e5dc5 100644 if let Ok(mut guard) = DONATORS_CACHE.lock() { *guard = Some(DonatorCache { diff --git a/crates/pumpkin/src/crash.rs b/crates/pumpkin/src/crash.rs -index 6f88670e3..9969598c2 100644 +index 6f88670e..0fc4b3a1 100644 --- a/crates/pumpkin/src/crash.rs +++ b/crates/pumpkin/src/crash.rs -@@ -13,6 +13,7 @@ use pumpkin_util::text::{ +@@ -12,7 +12,9 @@ use pumpkin_util::text::{ + color::{Color, NamedColor}, }; use pumpkin_world::CURRENT_MC_VERSION; ++#[cfg(feature = "crash-report")] use rustc_hash::FxHashMap; +#[cfg(feature = "crash-report")] use sysinfo::{Cpu, System}; use time::OffsetDateTime; use tracing::error; -@@ -206,6 +207,7 @@ impl CrashReport { +@@ -206,6 +208,7 @@ impl CrashReport { rustc_version_runtime::version() ); @@ -1195,7 +1334,7 @@ index 6f88670e3..9969598c2 100644 if sysinfo::IS_SUPPORTED_SYSTEM { writeln_output!(&mut output, "\n--- System Details ---"); -@@ -243,6 +245,7 @@ impl CrashReport { +@@ -243,6 +246,7 @@ impl CrashReport { output } @@ -1204,7 +1343,7 @@ index 6f88670e3..9969598c2 100644 writeln_output!(output); let cpus = sys.cpus(); diff --git a/crates/pumpkin/src/data/datapack/mod.rs b/crates/pumpkin/src/data/datapack/mod.rs -index 2f8f72349..eefa123d0 100644 +index 2f8f7234..eefa123d 100644 --- a/crates/pumpkin/src/data/datapack/mod.rs +++ b/crates/pumpkin/src/data/datapack/mod.rs @@ -253,7 +253,7 @@ impl DatapackManager { @@ -1217,7 +1356,7 @@ index 2f8f72349..eefa123d0 100644 format!("Failed to open structure '{display_path}': {error}") })?; diff --git a/crates/pumpkin/src/data/player_server.rs b/crates/pumpkin/src/data/player_server.rs -index 0899fa9c1..733e66780 100644 +index 0899fa9c..733e6678 100644 --- a/crates/pumpkin/src/data/player_server.rs +++ b/crates/pumpkin/src/data/player_server.rs @@ -87,7 +87,7 @@ impl ServerPlayerData { @@ -1230,7 +1369,7 @@ index 0899fa9c1..733e66780 100644 if let Err(e) = storage.save_player_data(&uuid, nbt) { error!("Failed to save player data for {uuid}: {e}"); diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs -index 7d64f9b1c..ca28e83b4 100644 +index 7d64f9b1..ca28e83b 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -2361,7 +2361,7 @@ impl Entity { @@ -1243,44 +1382,88 @@ index 7d64f9b1c..ca28e83b4 100644 let Some(entity_arc) = world_clone.get_entity_by_id(entity_id) else { return; diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs -index a19825bc2..45d6783e8 100644 +index a19825bc..ec620557 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs -@@ -572,6 +572,7 @@ impl Player { - .and_then(|m| m.model.as_deref()) - .is_some_and(|model| model == "slim"); +@@ -523,26 +523,33 @@ pub struct Player { + pub inbound_packets: SegQueue, + } -+ #[cfg(not(feature = "single-threaded"))] - let bytes = if let Ok(handle) = tokio::runtime::Handle::try_current() { - tokio::task::block_in_place(|| { - handle.block_on(async { -@@ -585,6 +586,13 @@ impl Player { - client.get(&url).send().await.ok()?.bytes().await.ok() - })? - }; -+ // Emscripten: no sync-blocking HTTP from the single-thread runtime; skip the -+ // skin fetch (players still join, just without a fetched skin texture). -+ #[cfg(feature = "single-threaded")] -+ let bytes: Vec = { -+ let _ = &url; -+ return None; -+ }; - let img = image::load_from_memory(&bytes).ok()?; ++#[cfg(not(feature = "single-threaded"))] + use base64::prelude::*; + use pumpkin_protocol::Property; ++#[cfg(not(feature = "single-threaded"))] + use serde::Deserialize; - let width = img.width(); -@@ -2583,6 +2591,7 @@ impl Player { - sender.prepare_batch(&world.level, player_chunk, epoch, version) - }); + // Bit masks for the Java skin pixels that Bedrock requires to be opaque. + // Adapted from Geyser's SkinProvider under the MIT License. ++#[cfg(not(feature = "single-threaded"))] + const SKIN_OPAQUE_MASK: &str = "AP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAADwD/D/D/8AAPAP8P8P/wAA8A/w/w//AADwD/D/D/8AAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwD/APAAAAAPAP8A8AAAAA8A/wDwAAAADwD/APAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAA="; ++#[cfg(not(feature = "single-threaded"))] + const LEGACY_SKIN_OPAQUE_MASK: &str = "AP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAADwD/D/D/APAPAP8P8P8A8A8A/w/w/wDwDwD/D/D/APAP////////8A/////////wD/////////AP////////8A/////////wD/////////AP////////8A/////////wD/////////AP////////8A/////////wD/////////AA=="; ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct TexturesProperty { + textures: Textures, + } + ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct Textures { + #[serde(rename = "SKIN")] + skin: Option, + } + ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct SkinTexture { + url: String, +@@ -550,6 +557,7 @@ struct SkinTexture { + metadata: Option, + } + ++#[cfg(not(feature = "single-threaded"))] + #[derive(Deserialize)] + struct SkinMetadata { + #[serde(default)] +@@ -557,6 +565,14 @@ struct SkinMetadata { + } + + impl Player { ++ #[cfg(feature = "single-threaded")] ++ #[must_use] ++ pub fn fetch_skin(_properties: &[Property]) -> Option { ++ // A synchronous skin fetch cannot block the hosted runtime. ++ None ++ } + - let total_sent_chunks = prepared_batch.map_or_else( - || { - self.chunk_sender ++ #[cfg(not(feature = "single-threaded"))] + #[must_use] + pub fn fetch_skin(properties: &[Property]) -> Option { + let textures_prop = properties.iter().find(|p| &*p.name == "textures")?; +@@ -2590,10 +2606,13 @@ impl Player { + .map_or(0, |s| s.sent_chunks_count()) + }, + |batch| match self.client.as_ref() { +- ClientPlatform::Java(_) => { ++ ClientPlatform::Java(java) => { + let mut per_player_cache = rustc_hash::FxHashMap::default(); +- let encoded = +- crate::net::ChunkSender::encode_batch(&batch, &mut per_player_cache); ++ let encoded = crate::net::ChunkSender::encode_batch_limited( ++ &batch, ++ &mut per_player_cache, ++ java.available_chunk_bytes(), ++ ); + let current_epoch = self.chunk_send_epoch.load(Ordering::Relaxed); + self.chunk_sender.try_lock().map_or(0, |mut sender| { + sender.commit_batch(&batch, &encoded, &self.client, current_epoch); diff --git a/crates/pumpkin/src/lib.rs b/crates/pumpkin/src/lib.rs -index 7110bc9e4..3438afb65 100644 +index 7110bc9e..e7e9ecb4 100644 --- a/crates/pumpkin/src/lib.rs +++ b/crates/pumpkin/src/lib.rs -@@ -8,12 +8,14 @@ extern crate pumpkin_macros; +@@ -8,29 +8,37 @@ extern crate pumpkin_macros; use crate::crash::CrashReport; use crate::data::VanillaData; @@ -1299,7 +1482,13 @@ index 7110bc9e4..3438afb65 100644 status::{IceSocket, StatusResponder}, }; use crate::net::java::JavaClient; -@@ -26,8 +28,11 @@ use plugin::server::server_load::{LoadType, ServerLoadEvent}; + use crate::net::java::pending::PendingConnection; + use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult, PacketRateLimiter}; + use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer}; ++#[cfg(feature = "console")] + use crate::plugin::server::server_command::ServerCommandEvent; + use crate::server::{Server, ticker::Ticker}; + use plugin::server::server_load::{LoadType, ServerLoadEvent}; use pumpkin_config::{AdvancedConfiguration, BasicConfiguration}; use pumpkin_util::text::TextComponent; use pumpkin_util::text::color::{Color, NamedColor}; @@ -1310,8 +1499,14 @@ index 7110bc9e4..3438afb65 100644 +#[cfg(feature = "console")] use rustyline::{Config, error::ReadlineError}; use std::collections::HashMap; - use std::io::{ErrorKind, IsTerminal, stdin}; -@@ -112,6 +117,7 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { +-use std::io::{ErrorKind, IsTerminal, stdin}; ++use std::io::ErrorKind; ++#[cfg(feature = "console")] ++use std::io::{IsTerminal, stdin}; + use std::process::exit; + use std::str::FromStr; + use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +@@ -112,6 +120,7 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { } }; @@ -1319,7 +1514,7 @@ index 7110bc9e4..3438afb65 100644 let (logger, rl): ( ConsoleWriter, Option>, -@@ -142,6 +148,8 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { +@@ -142,6 +151,8 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { } else { (ConsoleWriter::new(None), None) }; @@ -1328,7 +1523,7 @@ index 7110bc9e4..3438afb65 100644 let fmt_layer = fmt::layer() .with_writer(std::sync::Mutex::new(logger)) -@@ -194,7 +202,11 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { +@@ -194,7 +205,11 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { timestamp: advanced_config.logging.timestamp, }; @@ -1341,7 +1536,66 @@ index 7110bc9e4..3438afb65 100644 }); assert!( -@@ -251,7 +263,7 @@ impl PumpkinServer { +@@ -204,16 +219,57 @@ pub fn init_logger(advanced_config: &AdvancedConfiguration) { + } + + pub static SHOULD_STOP: AtomicBool = AtomicBool::new(false); +-pub static STOP_INTERRUPT: LazyLock = LazyLock::new(CancellationToken::new); ++pub static STOP_INTERRUPT: StopSignal = StopSignal(std::sync::RwLock::new(None)); + pub static SERVER_IS_STOPPING: AtomicBool = AtomicBool::new(false); + pub static CRASH_REPORT: OnceLock = OnceLock::new(); + pub static SERVER_EXIT_CODE: AtomicI32 = AtomicI32::new(0); + ++/// The process-wide stop token, replaceable so an embedding can start another ++/// server in the same process after one has stopped. ++pub struct StopSignal(std::sync::RwLock>); ++ ++impl StopSignal { ++ fn token(&self) -> CancellationToken { ++ if let Some(token) = self.0.read().unwrap_or_else(|e| e.into_inner()).as_ref() { ++ return token.clone(); ++ } ++ self.0 ++ .write() ++ .unwrap_or_else(|e| e.into_inner()) ++ .get_or_insert_with(CancellationToken::new) ++ .clone() ++ } ++ ++ pub fn cancel(&self) { ++ self.token().cancel(); ++ } ++ ++ pub fn is_cancelled(&self) -> bool { ++ self.token().is_cancelled() ++ } ++ ++ pub fn cancelled(&self) -> tokio_util::sync::WaitForCancellationFutureOwned { ++ self.token().cancelled_owned() ++ } ++ ++ #[allow(clippy::should_implement_trait)] ++ pub fn clone(&self) -> CancellationToken { ++ self.token() ++ } ++} ++ + pub fn stop_server() { + SHOULD_STOP.store(true, Ordering::Relaxed); + STOP_INTERRUPT.cancel(); + } + ++/// Clears the stop state so `PumpkinServer::start` can run again in this process. ++pub fn reset_stop() { ++ SHOULD_STOP.store(false, Ordering::Relaxed); ++ SERVER_IS_STOPPING.store(false, Ordering::Release); ++ *STOP_INTERRUPT.0.write().unwrap_or_else(|e| e.into_inner()) = Some(CancellationToken::new()); ++} ++ + pub fn stop_or_exit_server() { + if SERVER_IS_STOPPING.load(Ordering::Acquire) { + // Server is already stopping, so we forcefully exit. +@@ -251,7 +307,7 @@ impl PumpkinServer { ) -> Self { let server = Server::new(basic_config, advanced_config, vanilla_data).await; @@ -1350,7 +1604,7 @@ index 7110bc9e4..3438afb65 100644 adjust_file_descriptor_limit(); let rcon = server.advanced_config.networking.rcon.clone(); -@@ -320,19 +332,30 @@ impl PumpkinServer { +@@ -320,19 +376,30 @@ impl PumpkinServer { None }; @@ -1383,7 +1637,7 @@ index 7110bc9e4..3438afb65 100644 let (bedrock_status, ice_socket) = Self::bind_bedrock_status(&server).await; let nethernet_listener = Self::bind_nethernet(&server, ice_socket).await; -@@ -345,6 +368,15 @@ impl PumpkinServer { +@@ -345,6 +412,15 @@ impl PumpkinServer { } } @@ -1399,7 +1653,7 @@ index 7110bc9e4..3438afb65 100644 async fn bind_nethernet( server: &Arc, ice_socket: Option, -@@ -441,6 +473,7 @@ impl PumpkinServer { +@@ -441,6 +517,7 @@ impl PumpkinServer { } pub async fn start(&self) { @@ -1407,7 +1661,7 @@ index 7110bc9e4..3438afb65 100644 if self.server.advanced_config.commands.use_console && let Some((wrapper, _, _)) = LOGGER_IMPL.wait() { -@@ -527,6 +560,7 @@ impl PumpkinServer { +@@ -527,6 +604,7 @@ impl PumpkinServer { info!("Completed save!"); @@ -1415,141 +1669,7 @@ index 7110bc9e4..3438afb65 100644 if let Some((wrapper, _, _)) = LOGGER_IMPL.wait() && let Some(rl) = wrapper.take_readline() { -@@ -534,6 +568,72 @@ impl PumpkinServer { - } - } - -+ /// Run the normal Java client lifecycle for a host-provided byte stream. -+ pub async fn serve_connection(&self, connection: S, client_addr: SocketAddr, client_id: u64) -+ where -+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static, -+ { -+ let server_clone = self.server.clone(); -+ let packet_limiter = PacketRateLimiter::from_config( -+ &server_clone.advanced_config.networking.java.packet_limiter, -+ ); -+ let mut pending = -+ PendingConnection::from_stream(connection, client_addr, client_id, packet_limiter); -+ let login_result = pending.handle_login_sequence(&server_clone).await; -+ -+ match login_result { -+ PacketHandlerResult::Stop => { -+ pending.close(); -+ } -+ PacketHandlerResult::ReadyToPlay(profile, config) => { -+ let mut java_client = -+ JavaClient::from_pending(pending, profile.clone(), config.clone()); -+ java_client.start_outgoing_packet_task(); -+ -+ if let Some((player, world)) = server_clone.add_player( -+ Arc::new(ClientPlatform::Java(java_client)), -+ profile, -+ Some(config), -+ ) { -+ if let ClientPlatform::Java(client) = player.client.as_ref() { -+ client.set_player(player.clone()); -+ } -+ world -+ .spawn_java_player(&server_clone.basic_config, &player, &server_clone) -+ .await; -+ -+ if let ClientPlatform::Java(client) = player.client.as_ref() { -+ client.progress_player_packets(&player, &server_clone).await; -+ -+ // Close when done -+ client.close(); -+ client.await_tasks().await; -+ } -+ player.remove().await; -+ server_clone.remove_player(&player); -+ if let Err(e) = server_clone -+ .player_data_storage -+ .handle_player_leave(&player) -+ { -+ error!("Failed to save player data on disconnect: {e}"); -+ } -+ if let Err(e) = server_clone.advancement_manager.save_player(&player).await { -+ error!("Failed to save player advancement on disconnect: {e}"); -+ } -+ } -+ } -+ } -+ } -+ -+ fn clone_for_connection(&self) -> Self { -+ Self { -+ server: self.server.clone(), -+ tcp_listener: None, -+ bedrock_status: None, -+ nethernet_listener: None, -+ } -+ } -+ - #[allow(clippy::too_many_lines)] - pub async fn unified_listener_task( - &self, -@@ -559,66 +659,13 @@ impl PumpkinServer { - format!("{client_addr}") - }; - debug!("Accepted connection from Java Edition: {formatted_address} (id {client_id})"); -- let server_clone = self.server.clone(); -- -+ let server = self.clone_for_connection(); - tasks.spawn(async move { -- let packet_limiter = PacketRateLimiter::from_config( -- &server_clone.advanced_config.networking.java.packet_limiter, -- ); -- let mut pending = PendingConnection::new( -- connection, -- client_addr, -- client_id, -- packet_limiter, -- ); -- let login_result = pending.handle_login_sequence(&server_clone).await; -- -- match login_result { -- PacketHandlerResult::Stop => { -- pending.close(); -- }, -- PacketHandlerResult::ReadyToPlay(profile, config) => { -- let mut java_client = JavaClient::from_pending(pending, profile.clone(), config.clone()); -- java_client.start_outgoing_packet_task(); -- -- if let Some((player, world)) = server_clone -- .add_player(Arc::new(ClientPlatform::Java(java_client)), profile, Some(config)) -- { -- -- if let ClientPlatform::Java(client) = player.client.as_ref() { -- client.set_player(player.clone()); -- } -- world -- .spawn_java_player(&server_clone.basic_config, &player, &server_clone) -- .await; -- -- if let ClientPlatform::Java(client) = player.client.as_ref() { -- client.progress_player_packets(&player, &server_clone).await; -- -- // Close when done -- client.close(); -- client.await_tasks().await; -- } -- player.remove().await; -- server_clone.remove_player(&player); -- if let Err(e) = server_clone -- .player_data_storage -- .handle_player_leave(&player) -- { -- error!("Failed to save player data on disconnect: {e}"); -- } -- if let Err(e) = server_clone.advancement_manager -- .save_player(&player) -- .await { -- error!("Failed to save player advancement on disconnect: {e}"); -- } -- } -- }, -- } -+ server.serve_connection(connection, client_addr, client_id).await; +@@ -618,7 +696,7 @@ impl PumpkinServer { }); } Err(e) => { @@ -1558,7 +1678,7 @@ index 7110bc9e4..3438afb65 100644 if e.raw_os_error() == Some(libc::EMFILE) { error!( "Too many open files! Server reached file descriptor limit. \ -@@ -719,6 +766,7 @@ impl PumpkinServer { +@@ -719,6 +797,7 @@ impl PumpkinServer { } } @@ -1566,7 +1686,7 @@ index 7110bc9e4..3438afb65 100644 fn setup_stdin_console(server: &Arc) { let (tx, mut rx) = tokio::sync::mpsc::channel(1); let rt = tokio::runtime::Handle::current(); -@@ -759,6 +807,7 @@ fn setup_stdin_console(server: &Arc) { +@@ -759,6 +838,7 @@ fn setup_stdin_console(server: &Arc) { }); } @@ -1574,7 +1694,7 @@ index 7110bc9e4..3438afb65 100644 fn setup_console(mut rl: Editor, server: Arc) { let (tx, mut rx) = tokio::sync::mpsc::channel(1); let (tx_reply, mut rx_reply) = tokio::sync::mpsc::channel(1); -@@ -839,7 +888,7 @@ fn scrub_address(ip: &str) -> String { +@@ -839,7 +919,7 @@ fn scrub_address(ip: &str) -> String { .collect() } @@ -1584,10 +1704,10 @@ index 7110bc9e4..3438afb65 100644 let mut rlim = libc::rlimit { rlim_cur: 0, diff --git a/crates/pumpkin/src/logging.rs b/crates/pumpkin/src/logging.rs -index 85b51a45a..4006f6eca 100644 +index 85b51a45..033870ff 100644 --- a/crates/pumpkin/src/logging.rs +++ b/crates/pumpkin/src/logging.rs -@@ -2,11 +2,17 @@ +@@ -2,24 +2,35 @@ #![allow(clippy::print_stdout)] use flate2::write::GzEncoder; @@ -1603,9 +1723,27 @@ index 85b51a45a..4006f6eca 100644 use rustyline::validate::Validator; +#[cfg(feature = "console")] use rustyline::{Editor, Helper}; ++#[cfg(feature = "console")] use std::borrow::Cow; use std::fs::File; -@@ -85,15 +91,18 @@ const MAX_ATTEMPTS: u32 = 1000; + use std::io::{self, BufWriter, Write}; + use std::path::PathBuf; ++#[cfg(feature = "console")] + use std::sync::Arc; + use time::{Duration, OffsetDateTime}; + use tracing::Subscriber; + use tracing_subscriber::Layer; + use tracing_subscriber::filter::LevelFilter; + ++#[cfg(feature = "console")] + use crate::command::CommandSender; ++#[cfg(feature = "console")] + use crate::command::string_reader::StringReader; ++#[cfg(feature = "console")] + use crate::server::Server; + + #[macro_export] +@@ -85,15 +96,19 @@ const MAX_ATTEMPTS: u32 = 1000; /// A wrapper for our logger to hold the terminal input while no input is expected in order to /// properly flush logs to the output while they happen instead of batched pub struct ReadlineLogWrapper { @@ -1616,6 +1754,7 @@ index 85b51a45a..4006f6eca 100644 pub struct ConsoleWriter { + #[cfg(feature = "console")] printer: Option>, ++ #[cfg(feature = "console")] buffer: Vec, } @@ -1624,7 +1763,7 @@ index 85b51a45a..4006f6eca 100644 #[must_use] pub fn new(printer: Option>) -> Self { Self { -@@ -101,9 +110,16 @@ impl ConsoleWriter { +@@ -101,9 +116,16 @@ impl ConsoleWriter { buffer: Vec::new(), } } @@ -1632,7 +1771,7 @@ index 85b51a45a..4006f6eca 100644 + #[cfg(not(feature = "console"))] + #[must_use] + pub fn new() -> Self { -+ Self { buffer: Vec::new() } ++ Self {} + } } @@ -1641,7 +1780,7 @@ index 85b51a45a..4006f6eca 100644 fn write(&mut self, buf: &[u8]) -> io::Result { if let Some(ref mut printer) = self.printer { self.buffer.extend_from_slice(buf); -@@ -122,6 +138,12 @@ impl Write for ConsoleWriter { +@@ -122,6 +144,12 @@ impl Write for ConsoleWriter { } } @@ -1654,7 +1793,7 @@ index 85b51a45a..4006f6eca 100644 fn flush(&mut self) -> io::Result<()> { if let Some(ref mut printer) = self.printer { if !self.buffer.is_empty() { -@@ -138,6 +160,11 @@ impl Write for ConsoleWriter { +@@ -138,6 +166,11 @@ impl Write for ConsoleWriter { io::stdout().flush() } } @@ -1666,7 +1805,7 @@ index 85b51a45a..4006f6eca 100644 } struct GzipRollingLoggerData { -@@ -362,6 +389,7 @@ impl tracing::field::Visit for StringVisitor { +@@ -362,6 +395,7 @@ impl tracing::field::Visit for StringVisitor { } impl ReadlineLogWrapper { @@ -1674,7 +1813,7 @@ index 85b51a45a..4006f6eca 100644 #[must_use] pub const fn new(rl: Option>) -> Self { Self { -@@ -369,6 +397,13 @@ impl ReadlineLogWrapper { +@@ -369,6 +403,13 @@ impl ReadlineLogWrapper { } } @@ -1688,7 +1827,7 @@ index 85b51a45a..4006f6eca 100644 pub fn take_readline(&self) -> Option> { self.readline .lock() -@@ -377,6 +412,7 @@ impl ReadlineLogWrapper { +@@ -377,6 +418,7 @@ impl ReadlineLogWrapper { // This isn't really dead code. It is just only used by the lib and not the bin for this // crate, and as such creates a compiler warning. @@ -1696,7 +1835,7 @@ index 85b51a45a..4006f6eca 100644 #[allow(dead_code)] pub fn return_readline(&self, rl: Editor) { if let Ok(mut result) = self.readline.lock() { -@@ -385,12 +421,14 @@ impl ReadlineLogWrapper { +@@ -385,12 +427,14 @@ impl ReadlineLogWrapper { } } @@ -1711,7 +1850,7 @@ index 85b51a45a..4006f6eca 100644 impl PumpkinCommandCompleter { #[must_use] pub fn new() -> Self { -@@ -401,7 +439,9 @@ impl PumpkinCommandCompleter { +@@ -401,7 +445,9 @@ impl PumpkinCommandCompleter { } } @@ -1721,7 +1860,7 @@ index 85b51a45a..4006f6eca 100644 impl Highlighter for PumpkinCommandCompleter { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { line.find(' ').map_or_else( -@@ -417,6 +457,7 @@ impl Highlighter for PumpkinCommandCompleter { +@@ -417,6 +463,7 @@ impl Highlighter for PumpkinCommandCompleter { Cow::Owned(format!("\x1b[90m{hint}\x1b[0m")) } } @@ -1729,7 +1868,7 @@ index 85b51a45a..4006f6eca 100644 impl Hinter for PumpkinCommandCompleter { type Hint = String; fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { -@@ -440,8 +481,10 @@ impl Hinter for PumpkinCommandCompleter { +@@ -440,8 +487,10 @@ impl Hinter for PumpkinCommandCompleter { } } @@ -1741,7 +1880,7 @@ index 85b51a45a..4006f6eca 100644 type Candidate = String; diff --git a/crates/pumpkin/src/net/authentication.rs b/crates/pumpkin/src/net/authentication.rs -index f344d1978..b32c3bc2a 100644 +index f344d197..b32c3bc2 100644 --- a/crates/pumpkin/src/net/authentication.rs +++ b/crates/pumpkin/src/net/authentication.rs @@ -311,6 +311,7 @@ pub async fn lookup_profile_by_name( @@ -1771,7 +1910,7 @@ index f344d1978..b32c3bc2a 100644 uuid: Uuid, auth_config: &AuthenticationConfig, diff --git a/crates/pumpkin/src/net/bedrock/mod.rs b/crates/pumpkin/src/net/bedrock/mod.rs -index 5ea3e41ac..fd913a72b 100644 +index 5ea3e41a..fd913a72 100644 --- a/crates/pumpkin/src/net/bedrock/mod.rs +++ b/crates/pumpkin/src/net/bedrock/mod.rs @@ -1,3 +1,7 @@ @@ -1782,8 +1921,418 @@ index 5ea3e41ac..fd913a72b 100644 pub mod nethernet; pub mod play; pub mod status; +diff --git a/crates/pumpkin/src/net/bedrock/nethernet_stub.rs b/crates/pumpkin/src/net/bedrock/nethernet_stub.rs +new file mode 100644 +index 00000000..90f0989c +--- /dev/null ++++ b/crates/pumpkin/src/net/bedrock/nethernet_stub.rs +@@ -0,0 +1,56 @@ ++//! WebRTC-free stub of the NetherNet transport, compiled when the `nethernet` ++//! feature is off (e.g. the emscripten/embedded Java-only build). The types ++//! exist so the always-compiled Bedrock client code type-checks, but no Bedrock ++//! connection is ever created (the listener is never bound), so the method ++//! bodies are unreachable. ++use std::net::SocketAddr; ++use std::sync::Arc; ++ ++use bytes::Bytes; ++use pumpkin_util::p384::PublicKey; ++ ++pub type IncomingSession = (Arc, SocketAddr); ++ ++/// Stub listener: never bound when `nethernet` is off, so `accept` never yields. ++pub struct NetherNetListener { ++ _private: (), ++} ++ ++impl NetherNetListener { ++ pub async fn accept(&self) -> Option { ++ None ++ } ++ ++ pub const fn local_addr(&self) -> SocketAddr { ++ SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0) ++ } ++} ++ ++/// Stub session. Never constructed when `nethernet` is off. ++pub struct NetherNetSession { ++ _private: (), ++} ++ ++impl NetherNetSession { ++ pub async fn recv(&self) -> Option { ++ None ++ } ++ ++ pub async fn send(&self, _data: Bytes) -> Result<(), String> { ++ Err("NetherNet transport is disabled in this build".to_string()) ++ } ++ ++ pub async fn send_unreliable(&self, _data: Bytes) -> Result<(), String> { ++ Err("NetherNet transport is disabled in this build".to_string()) ++ } ++ ++ pub const fn is_closed(&self) -> bool { ++ true ++ } ++ ++ pub async fn close(&self) {} ++ ++ pub const fn client_public_key(&self) -> Option<&PublicKey> { ++ None ++ } ++} +diff --git a/crates/pumpkin/src/net/chunk_sender.rs b/crates/pumpkin/src/net/chunk_sender.rs +index efaa2a65..c2e4832b 100644 +--- a/crates/pumpkin/src/net/chunk_sender.rs ++++ b/crates/pumpkin/src/net/chunk_sender.rs +@@ -1,4 +1,5 @@ + use bytes::Bytes; ++#[cfg(not(feature = "single-threaded"))] + use rayon::prelude::*; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::sync::{Arc, Weak}; +@@ -15,6 +16,7 @@ use pumpkin_world::chunk::ChunkData; + use pumpkin_world::level::{Level, SyncChunk}; + + use crate::net::ClientPlatform; ++use crate::net::java::MAX_CHUNK_QUEUE_BYTES; + + const MIN_CHUNKS_PER_TICK: f32 = 0.1; + const MAX_CHUNKS_PER_TICK: f32 = 500.0; +@@ -188,65 +190,91 @@ impl ChunkSender { + batch: &PreparedBatch, + cache: &mut FxHashMap, EncodedChunk>, + ) -> Vec { ++ Self::encode_batch_limited(batch, cache, usize::MAX) ++ } ++ ++ pub fn encode_batch_limited( ++ batch: &PreparedBatch, ++ cache: &mut FxHashMap, EncodedChunk>, ++ byte_limit: usize, ++ ) -> Vec { ++ if byte_limit <= 16 { ++ return Vec::new(); ++ } + let version = batch.target_version; + let cached_map = &*cache; + +- let encoded_results: Vec> = batch +- .chunks +- .par_iter() +- .map(|candidate| { +- let pos = candidate.position; +- if let Some(cached) = cached_map.get(&pos) +- && cached.is_fresh_for(candidate) +- { +- return Some(cached.clone()); +- } ++ let encode = |candidate: &PreparedChunk| { ++ let pos = candidate.position; ++ if let Some(cached) = cached_map.get(&pos) ++ && cached.is_fresh_for(candidate) ++ { ++ return Some(cached.clone()); ++ } + +- let chunk = &candidate.chunk; +- let mut chunk_buf = Vec::with_capacity(32 * 1024); +- if chunk_buf +- .write_var_int(&VarInt(CChunkData::to_id(version))) +- .is_err() +- { +- return None; +- } +- if CChunkData(chunk) +- .write_packet_data(&mut chunk_buf, &version) +- .is_err() +- { +- return None; +- } ++ let chunk = &candidate.chunk; ++ let mut chunk_buf = Vec::with_capacity(32 * 1024); ++ if chunk_buf ++ .write_var_int(&VarInt(CChunkData::to_id(version))) ++ .is_err() ++ { ++ return None; ++ } ++ if CChunkData(chunk) ++ .write_packet_data(&mut chunk_buf, &version) ++ .is_err() ++ { ++ return None; ++ } + +- let light_payload = if version >= JavaMinecraftVersion::V_1_14 +- && version < JavaMinecraftVersion::V_1_18 +- { +- CLightUpdate::from_chunk(chunk, version) +- .ok() +- .and_then(|light_packet| { +- let mut light_buf = Vec::new(); +- (light_buf +- .write_var_int(&VarInt(CLightUpdate::to_id(version))) +- .is_ok() +- && light_packet +- .write_packet_data(&mut light_buf, &version) +- .is_ok()) +- .then(|| Bytes::from(light_buf)) +- }) +- } else { +- None +- }; +- +- Some(EncodedChunk { +- position: pos, +- payload: Bytes::from(chunk_buf), +- light_payload, +- chunk_ref: Arc::downgrade(chunk), +- }) ++ let light_payload = if version >= JavaMinecraftVersion::V_1_14 ++ && version < JavaMinecraftVersion::V_1_18 ++ { ++ CLightUpdate::from_chunk(chunk, version) ++ .ok() ++ .and_then(|light_packet| { ++ let mut light_buf = Vec::new(); ++ (light_buf ++ .write_var_int(&VarInt(CLightUpdate::to_id(version))) ++ .is_ok() ++ && light_packet ++ .write_packet_data(&mut light_buf, &version) ++ .is_ok()) ++ .then(|| Bytes::from(light_buf.into_boxed_slice())) ++ }) ++ } else { ++ None ++ }; ++ ++ Some(EncodedChunk { ++ position: pos, ++ payload: Bytes::from(chunk_buf.into_boxed_slice()), ++ light_payload, ++ chunk_ref: Arc::downgrade(chunk), + }) +- .collect(); ++ }; ++ #[cfg(not(feature = "single-threaded"))] ++ let encoded_results: Vec<_> = batch.chunks.par_iter().filter_map(encode).collect(); ++ #[cfg(feature = "single-threaded")] ++ let encoded_results = { ++ let mut results = Vec::new(); ++ let mut bytes = 0usize; ++ for candidate in &batch.chunks { ++ if bytes >= byte_limit.saturating_sub(16) { ++ break; ++ } ++ if let Some(encoded) = encode(candidate) { ++ bytes = bytes ++ .saturating_add(encoded.payload.len()) ++ .saturating_add(encoded.light_payload.as_ref().map_or(0, Bytes::len)); ++ results.push(encoded); ++ } ++ } ++ results ++ }; + + let mut output = Vec::with_capacity(encoded_results.len()); +- for encoded in encoded_results.into_iter().flatten() { ++ for encoded in encoded_results { + cache.insert(encoded.position, encoded.clone()); + output.push(encoded); + } +@@ -261,47 +289,90 @@ impl ChunkSender { + client: &ClientPlatform, + current_epoch: u32, + ) -> Vec> { +- if current_epoch != batch.epoch_snapshot || encoded_chunks.is_empty() { ++ let ClientPlatform::Java(java) = client else { + return Vec::new(); +- } +- +- let mut dispatched_positions = Vec::with_capacity(encoded_chunks.len()); +- let version = batch.target_version; ++ }; ++ self.commit_java_batch( ++ batch, ++ encoded_chunks, ++ current_epoch, ++ java.available_chunk_bytes(), ++ |selected| { ++ let mut packets = Vec::with_capacity(selected.len() * 2 + 2); ++ if batch.target_version >= JavaMinecraftVersion::V_1_20_2 { ++ let Ok(start) = java.serialize_packet(&CChunkBatchStart) else { ++ return false; ++ }; ++ packets.push(start); ++ } ++ for chunk in selected { ++ packets.push(chunk.payload.clone()); ++ if let Some(light) = &chunk.light_payload { ++ packets.push(light.clone()); ++ } ++ } ++ if batch.target_version >= JavaMinecraftVersion::V_1_20_2 { ++ let Ok(end) = ++ java.serialize_packet(&CChunkBatchEnd::new(selected.len() as u16)) ++ else { ++ return false; ++ }; ++ packets.push(end); ++ } ++ java.try_enqueue_chunk_batch(packets) ++ }, ++ ) ++ } + +- if version >= JavaMinecraftVersion::V_1_20_2 +- && let ClientPlatform::Java(java_client) = client +- { +- java_client.try_send_packet(&CChunkBatchStart); ++ fn commit_java_batch( ++ &mut self, ++ batch: &PreparedBatch, ++ encoded_chunks: &[EncodedChunk], ++ current_epoch: u32, ++ available_bytes: usize, ++ enqueue: impl FnOnce(&[&EncodedChunk]) -> bool, ++ ) -> Vec> { ++ if current_epoch != batch.epoch_snapshot || available_bytes <= 16 { ++ return Vec::new(); + } +- ++ // Leave room for the batch markers. A single larger chunk may use the ++ // whole budget, but only once the previous batch has released all credit. ++ let payload_limit = available_bytes.saturating_sub(16); ++ let mut bytes = 0usize; ++ let mut selected = Vec::new(); + for chunk in encoded_chunks { + if !self.pending_chunks.contains(&chunk.position) { + continue; + } +- +- client.try_enqueue_packet(chunk.payload.clone()); +- if let Some(ref light) = chunk.light_payload { +- client.try_enqueue_packet(light.clone()); ++ let size = chunk ++ .payload ++ .len() ++ .saturating_add(chunk.light_payload.as_ref().map_or(0, Bytes::len)); ++ if bytes.saturating_add(size) > payload_limit ++ && (!selected.is_empty() || available_bytes != MAX_CHUNK_QUEUE_BYTES) ++ { ++ break; + } +- ++ bytes = bytes.saturating_add(size); ++ selected.push(chunk); ++ if bytes >= payload_limit { ++ break; ++ } ++ } ++ if selected.is_empty() || !enqueue(&selected) { ++ return Vec::new(); ++ } ++ let mut dispatched = Vec::with_capacity(selected.len()); ++ for chunk in selected { + self.pending_chunks.remove(&chunk.position); + self.sent_chunks.insert(chunk.position); +- dispatched_positions.push(chunk.position); ++ dispatched.push(chunk.position); + } +- +- let sent_count = dispatched_positions.len(); +- if sent_count > 0 { +- if version >= JavaMinecraftVersion::V_1_20_2 +- && let ClientPlatform::Java(java_client) = client +- { +- java_client.try_send_packet(&CChunkBatchEnd::new(sent_count as u16)); +- self.in_flight_batches = self.in_flight_batches.saturating_add(1); +- } +- +- self.send_quota -= sent_count as f32; ++ if batch.target_version >= JavaMinecraftVersion::V_1_20_2 { ++ self.in_flight_batches = self.in_flight_batches.saturating_add(1); + } +- +- dispatched_positions ++ self.send_quota -= dispatched.len() as f32; ++ dispatched + } + + /// Marks a prepared Bedrock batch as dispatched and returns its chunks for encoding. +@@ -343,6 +414,66 @@ impl Default for ChunkSender { + mod tests { + use super::*; + ++ #[test] ++ fn byte_backpressure_keeps_unsent_chunks_pending_for_retry() { ++ let chunks: Vec<_> = (0..3) ++ .map(|x| PreparedChunk { ++ position: Vector2::new(x, 0), ++ chunk: ChunkData::empty_sync(x, 0), ++ }) ++ .collect(); ++ let encoded: Vec<_> = chunks ++ .iter() ++ .map(|chunk| EncodedChunk { ++ position: chunk.position, ++ payload: Bytes::from(vec![0; 20]), ++ light_payload: None, ++ chunk_ref: Arc::downgrade(&chunk.chunk), ++ }) ++ .collect(); ++ let batch = PreparedBatch { ++ chunks, ++ epoch_snapshot: 7, ++ target_version: JavaMinecraftVersion::V_1_20_2, ++ }; ++ let mut sender = ChunkSender::new(); ++ sender.send_quota = 3.0; ++ for chunk in &batch.chunks { ++ sender.enqueue_chunk(chunk.position); ++ } ++ assert!( ++ sender ++ .commit_java_batch(&batch, &encoded, 7, 64, |_| false) ++ .is_empty() ++ ); ++ assert_eq!(sender.pending_chunks.len(), 3); ++ assert_eq!(sender.in_flight_batches, 0); ++ assert_eq!(sender.send_quota, 3.0); ++ let sent = sender.commit_java_batch(&batch, &encoded, 7, 64, |selected| { ++ assert_eq!(selected.len(), 2); ++ true ++ }); ++ assert_eq!(sent.len(), 2); ++ assert_eq!(sender.pending_chunks.len(), 1); ++ assert_eq!(sender.sent_chunks_count(), 2); ++ assert_eq!(sender.in_flight_batches, 1); ++ assert_eq!(sender.send_quota, 1.0); ++ assert!( ++ sender ++ .commit_java_batch(&batch, &encoded, 8, 64, |_| panic!( ++ "stale batch was queued" ++ )) ++ .is_empty() ++ ); ++ assert_eq!( ++ sender ++ .commit_java_batch(&batch, &encoded, 7, 64, |_| true) ++ .len(), ++ 1 ++ ); ++ assert!(sender.pending_chunks.is_empty()); ++ } ++ + #[test] + fn bedrock_batch_moves_pending_chunks_to_sent() { + let position = Vector2::new(3, -2); diff --git a/crates/pumpkin/src/net/java/config/known_packs.rs b/crates/pumpkin/src/net/java/config/known_packs.rs -index 7e45c3db9..8a4d5af4d 100644 +index 7e45c3db..8a4d5af4 100644 --- a/crates/pumpkin/src/net/java/config/known_packs.rs +++ b/crates/pumpkin/src/net/java/config/known_packs.rs @@ -14,7 +14,7 @@ impl JavaClient { @@ -1796,67 +2345,41 @@ index 7e45c3db9..8a4d5af4d 100644 let mut packets = Vec::new(); let mut sent_dimension_type = false; diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs -index fb0e9c901..96e3b1101 100644 +index fb0e9c90..b1e2bfdf 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs -@@ -42,7 +42,6 @@ use pumpkin_util::text::TextComponent; - use pumpkin_util::version::JavaMinecraftVersion; +@@ -43,7 +43,7 @@ use pumpkin_util::version::JavaMinecraftVersion; use tokio::{ io::{BufReader, BufWriter}, -- net::tcp::{OwnedReadHalf, OwnedWriteHalf}, - sync::oneshot, + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, +- sync::oneshot, ++ sync::{OwnedSemaphorePermit, Semaphore, oneshot}, }; use tokio::{ -@@ -53,6 +52,9 @@ use tokio_util::sync::CancellationToken; - use tokio_util::task::TaskTracker; - use tracing::{debug, error, warn}; - -+pub type ConnectionReader = Box; -+pub type ConnectionWriter = Box; -+ - pub mod config; - pub mod handshake; - pub mod login; -@@ -103,9 +105,9 @@ pub struct JavaClient { + sync::mpsc::{Receiver, Sender, error::TryRecvError}, +@@ -102,6 +102,9 @@ pub struct JavaClient { + outgoing_packet_priority_send: Sender, /// A high-priority queue of serialized packets to send to the network. outgoing_packet_priority_recv: Option>, ++ // Keep chunk-batch start/end markers ordered when a batch yields between groups. ++ chunk_send_lock: tokio::sync::Mutex<()>, ++ chunk_queue_budget: Arc, /// The packet encoder for outgoing packets. -- network_writer: std::sync::Mutex>>>, -+ network_writer: std::sync::Mutex>>>, + network_writer: std::sync::Mutex>>>, /// The packet decoder for incoming packets. -- network_reader: std::sync::Mutex>>>, -+ network_reader: std::sync::Mutex>>>, - /// Keep Alive: - /// - /// Whether we are waiting for a response after sending a keep alive packet. -@@ -155,10 +157,10 @@ fn take_frame_batch(packets: &mut VecDeque) -> Vec>, ++ _chunk_budget: Option>, } - fn frame_packet_batch( -- mut writer: TCPNetworkEncoder>, -+ mut writer: TCPNetworkEncoder>, - batch: &[OutgoingPacket], - ) -> ( -- TCPNetworkEncoder>, -+ TCPNetworkEncoder>, - Vec, - Option, - ) { -@@ -174,11 +176,11 @@ fn frame_packet_batch( - } ++pub(crate) const MAX_CHUNK_QUEUE_BYTES: usize = 2 * 1024 * 1024; ++ + const MAX_FRAME_BATCH_DATA_SIZE: usize = MAX_PACKET_SIZE as usize; - async fn frame_batch_maybe_offload( -- writer: TCPNetworkEncoder>, -+ writer: TCPNetworkEncoder>, - packet_batch: Vec, - ) -> Result< - ( -- TCPNetworkEncoder>, -+ TCPNetworkEncoder>, - Vec, - Vec, - Option, -@@ -190,7 +192,7 @@ async fn frame_batch_maybe_offload( + fn take_frame_batch(packets: &mut VecDeque) -> Vec { +@@ -190,7 +196,7 @@ async fn frame_batch_maybe_offload( .any(|packet| writer.is_compressing_packet(&packet.data)); if needs_offload { @@ -1865,86 +2388,347 @@ index fb0e9c901..96e3b1101 100644 let (writer, frame, frame_err) = frame_packet_batch(writer, &packet_batch); (writer, packet_batch, frame, frame_err) }) -@@ -406,7 +408,7 @@ impl JavaClient { +@@ -206,6 +212,7 @@ impl OutgoingPacket { + Self { + data, + completion: None, ++ _chunk_budget: None, + } + } + +@@ -213,10 +220,44 @@ impl OutgoingPacket { + Self { + data, + completion: Some(completion), ++ _chunk_budget: None, + } + } + } + ++// Reserve the complete batch before publishing its start marker. Shared byte ++// credit remains held until every packet in the batch has left the writer. ++fn try_enqueue_chunk_packets( ++ sender: &Sender, ++ budget: &Arc, ++ limit: usize, ++ packets: Vec, ++) -> bool { ++ if packets.is_empty() { ++ return true; ++ } ++ let bytes = packets ++ .iter() ++ .fold(0usize, |total, packet| total.saturating_add(packet.len())); ++ // A single valid chunk larger than the target budget must still make progress. ++ let Ok(credit) = budget ++ .clone() ++ .try_acquire_many_owned(bytes.min(limit) as u32) ++ else { ++ return false; ++ }; ++ let Ok(slots) = sender.try_reserve_many(packets.len()) else { ++ return false; ++ }; ++ let credit = Arc::new(credit); ++ for (slot, data) in slots.zip(packets) { ++ let mut packet = OutgoingPacket::normal(data); ++ packet._chunk_budget = Some(credit.clone()); ++ slot.send(packet); ++ } ++ true ++} ++ + impl JavaClient { + #[must_use] + pub fn from_pending( +@@ -242,6 +283,8 @@ impl JavaClient { + outgoing_packet_priority_send: priority_send, + outgoing_packet_priority_recv: Some(priority_recv), + version: pending.version, ++ chunk_send_lock: tokio::sync::Mutex::new(()), ++ chunk_queue_budget: Arc::new(Semaphore::new(MAX_CHUNK_QUEUE_BYTES)), + network_writer: std::sync::Mutex::new(Some(pending.network_writer)), + network_reader: std::sync::Mutex::new(Some(pending.network_reader)), + brand: ArcSwap::from_pointee(pending.brand), +@@ -382,6 +425,48 @@ impl JavaClient { + } + } + ++ pub fn available_chunk_bytes(&self) -> usize { ++ if self.close_token.is_cancelled() || self.chunk_send_lock.try_lock().is_err() { ++ return 0; ++ } ++ self.chunk_queue_budget.available_permits() ++ } ++ ++ pub fn try_enqueue_chunk_batch(&self, packets: Vec) -> bool { ++ let Ok(_batch) = self.chunk_send_lock.try_lock() else { ++ return false; ++ }; ++ if self.close_token.is_cancelled() { ++ return false; ++ } ++ try_enqueue_chunk_packets( ++ &self.outgoing_packet_queue_send, ++ &self.chunk_queue_budget, ++ MAX_CHUNK_QUEUE_BYTES, ++ packets, ++ ) ++ } ++ ++ async fn enqueue_chunk_packet(&self, data: Bytes) { ++ let credit = tokio::select! { ++ biased; ++ () = self.close_token.cancelled() => return, ++ credit = self.chunk_queue_budget.clone().acquire_many_owned(data.len().min(MAX_CHUNK_QUEUE_BYTES) as u32) => credit, ++ }; ++ let Ok(credit) = credit else { ++ return; ++ }; ++ let mut packet = OutgoingPacket::normal(data); ++ packet._chunk_budget = Some(Arc::new(credit)); ++ tokio::select! { ++ biased; ++ () = self.close_token.cancelled() => {}, ++ result = self.outgoing_packet_queue_send.send(packet) => { ++ if result.is_err() { self.close(); } ++ } ++ } ++ } ++ + pub async fn send_chunks(&self, chunks: &[SyncChunk]) { + let player = self.player.load_full(); + let Some(player) = player.as_ref() else { +@@ -405,69 +490,78 @@ impl JavaClient { + } let version = self.version.load(); - let (tx, rx) = oneshot::channel(); +- let (tx, rx) = oneshot::channel(); - rayon::spawn(move || { -+ pumpkin_util::rayon_spawn(move || { - let mut serialized = Vec::with_capacity(valid_chunks.len()); - for chunk in valid_chunks { - let mut buf = Vec::with_capacity(32 * 1024); -@@ -532,7 +534,7 @@ impl JavaClient { +- let mut serialized = Vec::with_capacity(valid_chunks.len()); +- for chunk in valid_chunks { +- let mut buf = Vec::with_capacity(32 * 1024); +- if let Err(err) = buf.write_var_int(&VarInt(CChunkData::to_id(version))) { +- error!("Failed to write chunk data id: {err:?}"); +- continue; +- } +- if let Err(err) = CChunkData(&chunk).write_packet_data(&mut buf, &version) { +- error!("Failed to write chunk data: {err:?}"); +- continue; +- } ++ let _batch_guard = self.chunk_send_lock.lock().await; ++ if self.close_token.is_cancelled() { ++ return; ++ } ++ if version >= JavaMinecraftVersion::V_1_20_2 { ++ self.send_packet(&CChunkBatchStart).await; ++ } ++ // Bound temporary serialization storage independently of the player's view. ++ for group in valid_chunks.chunks(8) { ++ if self.close_token.is_cancelled() { ++ return; ++ } ++ let valid_chunks = group.to_vec(); ++ let (tx, rx) = oneshot::channel(); ++ pumpkin_util::rayon_spawn(move || { ++ let mut serialized = Vec::with_capacity(valid_chunks.len()); ++ for chunk in valid_chunks { ++ let mut buf = Vec::with_capacity(32 * 1024); ++ if let Err(err) = buf.write_var_int(&VarInt(CChunkData::to_id(version))) { ++ error!("Failed to write chunk data id: {err:?}"); ++ continue; ++ } ++ if let Err(err) = CChunkData(&chunk).write_packet_data(&mut buf, &version) { ++ error!("Failed to write chunk data: {err:?}"); ++ continue; ++ } - pub async fn get_packet_with_reader( - &self, -- network_reader: &mut TCPNetworkDecoder>, -+ network_reader: &mut TCPNetworkDecoder>, - ) -> Option { - tokio::select! { - () = self.await_close_interrupt() => { -diff --git a/crates/pumpkin/src/net/java/pending.rs b/crates/pumpkin/src/net/java/pending.rs -index 2522573e0..a627b07b8 100644 ---- a/crates/pumpkin/src/net/java/pending.rs -+++ b/crates/pumpkin/src/net/java/pending.rs -@@ -23,10 +23,7 @@ use pumpkin_protocol::{ - use pumpkin_util::{Hand, text::TextComponent, version::JavaMinecraftVersion}; - use tokio::{ - io::{BufReader, BufWriter}, -- net::{ -- TcpStream, -- tcp::{OwnedReadHalf, OwnedWriteHalf}, -- }, -+ net::TcpStream, - }; - use tokio_util::sync::CancellationToken; - use tracing::{debug, error, warn}; -@@ -40,7 +37,7 @@ use crate::{ - server::Server, - }; +- let light_buf = if version >= JavaMinecraftVersion::V_1_14 +- && version < JavaMinecraftVersion::V_1_18 +- { +- match CLightUpdate::from_chunk(&chunk, version) { +- Ok(light_packet) => { +- let mut light_buf = Vec::new(); +- if let Err(err) = +- light_buf.write_var_int(&VarInt(CLightUpdate::to_id(version))) +- { +- error!("Failed to write light update id: {err:?}"); +- None +- } else if let Err(err) = +- light_packet.write_packet_data(&mut light_buf, &version) +- { +- error!("Failed to write light update data: {err:?}"); ++ let light_buf = if version >= JavaMinecraftVersion::V_1_14 ++ && version < JavaMinecraftVersion::V_1_18 ++ { ++ match CLightUpdate::from_chunk(&chunk, version) { ++ Ok(light_packet) => { ++ let mut light_buf = Vec::new(); ++ if let Err(err) = ++ light_buf.write_var_int(&VarInt(CLightUpdate::to_id(version))) ++ { ++ error!("Failed to write light update id: {err:?}"); ++ None ++ } else if let Err(err) = ++ light_packet.write_packet_data(&mut light_buf, &version) ++ { ++ error!("Failed to write light update data: {err:?}"); ++ None ++ } else { ++ Some(Bytes::from(light_buf.into_boxed_slice())) ++ } ++ } ++ Err(err) => { ++ error!("Failed to create light update packet: {err:?}"); + None +- } else { +- Some(Bytes::from(light_buf)) + } + } +- Err(err) => { +- error!("Failed to create light update packet: {err:?}"); +- None +- } +- } +- } else { +- None +- }; +- +- serialized.push((Bytes::from(buf), light_buf)); +- } +- let _ = tx.send(serialized); +- }); ++ } else { ++ None ++ }; + +- let Ok(serialized) = rx.await else { +- return; +- }; ++ serialized.push((Bytes::from(buf.into_boxed_slice()), light_buf)); ++ } ++ let _ = tx.send(serialized); ++ }); --use super::JavaClient; -+use super::{ConnectionReader, ConnectionWriter, JavaClient}; - - const BRAND_CHANNEL_PREFIX: &str = "minecraft:brand"; - -@@ -61,8 +58,8 @@ pub struct PendingConnection { - pub version: AtomicCell, - pub connection_state: AtomicCell, - pub close_token: CancellationToken, -- pub network_writer: TCPNetworkEncoder>, -- pub network_reader: TCPNetworkDecoder>, -+ pub network_writer: TCPNetworkEncoder>, -+ pub network_reader: TCPNetworkDecoder>, - pub gameprofile: Option, - pub config: Option, - pub brand: Option, -@@ -78,7 +75,22 @@ impl PendingConnection { - id: u64, - packet_limiter: PacketRateLimiter, - ) -> Self { -- let (read, write) = tcp_stream.into_split(); -+ Self::from_stream(tcp_stream, address, id, packet_limiter) +- if version >= JavaMinecraftVersion::V_1_20_2 { +- self.send_packet(&CChunkBatchStart).await; +- } ++ let Ok(serialized) = rx.await else { ++ return; ++ }; + +- for (chunk_data, light_data) in serialized { +- self.enqueue_packet(chunk_data).await; +- if let Some(light_data) = light_data { +- self.enqueue_packet(light_data).await; ++ for (chunk_data, light_data) in serialized { ++ self.enqueue_chunk_packet(chunk_data).await; ++ if let Some(light_data) = light_data { ++ self.enqueue_chunk_packet(light_data).await; ++ } + } + } +- + if version >= JavaMinecraftVersion::V_1_20_2 { + self.send_packet(&CChunkBatchEnd::new(chunks.len() as u16)) + .await; +@@ -746,7 +840,7 @@ impl JavaClient { + } + + let mut packets_to_frame = VecDeque::from(packet_batch); +- let mut written_packets = Vec::with_capacity(packets_to_frame.len()); ++ let mut completions = Vec::with_capacity(packets_to_frame.len()); + let mut send_failed = false; + + while !packets_to_frame.is_empty() { +@@ -780,7 +874,11 @@ impl JavaClient { + break; + } + +- written_packets.extend(returned_batch); ++ for packet in returned_batch { ++ if let Some(completion) = packet.completion { ++ completions.push(completion); ++ } ++ } + } + + if !send_failed && let Err(err) = writer.flush().await { +@@ -796,10 +894,8 @@ impl JavaClient { + break; + } + +- for packet in written_packets { +- if let Some(completion) = packet.completion { +- let _ = completion.send(()); +- } ++ for completion in completions { ++ let _ = completion.send(()); + } + } + }); +@@ -1311,3 +1407,59 @@ impl JavaClient { + Ok(()) + } + } ++ ++#[cfg(test)] ++mod chunk_queue_tests { ++ use super::*; ++ ++ #[test] ++ fn full_byte_budget_rejects_the_whole_batch_without_publishing_a_marker() { ++ let (sender, mut receiver) = tokio::sync::mpsc::channel(4); ++ let budget = Arc::new(Semaphore::new(10)); ++ let busy = budget.clone().try_acquire_many_owned(6).unwrap(); ++ assert!(!try_enqueue_chunk_packets( ++ &sender, ++ &budget, ++ 10, ++ vec![Bytes::from_static(b"start"), Bytes::from_static(b"data")] ++ )); ++ assert!(receiver.try_recv().is_err()); ++ assert_eq!(sender.capacity(), 4); ++ assert_eq!(budget.available_permits(), 4); ++ drop(busy); ++ assert!(try_enqueue_chunk_packets( ++ &sender, ++ &budget, ++ 10, ++ vec![Bytes::from_static(b"start"), Bytes::from_static(b"data")] ++ )); ++ assert_eq!(budget.available_permits(), 1); ++ drop(receiver.try_recv().unwrap()); ++ assert_eq!(budget.available_permits(), 1); ++ drop(receiver.try_recv().unwrap()); ++ assert_eq!(budget.available_permits(), 10); + } + -+ /// Accept an injected byte stream while retaining the native TCP constructor. -+ pub fn from_stream( -+ stream: S, -+ address: SocketAddr, -+ id: u64, -+ packet_limiter: PacketRateLimiter, -+ ) -> Self -+ where -+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Sync + Unpin + 'static, -+ { -+ let (read, write) = tokio::io::split(stream); -+ let read: ConnectionReader = Box::new(read); -+ let write: ConnectionWriter = Box::new(write); - Self { - id, - address, ++ #[test] ++ fn insufficient_slots_return_byte_credit_and_leave_the_queue_unchanged() { ++ let (sender, mut receiver) = tokio::sync::mpsc::channel(1); ++ let budget = Arc::new(Semaphore::new(10)); ++ assert!(!try_enqueue_chunk_packets( ++ &sender, ++ &budget, ++ 10, ++ vec![Bytes::from_static(b"a"), Bytes::from_static(b"b")] ++ )); ++ assert!(receiver.try_recv().is_err()); ++ assert_eq!(budget.available_permits(), 10); ++ assert!(try_enqueue_chunk_packets( ++ &sender, ++ &budget, ++ 10, ++ vec![Bytes::from(vec![0; 20])] ++ )); ++ assert_eq!(budget.available_permits(), 0); ++ drop(receiver.try_recv().unwrap()); ++ assert_eq!(budget.available_permits(), 10); ++ } ++} diff --git a/crates/pumpkin/src/net/java/play/chat_message.rs b/crates/pumpkin/src/net/java/play/chat_message.rs -index 589277311..ba28c7450 100644 +index 58927731..ba28c745 100644 --- a/crates/pumpkin/src/net/java/play/chat_message.rs +++ b/crates/pumpkin/src/net/java/play/chat_message.rs @@ -266,7 +266,7 @@ impl JavaClient { @@ -1957,7 +2741,7 @@ index 589277311..ba28c7450 100644 let verifying_key = VerifyingKey::::new(key.clone()); verifying_key.verify(&signable, &key_signature).is_ok() diff --git a/crates/pumpkin/src/plugin/loader/mod.rs b/crates/pumpkin/src/plugin/loader/mod.rs -index fa7f8c5ab..61bbe63f1 100644 +index fa7f8c5a..61bbe63f 100644 --- a/crates/pumpkin/src/plugin/loader/mod.rs +++ b/crates/pumpkin/src/plugin/loader/mod.rs @@ -1,8 +1,12 @@ @@ -1983,32 +2767,66 @@ index fa7f8c5ab..61bbe63f1 100644 WasmInitializationError(#[from] PluginInitError), } diff --git a/crates/pumpkin/src/plugin/mod.rs b/crates/pumpkin/src/plugin/mod.rs -index b686898d0..33ed73b36 100644 +index b686898d..ed4d4cec 100644 --- a/crates/pumpkin/src/plugin/mod.rs +++ b/crates/pumpkin/src/plugin/mod.rs @@ -1,6 +1,9 @@ use arc_swap::ArcSwap; use futures::future::join_all; -use loader::{LoaderError, PluginLoader, native::NativePluginLoader}; -+use loader::{LoaderError, PluginLoader}; +#[cfg(feature = "plugin-loaders")] +use loader::native::NativePluginLoader; ++use loader::{LoaderError, PluginLoader}; +#[cfg(feature = "plugin-loaders")] use notify::{EventKind, RecursiveMode, Watcher, event::ModifyKind}; use std::{ any::Any, -@@ -27,7 +30,9 @@ pub mod loader; +@@ -12,11 +15,12 @@ use std::{ + time::Duration, + }; + use thiserror::Error; +-use tokio::{ +- sync::{Notify, RwLock}, +- task::JoinHandle, +-}; +-use tracing::{debug, error, info, warn}; ++use tokio::sync::{Notify, RwLock}; ++#[cfg(feature = "plugin-loaders")] ++use tokio::task::JoinHandle; ++#[cfg(feature = "plugin-loaders")] ++use tracing::debug; ++use tracing::{error, info, warn}; + + pub mod api; + pub mod cache; +@@ -27,7 +31,9 @@ pub mod loader; /// host features. pub mod permissions; -use crate::{LOGGER_IMPL, plugin::loader::wasm::WasmPluginLoader, server::Server}; -+use crate::{LOGGER_IMPL, server::Server}; +#[cfg(feature = "plugin-loaders")] +use crate::plugin::loader::wasm::WasmPluginLoader; ++use crate::{LOGGER_IMPL, server::Server}; pub use api::*; pub type BoxFuture<'a, T> = Pin + Send + 'a>>; -@@ -238,10 +243,20 @@ impl PluginManager { +@@ -192,6 +198,7 @@ pub struct PluginManager { + // Notification for plugin state changes + state_notify: Arc, + // Background task for hot reloading ++ #[cfg(feature = "plugin-loaders")] + hot_reload_task: RwLock>>, + hot_reload_enabled: AtomicBool, + // Thread ID of the thread that created the plugin manager. +@@ -210,6 +217,7 @@ struct LoadedPlugin { + loader_data: Option>, + is_active: bool, + context: Arc, ++ #[cfg(feature = "plugin-loaders")] + path: PathBuf, + } + +@@ -238,15 +246,26 @@ impl PluginManager { pub fn new(verify_plugin_signatures: bool) -> Self { Self { plugins: SyncRwLock::new(Vec::new()), @@ -2033,7 +2851,13 @@ index b686898d0..33ed73b36 100644 handlers: Arc::new(ArcSwap::from_pointee(HashMap::new())), unloaded_files: RwLock::new(HashSet::new()), services: Arc::new(RwLock::new(HashMap::new())), -@@ -285,6 +300,7 @@ impl PluginManager { + plugin_states: RwLock::new(HashMap::new()), + state_notify: Arc::new(Notify::new()), ++ #[cfg(feature = "plugin-loaders")] + hot_reload_task: RwLock::new(None), + hot_reload_enabled: AtomicBool::new(false), + main_thread_id: std::thread::current().id(), +@@ -285,6 +304,7 @@ impl PluginManager { } /// Start watching the plugins directory for changes @@ -2041,7 +2865,7 @@ index b686898d0..33ed73b36 100644 pub async fn start_watcher(self: &Arc, server: &Arc) -> Result<(), ManagerError> { if self.hot_reload_task.read().await.is_some() { return Ok(()); -@@ -368,6 +384,7 @@ impl PluginManager { +@@ -368,6 +388,7 @@ impl PluginManager { } /// Stop watching the plugins directory for changes @@ -2049,13 +2873,16 @@ index b686898d0..33ed73b36 100644 pub async fn stop_watcher(&self) { let mut task_lock = self.hot_reload_task.write().await; if let Some(handle) = task_lock.take() { -@@ -376,6 +393,15 @@ impl PluginManager { +@@ -376,6 +397,18 @@ impl PluginManager { self.set_hot_reload_enabled(false); } + /// No-op watcher controls when plugin loaders are compiled out. + #[cfg(not(feature = "plugin-loaders"))] -+ pub async fn start_watcher(self: &Arc, _server: &Arc) -> Result<(), ManagerError> { ++ pub async fn start_watcher( ++ self: &Arc, ++ _server: &Arc, ++ ) -> Result<(), ManagerError> { + Ok(()) + } + @@ -2065,7 +2892,7 @@ index b686898d0..33ed73b36 100644 pub fn set_hot_reload_enabled(&self, enabled: bool) { self.hot_reload_enabled .store(enabled, std::sync::atomic::Ordering::Relaxed); -@@ -512,6 +538,7 @@ impl PluginManager { +@@ -512,6 +545,7 @@ impl PluginManager { "Do you want to allow these permissions and load the plugin?".bold() ); @@ -2073,7 +2900,7 @@ index b686898d0..33ed73b36 100644 let mut rl_taken = if let Some(logger_option) = crate::LOGGER_IMPL.get() && let Some((wrapper, _, _)) = logger_option && let Some(rl) = wrapper.take_readline() -@@ -520,7 +547,10 @@ impl PluginManager { +@@ -520,7 +554,10 @@ impl PluginManager { } else { None }; @@ -2084,7 +2911,7 @@ index b686898d0..33ed73b36 100644 let result = if let Some((_, ref mut rl)) = rl_taken { rl.readline(&prompt).is_ok_and(|line| { let input = line.trim().to_lowercase(); -@@ -533,7 +563,17 @@ impl PluginManager { +@@ -533,7 +570,17 @@ impl PluginManager { ); false }; @@ -2102,7 +2929,26 @@ index b686898d0..33ed73b36 100644 if let Some((wrapper, rl)) = rl_taken { wrapper.return_readline(rl); } -@@ -731,14 +771,17 @@ impl PluginManager { +@@ -550,7 +597,7 @@ impl PluginManager { + metadata: PluginMetadata, + loader_data: Box, + loader: Arc, +- path: PathBuf, ++ _path: PathBuf, + ) -> Result, ManagerError> { + // Mark plugin as loading + self.plugin_states +@@ -574,7 +621,8 @@ impl PluginManager { + loader_data: Some(loader_data), + is_active: false, // Will be set to true after successful initialization + context: context.clone(), +- path, ++ #[cfg(feature = "plugin-loaders")] ++ path: _path, + }; + + let plugin_index = { +@@ -731,14 +779,17 @@ impl PluginManager { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("wasm")) { @@ -2128,7 +2974,7 @@ index b686898d0..33ed73b36 100644 } } -@@ -940,16 +983,19 @@ impl PluginManager { +@@ -940,16 +991,19 @@ impl PluginManager { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("wasm")) { @@ -2158,7 +3004,7 @@ index b686898d0..33ed73b36 100644 } } -@@ -1257,6 +1303,7 @@ impl PluginManager { +@@ -1257,6 +1311,7 @@ impl PluginManager { return; } @@ -2166,7 +3012,7 @@ index b686898d0..33ed73b36 100644 if tokio::runtime::Handle::try_current().is_ok() { tokio::task::block_in_place(|| { server.runtime.block_on(self.fire(server, event)); -@@ -1264,6 +1311,13 @@ impl PluginManager { +@@ -1264,6 +1319,13 @@ impl PluginManager { } else { server.runtime.block_on(self.fire(server, event)); } @@ -2181,7 +3027,7 @@ index b686898d0..33ed73b36 100644 #[expect(clippy::result_unit_err)] diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs -index 28690a867..5f58b8b58 100644 +index 28690a86..5f58b8b5 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -64,6 +64,7 @@ pub mod ticker; @@ -2235,15 +3081,24 @@ index 28690a867..5f58b8b58 100644 self.scheduled_functions.tick( self, diff --git a/crates/pumpkin/src/server/scheduler.rs b/crates/pumpkin/src/server/scheduler.rs -index e2056b48a..f6221d64a 100644 +index e2056b48..1cfb9bb5 100644 --- a/crates/pumpkin/src/server/scheduler.rs +++ b/crates/pumpkin/src/server/scheduler.rs -@@ -1,3 +1,4 @@ +@@ -1,12 +1,19 @@ +#[cfg(feature = "plugin-loaders")] use crate::plugin::loader::wasm::wasm_host::WasmPlugin; use crate::server::Server; use std::cmp::Ordering; -@@ -7,6 +8,7 @@ use std::sync::{Arc, Mutex, Weak}; +-use std::collections::{BinaryHeap, HashSet}; ++use std::collections::BinaryHeap; ++#[cfg(feature = "plugin-loaders")] ++use std::collections::HashSet; ++#[cfg(feature = "plugin-loaders")] ++use std::sync::Weak; ++#[cfg(feature = "plugin-loaders")] + use std::sync::atomic::Ordering as AtomicOrdering; +-use std::sync::{Arc, Mutex, Weak}; ++use std::sync::{Arc, Mutex}; pub type TaskId = u32; @@ -2251,7 +3106,7 @@ index e2056b48a..f6221d64a 100644 pub struct ScheduledTask { pub id: TaskId, pub plugin: Arc, -@@ -15,20 +17,24 @@ pub struct ScheduledTask { +@@ -15,20 +22,24 @@ pub struct ScheduledTask { pub period: Option, } @@ -2276,7 +3131,7 @@ index e2056b48a..f6221d64a 100644 impl Ord for ScheduledTask { fn cmp(&self, other: &Self) -> Ordering { // Reverse order so BinaryHeap is a min-heap -@@ -36,6 +42,7 @@ impl Ord for ScheduledTask { +@@ -36,6 +47,7 @@ impl Ord for ScheduledTask { } } @@ -2284,7 +3139,7 @@ index e2056b48a..f6221d64a 100644 pub struct TaskScheduler { tasks: Mutex>, cancelled_tasks: Mutex>, -@@ -43,12 +50,14 @@ pub struct TaskScheduler { +@@ -43,12 +55,14 @@ pub struct TaskScheduler { next_task_id: std::sync::atomic::AtomicU32, } @@ -2300,7 +3155,7 @@ index e2056b48a..f6221d64a 100644 #[must_use] pub fn new() -> Self { diff --git a/crates/pumpkin/src/server/ticker.rs b/crates/pumpkin/src/server/ticker.rs -index ae45c2b05..1f35b272b 100644 +index ae45c2b0..1f35b272 100644 --- a/crates/pumpkin/src/server/ticker.rs +++ b/crates/pumpkin/src/server/ticker.rs @@ -16,9 +16,14 @@ use tracing::debug; @@ -2386,10 +3241,18 @@ index ae45c2b05..1f35b272b 100644 if STOP_INTERRUPT.is_cancelled() { break 'ticker; diff --git a/crates/pumpkin/src/world/portal/mod.rs b/crates/pumpkin/src/world/portal/mod.rs -index aeadba752..85bf10aff 100644 +index aeadba75..c62761b3 100644 --- a/crates/pumpkin/src/world/portal/mod.rs +++ b/crates/pumpkin/src/world/portal/mod.rs -@@ -92,6 +92,7 @@ impl PortalType { +@@ -4,6 +4,7 @@ use pumpkin_data::Block; + use pumpkin_data::block_properties::HorizontalAxis; + use pumpkin_data::dimension::Dimension; + use pumpkin_util::math::position::BlockPos; ++#[cfg(not(feature = "single-threaded"))] + use pumpkin_util::math::vector2::Vector2; + use pumpkin_util::math::vector3::Vector3; + use pumpkin_world::world::BlockFlags; +@@ -92,6 +93,7 @@ impl PortalType { let platform_pos = BlockPos::new(100, 49, 0); // Ensure chunks covering the platform are loaded/generated @@ -2397,7 +3260,7 @@ index aeadba752..85bf10aff 100644 if let Ok(handle) = tokio::runtime::Handle::try_current() { tokio::task::block_in_place(|| { handle.block_on(async { -@@ -207,6 +208,7 @@ impl PortalType { +@@ -207,6 +209,7 @@ impl PortalType { ) .or_else(|| { // Ensure the chunks around approximate_exit_pos are generated/loaded in dest_world @@ -2405,65 +3268,3 @@ index aeadba752..85bf10aff 100644 if let Ok(handle) = tokio::runtime::Handle::try_current() { tokio::task::block_in_place(|| { handle.block_on(async { -diff --git a/crates/pumpkin/src/net/bedrock/nethernet_stub.rs b/crates/pumpkin/src/net/bedrock/nethernet_stub.rs -new file mode 100644 -index 000000000..90f0989c4 ---- /dev/null -+++ b/crates/pumpkin/src/net/bedrock/nethernet_stub.rs -@@ -0,0 +1,56 @@ -+//! WebRTC-free stub of the NetherNet transport, compiled when the `nethernet` -+//! feature is off (e.g. the emscripten/embedded Java-only build). The types -+//! exist so the always-compiled Bedrock client code type-checks, but no Bedrock -+//! connection is ever created (the listener is never bound), so the method -+//! bodies are unreachable. -+use std::net::SocketAddr; -+use std::sync::Arc; -+ -+use bytes::Bytes; -+use pumpkin_util::p384::PublicKey; -+ -+pub type IncomingSession = (Arc, SocketAddr); -+ -+/// Stub listener: never bound when `nethernet` is off, so `accept` never yields. -+pub struct NetherNetListener { -+ _private: (), -+} -+ -+impl NetherNetListener { -+ pub async fn accept(&self) -> Option { -+ None -+ } -+ -+ pub const fn local_addr(&self) -> SocketAddr { -+ SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0) -+ } -+} -+ -+/// Stub session. Never constructed when `nethernet` is off. -+pub struct NetherNetSession { -+ _private: (), -+} -+ -+impl NetherNetSession { -+ pub async fn recv(&self) -> Option { -+ None -+ } -+ -+ pub async fn send(&self, _data: Bytes) -> Result<(), String> { -+ Err("NetherNet transport is disabled in this build".to_string()) -+ } -+ -+ pub async fn send_unreliable(&self, _data: Bytes) -> Result<(), String> { -+ Err("NetherNet transport is disabled in this build".to_string()) -+ } -+ -+ pub const fn is_closed(&self) -> bool { -+ true -+ } -+ -+ pub async fn close(&self) {} -+ -+ pub const fn client_public_key(&self) -> Option<&PublicKey> { -+ None -+ } -+} diff --git a/patches/pumpkin-memory.patch b/patches/pumpkin-memory.patch index 3edd278..adcd3fb 100644 --- a/patches/pumpkin-memory.patch +++ b/patches/pumpkin-memory.patch @@ -1,4 +1,4 @@ -Subject: [PATCH] Reduce Pumpkin structure-template and generation memory +Subject: [PATCH] Reduce Pumpkin template, chunk, and idle pathfinder memory Loaded structure templates previously retained a block-state description for every block and a second placement list. Store palette indices, share the first @@ -12,11 +12,559 @@ dense arrays. Use a dense scratch buffer only during the active noise pass to avoid per-voxel palette bookkeeping, then compact it before retaining the chunk. Generation dependencies, dimensions, and block values remain unchanged. +Use packed in-memory palette indices, share light arrays with copy-on-write, +allocate carving-mask words on demand, and grow pathfinding heaps only when +needed. Preserve block values, lighting initialization, and search behavior. + +Limit pooled density buffers by bytes and prune structure-start entries that +no active generation task references. Concurrent cache misses share a canonical +collector so pointer-based structure deduplication remains valid. + Base-commit: b5b9b9d7010e793806a83c495af223c67e1d35ee --- +diff --git a/crates/pumpkin-world/src/chunk/format/mod.rs b/crates/pumpkin-world/src/chunk/format/mod.rs +index 68432e19..cd52b03b 100644 +--- a/crates/pumpkin-world/src/chunk/format/mod.rs ++++ b/crates/pumpkin-world/src/chunk/format/mod.rs +@@ -2,7 +2,7 @@ use std::{ + path::PathBuf, + str::FromStr, + sync::{ +- RwLock, ++ Arc, LazyLock, RwLock, + atomic::{AtomicBool, AtomicU64, Ordering}, + }, + }; +@@ -256,9 +256,9 @@ impl ChunkData { + }); + + block_lights[index] = +- block_light.map_or(LightContainer::Empty(0), LightContainer::Full); ++ block_light.map_or(LightContainer::Empty(0), LightContainer::new); + sky_lights[index] = +- sky_light.map_or(LightContainer::Empty(0), LightContainer::Full); ++ sky_light.map_or(LightContainer::Empty(0), LightContainer::new); + + if let Some(bs_compound) = section_compound.get_compound("block_states") { + let data = bs_compound +@@ -810,7 +810,8 @@ pub struct ChunkSectionBlockStates { + #[derive(Debug, Clone)] + pub enum LightContainer { + Empty(u8), +- Full(Box<[u8]>), ++ // Clones share immutable data; writes detach through Arc::make_mut. ++ Full(Arc<[u8]>), + } + + impl LightContainer { +@@ -830,14 +831,38 @@ impl LightContainer { + "Data length must be {}", + Self::ARRAY_SIZE + ); +- Self::Full(data) ++ let value = data[0]; ++ if value >> 4 == value & 15 && data.iter().all(|&byte| byte == value) { ++ Self::new_filled(value & 15) ++ } else { ++ Self::Full(data.into()) ++ } + } + + #[must_use] + pub fn new_filled(default: u8) -> Self { + assert!(default <= 15, "Default value must be between 0 and 15"); +- let value = default << 4 | default; +- Self::Full([value; Self::ARRAY_SIZE].into()) ++ // Keep initialized uniform light distinct from Empty: packet masks and ++ // saved light data must retain the same semantics as a full array. ++ static UNIFORM: LazyLock<[Arc<[u8]>; 16]> = LazyLock::new(|| { ++ std::array::from_fn(|value| vec![(value as u8) * 17; LightContainer::ARRAY_SIZE].into()) ++ }); ++ Self::Full(Arc::clone(&UNIFORM[default as usize])) ++ } ++ ++ /// Intern arrays that became uniform while lighting was propagated. ++ pub fn compact(&mut self) { ++ if let Self::Full(data) = self { ++ // Shared uniform arrays are already interned. Other shared arrays ++ // remain unchanged until a writer detaches them. ++ if Arc::strong_count(data) != 1 || data.len() != Self::ARRAY_SIZE { ++ return; ++ } ++ let value = data[0]; ++ if value >> 4 == value & 15 && data.iter().all(|&byte| byte == value) { ++ *self = Self::new_filled(value & 15); ++ } ++ } + } + + #[must_use] +@@ -869,7 +894,10 @@ impl LightContainer { + let index = Self::index(x, y, z); + let shift = 4 * (index & 1); + let mask = 0x0F << shift; +- data[index >> 1] = (data[index >> 1] & !mask) | (value << shift); ++ let updated = (data[index >> 1] & !mask) | (value << shift); ++ if data[index >> 1] != updated { ++ Arc::make_mut(data)[index >> 1] = updated; ++ } + } + Self::Empty(default) => { + if value != *default { +@@ -898,6 +926,10 @@ impl LightContainer { + let mask = 0x0F << shift; + let val = (value & 0x0F) << shift; + let mut byte_idx = (y_start * 256 + z * 16 + x) >> 1; ++ if (0..y_end - y_start).all(|dy| data[byte_idx + dy * 128] & mask == val) { ++ return; ++ } ++ let data = Arc::make_mut(data); + for _ in y_start..y_end { + data[byte_idx] = (data[byte_idx] & !mask) | val; + byte_idx += 128; +@@ -931,6 +963,58 @@ mod tests { + use pumpkin_nbt::compound::NbtCompound; + use pumpkin_nbt::tag::NbtTag; + ++ #[test] ++ fn propagated_uniform_light_is_compacted_without_becoming_empty() { ++ let mut light = LightContainer::new_empty(0); ++ for x in 0..16 { ++ for z in 0..16 { ++ light.set_column_y_range(x, z, 0, 16, 15); ++ } ++ } ++ light.compact(); ++ let shared = LightContainer::new_filled(15); ++ let (LightContainer::Full(a), LightContainer::Full(b)) = (&light, &shared) else { ++ panic!("compaction must preserve initialized light"); ++ }; ++ assert!(Arc::ptr_eq(a, b)); ++ assert!(a.iter().all(|&value| value == 255)); ++ light.set(0, 0, 0, 0); ++ assert_eq!(shared.get(0, 0, 0), 15); ++ } ++ ++ #[test] ++ fn uniform_light_is_shared_without_changing_initialized_state() { ++ let mut a = LightContainer::new_filled(15); ++ let b = LightContainer::new(vec![255; LightContainer::ARRAY_SIZE].into_boxed_slice()); ++ assert!(!a.is_empty()); ++ let (LightContainer::Full(a_data), LightContainer::Full(b_data)) = (&a, &b) else { ++ panic!("initialized light must remain Full"); ++ }; ++ assert!(Arc::ptr_eq(a_data, b_data)); ++ a.set(1, 2, 3, 15); ++ let LightContainer::Full(a_data) = &a else { ++ unreachable!() ++ }; ++ assert!(Arc::ptr_eq(a_data, b_data)); ++ a.set(1, 2, 3, 7); ++ assert_eq!(a.get(1, 2, 3), 7); ++ assert_eq!(a.get(0, 2, 3), 15); ++ assert_eq!(b.get(1, 2, 3), 15); ++ } ++ ++ #[test] ++ fn cloned_light_columns_do_not_mutate_the_source() { ++ let mut source = LightContainer::new_filled(0); ++ source.set(4, 3, 2, 6); ++ let mut copy = source.clone(); ++ copy.set_column_y_range(4, 2, 0, 16, 12); ++ for y in 0..16 { ++ assert_eq!(copy.get(4, y, 2), 12); ++ assert_eq!(copy.get(5, y, 2), 0); ++ assert_eq!(source.get(4, y, 2), if y == 3 { 6 } else { 0 }); ++ } ++ } ++ + #[test] + fn extract_u16_array_from_vanilla_compound_palette() { + let mut entry1 = NbtCompound::new(); +diff --git a/crates/pumpkin-world/src/chunk/palette.rs b/crates/pumpkin-world/src/chunk/palette.rs +index 39ed39a7..911e320d 100644 +--- a/crates/pumpkin-world/src/chunk/palette.rs ++++ b/crates/pumpkin-world/src/chunk/palette.rs +@@ -51,7 +51,66 @@ pub struct HeterogeneousPaletteData { + #[derive(Clone)] + enum PaletteStorage { + Dense(Box>), +- Indexed(Box>), ++ Indexed(PackedIndices), ++} ++ ++/// In-memory palette indices use 1, 2, 4, or 8 bits per block. Wire-format ++/// packing remains independent because Minecraft pads entries within each u64. ++#[derive(Clone)] ++struct PackedIndices { ++ bytes: Box<[u8]>, ++ len: usize, ++ bits: u8, ++} ++ ++impl PackedIndices { ++ fn width(palette_len: usize) -> u8 { ++ match palette_len { ++ 0..=2 => 1, ++ 3..=4 => 2, ++ 5..=16 => 4, ++ _ => 8, ++ } ++ } ++ ++ fn new(len: usize, palette_len: usize) -> Self { ++ let bits = Self::width(palette_len); ++ Self { ++ bytes: vec![0; len.div_ceil(8 / bits as usize)].into_boxed_slice(), ++ len, ++ bits, ++ } ++ } ++ ++ fn from_slice(indices: &[u8], palette_len: usize) -> Self { ++ let mut packed = Self::new(indices.len(), palette_len); ++ for (i, &index) in indices.iter().enumerate() { ++ packed.set(i, index); ++ } ++ packed ++ } ++ ++ #[inline] ++ fn get(&self, index: usize) -> u8 { ++ let bit = index * self.bits as usize; ++ (self.bytes[bit >> 3] >> (bit & 7)) & (u8::MAX >> (8 - self.bits)) ++ } ++ ++ #[inline] ++ fn set(&mut self, index: usize, value: u8) { ++ if value > (u8::MAX >> (8 - self.bits)) { ++ let mut wider = Self::new(self.len, value as usize + 1); ++ for i in 0..self.len { ++ wider.set(i, self.get(i)); ++ } ++ *self = wider; ++ } ++ let bit = index * self.bits as usize; ++ let shift = bit & 7; ++ let mask = (u8::MAX >> (8 - self.bits)) << shift; ++ let byte = &mut self.bytes[bit >> 3]; ++ *byte = (*byte & !mask) | (value << shift); ++ } + } + + impl HeterogeneousPaletteData { +@@ -62,7 +121,9 @@ impl HeterogeneousPaletteData cube[y][z][x], +- PaletteStorage::Indexed(indices) => self.palette[indices[y][z][x] as usize], ++ PaletteStorage::Indexed(indices) => { ++ self.palette[indices.get((y * DIM + z) * DIM + x) as usize] ++ } + } + } + +@@ -101,7 +162,7 @@ impl HeterogeneousPaletteData { + if new_index <= 255 { +- indices[y][z][x] = new_index as u8; ++ indices.set((y * DIM + z) * DIM + x, new_index as u8); + } else { + // Upgrade to Dense + let mut cube = Box::new([[[V::default(); DIM]; DIM]; DIM]); +@@ -111,10 +172,7 @@ impl HeterogeneousPaletteData HeterogeneousPaletteData PalettedContainer + indices.as_flattened_mut().as_flattened_mut()[i] = idx as u8; + } + Self::Heterogeneous(Box::new(HeterogeneousPaletteData { +- storage: PaletteStorage::Indexed(indices), ++ storage: PaletteStorage::Indexed(PackedIndices::from_slice( ++ indices.as_flattened().as_flattened(), ++ palette.len(), ++ )), + palette, + counts, + })) +@@ -241,7 +298,10 @@ impl PalettedContainer + } + + let storage = if palette.len() <= 256 && std::mem::size_of::() > 1 { +- PaletteStorage::Indexed(indices) ++ PaletteStorage::Indexed(PackedIndices::from_slice( ++ indices.as_flattened().as_flattened(), ++ palette.len(), ++ )) + } else { + PaletteStorage::Dense(cube) + }; +@@ -286,19 +346,16 @@ impl PalettedContainer + }) + }) + .collect(), +- PaletteStorage::Indexed(indices) => indices +- .as_flattened() +- .as_flattened() +- .chunks(blocks_per_i64 as usize) +- .map(|chunk| { +- chunk.iter().enumerate().fold(0, |acc, (index, key_index)| { +- let key_index = *key_index as usize; +- debug_assert!((1 << bits_per_entry) > key_index); +- +- let packed_offset_index = +- (key_index as u64) << (bits_per_entry as u64 * index as u64); +- acc | packed_offset_index as i64 +- }) ++ PaletteStorage::Indexed(indices) => (0..indices.len) ++ .step_by(blocks_per_i64 as usize) ++ .map(|start| { ++ (start..(start + blocks_per_i64 as usize).min(indices.len)) ++ .enumerate() ++ .fold(0i64, |word, (offset, i)| { ++ word | ((indices.get(i) as u64) ++ << (bits_per_entry as usize * offset)) ++ as i64 ++ }) + }) + .collect(), + }; +@@ -352,14 +409,15 @@ impl PalettedContainer + counts[idx] += 1; + } else { + warn!("Lookup index out of bounds! Defaulting..."); +- // value is already 0, and counts[0] will be updated correctly if we track it ++ counts[0] += 1; + } + } +- // fix counts[0] if it was skipped in out-of-bounds cases (rare) +- // But actually we should just ensure it's correct. + + return Self::Heterogeneous(Box::new(HeterogeneousPaletteData { +- storage: PaletteStorage::Indexed(indices), ++ storage: PaletteStorage::Indexed(PackedIndices::from_slice( ++ indices.as_flattened().as_flattened(), ++ palette.len(), ++ )), + palette: palette.to_vec(), + counts, + })); +@@ -459,13 +517,9 @@ impl PalettedContainer + PaletteStorage::Dense(cube) => { + Box::new(cube.as_flattened().as_flattened().iter().copied()) + } +- PaletteStorage::Indexed(indices) => Box::new( +- indices +- .as_flattened() +- .as_flattened() +- .iter() +- .map(|&idx| data.palette[idx as usize]), +- ), ++ PaletteStorage::Indexed(indices) => { ++ Box::new((0..indices.len).map(move |i| data.palette[indices.get(i) as usize])) ++ } + }, + } + } +@@ -494,13 +548,9 @@ impl<'a, V: Hash + Eq + Copy + Default, const DIM: usize> IntoIterator + PaletteStorage::Dense(cube) => { + Box::new(cube.as_flattened().as_flattened().iter().copied()) + } +- PaletteStorage::Indexed(indices) => Box::new( +- indices +- .as_flattened() +- .as_flattened() +- .iter() +- .map(|&idx| data.palette[idx as usize]), +- ), ++ PaletteStorage::Indexed(indices) => { ++ Box::new((0..indices.len).map(move |i| data.palette[indices.get(i) as usize])) ++ } + }, + } + } +@@ -1004,7 +1054,7 @@ pub(crate) const BIOME_NETWORK_MAX_BITS: u8 = 7; + + #[cfg(test)] + mod tests { +- use super::{BlockPalette, NetworkPalette}; ++ use super::{BlockPalette, NetworkPalette, PackedIndices, PaletteStorage, PalettedContainer}; + use pumpkin_data::{Block, BlockStateId}; + + fn network_palette_values(palette: NetworkPalette) -> Option> { +@@ -1047,6 +1097,65 @@ mod tests { + ); + } + ++ #[test] ++ fn packed_palette_growth_remapping_and_serialization_preserve_values() { ++ for colors in [2usize, 3, 5, 17, 257] { ++ let mut palette = PalettedContainer::::from_fn(|x, y, z| { ++ (((y * 16 + z) * 16 + x) % colors) as u16 ++ }); ++ let mut expected: Vec = (0..4096).map(|i| (i % colors) as u16).collect(); ++ assert_eq!(palette.iter().collect::>(), expected); ++ if colors <= 256 { ++ let PalettedContainer::Heterogeneous(data) = &palette else { ++ unreachable!() ++ }; ++ let PaletteStorage::Indexed(indices) = &data.storage else { ++ unreachable!() ++ }; ++ assert_eq!( ++ indices.bytes.len(), ++ 4096 * PackedIndices::width(colors) as usize / 8 ++ ); ++ } ++ for i in [0, 1, 15, 16, 63, 255, 1024, 4095] { ++ let value = (colors + i % 7) as u16; ++ let old = palette.set(i % 16, i / 256, i / 16 % 16, value); ++ assert_eq!(old, expected[i]); ++ expected[i] = value; ++ } ++ assert_eq!(palette.iter().collect::>(), expected); ++ let bits = palette.bits_per_entry().max(4); ++ let (keys, words) = palette.to_palette_and_packed_data(bits); ++ let decoded = ++ PalettedContainer::::from_palette_and_packed_data(&keys, &words, 4); ++ assert_eq!(decoded.iter().collect::>(), expected); ++ for i in 0..4096 { ++ palette.set(i % 16, i / 256, i / 16 % 16, 7); ++ } ++ assert!(matches!(palette, PalettedContainer::Homogeneous(7))); ++ } ++ } ++ ++ #[test] ++ fn packed_indices_expand_across_all_widths() { ++ let mut indices = PackedIndices::new(27, 2); ++ for value in [1, 3, 15, 255] { ++ for i in [0, 1, 7, 8, 26] { ++ indices.set(i, value); ++ } ++ for i in 0..27 { ++ assert_eq!( ++ indices.get(i), ++ if [0, 1, 7, 8, 26].contains(&i) { ++ value ++ } else { ++ 0 ++ } ++ ); ++ } ++ } ++ } ++ + #[test] + fn bulk_palette_matches_individual_mutations() { + let states = [ +diff --git a/crates/pumpkin-world/src/generation/carver/mask.rs b/crates/pumpkin-world/src/generation/carver/mask.rs +index f9ccd277..8e47504f 100644 +--- a/crates/pumpkin-world/src/generation/carver/mask.rs ++++ b/crates/pumpkin-world/src/generation/carver/mask.rs +@@ -1,6 +1,7 @@ + pub struct CarvingMask { + min_y: i32, + mask: Vec, ++ words: usize, + } + + impl CarvingMask { +@@ -11,7 +12,8 @@ impl CarvingMask { + let num_u64s = (total_bits as usize).div_ceil(64); + Self { + min_y, +- mask: vec![0; num_u64s], ++ mask: Vec::new(), ++ words: num_u64s, + } + } + +@@ -27,6 +29,14 @@ impl CarvingMask { + let bit_index = self.get_bit_index(x, y, z); + let u64_idx = bit_index / 64; + let bit_in_u64 = bit_index % 64; ++ assert!( ++ u64_idx < self.words, ++ "Carving mask position is outside the chunk" ++ ); ++ if u64_idx >= self.mask.len() { ++ self.mask.reserve_exact(u64_idx + 1 - self.mask.len()); ++ self.mask.resize(u64_idx + 1, 0); ++ } + self.mask[u64_idx] |= 1 << bit_in_u64; + } + +@@ -35,6 +45,38 @@ impl CarvingMask { + let bit_index = self.get_bit_index(x, y, z); + let u64_idx = bit_index / 64; + let bit_in_u64 = bit_index % 64; +- (self.mask[u64_idx] >> bit_in_u64) & 1 != 0 ++ assert!( ++ u64_idx < self.words, ++ "Carving mask position is outside the chunk" ++ ); ++ self.mask ++ .get(u64_idx) ++ .is_some_and(|word| (word >> bit_in_u64) & 1 != 0) ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use super::CarvingMask; ++ ++ #[test] ++ fn uncarved_space_needs_no_backing_array() { ++ let mut mask = CarvingMask::new(384, -64); ++ assert!(!mask.get(0, 319, 0)); ++ assert!(mask.mask.is_empty()); ++ mask.set(-1, -64, 17); ++ assert!(mask.get(15, -64, 1)); ++ assert!(!mask.get(14, -64, 1)); ++ assert!(!mask.get(15, 319, 1)); ++ assert!(mask.mask.capacity() < mask.words); ++ mask.set(15, 319, 15); ++ assert!(mask.get(15, 319, 15)); ++ assert!(mask.get(15, -64, 1)); ++ } ++ ++ #[test] ++ #[should_panic(expected = "outside the chunk")] ++ fn reads_outside_the_dimension_still_fail() { ++ let _ = CarvingMask::new(384, -64).get(0, -65, 0); + } + } diff --git a/crates/pumpkin-world/src/generation/feature/features/fossil.rs b/crates/pumpkin-world/src/generation/feature/features/fossil.rs -index 226ee1833..0e774e8bc 100644 +index 226ee183..0e774e8b 100644 --- a/crates/pumpkin-world/src/generation/feature/features/fossil.rs +++ b/crates/pumpkin-world/src/generation/feature/features/fossil.rs @@ -169,7 +169,7 @@ fn place_fossil_template( @@ -27,9 +575,115 @@ index 226ee1833..0e774e8bc 100644 + for block in template.blocks.iter() { let local_pos = rotation.transform_pos(block.pos, template.size); let world_pos = origin + local_pos; - + +diff --git a/crates/pumpkin-world/src/generation/noise/router/density_volume.rs b/crates/pumpkin-world/src/generation/noise/router/density_volume.rs +index 106a65d5..10f24c74 100644 +--- a/crates/pumpkin-world/src/generation/noise/router/density_volume.rs ++++ b/crates/pumpkin-world/src/generation/noise/router/density_volume.rs +@@ -6,9 +6,10 @@ pub use pumpkin_util::noise::volume::DensityVolume; + const BUFFER_SIZE_INCREMENT: usize = 16; + const MAX_REUSE_SIZE_FACTOR: usize = 2; + const MAX_POOLED_BUFFERS: usize = 1024; ++const MAX_POOLED_BYTES: usize = 4 * 1024 * 1024; + + thread_local! { +- static DENSITY_BUFFER_POOL: RefCell>> = const { RefCell::new(Vec::new()) }; ++ static DENSITY_BUFFER_POOL: RefCell = const { RefCell::new(BufferPool { buffers: Vec::new(), bytes: 0 }) }; + } + + fn take_best( +@@ -31,6 +32,27 @@ fn take_best( + best_index.map(|i| pool.remove(i)) + } + ++struct BufferPool { ++ buffers: Vec>, ++ bytes: usize, ++} ++ ++impl BufferPool { ++ fn take(&mut self, min: usize, max: usize) -> Option> { ++ let buffer = take_best(&mut self.buffers, min, max)?; ++ self.bytes -= std::mem::size_of_val(&*buffer); ++ Some(buffer) ++ } ++ ++ fn put(&mut self, buffer: Box<[f32]>) { ++ let bytes = std::mem::size_of_val(&*buffer); ++ if self.buffers.len() < MAX_POOLED_BUFFERS && bytes <= MAX_POOLED_BYTES - self.bytes { ++ self.bytes += bytes; ++ self.buffers.push(buffer); ++ } ++ } ++} ++ + pub struct DensityBuffer { + values: Box<[f32]>, + len: usize, +@@ -47,11 +69,8 @@ impl DensityBuffer { + let min_capacity = len.next_multiple_of(BUFFER_SIZE_INCREMENT); + let values = DENSITY_BUFFER_POOL + .with(|pool| { +- take_best( +- &mut pool.borrow_mut(), +- min_capacity, +- min_capacity * MAX_REUSE_SIZE_FACTOR, +- ) ++ pool.borrow_mut() ++ .take(min_capacity, min_capacity * MAX_REUSE_SIZE_FACTOR) + }) + .unwrap_or_else(|| vec![0.0; min_capacity].into_boxed_slice()); + Self { values, len } +@@ -83,10 +102,43 @@ impl Drop for DensityBuffer { + fn drop(&mut self) { + let values = std::mem::take(&mut self.values); + DENSITY_BUFFER_POOL.with(|pool| { +- let mut pool = pool.borrow_mut(); +- if pool.len() < MAX_POOLED_BUFFERS { +- pool.push(values); +- } ++ pool.borrow_mut().put(values); + }); + } + } ++ ++#[cfg(test)] ++mod tests { ++ use super::*; ++ ++ #[test] ++ fn retained_buffers_obey_byte_budget_and_return_credit_on_reuse() { ++ let mut pool = BufferPool { ++ buffers: Vec::new(), ++ bytes: 0, ++ }; ++ let len = MAX_POOLED_BYTES / std::mem::size_of::(); ++ pool.put(vec![7.0; len].into_boxed_slice()); ++ pool.put(vec![0.0; 16].into_boxed_slice()); ++ assert_eq!(pool.buffers.len(), 1); ++ assert_eq!(pool.bytes, MAX_POOLED_BYTES); ++ assert!(pool.take(16, 32).is_none()); ++ assert_eq!(pool.bytes, MAX_POOLED_BYTES); ++ let buffer = pool.take(len, len).unwrap(); ++ assert_eq!(pool.bytes, 0); ++ assert!(buffer.iter().all(|&value| value == 7.0)); ++ pool.put(buffer); ++ assert_eq!(pool.bytes, MAX_POOLED_BYTES); ++ } ++ ++ #[test] ++ fn oversized_buffers_are_not_retained() { ++ let mut pool = BufferPool { ++ buffers: Vec::new(), ++ bytes: 0, ++ }; ++ pool.put(vec![0.0; MAX_POOLED_BYTES / 4 + 16].into_boxed_slice()); ++ assert!(pool.buffers.is_empty()); ++ assert_eq!(pool.bytes, 0); ++ } ++} diff --git a/crates/pumpkin-world/src/generation/proto_chunk.rs b/crates/pumpkin-world/src/generation/proto_chunk.rs -index 727a58a31..cc4c8e77e 100644 +index 727a58a3..cc4c8e77 100644 --- a/crates/pumpkin-world/src/generation/proto_chunk.rs +++ b/crates/pumpkin-world/src/generation/proto_chunk.rs @@ -38,6 +38,7 @@ use super::{ @@ -68,7 +722,7 @@ index 727a58a31..cc4c8e77e 100644 @@ -506,13 +513,6 @@ impl ProtoChunk { x as usize * CHUNK_DIM as usize + z as usize } - + - #[inline] - const fn local_pos_to_block_index(&self, x: i32, y: i32, z: i32) -> usize { - self.height() as usize * CHUNK_DIM as usize * x as usize @@ -90,12 +744,12 @@ index 727a58a31..cc4c8e77e 100644 + } + self.block_sections[y as usize / 16].get(x as usize, y as usize & 15, z as usize) } - + #[inline] @@ -573,8 +575,17 @@ impl ProtoChunk { } } - + - let index = self.local_pos_to_block_index(local_x, local_y, local_z); - self.flat_block_map[index] = block_state.id; + if let Some(blocks) = &mut self.noise_blocks { @@ -110,12 +764,12 @@ index 727a58a31..cc4c8e77e 100644 + ); + } } - + #[inline] @@ -869,6 +880,18 @@ impl ProtoChunk { let volume = *noise_sampler.volume(); let densities = noise_sampler.sample_density(); - + + let height = self.height as usize; + let mut blocks = vec![BlockStateId::AIR; CHUNK_AREA * height]; + for x in 0..16 { @@ -141,10 +795,150 @@ index 727a58a31..cc4c8e77e 100644 + BlockPalette::from_fn(|x, y, z| blocks[(x * height + section * 16 + y) * 16 + z]); + } } - + pub fn spawn_mobs(cache: &mut T, block_registry: &dyn WorldPortalExt) { +diff --git a/crates/pumpkin-world/src/generation/structure/placement.rs b/crates/pumpkin-world/src/generation/structure/placement.rs +index 250263ff..c96f1ad3 100644 +--- a/crates/pumpkin-world/src/generation/structure/placement.rs ++++ b/crates/pumpkin-world/src/generation/structure/placement.rs +@@ -10,7 +10,8 @@ use pumpkin_util::{ + }, + }; + use std::f64::consts::PI; +-use std::sync::OnceLock; ++use std::sync::atomic::{AtomicUsize, Ordering}; ++use std::sync::{Arc, OnceLock}; + + use crate::ProtoChunk; + use dashmap::DashMap; +@@ -31,6 +32,7 @@ pub struct GlobalStructureCache { + /// world seed, so it is computed once here instead of being recomputed for every + /// surrounding chunk whose structure references overlap it. + structure_starts: OnceLock>>, ++ inserted_starts: AtomicUsize, + } + impl GlobalStructureCache { + /// Creates a new, empty global structure cache. +@@ -39,6 +41,7 @@ impl GlobalStructureCache { + Self { + stronghold_chunks: OnceLock::new(), + structure_starts: OnceLock::new(), ++ inserted_starts: AtomicUsize::new(0), + } + } + +@@ -66,8 +69,29 @@ impl GlobalStructureCache { + return cached.value().clone(); + } + let computed = compute(); +- cache.insert((key, chunk_x, chunk_z), computed.clone()); +- computed ++ // Concurrent requests must share the same collector: generation deduplicates ++ // structure references by Arc identity. ++ let result = cache ++ .entry((key, chunk_x, chunk_z)) ++ .or_insert(computed) ++ .clone(); ++ if self.inserted_starts.fetch_add(1, Ordering::Relaxed) % 256 == 255 && cache.len() > 2048 { ++ self.evict_unused_starts(); ++ } ++ result ++ } ++ ++ fn evict_unused_starts(&self) { ++ if let Some(cache) = self.structure_starts.get() { ++ // Active generation keeps its own Arc. Never replace a collector that ++ // another chunk can still use; idle placements can be recomputed from ++ // their world seed and start coordinates. ++ cache.retain(|_, position| { ++ position ++ .as_ref() ++ .is_some_and(|position| Arc::strong_count(&position.collector) > 1) ++ }); ++ } + } + + /// Retrieves the list of chunk coordinates for Concentric Ring structures. +@@ -450,3 +474,77 @@ mod tests { + )); + } + } ++ ++#[cfg(test)] ++mod cache_tests { ++ use super::*; ++ use crate::generation::structure::structures::StructurePiecesCollector; ++ use pumpkin_util::math::position::BlockPos; ++ use std::sync::Mutex; ++ ++ fn position() -> StructurePosition { ++ StructurePosition { ++ start_pos: BlockPos::new(1, 2, 3), ++ collector: Arc::new(Mutex::new(StructurePiecesCollector::new())), ++ } ++ } ++ ++ #[test] ++ fn exploration_releases_idle_entries_but_preserves_active_collectors() { ++ let cache = GlobalStructureCache::new(); ++ let active = cache ++ .get_or_compute_structure_start(StructureKeys::Monument, -1, -1, || Some(position())) ++ .unwrap(); ++ for x in 0..10_000 { ++ assert!( ++ cache ++ .get_or_compute_structure_start(StructureKeys::Monument, x, 0, || None) ++ .is_none() ++ ); ++ } ++ assert!(cache.structure_starts.get().unwrap().len() <= 2304); ++ cache.evict_unused_starts(); ++ let reused = cache ++ .get_or_compute_structure_start(StructureKeys::Monument, -1, -1, || { ++ panic!("active collector was evicted") ++ }) ++ .unwrap(); ++ assert!(Arc::ptr_eq(&active.collector, &reused.collector)); ++ drop(reused); ++ drop(active); ++ cache.evict_unused_starts(); ++ assert!(cache.structure_starts.get().unwrap().is_empty()); ++ let rebuilt = cache ++ .get_or_compute_structure_start(StructureKeys::Monument, -1, -1, || Some(position())) ++ .unwrap(); ++ assert_eq!(rebuilt.start_pos, BlockPos::new(1, 2, 3)); ++ } ++ ++ #[test] ++ fn concurrent_misses_return_one_canonical_collector() { ++ let cache = GlobalStructureCache::new(); ++ let barrier = std::sync::Barrier::new(2); ++ std::thread::scope(|scope| { ++ let a = scope.spawn(|| { ++ cache ++ .get_or_compute_structure_start(StructureKeys::Monument, 0, 0, || { ++ barrier.wait(); ++ Some(position()) ++ }) ++ .unwrap() ++ }); ++ let b = scope.spawn(|| { ++ cache ++ .get_or_compute_structure_start(StructureKeys::Monument, 0, 0, || { ++ barrier.wait(); ++ Some(position()) ++ }) ++ .unwrap() ++ }); ++ assert!(Arc::ptr_eq( ++ &a.join().unwrap().collector, ++ &b.join().unwrap().collector ++ )); ++ }); ++ } ++} diff --git a/crates/pumpkin-world/src/generation/structure/structures/end_city/layout.rs b/crates/pumpkin-world/src/generation/structure/structures/end_city/layout.rs -index a7f29d8dc..ca0dc47dd 100644 +index a7f29d8d..ca0dc47d 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/end_city/layout.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/end_city/layout.rs @@ -663,7 +663,7 @@ mod tests { @@ -157,7 +951,7 @@ index a7f29d8dc..ca0dc47dd 100644 Rotation::None, Rotation::Clockwise90, diff --git a/crates/pumpkin-world/src/generation/structure/structures/end_city/mod.rs b/crates/pumpkin-world/src/generation/structure/structures/end_city/mod.rs -index 8089b3e69..9bb82ed9c 100644 +index 8089b3e6..9bb82ed9 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/end_city/mod.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/end_city/mod.rs @@ -98,7 +98,7 @@ impl EndCityTemplatePiece { @@ -176,36 +970,36 @@ index 8089b3e69..9bb82ed9c 100644 - Self::place_block_entity(chunk, palette, block.nbt.as_ref(), position, random); + Self::place_block_entity(chunk, palette, block.nbt.as_deref(), position, random); } - + - for block in &self.descriptor.template.blocks { + for block in self.descriptor.template.blocks.iter() { let palette = &self.descriptor.template.palette[block.state as usize]; if palette.name != "minecraft:structure_block" { continue; diff --git a/crates/pumpkin-world/src/generation/structure/structures/igloo.rs b/crates/pumpkin-world/src/generation/structure/structures/igloo.rs -index 9130b4626..363993029 100644 +index 9130b462..36399302 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/igloo.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/igloo.rs @@ -190,7 +190,7 @@ impl IglooPiece { &mut context_rng, ); - + - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + let local_pos = @@ -226,7 +226,7 @@ impl IglooPiece { continue; } - + - let mut block_entity_nbt = block.nbt.clone(); + let mut block_entity_nbt = block.nbt.as_deref().cloned(); let placed_entry = palette_entry.clone(); - + let Some(state) = BlockStateResolver::resolve(&placed_entry, rotation, mirror) else { diff --git a/crates/pumpkin-world/src/generation/structure/structures/jigsaw.rs b/crates/pumpkin-world/src/generation/structure/structures/jigsaw.rs -index ef4f28879..17adbfbf0 100644 +index ef4f2887..17adbfbf 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/jigsaw.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/jigsaw.rs @@ -310,7 +310,7 @@ impl PoolElementKind { @@ -218,7 +1012,7 @@ index ef4f28879..17adbfbf0 100644 block, &template.palette[block.state as usize], diff --git a/crates/pumpkin-world/src/generation/structure/structures/mansion/mod.rs b/crates/pumpkin-world/src/generation/structure/structures/mansion/mod.rs -index e4cb49032..53b67b4a6 100644 +index e4cb4903..53b67b4a 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/mansion/mod.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/mansion/mod.rs @@ -276,7 +276,7 @@ impl MansionTemplatePiece { @@ -240,68 +1034,68 @@ index e4cb49032..53b67b4a6 100644 random, ); diff --git a/crates/pumpkin-world/src/generation/structure/structures/nether_fossil.rs b/crates/pumpkin-world/src/generation/structure/structures/nether_fossil.rs -index 73047f6d6..da14d13c1 100644 +index 73047f6d..da14d13c 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/nether_fossil.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/nether_fossil.rs @@ -182,10 +182,10 @@ impl NetherFossilPiece { &mut context_rng, ); - + - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + - let mut block_entity_nbt = block.nbt.clone(); + let mut block_entity_nbt = block.nbt.as_deref().cloned(); let placed_entry = palette_entry.clone(); - + let Some(state) = BlockStateResolver::resolve(&placed_entry, rotation, mirror) else { diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_ruin.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_ruin.rs -index 5823f93bd..abe91eba9 100644 +index 5823f93b..abe91eba 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/ocean_ruin.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/ocean_ruin.rs @@ -459,7 +459,7 @@ impl StructurePieceBase for OceanRuinPiece { - + let mut suspicious_count = 0; - + - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + if palette_entry.name == "minecraft:structure_void" diff --git a/crates/pumpkin-world/src/generation/structure/structures/ruined_portal.rs b/crates/pumpkin-world/src/generation/structure/structures/ruined_portal.rs -index 404a63b21..a486ec2a9 100644 +index 404a63b2..a486ec2a 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/ruined_portal.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/ruined_portal.rs @@ -293,10 +293,10 @@ impl RuinedPortalPiece { &mut context_rng, ); - + - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + - let mut block_entity_nbt = block.nbt.clone(); + let mut block_entity_nbt = block.nbt.as_deref().cloned(); let placed_entry = palette_entry.clone(); - + let Some(mut state) = BlockStateResolver::resolve(&placed_entry, rotation, mirror) diff --git a/crates/pumpkin-world/src/generation/structure/structures/shipwreck.rs b/crates/pumpkin-world/src/generation/structure/structures/shipwreck.rs -index 53ced600c..029775470 100644 +index 53ced600..02977547 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/shipwreck.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/shipwreck.rs @@ -307,7 +307,7 @@ impl StructurePieceBase for ShipwreckPiece { - + // Place non-marker blocks let box_limit = self.piece.bounding_box; - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + if palette_entry.name == "minecraft:structure_block" @@ -393,7 +393,7 @@ impl StructurePieceBase for ShipwreckPiece { } - + // Process data markers - for block in &self.template.blocks { + for block in self.template.blocks.iter() { @@ -309,43 +1103,43 @@ index 53ced600c..029775470 100644 if palette_entry.name == "minecraft:structure_block" && let Some(nbt) = &block.nbt diff --git a/crates/pumpkin-world/src/generation/structure/template/mod.rs b/crates/pumpkin-world/src/generation/structure/template/mod.rs -index 5e1f6636c..aa7938006 100644 +index 5e1f6636..aa793800 100644 --- a/crates/pumpkin-world/src/generation/structure/template/mod.rs +++ b/crates/pumpkin-world/src/generation/structure/template/mod.rs @@ -117,7 +117,7 @@ pub fn place_template_with_options( let mut context_rng = LegacyRand::from_seed(hash_block_pos(world_x, origin.y, world_z) as u64); let mut context = processor::ProcessorContext::new(origin, processors, &mut context_rng); - + - for block in &template.blocks { + for block in template.blocks.iter() { let palette_entry = &template.palette[block.state as usize]; - + // Structure blocks are data markers. @@ -125,7 +125,7 @@ pub fn place_template_with_options( continue; } - + - let mut block_entity_nbt = block.nbt.clone(); + let mut block_entity_nbt = block.nbt.as_deref().cloned(); let mut placed_entry = palette_entry.clone(); - + // Jigsaw blocks are replaced during template processing, before block entities are diff --git a/crates/pumpkin-world/src/generation/structure/template/structure_template.rs b/crates/pumpkin-world/src/generation/structure/template/structure_template.rs -index 211c79e90..bd0406775 100644 +index 211c79e9..bd040677 100644 --- a/crates/pumpkin-world/src/generation/structure/template/structure_template.rs +++ b/crates/pumpkin-world/src/generation/structure/template/structure_template.rs @@ -4,6 +4,7 @@ //! into a runtime representation with palettes, entities, block info, and transformations. - + use std::io::Cursor; +use std::sync::Arc; - + use pumpkin_data::{Mirror, Rotation}; use pumpkin_nbt::{compound::NbtCompound, nbt_compress::read_gzip_compound_tag, tag::NbtTag}; @@ -195,9 +196,9 @@ pub struct StructureTemplate { pub size: Vector3, pub author: String, - + - // Backward-compatible fields - pub palette: Vec, - pub blocks: Vec, @@ -354,11 +1148,11 @@ index 211c79e90..bd0406775 100644 + pub blocks: Arc<[TemplateBlock]>, pub entities: Vec, } - + @@ -332,6 +333,20 @@ pub struct StructureBlockInfo { pub nbt: Option, } - + +/// A borrowed view over an indexed template block. Only callers producing a +/// transformed/mutable block need to materialize owned strings and NBT. +pub struct StructureBlockInfoRef<'a> { @@ -385,7 +1179,7 @@ index 211c79e90..bd0406775 100644 + entries: Arc<[PaletteEntry]>, cached_jigsaws: Option>, } - + impl Palette { #[must_use] - pub const fn new(blocks: Vec) -> Self { @@ -405,7 +1199,7 @@ index 211c79e90..bd0406775 100644 cached_jigsaws: None, } } - + #[must_use] - pub fn blocks(&self) -> &[StructureBlockInfo] { - &self.blocks @@ -420,7 +1214,7 @@ index 211c79e90..bd0406775 100644 + nbt: block.nbt.as_deref(), + } } - + #[must_use] pub fn blocks_by_name(&self, filter: &str) -> Vec { - self.blocks @@ -431,7 +1225,7 @@ index 211c79e90..bd0406775 100644 + .map(StructureBlockInfoRef::into_owned) .collect() } - + pub fn jigsaws(&mut self) -> &[JigsawBlockInfo] { - self.cached_jigsaws.get_or_insert_with(|| { - self.blocks @@ -451,10 +1245,10 @@ index 211c79e90..bd0406775 100644 + self.cached_jigsaws.as_deref().unwrap() } } - + @@ -507,11 +542,11 @@ impl SimplePalette { } - + /// A single block placement in the template (legacy format). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] @@ -464,7 +1258,7 @@ index 211c79e90..bd0406775 100644 - pub nbt: Option, + pub nbt: Option>, } - + /// An entity to spawn when placing the template (legacy format). @@ -842,7 +877,7 @@ impl StructureTemplate { result.push(StructureBlockInfo::new( @@ -582,7 +1376,7 @@ index 211c79e90..bd0406775 100644 + }); Ok(()) } - + - fn add_to_lists( - info: StructureBlockInfo, - full_block_list: &mut Vec, @@ -604,7 +1398,7 @@ index 211c79e90..bd0406775 100644 @@ -1066,48 +1091,13 @@ impl StructureTemplate { || stripped.contains("scaffolding")) } - + - fn build_info_list( - mut full_block_list: Vec, - mut block_entities_list: Vec, @@ -652,12 +1446,12 @@ index 211c79e90..bd0406775 100644 + self.palette = Arc::default(); + self.blocks = Arc::default(); } - + self.entities = self @@ -1140,7 +1130,7 @@ impl StructureTemplate { let mut block_list = Vec::new(); let main_palette_blocks = self.palettes[0].blocks(); - + - for (i, block_info) in main_palette_blocks.iter().enumerate() { + for (i, block_info) in main_palette_blocks.enumerate() { let mut block_tag = NbtCompound::new(); @@ -666,14 +1460,14 @@ index 211c79e90..bd0406775 100644 @@ -1152,14 +1142,15 @@ impl StructureTemplate { let id = palettes[0].id_for(block_info.state.clone()); block_tag.put_int(Self::BLOCK_TAG_STATE, id as i32); - + - if let Some(nbt) = &block_info.nbt { + if let Some(nbt) = block_info.nbt { block_tag.put_compound(Self::BLOCK_TAG_NBT, nbt.clone()); } - + block_list.push(NbtTag::Compound(block_tag)); - + for p in 1..self.palettes.len() { - let state = self.palettes[p].blocks()[i].state.clone(); + let state = @@ -683,17 +1477,17 @@ index 211c79e90..bd0406775 100644 } @@ -1263,7 +1254,7 @@ impl StructureTemplate { } - + #[must_use] - pub const fn block_count(&self) -> usize { + pub fn block_count(&self) -> usize { self.blocks.len() } - + @@ -1282,6 +1273,102 @@ impl StructureTemplate { mod tests { use super::*; - + + fn assert_same_blocks(before: &Palette, after: &Palette) { + assert_eq!(before.blocks.len(), after.blocks.len()); + for (index, (a, b)) in before.blocks().zip(after.blocks()).enumerate() { @@ -794,24 +1588,87 @@ index 211c79e90..bd0406775 100644 fn palette_entry_creation() { let entry = PaletteEntry::new("minecraft:stone".to_string()); diff --git a/crates/pumpkin-world/src/generation/structure/template/template_piece.rs b/crates/pumpkin-world/src/generation/structure/template/template_piece.rs -index e3f9357b6..68c33f67a 100644 +index e3f9357b..68c33f67 100644 --- a/crates/pumpkin-world/src/generation/structure/template/template_piece.rs +++ b/crates/pumpkin-world/src/generation/structure/template/template_piece.rs @@ -176,7 +176,7 @@ impl TemplatePiece { .get_bounding_box(&self.place_settings, self.template_position); let box_limit = self.piece.bounding_box; - + - for block in &self.template.blocks { + for block in self.template.blocks.iter() { let palette_entry = &self.template.palette[block.state as usize]; - + // Structure blocks are data markers @@ -185,7 +185,7 @@ impl TemplatePiece { } - + let mut placed_entry = palette_entry.clone(); - let mut block_entity_nbt = block.nbt.clone(); + let mut block_entity_nbt = block.nbt.as_deref().cloned(); - + // Jigsaw blocks are replaced with final_state if palette_entry.name == "minecraft:jigsaw" { +diff --git a/crates/pumpkin-world/src/lighting/engine.rs b/crates/pumpkin-world/src/lighting/engine.rs +index 47318e43..d28e77f3 100644 +--- a/crates/pumpkin-world/src/lighting/engine.rs ++++ b/crates/pumpkin-world/src/lighting/engine.rs +@@ -786,6 +786,10 @@ impl LightEngine { + + self.block_light.clear(); + self.sky_light.clear(); ++ let light = &mut cache.get_center_chunk_mut().light; ++ for section in light.sky_light.iter_mut().chain(&mut light.block_light) { ++ section.compact(); ++ } + } + + pub fn update_block_light( +diff --git a/crates/pumpkin/src/entity/ai/pathfinder/binary_heap.rs b/crates/pumpkin/src/entity/ai/pathfinder/binary_heap.rs +index 5bf1caa5..e6990a67 100644 +--- a/crates/pumpkin/src/entity/ai/pathfinder/binary_heap.rs ++++ b/crates/pumpkin/src/entity/ai/pathfinder/binary_heap.rs +@@ -19,8 +19,8 @@ pub struct BinaryHeap { + impl BinaryHeap { + #[must_use] + pub fn new() -> Self { +- let mut heap = Vec::with_capacity(1024); +- heap.push(None); ++ // Most navigators are idle. Reserve nodes when a search actually needs them. ++ let heap = vec![None]; + + Self { + heap, +@@ -238,3 +238,32 @@ impl Default for BinaryHeap { + Self::new() + } + } ++ ++#[cfg(test)] ++mod tests { ++ use super::{BinaryHeap, Node}; ++ use pumpkin_util::math::position::BlockPos; ++ ++ #[test] ++ fn idle_heaps_grow_without_changing_pathfinding_order() { ++ let mut heap = BinaryHeap::new(); ++ assert!(heap.heap.capacity() < 1024); ++ assert!(heap.is_empty()); ++ for x in (0..1500).rev() { ++ let mut node = Node::new(BlockPos::new(x, 0, 0)); ++ node.f = x as f32; ++ heap.insert(node); ++ } ++ for x in 0..1500 { ++ assert_eq!(heap.pop().unwrap().pos, BlockPos::new(x, 0, 0)); ++ } ++ assert!(heap.pop().is_none()); ++ heap.clear(); ++ assert!(heap.get_heap().is_empty()); ++ let mut node = Node::new(BlockPos::new(42, 0, 0)); ++ node.f = 1.0; ++ heap.insert(node); ++ assert_eq!(heap.drain().len(), 1); ++ assert!(heap.is_empty()); ++ } ++} diff --git a/patches/wasm-bindgen-emscripten-closures.patch b/patches/wasm-bindgen-emscripten-closures.patch deleted file mode 100644 index 88700c3..0000000 --- a/patches/wasm-bindgen-emscripten-closures.patch +++ /dev/null @@ -1,30 +0,0 @@ -Subject: [PATCH] Escape Emscripten closure finalizer postsets - -The closure finalizer is emitted inside an Emscripten JavaScript string. -Interpolating its destructor expression directly can leave quotes and -backslashes unescaped, producing invalid generated JavaScript. - -Build the complete postset first, then format it as an escaped string -literal. This preserves the finalizer expression and the fallback used -when FinalizationRegistry is unavailable. - -Base-commit: 4b69f3b3ba4212c857be6854f77fa5aec8b62871 - ---- -diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs -index 4486b980..06d4a28a 100644 ---- a/crates/cli-support/src/js/mod.rs -+++ b/crates/cli-support/src/js/mod.rs -@@ -4326,10 +4326,11 @@ if (require('worker_threads').isMainThread) {{ - }; - - if matches!(self.config.mode, OutputMode::Emscripten) { -+ let postset = format!("CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') ? {{ register: () => {{}}, unregister: () => {{}} }} : new FinalizationRegistry({prevent_stale});"); - format!( - "addToLibrary({{ - $CLOSURE_DTORS: {{}}, -- $CLOSURE_DTORS__postset: \"CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') ? {{ register: () => {{}}, unregister: () => {{}} }} : new FinalizationRegistry({prevent_stale});\" -+ $CLOSURE_DTORS__postset: {postset:?} - }});\n" - ) - } else { diff --git a/patches/wasm-streams-rlib.patch b/patches/wasm-streams-rlib.patch deleted file mode 100644 index 8afa042..0000000 --- a/patches/wasm-streams-rlib.patch +++ /dev/null @@ -1,25 +0,0 @@ -Subject: [PATCH] Build wasm-streams as an rlib for Emscripten - -The embedded application links wasm-streams into its final WebAssembly -module. Declaring both rlib and cdylib also makes Cargo attempt a standalone -library link, which is incompatible with this static Emscripten build. - -Build only the rlib so the application supplies the final link and host -bindings. The Rust stream adapter API is unchanged. - -Base-commit: 35665f7b1da830b5ac51b4c6c3ff13f5c1a09ccb - ---- -diff --git a/Cargo.toml b/Cargo.toml -index c57b1c6..52f6805 100644 ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -16,7 +16,7 @@ exclude = [ - ] - - [lib] --crate-type = ["cdylib", "rlib"] -+crate-type = ["rlib"] - - [dependencies] - js-sys = "^0.3.85" diff --git a/patches/worker-fs-mount-open-mode.patch b/patches/worker-fs-mount-open-mode.patch new file mode 100644 index 0000000..3fd1d4b --- /dev/null +++ b/patches/worker-fs-mount-open-mode.patch @@ -0,0 +1,30 @@ +Subject: [PATCH] Mask numeric open/fchmod modes to their permission bits + +Node's fs accepts a numeric mode with type bits set (parseFileMode keeps +mode & 0o7777); worker-fs-mount's fd-sync rejected anything above 0o7777 +with ERR_OUT_OF_RANGE. Emscripten's FS.open passes S_IFREG | perms to its +backend, so file creation through NODERAWFS on a mounted path failed. + +Applied to the installed package by `npm install` (postinstall) until +danlapid/worker-fs-mount ships the fix (guybedford/worker-fs-mount +fix/open-mode-mask). + +Base-package: worker-fs-mount@0.2.0 + +--- +--- a/node_modules/worker-fs-mount/dist/fd-sync.js ++++ b/node_modules/worker-fs-mount/dist/fd-sync.js +@@ -36,10 +36,12 @@ + } + return value; + } ++// Like Node's parseFileMode: a numeric mode keeps only its permission bits, so ++// callers passing a full st_mode (e.g. S_IFREG | 0o644) are accepted. + function permissionMode(mode) { + if (typeof mode === 'string' && !/^[0-7]+$/.test(mode)) + fail('ERR_INVALID_ARG_VALUE', 'mode'); +- return integer(typeof mode === 'string' ? Number.parseInt(mode, 8) : mode, 'mode', 0o7777); ++ return integer(typeof mode === 'string' ? Number.parseInt(mode, 8) : mode, 'mode') & 0o7777; + } + function options(flags, mode) { + const c = realFs.constants; diff --git a/patches/workers-rs-emscripten-toolchain.patch b/patches/workers-rs-emscripten-toolchain.patch deleted file mode 100644 index 425e670..0000000 --- a/patches/workers-rs-emscripten-toolchain.patch +++ /dev/null @@ -1,184 +0,0 @@ -Subject: [PATCH] Embed workers-rs with the hosted Emscripten runtime - -The embedding host creates each Emscripten module and owns its lifetime. -The SDK's standalone wasm-bindgen loader and recovery hook cannot -reinitialize those instances, so leave initialization and panic handling -to the host on this target. - -Expose an Emscripten-only adapter that drives promise exports on one hosted -Tokio runtime per module and converts injected sockets into SDK streams. -It uses the hosted-runtime Tokio fork selected by the application. Align -the wasm-bindgen, js-sys, and web-sys version family with the pinned -Emscripten-enabled wasm-bindgen CLI so their generated bindings agree. - -Flush outstanding socket writes before closing the WritableStream. This -satisfies AsyncWrite's shutdown contract and releases the writer lock -before close, including when a pending write completes asynchronously. - -Base-commit: 7db011ec97658a5d907f3e3102028ce86c044f19 - ---- -diff --git a/Cargo.toml b/Cargo.toml -index 4e08234..9888881 100644 ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -28,7 +28,7 @@ chrono = { version = "0.4.41", default-features = false, features = [ - futures-channel = "0.3.31" - futures-util = { version = "0.3.31", default-features = false } - http = "1.3" --js-sys = { version = "0.3.104" } -+js-sys = { version = "0.3.103" } - serde = { version = "1.0.164", features = ["derive"] } - strum = { version = "0.28", features = ["derive"] } - serde_json = "1.0.140" -@@ -37,14 +37,14 @@ syn = "3.0.3" - trybuild = "1.0" - proc-macro2 = "1.0.60" - quote = "1.0.28" --wasm-bindgen = { version = "0.2.127" } --wasm-bindgen-cli-support = { version = "0.2.127" } --wasm-bindgen-futures = { version = "0.4.77" } --wasm-bindgen-macro-support = { version = "0.2.127" } --wasm-bindgen-shared = { version = "0.2.127" } --wasm-bindgen-test = { version = "0.3.77" } -+wasm-bindgen = { version = "0.2.126" } -+wasm-bindgen-cli-support = { version = "0.2.126" } -+wasm-bindgen-futures = { version = "0.4.76" } -+wasm-bindgen-macro-support = { version = "0.2.126" } -+wasm-bindgen-shared = { version = "0.2.126" } -+wasm-bindgen-test = { version = "0.3.76" } - wasm-streams = { version = "0.6.0" } --web-sys = { version = "0.3.104", features = [ -+web-sys = { version = "0.3.103", features = [ - "AbortController", - "AbortSignal", - "BinaryType", -@@ -106,11 +106,11 @@ opt-level = "z" - # These are local patches we use to test against local wasm bindgen - # We always align on the exact stable wasm bindgen version for releases - [patch.crates-io] --js-sys = { version = "0.3.104", path = './wasm-bindgen/crates/js-sys' } --wasm-bindgen = { version = "0.2.127", path = './wasm-bindgen' } --wasm-bindgen-cli-support = { version = "0.2.127", path = "./wasm-bindgen/crates/cli-support" } --wasm-bindgen-futures = { version = "0.4.77", path = './wasm-bindgen/crates/futures' } --wasm-bindgen-macro-support = { version = "0.2.127", path = "./wasm-bindgen/crates/macro-support" } --wasm-bindgen-shared = { version = "0.2.127", path = "./wasm-bindgen/crates/shared" } --wasm-bindgen-test = { version = "0.3.77", path = "./wasm-bindgen/crates/test" } --web-sys = { version = "0.3.104", path = './wasm-bindgen/crates/web-sys' } -+js-sys = { version = "0.3.103", path = './wasm-bindgen/crates/js-sys' } -+wasm-bindgen = { version = "0.2.126", path = './wasm-bindgen' } -+wasm-bindgen-cli-support = { version = "0.2.126", path = "./wasm-bindgen/crates/cli-support" } -+wasm-bindgen-futures = { version = "0.4.76", path = './wasm-bindgen/crates/futures' } -+wasm-bindgen-macro-support = { version = "0.2.126", path = "./wasm-bindgen/crates/macro-support" } -+wasm-bindgen-shared = { version = "0.2.126", path = "./wasm-bindgen/crates/shared" } -+wasm-bindgen-test = { version = "0.3.76", path = "./wasm-bindgen/crates/test" } -+web-sys = { version = "0.3.103", path = './wasm-bindgen/crates/web-sys' } -diff --git a/worker/Cargo.toml b/worker/Cargo.toml -index 573a8f2..d286f02 100644 ---- a/worker/Cargo.toml -+++ b/worker/Cargo.toml -@@ -45,6 +45,9 @@ web-sys.workspace = true - worker-macros.workspace = true - worker-sys.workspace = true - -+[target.'cfg(target_os = "emscripten")'.dependencies] -+tokio = { version = "1.28", default-features = false, features = ["rt", "time", "net"] } -+ - [features] - queue = ["worker-macros/queue", "worker-sys/queue"] - d1 = ["worker-sys/d1"] -diff --git a/worker/src/lib.rs b/worker/src/lib.rs -index bc2cdc9..72097fe 100644 ---- a/worker/src/lib.rs -+++ b/worker/src/lib.rs -@@ -223,6 +223,11 @@ mod container; - mod context; - mod cors; - pub mod crypto; -+#[cfg(target_os = "emscripten")] -+pub mod emscripten; -+// The embedding host owns Emscripten module instances and panic handling. -+// SDK reinitialization targets wasm-bindgen's standalone loader instead. -+#[cfg(not(target_os = "emscripten"))] - mod init; - // Require pub module for macro export - #[cfg(feature = "d1")] -diff --git a/worker/src/socket.rs b/worker/src/socket.rs -index bc46cd4..a32d2bd 100644 ---- a/worker/src/socket.rs -+++ b/worker/src/socket.rs -@@ -289,6 +289,12 @@ impl AsyncWrite for Socket { - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { -+ // An outstanding write owns the stream's writer lock. Finish it before -+ // closing the WritableStream, as required by AsyncWrite::poll_shutdown. -+ match self.as_mut().poll_flush(cx) { -+ Poll::Ready(Ok(())) => {} -+ pending_or_error => return pending_or_error, -+ } - fn handle_future(cx: &mut Context<'_>, mut fut: JsFuture) -> (Closing, Poll>) { - match fut.poll_unpin(cx) { - Poll::Pending => (Closing::Pending(fut), Poll::Pending), -diff --git a/worker/src/emscripten.rs b/worker/src/emscripten.rs -new file mode 100644 -index 0000000..e3516ed ---- /dev/null -+++ b/worker/src/emscripten.rs -@@ -0,0 +1,56 @@ -+//! Promise exports driven by the Emscripten hosted Tokio runtime. -+//! -+//! Each modularized Wasm instance owns its runtime. This adapter keeps Tokio -+//! timers and I/O on that executor when JavaScript invokes an async Rust export. -+//! It requires the Emscripten-enabled Tokio toolchain. -+ -+use std::future::Future; -+use wasm_bindgen::prelude::*; -+ -+thread_local! { -+ static RUNTIME: tokio::runtime::HostedRuntime = tokio::runtime::Builder::new_current_thread() -+ .enable_all() -+ .build_hosted_event_loop_runtime() -+ .expect("hosted runtime"); -+} -+ -+/// All exports in one wasm instance use the same runtime. Emscripten's module -+/// factory gives each world its own wasm memory, thread locals, and callbacks. -+pub fn future_to_promise(future: F) -> js_sys::Promise -+where -+ F: Future> + 'static, -+{ -+ // Tokio catches future panics and delivers JoinError to the completion callback. -+ let mut future = std::panic::AssertUnwindSafe(Some(future)); -+ js_sys::Promise::new(&mut move |resolve, reject| { -+ let future = future.take().expect("Promise executor runs once"); -+ RUNTIME.with(|runtime| { -+ runtime.schedule(future, move |result| { -+ let result = result -+ .unwrap_or_else(|error| Err(js_sys::Error::new(&error.to_string()).into())); -+ match result { -+ Ok(value) => { -+ let _ = resolve.call1(&JsValue::UNDEFINED, &value); -+ } -+ Err(error) => { -+ let _ = reject.call1(&JsValue::UNDEFINED, &error); -+ } -+ } -+ }); -+ runtime.drive(); -+ }); -+ }) -+} -+ -+pub fn js_error(error: impl std::fmt::Display) -> JsValue { -+ js_sys::Error::new(&error.to_string()).into() -+} -+ -+/// Convert the socket passed to an Emscripten export into the SDK's async stream. -+pub fn socket_from_value(value: JsValue) -> Result { -+ use crate::FromSocket; -+ crate::Socket::from_raw(value.unchecked_into()).map_err(|error| { -+ let error: Box = error.into(); -+ js_error(error) -+ }) -+} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7fced56..6f67d1c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2026-07-20" +channel = "beta" profile = "minimal" targets = ["wasm32-unknown-emscripten"] diff --git a/scripts/build.sh b/scripts/build.sh index 52c7d3f..be1e37e 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,4 +4,4 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" require_node require_toolchain cd "$REPO" -cargo build --locked --release --bin pumpkin-do +worker-build --emscripten --release -- --locked diff --git a/scripts/common.sh b/scripts/common.sh index acc92a8..b976991 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -4,6 +4,9 @@ set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORK="$REPO/.work" NODE="${NODE:-node}" +# Setup provisions the patched Pumpkin checkout and the worker-build CLI under +# .work/; worker-build provisions Emscripten and the wasm-bindgen CLI itself. +BIN="$WORK/bin" require_node() { local major @@ -15,17 +18,16 @@ require_node() { } require_toolchain() { - if [ ! -f "$WORK/emscripten/.emscripten_cf" ] || - [ ! -x "$WORK/bin/wasm-bindgen" ] || - [ ! -f "$WORK/pumpkin/crates/pumpkin/Cargo.toml" ] || - [ ! -f "$WORK/workers-rs/worker/Cargo.toml" ]; then + if [ ! -x "$BIN/worker-build" ] || + [ ! -f "$WORK/pumpkin/crates/pumpkin/Cargo.toml" ]; then echo "error: run bash scripts/setup.sh first." >&2 exit 1 fi - export EM_CONFIG="$WORK/emscripten/.emscripten_cf" - export PATH="$WORK/emscripten:$WORK/bin:$PATH" - export CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER="$WORK/emscripten/emcc" + export PATH="$BIN:$PATH" export CARGO_TARGET_DIR="$REPO/target/workers" + # rustc's LLVM const emission recurses deeply on pumpkin-data's generated tables + # and overflows its default 8 MiB compile-thread stack on current toolchains. + export RUST_MIN_STACK="${RUST_MIN_STACK:-268435456}" } require_packages() { diff --git a/scripts/patch-packages.mjs b/scripts/patch-packages.mjs new file mode 100644 index 0000000..d202c15 --- /dev/null +++ b/scripts/patch-packages.mjs @@ -0,0 +1,25 @@ +// Applies patches/*.patch that target node_modules to the installed packages. +// Runs from npm's postinstall; idempotent, so reinstalls and repeated runs are safe. +import { execFileSync } from 'node:child_process'; +import { readdirSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repo = fileURLToPath(new URL('../', import.meta.url)); +const patches = readdirSync(path.join(repo, 'patches')) + .filter(name => name.endsWith('.patch')) + .map(name => path.join(repo, 'patches', name)) + .filter(file => /^\+\+\+ b\/node_modules\//m.test(readFileSync(file, 'utf8'))); + +for (const patch of patches) { + const apply = (...flags) => + execFileSync('git', ['apply', ...flags, patch], { cwd: repo, stdio: ['ignore', 'ignore', 'pipe'] }); + try { + apply('--reverse', '--check'); + console.log(` already applied: ${path.basename(patch)}`); + continue; + } catch {} + apply('--check'); + apply(); + console.log(` applied: ${path.basename(patch)}`); +} diff --git a/scripts/setup.sh b/scripts/setup.sh index a640927..ae1901f 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Clone pinned sources, apply dependency patches, and build the host toolchain. +# Clone pinned sources, apply dependency patches, and provision the toolchain. set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/common.sh" @@ -12,7 +12,7 @@ mkdir -p "$WORK" checkout() { # name url branch commit local name="$1" url="$2" branch="$3" commit="$4" dir="$WORK/$1" - if [ ! -d "$dir/.git" ]; then + if [ ! -d "$dir/.git" ] && [ ! -f "$dir/.git" ]; then if [ -e "$dir" ]; then echo "error: $dir exists but is not a Git checkout." >&2 exit 1 @@ -20,7 +20,8 @@ checkout() { # name url branch commit git clone --filter=blob:none --branch "$branch" --single-branch "$url" "$dir" fi if [ "$(git -C "$dir" rev-parse HEAD)" != "$commit" ]; then - if [ -n "$(git -C "$dir" status --porcelain)" ]; then + # Submodule pointers move with the pin and are not local changes. + if [ -n "$(git -C "$dir" status --porcelain --ignore-submodules=all)" ]; then echo "error: refusing to repin modified checkout $dir; preserve your changes first." >&2 exit 1 fi @@ -32,6 +33,29 @@ checkout() { # name url branch commit echo " $name @ $commit" } +checkout_tag() { # name url tag + local name="$1" url="$2" tag="$3" dir="$WORK/$1" + if [ ! -d "$dir/.git" ] && [ ! -f "$dir/.git" ]; then + if [ -e "$dir" ]; then + echo "error: $dir exists but is not a Git checkout." >&2 + exit 1 + fi + git clone --filter=blob:none --branch "$tag" --single-branch "$url" "$dir" + fi + if ! git -C "$dir" cat-file -e "refs/tags/$tag^{commit}" 2>/dev/null; then + git -C "$dir" fetch --filter=blob:none origin "tag" "$tag" --no-tags + fi + if [ "$(git -C "$dir" rev-parse HEAD)" != "$(git -C "$dir" rev-parse "refs/tags/$tag^{commit}")" ]; then + # Submodule pointers move with the pin and are not local changes. + if [ -n "$(git -C "$dir" status --porcelain --ignore-submodules=all)" ]; then + echo "error: refusing to repin modified checkout $dir; preserve your changes first." >&2 + exit 1 + fi + git -C "$dir" switch --detach "refs/tags/$tag" + fi + echo " $name @ $tag ($(git -C "$dir" rev-parse --short HEAD))" +} + apply_patch() { local dir="$WORK/$1" patch="$REPO/patches/$2" if git -C "$dir" apply --reverse --check "$patch" 2>/dev/null; then @@ -43,53 +67,26 @@ apply_patch() { } echo "==> Pinned dependencies" -checkout emscripten https://github.com/guybedford/emscripten cf 21166256c4c4d73d39b3685c8973d7cbe427ce8c -checkout wasm-bindgen https://github.com/guybedford/wasm-bindgen emscripten-non-identifier-names 4b69f3b3ba4212c857be6854f77fa5aec8b62871 -checkout tokio https://github.com/guybedford/tokio emscripten-layering 7c1d4977c510866775ed6164b58b2218a6a2955b -checkout libc https://github.com/guybedford/libc libc-0.2-emscripten 4091fe0b0dc5f9c1a27bed75be1ff02bb27e756d -checkout ring https://github.com/guybedford/ring emscripten 6671f7cfbb13f249b571ffa6326275a8596e0ca2 checkout pumpkin https://github.com/Pumpkin-MC/Pumpkin master b5b9b9d7010e793806a83c495af223c67e1d35ee -checkout workers-rs https://github.com/ThomasRubini/workers-rs connect-bindings 7db011ec97658a5d907f3e3102028ce86c044f19 -checkout wasm-streams https://github.com/MattiasBuelens/wasm-streams main 35665f7b1da830b5ac51b4c6c3ff13f5c1a09ccb -apply_patch wasm-bindgen wasm-bindgen-emscripten-closures.patch apply_patch pumpkin pumpkin-emscripten.patch apply_patch pumpkin pumpkin-memory.patch -apply_patch workers-rs workers-rs-emscripten-toolchain.patch -apply_patch wasm-streams wasm-streams-rlib.patch if [ "${1:-}" = --sources-only ]; then exit 0; fi -echo "==> Pinned Rust toolchain" +echo "==> Rust toolchain" RUST_CHANNEL="$(python3 -c 'import tomllib,sys; print(tomllib.load(open(sys.argv[1],"rb"))["toolchain"]["channel"])' "$REPO/rust-toolchain.toml")" rustup toolchain install "$RUST_CHANNEL" --profile minimal --target wasm32-unknown-emscripten --no-self-update -echo "==> Emscripten frontend and Homebrew compiler backend" -EM_PREFIX="$(brew --prefix emscripten)" || { - echo "error: install the backend with 'brew install emscripten'." >&2 - exit 1 -} -# Resolve NODE before entering the checkout (it may be a relative path). -NODE_PATH="$("$NODE" -p 'process.execPath')" -export PATH="$(dirname "$NODE_PATH"):$PATH" -(cd "$WORK/emscripten" && npm ci --no-audit --no-fund && python3 bootstrap.py) -python3 - "$WORK/emscripten/.emscripten_cf" "$EM_PREFIX" "$NODE_PATH" <<'PY' -from pathlib import Path -import sys -config, prefix, node = sys.argv[1:] -Path(config).write_text( - f"LLVM_ROOT={prefix + '/libexec/llvm/bin'!r}\n" - f"BINARYEN_ROOT={prefix + '/libexec/binaryen'!r}\n" - f"NODE_JS={node!r}\n" -) -PY +echo "==> worker-build" +# The release matching the `worker` crate version in Cargo.toml. Built for the +# host, overriding the wasm target .cargo/config.toml sets. +WORKER_VERSION="$(python3 -c 'import tomllib,sys; print(tomllib.load(open(sys.argv[1],"rb"))["dependencies"]["worker"]["version"])' "$REPO/Cargo.toml")" +if [ "$("$BIN/worker-build" --version 2>/dev/null || true)" != "worker-build $WORKER_VERSION" ]; then + HOST="$(rustc "+$RUST_CHANNEL" -vV | sed -n 's/^host: //p')" + cargo "+$RUST_CHANNEL" install --force --locked --target "$HOST" worker-build --version "$WORKER_VERSION" --root "$WORK" +fi + +echo "==> Worker dependencies" +(cd "$REPO" && "$NODE" "$(dirname "$("$NODE" -p 'process.execPath')")/npm" ci --no-audit --no-fund) -echo "==> Patched wasm-bindgen CLI" -# Upstream does not commit this workspace lockfile. Preserve the proven CLI resolution. -cp "$REPO/toolchain/wasm-bindgen.Cargo.lock" "$WORK/wasm-bindgen/Cargo.lock" -# Override nested dependency toolchain files with the repository's dated nightly. -HOST_TARGET="$(rustc "+$RUST_CHANNEL" -vV | sed -n 's/^host: //p')" -(cd "$WORK/wasm-bindgen" && cargo "+$RUST_CHANNEL" build --locked --release -p wasm-bindgen-cli \ - --target "$HOST_TARGET" --target-dir "$WORK/wasm-bindgen/target") -mkdir -p "$WORK/bin" -cp "$WORK/wasm-bindgen/target/$HOST_TARGET/release/wasm-bindgen" "$WORK/bin/wasm-bindgen" echo "Setup complete. Run: bash scripts/test.sh or bash scripts/serve.sh" diff --git a/scripts/test-scheduler.sh b/scripts/test-scheduler.sh index ddbe660..03b74bb 100644 --- a/scripts/test-scheduler.sh +++ b/scripts/test-scheduler.sh @@ -6,3 +6,6 @@ TEST_TARGET="$(rustc -vV | sed -n 's/^host: //p')" CARGO_TARGET_DIR="$REPO/target/native-tests" cargo test \ --manifest-path .work/pumpkin/Cargo.toml --target "$TEST_TARGET" \ -p pumpkin-world --no-default-features --features single-threaded --lib storage_tests +CARGO_TARGET_DIR="$REPO/target/native-tests" cargo test \ + --manifest-path .work/pumpkin/Cargo.toml --target "$TEST_TARGET" \ + -p pumpkin --no-default-features --features single-threaded --lib -- chunk_sender chunk_queue_tests diff --git a/scripts/test.sh b/scripts/test.sh index 6d67bd4..9d2eb06 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,10 +4,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/common.sh" require_node require_packages require_toolchain -"$NODE" "$REPO/scripts/check-ports.mjs" 25565 8787 +"$NODE" "$REPO/scripts/check-ports.mjs" "${TEST_TCP_PORT:-25565}" "${TEST_HTTP_PORT:-8787}" bash "$REPO/scripts/build.sh" cd "$REPO" -cargo build --locked --release --features test-fixtures --bin filesystem-probe -"$NODE" node_modules/wrangler/bin/wrangler.js types worker/env.d.ts --strict-vars=false -"$NODE" node_modules/typescript/bin/tsc --noEmit +"$NODE" --test tests/*.test.mjs exec "$NODE" tests/integration.mjs diff --git a/src/config.rs b/src/config.rs index 22360a8..4ddb6c4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,25 +1,27 @@ +use crate::settings::Settings; use pumpkin_config::{AdvancedConfiguration, BasicConfiguration}; use std::num::NonZero; -pub fn configuration() -> (BasicConfiguration, AdvancedConfiguration) { +pub fn configuration(settings: &Settings) -> (BasicConfiguration, AdvancedConfiguration) { let mut basic = BasicConfiguration { default_level_name: "world".to_string(), ..Default::default() }; - // Workers exposes configured string variables through the Emscripten - // environment. Existing worlds retain the seed stored in level.dat. - if let Ok(seed) = std::env::var("WORLD_SEED") { + // Existing worlds retain the seed stored in level.dat. + if let Some(seed) = &settings.seed { basic.seed = seed.as_str().into(); } let mut adv = AdvancedConfiguration::default(); - // Offline Java server with a bounded view distance. - adv.networking.java.enabled = false; // Connections are supplied by the Durable Object. + // Offline Java server with a bounded view distance. The listener binds the + // default 0.0.0.0:25565 in the Durable Object's port table; the host routes + // inbound sockets to it by port. adv.networking.java.online_mode = false; adv.networking.java.encryption = false; - adv.networking.java.view_distance = NonZero::new(3).unwrap(); - adv.networking.java.simulation_distance = NonZero::new(3).unwrap(); - adv.networking.java.motd = "Minecraft on Cloudflare Workers".to_string(); + adv.networking.java.view_distance = NonZero::new(settings.view_distance).unwrap(); + adv.networking.java.simulation_distance = NonZero::new(settings.simulation_distance).unwrap(); + adv.networking.java.max_players = settings.max_players; + adv.networking.java.motd = settings.motd.clone(); // Disable native transports, plugins, and console input. adv.networking.bedrock.enabled = false; // The Bedrock OIDC key fetch spawns on online_mode && auth.enabled regardless of @@ -31,8 +33,10 @@ pub fn configuration() -> (BasicConfiguration, AdvancedConfiguration) { adv.networking.lan_broadcast.enabled = false; adv.plugins.enabled = false; adv.commands.use_console = false; - // Packet compression is disabled for this configuration. - adv.networking.java.compression.enabled = false; + // A negative threshold disables packet compression. + adv.networking.java.compression.enabled = settings.compression_threshold >= 0; + adv.networking.java.compression.info.threshold = settings.compression_threshold.max(0) as u32; + adv.networking.java.compression.info.level = settings.compression_level; adv.logging.enabled = true; adv.logging.file.clear(); diff --git a/src/handshake.rs b/src/handshake.rs new file mode 100644 index 0000000..11ed827 --- /dev/null +++ b/src/handshake.rs @@ -0,0 +1,180 @@ +//! The Minecraft handshake prefix of an inbound connection, read before the +//! connection is forwarded. Server-list requests are answered here from the +//! object's metadata so a ping never starts a server; login connections are +//! forwarded with the consumed bytes replayed. + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +const MAX_PACKET: usize = 1024; +const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + +pub enum Handshake { + /// A server-list exchange was answered; the connection is done. + Status, + /// A login handshake; `consumed` holds the bytes read so far. + Login { consumed: Vec }, +} + +fn varint(mut value: u32) -> Vec { + let mut bytes = Vec::new(); + loop { + let byte = (value & 127) as u8; + value >>= 7; + if value == 0 { + bytes.push(byte); + return bytes; + } + bytes.push(byte | 128); + } +} + +fn read_varint(bytes: &[u8]) -> Result, String> { + let mut value = 0u32; + for i in 0..5 { + let Some(&byte) = bytes.get(i) else { + return Ok(None); + }; + if i == 4 && byte & 0xf0 != 0 { + return Err("Invalid VarInt".into()); + } + value |= u32::from(byte & 127) << (7 * i); + if byte & 128 == 0 { + return Ok(Some((value, i + 1))); + } + } + Err("Invalid VarInt".into()) +} + +fn required_varint(bytes: &[u8]) -> Result<(u32, usize), String> { + read_varint(bytes)?.ok_or_else(|| "Truncated handshake".into()) +} + +fn frame(id: u32, body: &[u8]) -> Vec { + let mut content = varint(id); + content.extend_from_slice(body); + let mut out = varint(content.len() as u32); + out.extend(content); + out +} + +/// `(protocol, next state)` from a handshake packet body. +pub fn handshake_state(packet: &[u8]) -> Result<(u32, u32), String> { + let (id, a) = required_varint(packet)?; + let (protocol, b) = required_varint(&packet[a..])?; + let (host_length, c) = required_varint(&packet[a + b..])?; + let host_end = a + b + c + host_length as usize; + if id != 0 || host_length as usize > MAX_PACKET || host_end + 2 >= packet.len() { + return Err("Invalid handshake".into()); + } + let (state, end) = required_varint(&packet[host_end + 2..])?; + if host_end + 2 + end != packet.len() { + return Err("Invalid handshake length".into()); + } + if !(1..=3).contains(&state) { + return Err("Invalid handshake state".into()); + } + Ok((protocol, state)) +} + +/// Reads whole handshake/status packets from a byte stream, keeping every +/// byte consumed so a login connection can be replayed. +struct Packets { + reader: R, + buffer: Vec, + consumed: Vec, + deadline: tokio::time::Instant, +} + +impl Packets { + fn new(reader: R) -> Self { + Packets { + reader, + buffer: Vec::new(), + consumed: Vec::new(), + deadline: tokio::time::Instant::now() + TIMEOUT, + } + } + + /// The next packet body, or `None` at a clean end of stream. + async fn read(&mut self) -> Result>, String> { + loop { + if let Some((size, start)) = read_varint(&self.buffer)? { + let size = size as usize; + if size < 1 || size > MAX_PACKET { + return Err("Invalid handshake/status packet size".into()); + } + if self.buffer.len() >= start + size { + let rest = self.buffer.split_off(start + size); + let whole = std::mem::replace(&mut self.buffer, rest); + self.consumed.extend_from_slice(&whole); + return Ok(Some(whole[start..].to_vec())); + } + } + let mut chunk = [0u8; 512]; + let read = tokio::time::timeout_at(self.deadline, self.reader.read(&mut chunk)) + .await + .map_err(|_| "Handshake/status timed out".to_string())? + .map_err(|e| e.to_string())?; + if read == 0 { + if !self.buffer.is_empty() { + return Err("Truncated handshake/status packet".into()); + } + return Ok(None); + } + self.buffer.extend_from_slice(&chunk[..read]); + } + } + + /// Everything read from the stream so far, including unparsed bytes. + fn into_consumed(mut self) -> Vec { + self.consumed.append(&mut self.buffer); + self.consumed + } +} + +/// Classifies a connection by its handshake. `server_list(protocol)` supplies +/// the status JSON. +pub async fn accept(socket: &mut S, server_list: F) -> Result +where + S: AsyncRead + AsyncWrite + Unpin, + F: FnOnce(u32) -> Fut, + Fut: std::future::Future>, +{ + let mut packets = Packets::new(&mut *socket); + let Some(handshake) = packets.read().await? else { + return Ok(Handshake::Status); + }; + let (protocol, state) = handshake_state(&handshake)?; + if state != 1 { + return Ok(Handshake::Login { + consumed: packets.into_consumed(), + }); + } + let request = packets.read().await?; + if request.as_deref() != Some(&[0][..]) { + return Err("Invalid status request".into()); + } + let json = server_list(protocol).await?; + let mut body = varint(json.len() as u32); + body.extend_from_slice(json.as_bytes()); + let response = frame(0, &body); + packets + .reader + .write_all(&response) + .await + .map_err(|e| e.to_string())?; + packets.reader.flush().await.map_err(|e| e.to_string())?; + match packets.read().await? { + None => {} + Some(ping) if ping.len() == 9 && ping[0] == 1 => { + packets + .reader + .write_all(&frame(1, &ping[1..])) + .await + .map_err(|e| e.to_string())?; + packets.reader.flush().await.map_err(|e| e.to_string())?; + } + Some(_) => return Err("Invalid status ping".into()), + } + Ok(Handshake::Status) +} diff --git a/src/host.rs b/src/host.rs new file mode 100644 index 0000000..27b80c0 --- /dev/null +++ b/src/host.rs @@ -0,0 +1,40 @@ +//! Platform calls, each returning the platform's own Promise or value. + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "/src/js/mount.js")] +extern "C" { + /// Mounts the object's SQLite storage as the world's filesystem and routes + /// NODERAWFS through it; `ROOT` is the mount path. + #[wasm_bindgen(js_name = mountStorage)] + pub fn mount_storage(storage: &worker::worker_sys::DurableObjectStorage); + #[wasm_bindgen(js_name = ROOT, thread_local_v2)] + pub static MOUNT_ROOT: JsValue; +} + +#[wasm_bindgen(module = "cloudflare:workers")] +extern "C" { + /// The entrypoint's declared exports (`exports` in wrangler.jsonc), keyed + /// by class name; Durable Object namespaces among them. + #[wasm_bindgen(js_name = exports, thread_local_v2)] + pub static EXPORTS: JsValue; +} + +pub fn property(target: &JsValue, name: &str) -> Result { + js_sys::Reflect::get(target, &name.into()) +} + +pub fn method(target: &JsValue, name: &str, args: &[&JsValue]) -> Result { + let function: js_sys::Function = property(target, name)?.unchecked_into(); + let array = js_sys::Array::new(); + for arg in args { + array.push(arg); + } + js_sys::Reflect::apply(&function, target, &array) +} + +/// `promise.then(on_fulfilled, on_rejected)` on any thenable; the callbacks +/// are one-shot closures already converted with `Closure::once_into_js`. +pub fn js_error(error: impl std::fmt::Display) -> JsValue { + js_sys::Error::new(&error.to_string()).into() +} diff --git a/src/js/mount.js b/src/js/mount.js new file mode 100644 index 0000000..596a24c --- /dev/null +++ b/src/js/mount.js @@ -0,0 +1,18 @@ +// The world's filesystem: worker-fs-mount's node:fs implementation routes paths +// under a mount to the Durable Object's SQLite storage and everything else to +// workerd's node:fs. Emscripten's NODERAWFS is rebound to it (src/workerd.js). +import { LocalDOFilesystem } from 'durable-object-fs/local'; +import { mount, unmount } from 'worker-fs-mount'; +import * as fs from 'worker-fs-mount/fs-sync'; + +export const ROOT = '/data'; + +// The mount table is per isolate and an isolate outlives an object instance +// (and may host several), so each constructor takes the mount over: one wasm +// instance serves one object at a time, and the server runs only while the +// constructing object holds it. +export function mountStorage(storage) { + unmount(ROOT); + mount(ROOT, new LocalDOFilesystem(storage)); + globalThis.__pumpkin_fs = fs; +} diff --git a/src/main.rs b/src/main.rs index 5bee898..01c5144 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,111 +1,88 @@ mod config; +mod handshake; +mod host; mod memory; +mod settings; +mod world; -use pumpkin::{data::VanillaData, PumpkinServer}; -use std::{cell::RefCell, sync::atomic::Ordering, sync::Arc}; +use host::method; use wasm_bindgen::prelude::*; -use worker::emscripten::{future_to_promise, js_error, socket_from_value}; +use worker::{event, Context, Env, Request, Response, Socket}; fn main() {} -thread_local! { - static FAILURE: RefCell> = const { RefCell::new(None) }; - static SERVER: RefCell>> = const { RefCell::new(None) }; - static NEXT_CLIENT: RefCell = const { RefCell::new(0) }; +/// The world's Durable Object stub, from the declared `MinecraftWorld` export. +fn world(env: &Env) -> worker::Result { + let name = env.var("WORLD_NAME")?.to_string(); + let namespace = host::EXPORTS.with(|exports| host::property(exports, "MinecraftWorld"))?; + Ok(method(&namespace, "getByName", &[&JsValue::from(name)])?.unchecked_into()) } -fn server() -> Result, JsValue> { - if let Some(error) = FAILURE.with(|failure| failure.borrow().clone()) { - return Err(js_error(error)); - } - SERVER - .with(|server| server.borrow().clone()) - .ok_or_else(|| js_error("Server is not started")) -} - -#[wasm_bindgen] -pub fn pumpkin_start() -> js_sys::Promise { - future_to_promise(async { - if SERVER.with(|server| server.borrow().is_some()) { - return Ok(JsValue::UNDEFINED); +/// Serves an inbound TCP connection. Server-list pings are answered from the +/// object's metadata without starting a server; a login handshake is piped to +/// the world's object until either side closes. +#[event(connect)] +async fn connect(mut socket: Socket, env: Env, _ctx: Context) -> worker::Result<()> { + let world = world(&env)?; + let rpc: JsValue = world.clone().into(); + let server_list = |protocol: u32| async move { + let promise = + method(&rpc, "serverList", &[&JsValue::from(protocol)]).map_err(js_message)?; + let json = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise)) + .await + .map_err(js_message)?; + json.as_string() + .ok_or_else(|| "serverList returned no text".to_string()) + }; + let consumed = match handshake::accept(&mut socket, server_list).await { + Ok(handshake::Handshake::Login { consumed }) => consumed, + // Invalid or interrupted pre-login traffic never starts a game runtime. + Ok(handshake::Handshake::Status) => return Ok(()), + Err(error) => { + worker::console_debug!("Minecraft connection ended: {error}"); + return Ok(()); } - std::panic::set_hook(Box::new(|info| { - FAILURE.with(|failure| { - failure.borrow_mut().get_or_insert_with(|| info.to_string()); - }); - eprintln!("RUST PANIC: {info}"); - })); - rayon::ThreadPoolBuilder::new() - .num_threads(1) - .use_current_thread() - .build_global() - .map_err(js_error)?; - let (basic, advanced) = config::configuration(); - pumpkin::init_logger(&advanced); - let server = Arc::new(PumpkinServer::new(basic, advanced, VanillaData::load()).await); - server.init_plugins().await; - SERVER.with(|slot| *slot.borrow_mut() = Some(server)); - Ok(JsValue::UNDEFINED) - }) + }; + let options = js_sys::Object::new(); + js_sys::Reflect::set(&options, &"allowHalfOpen".into(), &JsValue::TRUE)?; + let mut upstream = Socket::from(world.connect(&world::authority(), options.into())?); + // Either side closing ends the connection; a disconnect is not an error + // of the handler (which the `connect` event treats as fatal). + if let Err(error) = tokio::io::AsyncWriteExt::write_all(&mut upstream, &consumed).await { + worker::console_debug!("Minecraft connection ended: {error}"); + return Ok(()); + } + let (mut client_read, mut client_write) = tokio::io::split(socket); + let (mut server_read, mut server_write) = tokio::io::split(upstream); + let _ = tokio::select! { + r = tokio::io::copy(&mut client_read, &mut server_write) => r, + r = tokio::io::copy(&mut server_read, &mut client_write) => r, + }; + Ok(()) } -#[wasm_bindgen] -pub fn pumpkin_connect(raw: JsValue) -> js_sys::Promise { - future_to_promise(async move { - let server = server()?; - let socket = socket_from_value(raw)?; - let id = NEXT_CLIENT.with(|slot| { - let mut id = slot.borrow_mut(); - *id += 1; - *id - }); - // The SDK's socket is the injected Tokio byte stream. Its adapter handles - // backpressure, EOF and JS stream errors; no emulated TCP listener is used. - server - .serve_connection(socket, ([127, 0, 0, 1], 0).into(), id) - .await; - Ok(JsValue::UNDEFINED) - }) +fn js_message(error: JsValue) -> String { + js_sys::Error::from(error).message().into() } -#[wasm_bindgen] -pub fn pumpkin_status() -> Result { - let server = server()?; - let result = js_sys::Object::new(); - js_sys::Reflect::set( - &result, - &"players".into(), - &(server.server.get_all_players().len() as f64).into(), - )?; - js_sys::Reflect::set( - &result, - &"ticks".into(), - &(server.server.tick_count.load(Ordering::Relaxed) as f64).into(), - )?; - js_sys::Reflect::set( - &result, - &"wasm_memory_bytes".into(), - &((core::arch::wasm32::memory_size::<0>() * 65536) as f64).into(), - )?; - for (name, bytes) in memory::heap_usage() { - js_sys::Reflect::set(&result, &name.into(), &(bytes as f64).into())?; +#[event(fetch)] +async fn fetch(request: Request, env: Env, _ctx: Context) -> worker::Result { + match request.path().as_str() { + "/health" => Response::ok(r#"{"ready":true}"#).map(json), + "/" if request.method() == worker::Method::Get => { + let rpc: JsValue = world(&env)?.into(); + let status = method(&rpc, "status", &[])?; + let status = + wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(status)).await?; + let body = js_sys::JSON::stringify(&status)?; + Response::ok(String::from(body)).map(json) + } + _ => Response::error("Not found", 404), } - Ok(result.into()) } -#[wasm_bindgen] -pub fn pumpkin_shutdown() -> js_sys::Promise { - future_to_promise(async { - let server = server()?; - if !server.server.get_all_players().is_empty() { - return Err(js_error( - "Disconnect clients before shutting down the world", - )); - } - pumpkin::stop_server(); - server.server.save_all().await.map_err(js_error)?; - server.server.shutdown().await; - SERVER.with(|slot| slot.borrow_mut().take()); - Ok(JsValue::UNDEFINED) - }) +fn json(response: Response) -> Response { + let headers = worker::Headers::new(); + let _ = headers.set("content-type", "application/json"); + response.with_headers(headers) } diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..2438ce4 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,88 @@ +//! Operator settings, read from the Worker's environment variables per world +//! before its server starts. + +use crate::host::property; +use wasm_bindgen::JsValue; + +#[derive(Clone, Debug, PartialEq)] +pub struct Settings { + pub view_distance: u8, + pub simulation_distance: u8, + pub max_players: u32, + pub compression_threshold: i32, + pub compression_level: u32, + pub idle_timeout_seconds: u32, + pub motd: String, + pub seed: Option, +} + +impl Settings { + pub fn from_env(env: &JsValue) -> Result { + let var = |name: &str| -> Result, String> { + let value = property(env, name).map_err(|_| format!("{name} is unreadable"))?; + if value.is_undefined() || value.is_null() { + return Ok(None); + } + value + .as_string() + .map(Some) + .ok_or_else(|| format!("{name} must be a string")) + }; + let integer = |name: &str, fallback: i64, min: i64, max: i64| -> Result { + let Some(raw) = var(name)? else { + return Ok(fallback); + }; + let value = raw + .parse::() + .ok() + .filter(|v| (min..=max).contains(v)) + .ok_or_else(|| format!("{name} must be an integer between {min} and {max}"))?; + Ok(value) + }; + let view_distance = integer("VIEW_DISTANCE", 4, 2, 32)? as u8; + let simulation_distance = integer("SIMULATION_DISTANCE", 3, 2, 32)? as u8; + if simulation_distance > view_distance { + return Err("SIMULATION_DISTANCE must not exceed VIEW_DISTANCE".into()); + } + let motd = var("MOTD")?.unwrap_or_else(|| "Minecraft on Cloudflare Workers".into()); + if motd.trim().is_empty() || motd.len() > 512 { + return Err("MOTD must contain between 1 and 512 characters".into()); + } + let seed = var("WORLD_SEED")?; + if let Some(seed) = &seed { + if seed.trim().is_empty() || seed.len() > 256 { + return Err( + "WORLD_SEED must be a nonempty string of at most 256 characters".into(), + ); + } + } + Ok(Settings { + view_distance, + simulation_distance, + max_players: integer("MAX_PLAYERS", 20, 1, 1000)? as u32, + compression_threshold: integer("COMPRESSION_THRESHOLD", 512, -1, 2 * 1024 * 1024)? + as i32, + compression_level: integer("COMPRESSION_LEVEL", 1, 0, 9)? as u32, + idle_timeout_seconds: integer("IDLE_TIMEOUT_SECONDS", 10, 0, 30)? as u32, + motd, + seed, + }) + } + + pub fn to_js(&self) -> Result { + let object = js_sys::Object::new(); + let set = |key: &str, value: JsValue| js_sys::Reflect::set(&object, &key.into(), &value); + set("view_distance", self.view_distance.into())?; + set("simulation_distance", self.simulation_distance.into())?; + set("max_players", self.max_players.into())?; + set("compression_threshold", self.compression_threshold.into())?; + set("compression_level", self.compression_level.into())?; + set("idle_timeout_seconds", self.idle_timeout_seconds.into())?; + set("motd", self.motd.as_str().into())?; + set( + "seed", + self.seed.as_deref().map_or(JsValue::NULL, JsValue::from), + )?; + Ok(object.into()) + } +} diff --git a/src/workerd.js b/src/workerd.js new file mode 100644 index 0000000..dc86445 --- /dev/null +++ b/src/workerd.js @@ -0,0 +1,54 @@ +// Emscripten library adjustments for NODERAWFS on workerd's Node filesystem. +addToLibrary({ + // Node's deprecated process.binding is absent; fs.constants is the public + // form of the same table. + $NODEFS__postset: ` + NODEFS.isWindows = false; + NODEFS.flagsForNodeMap = Object.fromEntries([ + [{{{ cDefs.O_APPEND }}}, 'O_APPEND'], + [{{{ cDefs.O_CREAT }}}, 'O_CREAT'], + [{{{ cDefs.O_EXCL }}}, 'O_EXCL'], + [{{{ cDefs.O_NOCTTY }}}, 'O_NOCTTY'], + [{{{ cDefs.O_RDONLY }}}, 'O_RDONLY'], + [{{{ cDefs.O_RDWR }}}, 'O_RDWR'], + [{{{ cDefs.O_DSYNC }}}, 'O_SYNC'], + [{{{ cDefs.O_TRUNC }}}, 'O_TRUNC'], + [{{{ cDefs.O_WRONLY }}}, 'O_WRONLY'], + [{{{ cDefs.O_NOFOLLOW }}}, 'O_NOFOLLOW'], + ].map(([flag, name]) => [flag, fs.constants[name]])); + `, + + // The host enforces permissions; workerd reports directories as 0666, which + // would otherwise fail the VFS's execute check in getdents. Once the object + // mounts its storage (src/js/mount.js), NODERAWFS reads and writes through + // worker-fs-mount's fs, which routes the mount to SQLite and the rest to + // workerd's own fs. + $workerdFs__deps: ['$FS', '$PATH_FS'], + $workerdFs__postset: () => addAtPreRun('workerdFs()'), + $workerdFs: () => { + Object.defineProperty(FS, 'ignorePermissions', { get: () => true, set() {} }); + // The working directory is the object's, not the isolate process's, so + // relative paths (which NODERAWFS otherwise leaves to the host's cwd) are + // resolved here before they reach the fs implementation. + let cwd = '/'; + FS.cwd = () => cwd; + FS.chdir = (path) => { cwd = PATH_FS.resolve(path); }; + const resolve = (path) => (typeof path === 'string' && path[0] !== '/' ? PATH_FS.resolve(path) : path); + const twoPaths = new Set(['renameSync', 'copyFileSync', 'linkSync']); + const resolving = (impl) => new Proxy(impl, { + get(target, name) { + const value = target[name]; + if (typeof value !== 'function' || !String(name).endsWith('Sync')) return value; + return (first, second, ...rest) => + value.call(target, resolve(first), twoPaths.has(name) ? resolve(second) : second, ...rest); + }, + }); + const nodeFs = fs; + fs = resolving(nodeFs); + Object.defineProperty(globalThis, '__pumpkin_fs', { + configurable: true, + get: () => nodeFs, + set: (mounted) => { fs = resolving(mounted); }, + }); + }, +}); diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..0e8740f --- /dev/null +++ b/src/world.rs @@ -0,0 +1,408 @@ +//! The Durable Object hosting one Pumpkin server. +//! +//! A `#[durable_object(connect)]`: its handlers run on the thread's ambient +//! Tokio event loop, whose wait is the host event loop, so nothing blocks or +//! suspends. The server lifetime is one such future: the first `connect` after +//! idle runs it on the world mounted from the object's storage, and it +//! completes after the final save is synced. Later connections are routed to +//! the running listener, and the handler outlives each routed connection. + +use crate::{config, host, memory, settings::Settings}; +use host::{js_error, method, property}; +use pumpkin::{data::VanillaData, server::Server, PumpkinServer}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, + sync::{atomic::Ordering, Arc}, +}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use worker::{durable_object, DurableObject, Env, Socket, State}; + +pub const PORT: u16 = 25565; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Phase { + Idle, + Starting, + Running, + Stopping, + Failed, +} + +impl Phase { + fn name(self) -> &'static str { + match self { + Phase::Idle => "idle", + Phase::Starting => "starting", + Phase::Running => "running", + Phase::Stopping => "stopping", + Phase::Failed => "failed", + } + } +} + +thread_local! { + static PANIC: RefCell> = const { RefCell::new(None) }; + static SERVER: RefCell>> = const { RefCell::new(None) }; + /// Pumpkin's logger is process-wide and installed once. + static LOGGER: Cell = const { Cell::new(false) }; +} + +/// State shared between the exports and the server future. +struct Shared { + storage: worker::durable::Storage, + phase: Cell, + failure: RefCell>, + connections: Cell, + startup_ms: Cell>, + saved_at: RefCell>, + /// Servers started in this object's lifetime. + starts: Cell, + /// While running with no players: the time the server will stop. + idle_deadline: Cell>, + settings: Result, + /// Signals the server root that the last connection has closed. + idle: Arc, + /// Resolves at the next phase change; connections arriving mid-transition + /// wait on it and retry. + transition: RefCell>, +} + +// Closures run under catch_unwind; the object is only ever touched from this +// thread, so a poisoned borrow cannot be observed. +impl std::panic::RefUnwindSafe for Shared {} + +#[durable_object(connect)] +pub struct MinecraftWorld { + shared: Rc, +} + +impl DurableObject for MinecraftWorld { + fn new(state: State, env: Env) -> Self { + std::panic::set_hook(Box::new(|info| { + PANIC.with(|slot| { + slot.borrow_mut().get_or_insert_with(|| info.to_string()); + }); + eprintln!("RUST PANIC: {info}"); + })); + let storage = state.storage(); + host::mount_storage(storage.as_raw()); + MinecraftWorld { + shared: Rc::new(Shared { + storage, + phase: Cell::new(Phase::Idle), + failure: RefCell::new(None), + connections: Cell::new(0), + startup_ms: Cell::new(None), + saved_at: RefCell::new(None), + starts: Cell::new(0), + idle_deadline: Cell::new(None), + settings: Settings::from_env(&env), + idle: Arc::new(tokio::sync::Notify::new()), + transition: RefCell::new(None), + }), + } + } + + async fn fetch(&self, _req: worker::Request) -> worker::Result { + worker::Response::error("tcp only", 404) + } + + /// Serves one inbound connection; resolves when it closes. The first call + /// while idle also runs the server, resolving once the final save is synced. + async fn connect(&self, socket: Socket) -> worker::Result<()> { + let shared = self.shared.clone(); + // On a running server `connect` resolves to the promise of the routed + // connection; this handler must outlive it, or the host closes the socket. + let served = shared.connect(socket).await.map_err(worker::Error::from)?; + if let Ok(promise) = served.dyn_into::() { + JsFuture::from(promise).await.map_err(worker::Error::from)?; + } + Ok(()) + } +} + +/// RPC methods for the Worker's status and server-list responders. +#[wasm_bindgen] +impl MinecraftWorld { + pub fn status(&self) -> Result { + let shared = &self.shared; + let result = js_sys::Object::new(); + let set = |key: &str, value: JsValue| js_sys::Reflect::set(&result, &key.into(), &value); + set("phase", shared.phase.get().name().into())?; + set("connections", (shared.connections.get() as f64).into())?; + set( + "failure", + shared + .failure + .borrow() + .as_deref() + .map_or(JsValue::NULL, JsValue::from), + )?; + set( + "startup_ms", + shared.startup_ms.get().map_or(JsValue::NULL, JsValue::from), + )?; + set( + "saved_at", + shared + .saved_at + .borrow() + .as_deref() + .map_or(JsValue::NULL, JsValue::from), + )?; + set("runtime_starts", shared.starts.get().into())?; + set( + "idle_deadline", + shared + .idle_deadline + .get() + .map_or(JsValue::NULL, JsValue::from), + )?; + set( + "settings", + match &shared.settings { + Ok(settings) => settings.to_js()?, + Err(_) => JsValue::NULL, + }, + )?; + let server = SERVER.with(|slot| slot.borrow().clone()); + set( + "server", + match (shared.phase.get(), server) { + (Phase::Running, Some(server)) => server_status(&server)?, + _ => JsValue::NULL, + }, + )?; + Ok(result.into()) + } + + /// Server-list metadata for the Worker's status responder, without + /// starting a server. + #[wasm_bindgen(js_name = serverList)] + pub fn server_list(&self, protocol: f64) -> Result { + let shared = &self.shared; + let settings = shared.settings.as_ref().map_err(|e| js_error(e.clone()))?; + let online = SERVER.with(|slot| match (shared.phase.get(), slot.borrow().as_ref()) { + (Phase::Running, Some(server)) => server.get_all_players().len(), + _ => 0, + }); + let text = if shared.phase.get() == Phase::Failed { + "Server unavailable" + } else { + &settings.motd + }; + let protocol = if (MIN_PROTOCOL..=MAX_PROTOCOL).contains(&protocol) { + protocol + } else { + MIN_PROTOCOL + }; + let json = serde_json::json!({ + "version": { "name": VERSION_NAME, "protocol": protocol }, + "players": { "max": settings.max_players, "online": online }, + "description": { "text": text }, + "enforcesSecureChat": false, + }); + Ok(JsValue::from(json.to_string())) + } +} + +/// The Java versions the pinned Pumpkin accepts. +const MIN_PROTOCOL: f64 = 4.0; +const MAX_PROTOCOL: f64 = 776.0; +const VERSION_NAME: &str = "1.7.2-26.2"; + +impl Shared { + async fn connect(self: Rc, socket: worker::Socket) -> Result { + loop { + match self.phase.get() { + Phase::Running => return self.route(socket), + Phase::Failed => { + return Err(js_error( + self.failure.borrow().as_deref().unwrap_or("failed"), + )) + } + Phase::Starting | Phase::Stopping => { + JsFuture::from(self.transition()?).await?; + } + Phase::Idle => break, + } + } + self.set_phase(Phase::Starting); + let started = js_sys::Date::now(); + // A task of its own so a panic arrives as a `JoinError` and fails the + // object rather than escaping the export. + let outcome = match tokio::task::spawn_local(self.clone().run(socket, started)).await { + Ok(outcome) => outcome, + Err(panic) => Err(js_error(format!("server task failed: {panic}"))), + }; + if let Err(error) = &outcome { + self.fail(error); + } + outcome + } + + /// The promise of the next phase change. + fn transition(&self) -> Result { + let mut slot = self.transition.borrow_mut(); + if let Some((promise, _)) = slot.as_ref() { + return Ok(promise.clone()); + } + let resolvers = with_resolvers()?; + let promise: js_sys::Promise = property(&resolvers, "promise")?.unchecked_into(); + let resolve: js_sys::Function = property(&resolvers, "resolve")?.unchecked_into(); + Ok(slot.insert((promise, resolve)).0.clone()) + } + + fn set_phase(&self, phase: Phase) { + self.phase.set(phase); + if let Some((_, resolve)) = self.transition.borrow_mut().take() { + let _ = resolve.call0(&JsValue::UNDEFINED); + } + } + + fn fail(&self, error: &JsValue) { + if self.phase.get() != Phase::Failed { + self.set_phase(Phase::Failed); + let message = js_sys::Error::from(error.clone()).message(); + *self.failure.borrow_mut() = Some(String::from(message)); + } + } + + /// Routes a socket to Pumpkin's listener and tracks it until it closes. + fn route(self: &Rc, socket: worker::Socket) -> Result { + self.connections.set(self.connections.get() + 1); + self.idle_deadline.set(None); + let shared = self.clone(); + let served = async move { + let outcome = socket.handle_as_node_connection().await; + shared.connections.set(shared.connections.get() - 1); + if shared.connections.get() == 0 { + shared.idle.notify_one(); + } + outcome + .map(|()| JsValue::UNDEFINED) + .map_err(|e| JsValue::from(e)) + }; + Ok(wasm_bindgen_futures::future_to_promise(served).into()) + } + + async fn run(self: Rc, first: worker::Socket, started: f64) -> Result { + let root = host::MOUNT_ROOT + .with(|root| root.as_string()) + .unwrap_or_default(); + std::fs::create_dir_all(&root).map_err(js_error)?; + std::env::set_current_dir(&root).map_err(js_error)?; + pumpkin::reset_stop(); + let settings = self.settings.clone().map_err(js_error)?; + let (basic, advanced) = config::configuration(&settings); + if !LOGGER.replace(true) { + pumpkin::init_logger(&advanced); + } + let server = PumpkinServer::new(basic, advanced, VanillaData::load()).await; + server.init_plugins().await; + SERVER.with(|slot| *slot.borrow_mut() = Some(server.server.clone())); + self.set_phase(Phase::Running); + self.startup_ms.set(Some(js_sys::Date::now() - started)); + self.starts.set(self.starts.get() + 1); + + // The first connection's promise belongs to the platform; the server + // root tracks it only through `connections`. + drop(self.route(first)?); + let idle = self.idle.clone(); + let shared = self.clone(); + let timeout = std::time::Duration::from_secs(settings.idle_timeout_seconds.into()); + tokio::task::spawn_local(async move { + loop { + idle.notified().await; + // A connection may have been routed since the notification. + if shared.connections.get() != 0 { + continue; + } + // Players are saved on disconnect; save the world too, so the + // mount holds everything before the idle period rather than + // only at stop. + if let Some(server) = SERVER.with(|slot| slot.borrow().clone()) { + save_world(&server).await; + } + if shared.connections.get() != 0 { + continue; + } + // Keep the world up briefly so a rejoining player finds it running. + shared + .idle_deadline + .set(Some(js_sys::Date::now() + timeout.as_millis() as f64)); + tokio::time::sleep(timeout).await; + shared.idle_deadline.set(None); + if shared.connections.get() == 0 { + break; + } + } + // Stopping before the listener closes, so no connection is routed + // to a server that will not accept it. + shared.set_phase(Phase::Stopping); + pumpkin::stop_server(); + }); + // Returns after the final save once stopped. + server.start().await; + SERVER.with(|slot| slot.borrow_mut().take()); + if let Some(panic) = PANIC.with(|slot| slot.borrow_mut().take()) { + return Err(js_error(panic)); + } + // Writes issued through the mount become durable before the connection + // is reported closed. + self.storage.sync().await.map_err(JsValue::from)?; + self.saved_at + .replace(Some(String::from(js_sys::Date::new_0().to_iso_string()))); + self.set_phase(Phase::Idle); + Ok(JsValue::UNDEFINED) + } +} + +/// Saves players, world data and dirty chunks, returning once the chunk writer +/// has handed them to the filesystem. +async fn save_world(server: &Server) { + use pumpkin_world::chunk::io::FileIO; + if let Err(error) = server.save_all().await { + eprintln!("World save after last disconnect failed: {error}"); + } + for world in server.worlds.load().iter() { + let level = &world.level; + // `save_all` flags the scheduler; the flag clears once it has queued the + // dirty chunks for writing. + while level.should_save.load(Ordering::Relaxed) { + tokio::task::yield_now().await; + } + level.chunk_saver.block_and_await_ongoing_tasks().await; + } +} + +fn with_resolvers() -> Result { + method( + &js_sys::Promise::resolve(&JsValue::UNDEFINED) + .constructor() + .into(), + "withResolvers", + &[], + ) +} + +fn server_status(server: &Server) -> Result { + let result = js_sys::Object::new(); + let set = |key: &str, value: f64| js_sys::Reflect::set(&result, &key.into(), &value.into()); + set("players", server.get_all_players().len() as f64)?; + set("ticks", server.tick_count.load(Ordering::Relaxed) as f64)?; + set( + "wasm_memory_bytes", + (core::arch::wasm32::memory_size::<0>() * 65536) as f64, + )?; + for (name, bytes) in memory::heap_usage() { + set(name, bytes as f64)?; + } + Ok(result.into()) +} + +pub fn authority() -> String { + format!("world:{PORT}") +} diff --git a/tests/client.mjs b/tests/client.mjs index 2399e40..360b3c5 100644 --- a/tests/client.mjs +++ b/tests/client.mjs @@ -14,6 +14,15 @@ export async function waitFor(check, description, milliseconds = 60_000) { throw new Error(`Timed out waiting for ${description}`); } +/** Expected initial chunk count for the pinned Pumpkin view shape. */ +export function viewChunkCount(view) { + let count = 0; + for (let x = -view - 1; x <= view + 1; x++) for (let z = -view - 1; z <= view + 1; z++) { + if (Math.max(0, Math.abs(x) - 2) ** 2 + Math.max(0, Math.abs(z) - 2) ** 2 < view ** 2) count++; + } + return count; +} + async function socket() { const stream = net.connect({ host: HOST, port: PORT }); stream.setNoDelay(true); @@ -25,25 +34,38 @@ async function socket() { return stream; } -export async function status() { +export async function status({ fragmented = false } = {}) { const stream = await socket(); - stream.write(handshake(1)); + const hello = handshake(1); + if (fragmented) { + for (const byte of hello) { stream.write(Buffer.from([byte])); await delay(1); } + } else stream.write(hello); stream.write(frame(0)); + const nonce = Buffer.from('pingtest'); + let response; try { for await (const { id, payload } of packets(stream)) { - assert.equal(id, 0); - const [length, offset] = readVarint(payload); - const status = JSON.parse(payload.subarray(offset, offset + length).toString()); - assert.equal(status.version.protocol, PROTOCOL); - return status; + if (id === 0 && !response) { + const [length, offset] = readVarint(payload); + response = JSON.parse(payload.subarray(offset, offset + length).toString()); + assert.equal(response.version.protocol, PROTOCOL); + stream.write(frame(1, nonce)); + } else { + assert.equal(id, 1); + assert.deepEqual(payload, nonce); + assert.ok(response); + return response; + } } - throw new Error('No status response'); + throw new Error('Incomplete status response'); } finally { stream.destroy(); } } /** A small vanilla-protocol client that remains connected after receiving chunks. */ -export async function play(name) { +export async function play(name, { viewDistance = 32 } = {}) { const stream = await socket(); + const compression = { threshold: null, decodedBytes: 0 }; + const send = (id, payload) => stream.write(frame(id, payload, compression.threshold)); const closed = new Promise(resolve => stream.once('close', resolve)); const uuid = createHash('md5').update(`OfflinePlayer:${name}`).digest(); uuid[6] = (uuid[6] & 0x0f) | 0x30; @@ -60,23 +82,28 @@ export async function play(name) { let readyResolve, readyReject; const ready = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; }); stream.write(handshake(2)); - stream.write(frame(0, Buffer.concat([mcString(name), uuid]))); + send(0, Buffer.concat([mcString(name), uuid])); const reading = (async () => { - for await (const { id, payload } of packets(stream)) { + for await (const { id, payload } of packets(stream, compression)) { if (state === 'login') { if (id === 0) throw new Error(`${name}: login rejected`); - if (id === 1 || id === 3) throw new Error('Probe requires offline mode with compression disabled'); + if (id === 1) throw new Error('Probe requires offline mode'); + if (id === 3) { + const value = readVarint(payload); + if (!value || value[0] > 2 * 1024 * 1024) throw new Error('Invalid compression threshold'); + compression.threshold = value[0]; + } if (id === 2) { - stream.write(frame(3)); + send(3); // Vanilla clients opt into the server listing through Client Information. - stream.write(frame(0, Buffer.concat([mcString('en_us'), Buffer.from([3, 0, 1, 0x7f, 1, 0, 1])]))); + send(0, Buffer.concat([mcString('en_us'), Buffer.from([viewDistance, 0, 1, 0x7f, 1, 0, 1])])); state = 'configuration'; } } else if (state === 'configuration') { if (id === 2) throw new Error(`${name}: disconnected during configuration`); - if (id === 0x0e) stream.write(frame(7, varint(0))); - if (id === 4 || id === 5) stream.write(frame(id, payload)); - if (id === 3) { stream.write(frame(3)); state = 'play'; } + if (id === 0x0e) send(7, varint(0)); + if (id === 4 || id === 5) send(id, payload); + if (id === 3) { send(3); state = 'play'; } } else { if (id === 0x20) throw new Error(`${name}: disconnected during Play`); if (id === 0x54) { @@ -88,18 +115,18 @@ export async function play(name) { const position = readBlockPosition(payload); blockUpdates.set(`${position.x},${position.y},${position.z}`, readVarint(payload, 8)[0]); } - if (id === 0x2c) stream.write(frame(0x1c, payload)); + if (id === 0x2c) send(0x1c, payload); if (id === 0x48) { const [teleport, offset] = readVarint(payload); position = { x: payload.readDoubleBE(offset), y: payload.readDoubleBE(offset + 8), z: payload.readDoubleBE(offset + 16) }; - stream.write(frame(0, varint(teleport))); + send(0, varint(teleport)); teleported = true; } if (id === 0x2d) { assert.ok(payload.length > 8); const x = payload.readInt32BE(0), z = payload.readInt32BE(4); chunks.set(`${x},${z}`, payload); - if (!loaded) { stream.write(frame(0x2c)); loaded = true; } + if (!loaded) { send(0x2c); loaded = true; } } // Player-info packets carry profile names. Preserve observations for // the two-client test without requiring an entity/render implementation. @@ -111,7 +138,7 @@ export async function play(name) { if (id === 0x0b) { const rate = Buffer.alloc(4); rate.writeFloatBE(8); - stream.write(frame(0x0b, rate)); + send(0x0b, rate); if (teleported && chunks.size > 0) readyResolve(); } } @@ -130,6 +157,7 @@ export async function play(name) { finally { clearTimeout(timer); } return { chunks, seenNames, + get traffic() { return { wireBytes: stream.bytesRead, decodedBytes: compression.decodedBytes, compressionThreshold: compression.threshold }; }, block(position) { const update = blockUpdates.get(`${position.x},${position.y},${position.z}`); if (update !== undefined) return update; @@ -137,9 +165,9 @@ export async function play(name) { return chunk && chunkBlock(chunk, position); }, async breakBlock(position) { - const action = (status, sequence) => stream.write(frame(0x29, Buffer.concat([ + const action = (status, sequence) => send(0x29, Buffer.concat([ varint(status), blockPosition(position), Buffer.from([1]), varint(sequence), - ]))); + ])); action(0, 1); await delay(1000); action(2, 2); @@ -155,7 +183,7 @@ export async function play(name) { payload.writeDoubleBE(next.y, 8); payload.writeDoubleBE(next.z, 16); payload[24] = 1; // On ground - stream.write(frame(0x1e, payload)); + send(0x1e, payload); }, assertHealthy() { if (failure) throw failure; }, async close() { diff --git a/tests/experience.mjs b/tests/experience.mjs new file mode 100644 index 0000000..20d5c8b --- /dev/null +++ b/tests/experience.mjs @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict'; +import { readFile, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { setTimeout as delay } from 'node:timers/promises'; +import { parseArgs } from 'node:util'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { play, status, waitFor, viewChunkCount } from './client.mjs'; +import { testServer } from './server.mjs'; + +const repo = fileURLToPath(new URL('../', import.meta.url)); +const { values } = parseArgs({ options: { + seed: { type: 'string', default: '1789195700909824175' }, + view: { type: 'string', default: '4' }, + simulation: { type: 'string', default: '3' }, + players: { type: 'string', default: '2' }, + 'max-players': { type: 'string' }, + compression: { type: 'string', default: '512' }, + level: { type: 'string', default: '1' }, + label: { type: 'string', default: 'experience' }, + steps: { type: 'string', default: '32' }, +} }); +const playerCount = Number(values.players); +const steps = Number(values.steps); +assert.ok(Number.isInteger(playerCount) && playerCount >= 1 && playerCount <= 20); +assert.ok(Number.isInteger(steps) && steps >= 0 && steps <= 128); +const vars = { + VIEW_DISTANCE: values.view, SIMULATION_DISTANCE: values.simulation, + MAX_PLAYERS: values['max-players'] ?? values.players, COMPRESSION_THRESHOLD: values.compression, + COMPRESSION_LEVEL: values.level, IDLE_TIMEOUT_SECONDS: '0', +}; +const { probe, startWorker, request, checkpoint, seed } = await testServer({ seed: values.seed, vars }); +const worker = await startWorker(); +const clients = [], samples = []; +let phase = 'login', sampling = true, sampleFailure, failure, joinedMs, starting; +const began = performance.now(); +const elapsed = () => performance.now() - began; +const healthy = () => { + worker.healthy(); + clients.forEach(c => c.assertHealthy()); + if (sampleFailure) throw sampleFailure; +}; +const sampler = (async () => { + while (sampling) { + const beganRequest = performance.now(); + const state = await request(); + if (state.failure) throw new Error(state.failure); + if (state.server) samples.push({ elapsed_ms: elapsed(), phase, status_ms: performance.now() - beganRequest, ...state.server }); + await delay(100); + } +})().catch(error => { sampleFailure = error; }); +const expectedChunks = viewChunkCount(Number(values.view)); +const destinations = []; +let pingMs; +try { + const pingStart = performance.now(); + await status(); + pingMs = performance.now() - pingStart; + assert.equal((await request()).runtime_starts, 0); + for (let i = 0; i < playerCount; i++) clients.push(await play(`Probe${String.fromCharCode(65 + i)}`)); + await waitFor(() => { healthy(); return clients.every(c => c.chunks.size >= expectedChunks); }, 'complete initial views'); + joinedMs = elapsed(); + starting = await request(); + phase = 'stationary'; + await delay(5000); + healthy(); + const origins = clients.map(c => ({ ...c.position })); + const directions = playerCount <= 4 ? [[1, 0], [-1, 0], [0, 1], [0, -1]] + : Array.from({ length: playerCount }, (_, i) => [Math.cos(i * 2 * Math.PI / playerCount), Math.sin(i * 2 * Math.PI / playerCount)]); + phase = 'exploration'; + for (let step = 1; step <= steps; step++) { + for (let i = 0; i < clients.length; i++) { + destinations[i] = { ...origins[i], x: origins[i].x + directions[i][0] * step * 4, z: origins[i].z + directions[i][1] * step * 4 }; + clients[i].move(destinations[i]); + } + await delay(500); + healthy(); + } + phase = 'settling'; + await delay(5000); + healthy(); +} catch (error) { failure = String(error); } +finally { + sampling = false; + await sampler; + failure ??= sampleFailure && String(sampleFailure); + const percentile = (numbers, q) => { + if (!numbers.length) return null; + const ordered = [...numbers].sort((a, b) => a - b); + return ordered[Math.ceil(q * ordered.length) - 1]; + }; + const phases = {}; + for (const phase of new Set(samples.map(s => s.phase))) { + const selected = samples.filter(s => s.phase === phase); + const first = selected[0], last = selected.at(-1); + phases[phase] = { + samples: selected.length, + status_p95_ms: percentile(selected.map(s => s.status_ms), 0.95), + status_max_ms: Math.max(...selected.map(s => s.status_ms)), + observed_tps: selected.length > 1 ? (last.ticks - first.ticks) * 1000 / (last.elapsed_ms - first.elapsed_ms) : null, + mean_tick_ms: last.mean_tick_ms, + }; + } + const result = { + label: values.label, seed, vars, players: playerCount, steps, + wasm_sha256: createHash('sha256').update(await readFile(path.join(repo, 'target/workers/wasm32-unknown-emscripten/release/pumpkin_do.wasm'))).digest('hex'), + status_ping_ms: pingMs, startup_ms: starting?.startup_ms, joined_ms: joinedMs, + peak_wasm_bytes: samples.length ? Math.max(...samples.map(s => s.wasm_memory_bytes)) : null, + peak_heap_bytes: samples.length ? Math.max(...samples.map(s => s.heap_allocated_bytes)) : null, + final_sample: samples.at(-1), phases, + clients: clients.map((c, i) => ({ chunks_received: c.chunks.size, ...c.traffic, commanded_destination: destinations[i] })), + failure: failure ?? null, samples, + }; + await writeFile(path.join(probe, 'experience.json'), JSON.stringify(result, null, 2) + '\n'); + console.log(JSON.stringify({ ...result, samples: `${samples.length} samples in ${probe}/experience.json` }, null, 2)); + await Promise.allSettled(clients.map(c => c.close())); + try { if (!failure) await checkpoint(worker); } + finally { await worker.stop(); } +} +if (failure) throw new Error(failure); +console.log('PUMPKIN-EXPERIENCE-PROBE-OK'); diff --git a/tests/fixtures/filesystem.rs b/tests/fixtures/filesystem.rs deleted file mode 100644 index cf6c210..0000000 --- a/tests/fixtures/filesystem.rs +++ /dev/null @@ -1,76 +0,0 @@ -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use wasm_bindgen::prelude::*; -use worker::emscripten::{future_to_promise, js_error, socket_from_value}; - -fn main() {} - -#[wasm_bindgen] -pub fn echo(socket: JsValue) -> js_sys::Promise { - future_to_promise(async move { - let mut socket = socket_from_value(socket)?; - let mut bytes = [0u8; 16 * 1024]; - loop { - let count = socket.read(&mut bytes).await.map_err(js_error)?; - if count == 0 { - break; - } - socket.write_all(&bytes[..count]).await.map_err(js_error)?; - } - socket.shutdown().await.map_err(js_error)?; - Ok(JsValue::UNDEFINED) - }) -} - -#[wasm_bindgen] -pub fn tick_after(milliseconds: u32) -> js_sys::Promise { - future_to_promise(async move { - tokio::time::sleep(std::time::Duration::from_millis(milliseconds.into())).await; - Ok(JsValue::TRUE) - }) -} - -/// Exercise the filesystem syscall path before loading Pumpkin's world data. -#[wasm_bindgen] -pub fn filesystem_probe(value: String) -> Result { - use std::io::{Read, Seek, SeekFrom, Write}; - std::fs::create_dir_all("transport-probe").map_err(js_error)?; - let path = "transport-probe/state.bin"; - let previous = std::fs::read_to_string(path).unwrap_or_default(); - let mut file = std::fs::OpenOptions::new() - .create(true) - .truncate(true) - .read(true) - .write(true) - .open("transport-probe/pending.bin") - .map_err(js_error)?; - file.write_all(b"----").map_err(js_error)?; - file.seek(SeekFrom::Start(0)).map_err(js_error)?; - file.write_all(value.as_bytes()).map_err(js_error)?; - file.set_len(value.len() as u64).map_err(js_error)?; - file.sync_all().map_err(js_error)?; - file.seek(SeekFrom::Start(0)).map_err(js_error)?; - let mut check = String::new(); - file.read_to_string(&mut check).map_err(js_error)?; - if check != value { - return Err(js_error("Filesystem descriptor roundtrip failed")); - } - drop(file); - std::fs::rename("transport-probe/pending.bin", path).map_err(js_error)?; - let mut large = vec![0x5a; 3 * 1024 * 1024]; - std::fs::write("transport-probe/large.bin", &large).map_err(js_error)?; - let mut file = std::fs::OpenOptions::new() - .write(true) - .open("transport-probe/large.bin") - .map_err(js_error)?; - file.seek(SeekFrom::Start(65534)).map_err(js_error)?; - file.write_all(&[1, 2, 3, 4]).map_err(js_error)?; - large[65534..65538].copy_from_slice(&[1, 2, 3, 4]); - file.set_len(large.len() as u64 + 3).map_err(js_error)?; - large.extend_from_slice(&[0, 0, 0]); - file.sync_all().map_err(js_error)?; - drop(file); - if std::fs::read("transport-probe/large.bin").map_err(js_error)? != large { - return Err(js_error("Large file roundtrip failed")); - } - Ok(previous) -} diff --git a/tests/fixtures/startup-failure.mjs b/tests/fixtures/startup-failure.mjs deleted file mode 100644 index 10849da..0000000 --- a/tests/fixtures/startup-failure.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { MinecraftWorld } from '../../worker/world'; - -export class StartupFailure extends MinecraftWorld { - loadRuntime() { throw new Error('Expected startup failure'); } - - async verify() { - const attempt = this.getRuntime(); - const starting = this.status(); - try { await attempt; } catch {} - const failed = this.status(); - try { await this.getRuntime(); } catch {} - return { starting, failed, repeated: this.status() }; - } -} diff --git a/tests/fixtures/worker.mjs b/tests/fixtures/worker.mjs deleted file mode 100644 index f8300ac..0000000 --- a/tests/fixtures/worker.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { DurableObject, exports } from 'cloudflare:workers'; -export { StartupFailure } from './startup-failure.mjs'; -import createModule from '../../target/workers/wasm32-unknown-emscripten/release/filesystem-probe.js'; -import wasm from '../../target/workers/wasm32-unknown-emscripten/release/filesystem_probe.wasm'; -import { createWorldRuntime } from '../../worker/filesystem'; -import { forwardSocket } from '../../worker/socket'; - -export class FilesystemTest extends DurableObject { - runtime; - - getRuntime() { - return this.runtime ??= createWorldRuntime(this.ctx.storage, options => createModule({ - ...options, - instantiateWasm(imports, receive) { - const instance = new WebAssembly.Instance(wasm, imports); - receive(instance); - return instance.exports; - }, - })); - } - - async connect(socket) { - void socket.closed.catch(() => undefined); - const runtime = await this.getRuntime(); - await runtime.run(() => runtime.instance.echo(socket)); - } - - async status() { - const runtime = await this.getRuntime(); - return { timer: await runtime.run(() => runtime.instance.tick_after(20)) }; - } - - async filesystemProbe(value) { - const runtime = await this.getRuntime(); - return runtime.run(() => runtime.instance.filesystem_probe(value)); - } -} - -export default { - async connect(socket, env) { - const target = exports.FilesystemTest.getByName(env.WORLD_NAME).connect('world:25565', { allowHalfOpen: true }); - await forwardSocket(socket, target); - }, - async fetch(request, env) { - const url = new URL(request.url); - if (url.pathname === '/health') return Response.json({ ready: true }); - if (url.pathname === '/startup-failure') { - return Response.json(await exports.StartupFailure.getByName('failure').verify()); - } - const stub = exports.FilesystemTest.getByName(url.searchParams.get('world') ?? env.WORLD_NAME); - if (request.method === 'POST' && url.pathname === '/filesystem-probe') { - return Response.json({ previous: await stub.filesystemProbe(await request.text()) }); - } - return Response.json(await stub.status()); - }, -}; diff --git a/tests/fixtures/wrangler.jsonc b/tests/fixtures/wrangler.jsonc deleted file mode 100644 index 8ab097a..0000000 --- a/tests/fixtures/wrangler.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../../node_modules/wrangler/config-schema.json", - "name": "minecraft-filesystem-tests", - "main": "worker.mjs", - "compatibility_date": "2026-09-05", - "compatibility_flags": ["nodejs_compat", "new_module_registry", "no_handle_cross_request_promise_resolution"], - "dev": { "ip": "127.0.0.1", "port": 8787 }, - "connect": [{ "protocol": "tcp", "address": "127.0.0.1", "port": 25565 }], - "exports": { - "FilesystemTest": { "type": "durable-object", "storage": "sqlite" }, - "StartupFailure": { "type": "durable-object", "storage": "sqlite" } - }, - "vars": { "WORLD_NAME": "integration" } -} diff --git a/tests/integration.mjs b/tests/integration.mjs index ddeceb7..1e8a270 100644 --- a/tests/integration.mjs +++ b/tests/integration.mjs @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; -import net from 'node:net'; import { readFile } from 'node:fs/promises'; import { setTimeout as delay } from 'node:timers/promises'; import { status, play, waitFor } from './client.mjs'; import { testServer } from './server.mjs'; -const { startWorker, request, checkpoint } = await testServer(); +const { startWorker, request, checkpoint } = await testServer({ vars: { IDLE_TIMEOUT_SECONDS: '5', MAX_PLAYERS: '2' } }); const { blocks } = JSON.parse(await readFile(new URL('../.work/pumpkin/assets/blocks.json', import.meta.url))); const states = new Map(blocks.flatMap(block => block.states.map(state => [state.id, { ...state, name: block.name }]))); @@ -30,58 +29,14 @@ function editableBlock(client) { throw new Error(`No stable block within reach of ${JSON.stringify(client.position)}`); } -async function echo() { - const socket = net.connect({ host: '127.0.0.1', port: 25565 }); - socket.setTimeout(15000, () => socket.destroy(new Error('Echo timed out'))); - const payload = Buffer.from(Array.from({ length: 65536 }, (_, index) => index & 255)); - const chunks = []; - let length = 0; - try { - await new Promise((resolve, reject) => { - socket.once('connect', resolve); - socket.once('error', reject); - }); - for (let offset = 0; offset < payload.length; offset += 997) socket.write(payload.subarray(offset, offset + 997)); - for await (const chunk of socket) { - chunks.push(chunk); - length += chunk.length; - if (length >= payload.length) break; - } - assert.ok(Buffer.concat(chunks).equals(payload), `TCP echo differed: received ${length} of ${payload.length} bytes`); - } finally { socket.destroy(); } -} - -async function marker(world, value) { - return (await request(`/filesystem-probe?world=${world}`, { method: 'POST', body: value })).previous; -} - -let worker = await startWorker(true); -try { - await Promise.all([echo(), echo()]); - assert.equal((await request('/')).timer, true); - const failure = await request('/startup-failure'); - assert.equal(failure.starting.phase, 'starting'); - assert.equal(failure.failed.phase, 'failed'); - assert.equal(failure.failed.failure, 'Expected startup failure'); - assert.deepEqual(failure.repeated, failure.failed); - assert.equal(await marker('isolation-a', 'alpha'), ''); - assert.equal(await marker('isolation-b', 'beta'), ''); - assert.equal(await marker('isolation-a', 'alpha-2'), 'alpha'); - worker.healthy(); -} finally { await worker.stop(); } -worker = await startWorker(true); -try { - assert.equal(await marker('isolation-a', 'alpha-3'), 'alpha-2'); - assert.equal(await marker('isolation-b', 'beta-2'), 'beta'); - worker.healthy(); -} finally { await worker.stop(); } -console.log('Filesystem, socket, and isolation tests passed'); - let savedChunk, savedPosition, changedBlock; -worker = await startWorker(); +let worker = await startWorker(); const clients = []; try { - assert.equal((await status()).version.protocol, 776); + assert.equal((await request('/')).runtime_starts, 0); + assert.equal((await status({ fragmented: true })).version.protocol, 776); + assert.equal((await status()).players.max, 2); + assert.equal((await request('/')).runtime_starts, 0, 'Status ping started Wasm'); const a = await play('ProbeA'); clients.push(a); const b = await play('ProbeB'); clients.push(b); await waitFor(() => { @@ -89,6 +44,9 @@ try { return a.seenNames.has('ProbeB') && b.seenNames.has('ProbeA'); }, 'both players to see each other'); assert.equal((await status()).players.online, 2); + assert.equal(a.traffic.compressionThreshold, 512); + assert.ok(a.traffic.decodedBytes > a.traffic.wireBytes, 'Compression did not reduce traffic'); + await assert.rejects(play('ProbeExtra'), /login rejected/); changedBlock = editableBlock(a); savedChunk = `${Math.floor(changedBlock.x / 16)},${Math.floor(changedBlock.z / 16)}`; assert.ok(a.chunks.has(savedChunk) && b.chunks.has(savedChunk), 'Clients did not receive the spawn chunk'); @@ -106,18 +64,29 @@ try { assert.equal(a.block(changedBlock), 0, 'Edited block changed before checkpoint'); assert.equal(b.block(changedBlock), 0, 'Second client lost the edit before checkpoint'); await a.close(); await b.close(); clients.length = 0; - const saved = await checkpoint(worker); - console.log(`World checkpoint completed at ${saved.checkpointed_at}`); + const warm = await checkpoint(worker, { stopped: false }); + const reconnected = await play('ProbeA'); clients.push(reconnected); + const resumed = await request('/'); + assert.equal(resumed.runtime_starts, warm.runtime_starts, 'Reconnect restarted Wasm'); + await delay(5500); // Outlive the first disconnect's timer while connected. + reconnected.assertHealthy(); worker.healthy(); + assert.ok((await request('/')).server.ticks > resumed.server.ticks, 'Reconnected world stopped ticking'); + await reconnected.close(); clients.length = 0; + await checkpoint(worker, { stopped: false }); + // Stop this test worker before the idle timer fires: writes through the + // mounted filesystem alone must be sufficient for player and block restoration. + console.log('World writes committed; stopping before the idle timer'); } finally { await Promise.all(clients.map(client => client.close())); await worker.stop(); } -worker = await startWorker(); +worker = await startWorker({ COMPRESSION_THRESHOLD: '-1' }); try { assert.equal((await request('/')).phase, 'idle'); const a = await play('ProbeA'); try { + assert.equal(a.traffic.compressionThreshold, null); await waitFor(() => { a.assertHealthy(); worker.healthy(); return a.chunks.has(savedChunk); }, 'saved chunk after restart'); assert.equal(a.block(changedBlock), 0, 'Edited block was regenerated instead of restored'); assert.equal(a.position.x, savedPosition.x, 'Player X position was not restored'); diff --git a/tests/memory.mjs b/tests/memory.mjs index d0cca9d..7a5e0d0 100644 --- a/tests/memory.mjs +++ b/tests/memory.mjs @@ -6,7 +6,7 @@ import { createHash } from 'node:crypto'; import { setTimeout as delay } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; -import { play, waitFor } from './client.mjs'; +import { play, waitFor, viewChunkCount } from './client.mjs'; import { testServer } from './server.mjs'; const repo = fileURLToPath(new URL('../', import.meta.url)); @@ -35,13 +35,14 @@ const sampler = (async () => { } })().catch(error => { sampleFailure = error; }); try { + const expectedChunks = viewChunkCount((await request()).settings.viewDistance); clients.push(await play('ProbeA')); clients.push(await play('ProbeB')); await waitFor(() => { worker.healthy(); clients.forEach(client => client.assertHealthy()); if (sampleFailure) throw sampleFailure; - return clients.every(client => client.chunks.size >= 81); + return clients.every(client => client.chunks.size >= expectedChunks); }, 'both clients to receive their view distance'); const joinedMs = Date.now() - started; phase = 'stationary'; diff --git a/tests/protocol.mjs b/tests/protocol.mjs index 64208f0..7964a56 100644 --- a/tests/protocol.mjs +++ b/tests/protocol.mjs @@ -1,7 +1,9 @@ -// Minimal, uncompressed Java 26.2 framing for this demo's smoke probe. +// Java 26.2 framing, including negotiated packet compression. +import { deflateSync, inflateSync } from 'node:zlib'; export const PROTOCOL = 776; export const HOST = '127.0.0.1'; -export const PORT = 25565; +export const PORT = Number(process.env.TEST_TCP_PORT ?? 25565); +export const HTTP_PORT = Number(process.env.TEST_HTTP_PORT ?? 8787); export function varint(value) { const bytes = []; @@ -29,8 +31,13 @@ export function mcString(value) { return Buffer.concat([varint(bytes.length), bytes]); } -export function frame(id, payload = Buffer.alloc(0)) { - const body = Buffer.concat([varint(id), payload]); +export function frame(id, payload = Buffer.alloc(0), threshold = null) { + let body = Buffer.concat([varint(id), payload]); + if (threshold !== null) { + body = body.length >= threshold + ? Buffer.concat([varint(body.length), deflateSync(body, { level: 1 })]) + : Buffer.concat([varint(0), body]); + } return Buffer.concat([varint(body.length), body]); } @@ -40,7 +47,7 @@ export function handshake(state) { return frame(0, Buffer.concat([varint(PROTOCOL), mcString(HOST), port, varint(state)])); } -export async function* packets(socket) { +export async function* packets(socket, compression = { threshold: null }) { let buffer = Buffer.alloc(0); for await (const data of socket) { buffer = Buffer.concat([buffer, data]); @@ -50,8 +57,21 @@ export async function* packets(socket) { const [size, start] = length; if (size === 0 || size > 8 * 1024 * 1024) throw new Error('Invalid packet size'); if (buffer.length < start + size) break; - const body = buffer.subarray(start, start + size); + let body = buffer.subarray(start, start + size); buffer = buffer.subarray(start + size); + if (compression.threshold !== null) { + const length = readVarint(body); + if (!length) throw new Error('Missing uncompressed packet length'); + const [expected, offset] = length; + if (expected > 8 * 1024 * 1024) throw new Error('Uncompressed packet exceeds limit'); + body = body.subarray(offset); + if (expected !== 0) { + if (expected < compression.threshold) throw new Error('Compressed packet below threshold'); + body = inflateSync(body, { maxOutputLength: 8 * 1024 * 1024 }); + if (body.length !== expected) throw new Error('Wrong uncompressed packet length'); + } + } + compression.decodedBytes = (compression.decodedBytes ?? 0) + body.length; const id = readVarint(body); if (!id) throw new Error('Missing packet ID'); yield { id: id[0], payload: body.subarray(id[1]) }; diff --git a/tests/protocol.test.mjs b/tests/protocol.test.mjs new file mode 100644 index 0000000..323129b --- /dev/null +++ b/tests/protocol.test.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { frame, packets, varint } from './protocol.mjs'; + +test('compression starts immediately after negotiation, including coalesced packets', async () => { + const payload = Buffer.alloc(4096, 97); + const incoming = Buffer.concat([frame(3, varint(512)), frame(42, payload, 512), frame(43, Buffer.from('small'), 512)]); + const compression = { threshold: null }; + const decoded = []; + for await (const packet of packets([incoming], compression)) { + if (packet.id === 3) compression.threshold = 512; + else decoded.push(packet); + } + assert.deepEqual(decoded, [{ id: 42, payload }, { id: 43, payload: Buffer.from('small') }]); + assert.ok(incoming.length < payload.length); +}); + +test('compressed framing rejects inflated lengths above the packet limit', async () => { + const body = varint(8 * 1024 * 1024 + 1); + await assert.rejects(async () => { + for await (const _ of packets([Buffer.concat([varint(body.length), body])], { threshold: 512 })) {} + }, /exceeds limit/); +}); diff --git a/tests/server.mjs b/tests/server.mjs index 37ac929..491b404 100644 --- a/tests/server.mjs +++ b/tests/server.mjs @@ -2,18 +2,24 @@ import assert from 'node:assert/strict'; import net from 'node:net'; import path from 'node:path'; import { spawn } from 'node:child_process'; -import { mkdir, mkdtemp, appendFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, appendFile, readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { waitFor } from './client.mjs'; +import { PORT, HTTP_PORT } from './protocol.mjs'; const repo = fileURLToPath(new URL('../', import.meta.url)); -export async function testServer({ seed = '1789195700909824175' } = {}) { +export async function testServer({ seed = '1789195700909824175', vars = {} } = {}) { + const inspectorPort = process.env.TEST_INSPECTOR_PORT; + if (inspectorPort !== undefined) { + assert.ok(/^\d+$/.test(inspectorPort) && Number(inspectorPort) >= 1 && Number(inspectorPort) <= 65535, + 'TEST_INSPECTOR_PORT must be a port between 1 and 65535'); + } await mkdir(path.join(repo, '.data/probes'), { recursive: true }); const probe = await mkdtemp(path.join(repo, '.data/probes/workers.')); const persistence = path.join(probe, 'state'); console.log(`Workers probe state: ${probe}`); - for (const port of [25565, 8787]) { + for (const port of [PORT, HTTP_PORT, ...(inspectorPort ? [Number(inspectorPort)] : [])]) { const listener = net.createServer(); await new Promise((resolve, reject) => { listener.once('error', reject); @@ -23,11 +29,29 @@ export async function testServer({ seed = '1789195700909824175' } = {}) { } let phase = 0; - async function startWorker(fixture = false) { - const log = path.join(probe, `${++phase}-${fixture ? "filesystem" : "minecraft"}.log`); + async function startWorker(overrides = {}) { + const log = path.join(probe, `${++phase}-minecraft.log`); + const sourceConfig = path.join(repo, 'wrangler.jsonc'); + const config = JSON.parse(await readFile(sourceConfig, 'utf8')); + config.name += '-' + path.basename(probe).split('.').at(-1).toLowerCase(); + // The probe config lives outside the repository root. Keep the real build + // hook and resolve its paths so tests exercise the same startup as dev. + if (config.build) { + config.build.cwd = repo; + const watch = config.build.watch_dir ?? 'src'; + config.build.watch_dir = (Array.isArray(watch) ? watch : [watch]) + .map(directory => path.relative(probe, path.resolve(repo, directory))); + } + config.main = path.resolve(path.dirname(sourceConfig), config.main); + config.dev.port = HTTP_PORT; + config.connect[0].port = PORT; + config.vars = { ...config.vars, ...vars, ...overrides }; + const configPath = path.join(probe, 'wrangler.json'); + await writeFile(configPath, JSON.stringify(config)); const child = spawn(process.execPath, [ path.join(repo, 'node_modules/wrangler/bin/wrangler.js'), 'dev', '--local', - '--config', path.join(repo, fixture ? 'tests/fixtures/wrangler.jsonc' : 'wrangler.jsonc'), + '--config', configPath, + ...(inspectorPort ? ['--inspector-port', inspectorPort] : []), '--persist-to', persistence, '--var', 'WORLD_NAME:integration', // Known terrain with a stable editable spawn block; random water spawns // cannot exercise the integration test's shared block-edit assertions. @@ -67,7 +91,7 @@ export async function testServer({ seed = '1789195700909824175' } = {}) { await waitFor(async () => { if (failure) throw failure; if (exited) throw new Error(`Wrangler exited; see ${log}`); - try { return (await fetch('http://127.0.0.1:8787/health', { signal: AbortSignal.timeout(2000) })).ok; } + try { return (await fetch(`http://127.0.0.1:${HTTP_PORT}/health`, { signal: AbortSignal.timeout(2000) })).ok; } catch { return false; } }, `Wrangler (${log})`); } catch (error) { await stop(); throw error; } @@ -78,13 +102,13 @@ export async function testServer({ seed = '1789195700909824175' } = {}) { } async function request(route = '/', options) { - const response = await fetch(`http://127.0.0.1:8787${route}`, { ...options, signal: AbortSignal.timeout(60_000) }); + const response = await fetch(`http://127.0.0.1:${HTTP_PORT}${route}`, { ...options, signal: AbortSignal.timeout(60_000) }); const text = await response.text(); assert.ok(response.ok, `HTTP ${response.status}: ${text.slice(0,2000)}`); return JSON.parse(text); } - async function checkpoint(worker) { + async function checkpoint(worker, { stopped = true, after } = {}) { return waitFor(async () => { worker.healthy(); let state; @@ -95,7 +119,8 @@ export async function testServer({ seed = '1789195700909824175' } = {}) { throw error; } if (state.failure) throw new Error(state.failure); - return state.phase === 'idle' && state.checkpointed_at && state.connections === 0 && state; + return (stopped ? state.phase === 'idle' && state.saved_at && state.saved_at !== after : state.phase === 'running' && state.idle_deadline) + && state.connections === 0 && state; }, 'world checkpoint'); } diff --git a/toolchain/wasm-bindgen.Cargo.lock b/toolchain/wasm-bindgen.Cargo.lock deleted file mode 100644 index e772975..0000000 --- a/toolchain/wasm-bindgen.Cargo.lock +++ /dev/null @@ -1,4510 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "add" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - -[[package]] -name = "askama" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" -dependencies = [ - "askama_macros", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash", - "serde", - "serde_derive", - "syn 2.0.119", -] - -[[package]] -name = "askama_macros" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0756b45480437dded0565dfc568af62ccce146fb6cfe902e808ba86e445f44f" -dependencies = [ - "askama_derive", -] - -[[package]] -name = "askama_parser" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868" -dependencies = [ - "rustc-hash", - "serde", - "serde_derive", - "unicode-ident", - "winnow 1.0.4", -] - -[[package]] -name = "assert_cmd" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-trait" -version = "0.1.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.2", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "aws-lc-rs" -version = "1.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" -dependencies = [ - "memchr", - "regex-automata", - "serde_core", -] - -[[package]] -name = "buf_redux" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" -dependencies = [ - "memchr", - "safemem", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "canvas" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex 2.0.1", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core 0.10.1", -] - -[[package]] -name = "char" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits 0.2.19", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - -[[package]] -name = "clap" -version = "4.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "closures" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "console_log" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86919cef3e37b9356ccf54d4421208c17ecfda01beae61393e7ffd72916c0ef1" -dependencies = [ - "log", - "web-sys", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "deflate" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" -dependencies = [ - "adler32", - "byteorder", -] - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.19", -] - -[[package]] -name = "der" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" -dependencies = [ - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dom" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum_primitive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" -dependencies = [ - "num-traits 0.1.43", -] - -[[package]] -name = "env_filter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "explicit-resource-management" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fetch" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-channel-preview" -version = "0.3.0-alpha.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5e5f4df964fa9c1c2f8bddeb5c3611631cacd93baf810fc8bb2fb4b495c263a" -dependencies = [ - "futures-core-preview", -] - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-core-preview" -version = "0.3.0-alpha.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35b6263fb1ef523c3056565fa67b1d16f0a8604ff12b11b08c25f28a734c60a" - -[[package]] -name = "futures-executor" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "futures-sink" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-timer" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "rand_core 0.10.1", - "wasm-bindgen", -] - -[[package]] -name = "gg-alloc" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee5134f1abeb59e9ba515150ddc26bf2b2207f8bce38e1f5cf8d1596efa22ba" - -[[package]] -name = "gif" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f" -dependencies = [ - "color_quant", - "lzw", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gloo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" -dependencies = [ - "gloo-console", - "gloo-dialogs", - "gloo-events", - "gloo-file", - "gloo-history", - "gloo-net", - "gloo-render", - "gloo-storage", - "gloo-timers", - "gloo-utils", - "gloo-worker", -] - -[[package]] -name = "gloo-console" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" -dependencies = [ - "gloo-utils", - "js-sys", - "serde", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-dialogs" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-events" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-file" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" -dependencies = [ - "gloo-events", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-history" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" -dependencies = [ - "getrandom 0.2.17", - "gloo-events", - "gloo-utils", - "serde", - "serde-wasm-bindgen", - "serde_urlencoded", - "thiserror 1.0.69", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-net" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "http 0.2.12", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "gloo-render" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-storage" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" -dependencies = [ - "gloo-utils", - "js-sys", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gloo-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "gloo-worker" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" -dependencies = [ - "bincode", - "futures", - "gloo-utils", - "gloo-worker-macros", - "js-sys", - "pinned", - "serde", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "gloo-worker-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "guide-supported-types-examples" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.2", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", - "serde", - "serde_core", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hello_world" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http 1.4.2", -] - -[[package]] -name = "http-body-util" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.2", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humantime" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" - -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http 1.4.2", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http 1.4.2", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http 1.4.2", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -dependencies = [ - "rayon", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95816db758249fe16f23a4e23f1a3a817fe11892dbfd1c5836f625324702158" -dependencies = [ - "byteorder", - "enum_primitive", - "gif", - "jpeg-decoder", - "num-iter", - "num-rational 0.1.43", - "num-traits 0.1.43", - "png", - "scoped_threadpool", -] - -[[package]] -name = "import_js" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inflate" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e0062d2dc2f17d2f13750d95316ae8a2ff909af0fda957084f5defd87c43bb" - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt", -] - -[[package]] -name = "jiff-static" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.19", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -dependencies = [ - "cfg-if", - "futures-channel", - "futures-core", - "futures-lite", - "futures-util", - "half", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", - "web-sys", -] - -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "julia_set" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lzw" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" - -[[package]] -name = "matrixmultiply" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" -dependencies = [ - "autocfg", - "rawpointer", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "multipart" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" -dependencies = [ - "buf_redux", - "httparse", - "log", - "mime", - "mime_guess", - "quick-error", - "rand 0.8.7", - "safemem", - "tempfile", - "twoway", -] - -[[package]] -name = "nalgebra" -version = "0.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d43ddcacf343185dfd6de2ee786d9e8b1c2301622afab66b6c73baf9882abfd" -dependencies = [ - "approx", - "matrixmultiply", - "num-complex", - "num-rational 0.4.2", - "num-traits 0.2.19", - "rand 0.8.7", - "rand_distr", - "simba", - "typenum", -] - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "no-std" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "node_and_deno" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "nodejs-threads" -version = "0.1.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "nom" -version = "5.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" -dependencies = [ - "memchr", - "version_check", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits 0.2.19", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits 0.2.19", -] - -[[package]] -name = "num-rational" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfff0773e8a07fb033d726b9ff1327466709820788e5298afce4d752965ff1e" -dependencies = [ - "autocfg", - "num-integer", - "num-traits 0.2.19", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits 0.2.19", -] - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -dependencies = [ - "num-traits 0.2.19", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-src" -version = "300.6.1+3.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "performance" -version = "0.0.0" -dependencies = [ - "humantime", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pinned" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" -dependencies = [ - "futures", - "rustversion", - "thiserror 1.0.69", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "png" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb773e9a557edb568ce9935cf783e3cdcabe06a9449d41b3e5506d88e582c82" -dependencies = [ - "bitflags 0.7.0", - "deflate", - "inflate", - "num-iter", -] - -[[package]] -name = "portable-atomic" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" - -[[package]] -name = "predicates-tree" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.19", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.19", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits 0.2.19", - "rand 0.8.7", -] - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "raytrace-parallel" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "futures-channel-preview", - "js-sys", - "rayon", - "rayon-core", - "raytracer", - "serde-wasm-bindgen", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "raytracer" -version = "0.1.0" -source = "git+https://github.com/alexcrichton/raytracer?branch=update-deps#42faa13859f7d8d47fd18be785c108003a207786" -dependencies = [ - "image", - "serde", - "serde_derive", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "request-animation-frame" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http 1.4.2", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rouille" -version = "3.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" -dependencies = [ - "base64 0.13.1", - "chrono", - "filetime", - "multipart", - "percent-encoding", - "rand 0.8.7", - "serde", - "serde_derive", - "serde_json", - "sha1_smol", - "threadpool", - "time", - "tiny_http", - "url", -] - -[[package]] -name = "rstest" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros", -] - -[[package]] -name = "rstest_macros" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.119", - "unicode-ident", -] - -[[package]] -name = "rust-duck-typed-interfaces" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "rust-webassembly-weather-reports" -version = "0.0.0" -dependencies = [ - "chrono", - "gloo", - "json", - "reqwest", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "sample" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.2", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simba" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" -dependencies = [ - "approx", - "num-complex", - "num-traits 0.2.19", - "paste", - "wide", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "sourcefile" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654ee0ca20343b1b2d6cb222c3c136b6209e20324ef53f7bde19cdd1e766e986" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "statrs" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" -dependencies = [ - "approx", - "nalgebra", - "num-traits 0.2.19", - "rand 0.8.7", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synchronous-instantiation" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "target-triple" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl 2.0.19", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.2", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "time" -version = "0.3.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "tiny_http" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" -dependencies = [ - "ascii", - "chunked_transfer", - "httpdate", - "log", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "todomvc" -version = "0.0.0" -dependencies = [ - "askama", - "console_error_panic_hook", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "futures-util", - "http 1.4.2", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "trybuild" -version = "1.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06649c6f63d86604ba0c8950d5a1829fc9a17afd70fc6629f481d75b6a624c78" -dependencies = [ - "glob", - "serde", - "serde_derive", - "serde_json", - "target-triple", - "termcolor", - "toml", -] - -[[package]] -name = "twoway" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" -dependencies = [ - "memchr", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "typescript-tests" -version = "0.0.0" -dependencies = [ - "js-sys", - "serde", - "serde-wasm-bindgen", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" -dependencies = [ - "base64 0.22.1", - "brotli-decompressor", - "der", - "flate2", - "log", - "native-tls", - "percent-encoding", - "rustls", - "rustls-pki-types", - "ureq-proto", - "utf8-zero", - "webpki-root-certs", - "webpki-roots", -] - -[[package]] -name = "ureq-proto" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" -dependencies = [ - "base64 0.22.1", - "http 1.4.2", - "httparse", - "log", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "walrus" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bfa49767bb3a9e1afb02aa95bbcbde8d82f2db4ca377afae94d688f14f62378" -dependencies = [ - "anyhow", - "gimli", - "id-arena", - "leb128", - "log", - "rayon", - "walrus-macro", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", -] - -[[package]] -name = "walrus-macro" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9b0525d7ea6e5f906aca581a172e5c91b4c595290dfa8ad4a2bc9ffef33b44" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasm-audio-worklet" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "console_log 1.1.0", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "paste", - "rustversion", - "serde", - "serde_derive", - "serde_json", - "wasm-bindgen-futures", - "wasm-bindgen-macro", - "wasm-bindgen-shared", - "wasm-bindgen-test", - "wasm-bindgen-test-crate-a", - "wasm-bindgen-test-crate-b", -] - -[[package]] -name = "wasm-bindgen-benchmark" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-cli" -version = "0.2.126" -dependencies = [ - "anyhow", - "assert_cmd", - "clap", - "env_logger", - "log", - "native-tls", - "predicates", - "pretty_assertions", - "regex", - "rouille", - "rstest", - "serde", - "serde_derive", - "serde_json", - "shlex 1.3.0", - "tempfile", - "ureq", - "walrus", - "wasm-bindgen-cli-support", - "wasm-bindgen-test-shared", - "wasmparser 0.245.1", - "wasmprinter 0.245.1", -] - -[[package]] -name = "wasm-bindgen-cli-support" -version = "0.2.126" -dependencies = [ - "anyhow", - "base64 0.22.1", - "leb128", - "log", - "rstest", - "rustc-demangle", - "serde", - "serde_json", - "walrus", - "wasm-bindgen-shared", - "wasmparser 0.245.1", - "wasmprinter 0.243.0", - "wast 243.0.0", - "wat", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -dependencies = [ - "futures-channel", - "futures-lite", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-test", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -dependencies = [ - "js-sys", - "quote", - "trybuild", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-macro-support", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-paint" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.76" -dependencies = [ - "async-trait", - "cast", - "gg-alloc", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits 0.2.19", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-test-crate-a" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-test-crate-b" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.76" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "tokio", - "trybuild", - "wasm-bindgen-test", -] - -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.126" - -[[package]] -name = "wasm-bindgen-webidl" -version = "0.0.0" -dependencies = [ - "anyhow", - "clap", - "env_logger", - "heck", - "lazy_static", - "log", - "once_cell", - "proc-macro2", - "quote", - "sourcefile", - "syn 2.0.119", - "weedle", -] - -[[package]] -name = "wasm-encoder" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" -dependencies = [ - "leb128fmt", - "wasmparser 0.243.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" -dependencies = [ - "leb128fmt", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasm-encoder" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" -dependencies = [ - "leb128fmt", - "wasmparser 0.254.0", -] - -[[package]] -name = "wasm-in-wasm" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", -] - -[[package]] -name = "wasm-in-wasm-imports" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", -] - -[[package]] -name = "wasm-in-web-worker" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" -dependencies = [ - "bitflags 2.13.1", - "indexmap", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.16.1", - "indexmap", - "semver", - "serde", -] - -[[package]] -name = "wasmparser" -version = "0.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" -dependencies = [ - "bitflags 2.13.1", - "indexmap", - "semver", -] - -[[package]] -name = "wasmprinter" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.243.0", -] - -[[package]] -name = "wasmprinter" -version = "0.245.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.245.1", -] - -[[package]] -name = "wast" -version = "243.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df21d01c2d91e46cb7a221d79e58a2d210ea02020d57c092e79255cc2999ca7f" -dependencies = [ - "bumpalo", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder 0.243.0", -] - -[[package]] -name = "wast" -version = "254.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32" -dependencies = [ - "bumpalo", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder 0.254.0", -] - -[[package]] -name = "wat" -version = "1.254.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c" -dependencies = [ - "wast 254.0.0", -] - -[[package]] -name = "wcodspeed" -version = "0.1.0" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "statrs", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -dependencies = [ - "futures", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webaudio" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "webgl" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "webidl-tests" -version = "0.0.0" -dependencies = [ - "env_logger", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", - "wasm-bindgen-webidl", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webrtc_datachannel" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "websockets" -version = "0.0.0" -dependencies = [ - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "webxr" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "futures", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "weedle" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5352ce9558ad6aa2c4eca1b18329177f5f77ba71f54eac09709ef47701137eb" -dependencies = [ - "nom", -] - -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "without-a-bundler" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "without-a-bundler-no-modules" -version = "0.0.0" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index afefe46..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["ES2022"], - "strict": true, - "noEmit": true, - "skipLibCheck": true - }, - "include": ["worker/**/*.ts"] -} diff --git a/worker/filesystem.ts b/worker/filesystem.ts deleted file mode 100644 index 79dcf6e..0000000 --- a/worker/filesystem.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import { LocalDOFilesystem } from 'durable-object-fs/local'; -import { mount, withMounts } from 'worker-fs-mount'; -import * as nodeFs from 'worker-fs-mount/fs-sync'; - -/** Each wasm instance keeps one mount context across requests and Tokio callbacks. */ -export function createWorldRuntime( - storage: DurableObjectStorage, - create: (options: { nodeFs: typeof nodeFs; cwd: string }) => Promise, -) { - return withMounts(async () => { - mount('/data', new LocalDOFilesystem(storage)); - const run = AsyncLocalStorage.snapshot(); - return { instance: await create({ nodeFs, cwd: '/data' }), run }; - }); -} diff --git a/worker/fs-library.js b/worker/fs-library.js deleted file mode 100644 index fd7153d..0000000 --- a/worker/fs-library.js +++ /dev/null @@ -1,114 +0,0 @@ -// Emscripten's Node loader uses createRequire at runtime, so Wrangler's build-time -// aliases cannot replace its fs object. Supply the mounted node:fs implementation -// to each factory, with a cwd that does not change the isolate's process.cwd. -addToLibrary({ - // Workers exposes public fs.constants, but not Node's internal process.binding. - $NODEFS__postset: ` - NODEFS.isWindows = false; - NODEFS.flagsForNodeMap = Object.fromEntries([ - [{{{ cDefs.O_APPEND }}}, 'O_APPEND'], - [{{{ cDefs.O_CREAT }}}, 'O_CREAT'], - [{{{ cDefs.O_EXCL }}}, 'O_EXCL'], - [{{{ cDefs.O_NOCTTY }}}, 'O_NOCTTY'], - [{{{ cDefs.O_RDONLY }}}, 'O_RDONLY'], - [{{{ cDefs.O_RDWR }}}, 'O_RDWR'], - [{{{ cDefs.O_DSYNC }}}, 'O_SYNC'], - [{{{ cDefs.O_TRUNC }}}, 'O_TRUNC'], - [{{{ cDefs.O_WRONLY }}}, 'O_WRONLY'], - [{{{ cDefs.O_NOFOLLOW }}}, 'O_NOFOLLOW'], - ].map(([flag, name]) => [flag, fs.constants[name]])); - `, - - $workerFilesystem__deps: ['$FS', '$PATH_FS', '$TTY'], - $workerFilesystem__postset: () => addAtPreRun('workerFilesystem()'), - $workerFilesystem: () => { - // biome-ignore lint/complexity/useLiteralKeys: preserve the external Emscripten property name - fs = Module['nodeFs']; - // biome-ignore lint/complexity/useLiteralKeys: preserve the external Emscripten property name - let workerCwd = Module['cwd']; - FS.cwd = () => workerCwd; - // Keep standard output on Emscripten's console callbacks, including printErr - // so the host can record Rust panics. Native Worker fds are not stdio handles. - const consoles = [ - null, - { output: [], ops: TTY.default_tty_ops }, - { output: [], ops: TTY.default_tty1_ops }, - ]; - // NODERAWFS's stdio streams have no nodes; expose character-device metadata - // without asking the native Workers filesystem about those descriptor numbers. - const fstat = FS.fstat; - FS.fstat = (fd) => { - const stream = FS.getStreamChecked(fd); - if (stream.nfd === 0 || stream.nfd === 1 || stream.nfd === 2) { - return { - dev: 0, - ino: stream.nfd, - mode: 0o020666, - nlink: 1, - uid: 0, - gid: 0, - rdev: 0, - size: 0, - blksize: 4096, - blocks: 0, - atime: new Date(0), - mtime: new Date(0), - ctime: new Date(0), - }; - } - return fstat(fd); - }; - const write = FS.write; - FS.write = (stream, buffer, offset, length, position) => { - const tty = consoles[stream.nfd]; - if (!tty) return write(stream, buffer, offset, length, position); - for (let i = offset; i < offset + length; i++) tty.ops.put_char(tty, buffer[i]); - return length; - }; - // Non-*at syscalls can pass relative paths directly to NODERAWFS. Resolve - // those against this wasm instance, not the shared Node process directory. - for (const name of [ - 'lookupPath', - 'stat', - 'chmod', - 'chown', - 'truncate', - 'utime', - 'open', - 'mkdir', - 'rmdir', - 'readdir', - 'unlink', - 'readlink', - ]) { - const operation = FS[name]; - FS[name] = (path, ...args) => operation(PATH_FS.resolve(path), ...args); - } - const rename = FS.rename; - FS.rename = (from, to) => rename(PATH_FS.resolve(from), PATH_FS.resolve(to)); - const symlink = FS.symlink; - FS.symlink = (target, path) => symlink(target, PATH_FS.resolve(path)); - FS.chdir = (path) => { - path = PATH_FS.resolve(path); - if (!FS.isDir(FS.stat(path).mode)) throw new FS.ErrnoError(ERRNO_CODES.ENOTDIR); - workerCwd = path; - }; - }, - - // SQLite operations are synchronous. Checkpoint completion separately awaits - // storage.sync(); ordinary Rust File::sync_all must not suspend a sync export. - fd_sync__deps: ['$FS', '$workerFilesystem'], - fd_sync__async: false, - fd_sync: (fd) => { - try { - const stream = FS.getStreamChecked(fd); - if (typeof stream.nfd === 'number') fs.fsyncSync(stream.nfd); - else stream.stream_ops?.fsync?.(stream); - return 0; - } catch (error) { - if (typeof error.errno === 'number') return error.errno; - if (error.code) return ERRNO_CODES[error.code]; - throw error; - } - }, -}); diff --git a/worker/index.ts b/worker/index.ts deleted file mode 100644 index 142919f..0000000 --- a/worker/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { exports } from 'cloudflare:workers'; -import { forwardSocket } from './socket'; -export { MinecraftWorld } from './world'; - -export default { - async connect(socket: Socket, env: Env): Promise { - const target = exports.MinecraftWorld.getByName(env.WORLD_NAME).connect('world:25565', { allowHalfOpen: true }); - await forwardSocket(socket, target); - }, - async fetch(request: Request, env: Env): Promise { - const url = new URL(request.url); - if (url.pathname === '/health') return Response.json({ ready: true }); - const stub = exports.MinecraftWorld.getByName(env.WORLD_NAME); - if (request.method === 'GET' && url.pathname === '/') return Response.json(await stub.status()); - return new Response('Not found', { status: 404 }); - }, -} satisfies ExportedHandler; diff --git a/worker/modules.d.ts b/worker/modules.d.ts deleted file mode 100644 index 552bea7..0000000 --- a/worker/modules.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -declare module '*.wasm' { - const module: WebAssembly.Module; - export default module; -} - -declare module '*pumpkin-do.js' { - interface Options { - cwd: string; - nodeFs: typeof import('worker-fs-mount/fs-sync'); - instantiateWasm(imports: WebAssembly.Imports, receive: (instance: WebAssembly.Instance) => void): WebAssembly.Exports; - printErr(message: string): void; - } - interface Pumpkin { - pumpkin_start(): Promise; - pumpkin_connect(socket: Socket): Promise; - pumpkin_status(): { players: number; ticks: number; wasm_memory_bytes: number }; - pumpkin_shutdown(): Promise; - } - export default function create(options: Options): Promise; -} diff --git a/worker/pumpkin.ts b/worker/pumpkin.ts deleted file mode 100644 index c8b86fd..0000000 --- a/worker/pumpkin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import createPumpkin from '../target/workers/wasm32-unknown-emscripten/release/pumpkin-do.js'; -import pumpkinWasm from '../target/workers/wasm32-unknown-emscripten/release/pumpkin_do.wasm'; -import { createWorldRuntime } from './filesystem'; - -/** Bind the server API to its instance's filesystem and async context once. */ -export async function startPumpkin(storage: DurableObjectStorage) { - const { instance, run } = await createWorldRuntime(storage, options => createPumpkin({ - ...options, - instantiateWasm(imports, receive) { - const instance = new WebAssembly.Instance(pumpkinWasm, imports); - receive(instance); - return instance.exports; - }, - printErr: console.error, - })); - await run(() => instance.pumpkin_start()); - return { - connect: (socket: Socket) => run(() => instance.pumpkin_connect(socket)), - stop: () => run(() => instance.pumpkin_shutdown()), - status: () => run(() => instance.pumpkin_status()), - }; -} diff --git a/worker/socket.ts b/worker/socket.ts deleted file mode 100644 index 47f9d3b..0000000 --- a/worker/socket.ts +++ /dev/null @@ -1,25 +0,0 @@ -export async function forwardSocket(socket: Socket, target: Socket): Promise { - // Read/write failures are observed by the pumps below. Also consume the - // lifetime promises so a normal peer disconnect is not an unhandled rejection. - void socket.closed.catch(() => undefined); - void target.closed.catch(() => undefined); - const abort = new AbortController(); - const pipe = (source: ReadableStream, destination: WritableStream) => - source.pipeTo(destination, { signal: abort.signal }).catch(error => { - abort.abort(); - throw error; - }); - try { - const results = await Promise.allSettled([ - pipe(socket.readable, target.writable), - pipe(target.readable, socket.writable), - ]); - for (const result of results) { - if (result.status === 'rejected' && !/closed|closing|abort|cancel|reset|network connection lost/i.test(String(result.reason))) { - console.error('TCP forwarding failed', result.reason); - } - } - } finally { - await Promise.allSettled([socket.close(), target.close()]); - } -} diff --git a/worker/world.ts b/worker/world.ts deleted file mode 100644 index f457cfa..0000000 --- a/worker/world.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { DurableObject } from 'cloudflare:workers'; -import { startPumpkin } from './pumpkin'; - -type Runtime = Awaited>; -type State = - | { phase: 'idle' } - | { phase: 'starting'; pending: Promise } - | { phase: 'running'; runtime: Runtime } - | { phase: 'stopping'; pending: Promise } - | { phase: 'failed'; error: string }; - -export class MinecraftWorld extends DurableObject { - private state: State = { phase: 'idle' }; - private connections = 0; - private startupMilliseconds?: number; - private checkpointedAt?: string; - - protected loadRuntime(): Promise { - return startPumpkin(this.ctx.storage); - } - - private fail(error: unknown): void { - if (this.state.phase !== 'failed') { - this.state = { phase: 'failed', error: error instanceof Error ? error.message : String(error) }; - } - } - - protected async getRuntime(): Promise { - const state = this.state; - switch (state.phase) { - case 'running': return state.runtime; - case 'starting': return state.pending; - case 'failed': throw new Error(state.error); - case 'stopping': - await state.pending; - return this.getRuntime(); - case 'idle': { - const started = Date.now(); - const pending = Promise.resolve().then(() => this.loadRuntime()).then(runtime => { - this.startupMilliseconds = Date.now() - started; - this.state = { phase: 'running', runtime }; - return runtime; - }).catch(error => { this.fail(error); throw error; }); - this.state = { phase: 'starting', pending }; - return pending; - } - } - } - - async connect(socket: Socket): Promise { - void socket.closed.catch(() => undefined); - if (this.state.phase === 'stopping') await this.state.pending; - this.connections++; - try { - await (await this.getRuntime()).connect(socket); - } finally { - this.connections--; - await socket.close().catch(() => undefined); - if (this.connections === 0) await this.checkpoint(); - } - } - - private async checkpoint(): Promise { - const state = this.state; - if (state.phase === 'stopping') return state.pending; - if (state.phase !== 'running') return; - const pending = (async () => { - await state.runtime.stop(); - await this.ctx.storage.sync(); - this.checkpointedAt = new Date().toISOString(); - this.state = { phase: 'idle' }; - })().catch(error => { this.fail(error); throw error; }); - this.state = { phase: 'stopping', pending }; - return pending; - } - - status() { - let server: ReturnType | null = null; - if (this.state.phase === 'running') { - try { server = this.state.runtime.status(); } - catch (error) { this.fail(error); } - } - return { - phase: this.state.phase, - connections: this.connections, - failure: this.state.phase === 'failed' ? this.state.error : null, - startup_ms: this.startupMilliseconds ?? null, - checkpointed_at: this.checkpointedAt ?? null, - server, - }; - } -} diff --git a/wrangler.jsonc b/wrangler.jsonc index 7fb22ab..2a86dec 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,12 +1,26 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "rust-workers-minecraft", - "main": "worker/index.ts", + "main": "build/index.js", + "build": { + "command": "bash scripts/build.sh", + "watch_dir": ["src", "scripts", ".cargo", "Cargo.toml", "Cargo.lock", "build.rs", "rust-toolchain.toml"] + }, "compatibility_date": "2026-09-05", "compatibility_flags": ["nodejs_compat", "new_module_registry", "no_handle_cross_request_promise_resolution"], "dev": { "ip": "127.0.0.1", "port": 8787 }, "connect": [{ "protocol": "tcp", "address": "127.0.0.1", "port": 25565 }], "exports": { "MinecraftWorld": { "type": "durable-object", "storage": "sqlite" } }, - "vars": { "WORLD_NAME": "world" }, + "limits": { "cpu_ms": 3000000 }, + "vars": { + "WORLD_NAME": "world", + "VIEW_DISTANCE": "4", + "SIMULATION_DISTANCE": "3", + "MAX_PLAYERS": "20", + "COMPRESSION_THRESHOLD": "512", + "COMPRESSION_LEVEL": "1", + "IDLE_TIMEOUT_SECONDS": "10", + "MOTD": "Minecraft on Cloudflare Workers" + }, "observability": { "enabled": true, "head_sampling_rate": 1 } }