diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d4c197..0f48e93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: "true" - name: Set up Java uses: actions/setup-java@v4 @@ -42,7 +44,7 @@ jobs: mkdir -p minecraft-server cd minecraft-server # Download Fabric 26.1.2 (latest compatible version) - wget -O server.jar https://meta.fabricmc.net/v2/versions/loader/26.1.2/0.19.2/1.1.1/server/jar + wget -O server.jar https://meta.fabricmc.net/v2/versions/loader/26.2/0.19.3/1.1.1/server/jar echo "eula=true" > eula.txt - name: Configure Minecraft Server @@ -55,6 +57,8 @@ jobs: broadcast-console-to-ops=true broadcast-rcon-to-ops=true bug-report-link= + chat-spam-threshold-seconds=0 + command-spam-threshold-seconds=0 difficulty=easy enable-code-of-conduct=false enable-jmx-monitoring=false @@ -108,14 +112,14 @@ jobs: resource-pack-sha1= server-ip= server-port=25565 - simulation-distance=10 + simulation-distance=16 spawn-protection=16 status-heartbeat-interval=0 sync-chunk-writes=true text-filtering-config= text-filtering-version=0 use-native-transport=true - view-distance=10 + view-distance=16 white-list=false EOF @@ -151,12 +155,12 @@ jobs: sleep 2 - name: Build FlintMC - run: cargo build --release + run: cargo build - name: Run FlintMC Tests run: | # Run all example tests - cargo run --release -- example_tests/ --server localhost:25565 --recursive --verbose + cargo run -- FlintBenchmark/tests/ --server localhost:25565 --recursive --verbose timeout-minutes: 10 - name: Stop Minecraft Server diff --git a/Cargo.lock b/Cargo.lock index 0b244d3..613cb45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", @@ -36,9 +36,9 @@ checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -51,50 +51,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "assert_type_match" @@ -134,9 +134,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -177,15 +177,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.15.3" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -193,20 +193,21 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.36.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] name = "azalea" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-auth", "azalea-block", @@ -243,8 +244,8 @@ dependencies = [ [[package]] name = "azalea-auth" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf", "azalea-crypto", @@ -263,8 +264,8 @@ dependencies = [ [[package]] name = "azalea-block" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-block-macros", "azalea-buf", @@ -273,8 +274,8 @@ dependencies = [ [[package]] name = "azalea-block-macros" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "proc-macro2", "quote", @@ -283,8 +284,8 @@ dependencies = [ [[package]] name = "azalea-brigadier" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf", "azalea-chat", @@ -293,8 +294,8 @@ dependencies = [ [[package]] name = "azalea-buf" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf-macros", "byteorder", @@ -308,8 +309,8 @@ dependencies = [ [[package]] name = "azalea-buf-macros" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "proc-macro2", "quote", @@ -318,8 +319,8 @@ dependencies = [ [[package]] name = "azalea-chat" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf", "azalea-language", @@ -332,8 +333,8 @@ dependencies = [ [[package]] name = "azalea-client" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "async-compat", "azalea-auth", @@ -370,8 +371,8 @@ dependencies = [ [[package]] name = "azalea-core" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf", "azalea-chat", @@ -391,15 +392,15 @@ dependencies = [ [[package]] name = "azalea-crypto" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "aes", "azalea-buf", "cfb8", "md-5 0.11.0", - "num-bigint", - "rand 0.10.1", + "num-bigint 0.5.1", + "rand 0.10.2", "rand_core 0.10.1", "rsa", "rsa_public_encrypt_pkcs1", @@ -410,8 +411,8 @@ dependencies = [ [[package]] name = "azalea-entity" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-block", "azalea-buf", @@ -433,8 +434,8 @@ dependencies = [ [[package]] name = "azalea-inventory" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-auth", "azalea-buf", @@ -451,8 +452,8 @@ dependencies = [ [[package]] name = "azalea-inventory-macros" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "proc-macro2", "quote", @@ -461,8 +462,8 @@ dependencies = [ [[package]] name = "azalea-language" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "compact_str", "serde_json", @@ -470,8 +471,8 @@ dependencies = [ [[package]] name = "azalea-physics" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-block", "azalea-core", @@ -487,8 +488,8 @@ dependencies = [ [[package]] name = "azalea-protocol" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-auth", "azalea-block", @@ -522,8 +523,8 @@ dependencies = [ [[package]] name = "azalea-protocol-macros" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "proc-macro2", "quote", @@ -532,8 +533,8 @@ dependencies = [ [[package]] name = "azalea-registry" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-buf", "azalea-registry-macros", @@ -543,8 +544,8 @@ dependencies = [ [[package]] name = "azalea-registry-macros" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "quote", "syn", @@ -552,8 +553,8 @@ dependencies = [ [[package]] name = "azalea-world" -version = "0.16.0+mc26.1.2" -source = "git+https://github.com/azalea-rs/azalea?rev=5cefefe#5cefefeef3e38df098c71ab28e8b90e6dcd36bed" +version = "0.16.0+mc26.2" +source = "git+https://github.com/azalea-rs/azalea?rev=a8959f62#a8959f6264b2d6bc15d64db22cd957797818cef2" dependencies = [ "azalea-block", "azalea-buf", @@ -570,9 +571,9 @@ dependencies = [ [[package]] name = "base16ct" -version = "0.3.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -582,15 +583,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bevy_app" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def9f41aa5bf9b9dec8beda307a332798609cffb9d44f71005e0cfb45164f2f6" +checksum = "671ae6f3a8d84f9ed696c531a138558596e041231a8414b6efeafa8469e841f9" dependencies = [ "bevy_derive", "bevy_ecs", @@ -598,7 +599,6 @@ dependencies = [ "bevy_reflect", "bevy_tasks", "bevy_utils", - "cfg-if", "console_error_panic_hook", "ctrlc", "downcast-rs", @@ -611,9 +611,9 @@ dependencies = [ [[package]] name = "bevy_derive" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "318ee0532c3da93749859d18f89a889c638fbc56aabac4d866583df7b951d103" +checksum = "293df2b2f7ed65ef2ce02b2e7359faac1a9a92a8cc96c182c9de77eb06049ae0" dependencies = [ "bevy_macro_utils", "quote", @@ -622,9 +622,9 @@ dependencies = [ [[package]] name = "bevy_ecs" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cf7a3ee41342dd7b5a5d82e200d0e8efb933169247fce853b4ad633d51e87d" +checksum = "4654b9b3535fa7813d2878a50e1b5ad80a361dc1c913b96ded57667f65d70a01" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -648,12 +648,25 @@ dependencies = [ "variadics_please", ] +[[package]] +name = "bevy_ecs_macro_logic" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0b98a74141ce8dce4654c60020ebbaefab32646f42af6bbae55f282ae65ff7" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bevy_ecs_macros" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908baf585e2ea16bd53ef0da57b69580478af0059d2dbdb4369991ac9794b618" +checksum = "4dc958a194d226d618404e0fea99a3c8b4146207a00a3ba07fa712bf71607240" dependencies = [ + "bevy_ecs_macro_logic", "bevy_macro_utils", "proc-macro2", "quote", @@ -662,9 +675,9 @@ dependencies = [ [[package]] name = "bevy_log" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2aac1187f83a1ab2eae887564f7fb14b4abb3fbe8b2267a6426663463923120" +checksum = "2017a5fe12ea230d00cfdca00c65c262924be26e0324f7e0033c64f8cc265888" dependencies = [ "android_log-sys", "bevy_app", @@ -680,9 +693,9 @@ dependencies = [ [[package]] name = "bevy_macro_utils" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b147843b81a7ec548876ff97fa7bfdc646ef2567cb465566259237b39664438" +checksum = "746a19912c6dc1bbe79188778573e8a253d5832c696b2fcb95578c17b29ff7ba" dependencies = [ "proc-macro2", "quote", @@ -692,13 +705,14 @@ dependencies = [ [[package]] name = "bevy_platform" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6b36504169b644acd26a5469fd8d371aa6f1d73ee5c01b1b1181ae1cefbf9b" +checksum = "3120670bb2308980f723477c9d82476fcda31f08be9b256c3f0acdee82a65094" dependencies = [ "critical-section", - "foldhash 0.2.0", + "foldhash", "futures-channel", + "futures-lite", "hashbrown 0.16.1", "js-sys", "portable-atomic", @@ -708,19 +722,20 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-time", + "windows-sys 0.61.2", ] [[package]] name = "bevy_ptr" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a9329e8dc4e01ced480eeec4902e6d7cb56e56ec37f6fbc4323e5c937290a7" +checksum = "b511e89f7078fd21fdea77061a0ca3e737297c7011bb10c073e0aecb17c9fea6" [[package]] name = "bevy_reflect" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1dfeb67a9fe4f59003a84f5f99ba6302141c70e926601cbc6abfd4a1eea9ca9" +checksum = "92abd59cbba1524c1847e9a50f74d94e6b7836c80a8edfa9c47e59f7fd81021d" dependencies = [ "assert_type_match", "bevy_platform", @@ -731,7 +746,7 @@ dependencies = [ "disqualified", "downcast-rs", "erased-serde", - "foldhash 0.2.0", + "foldhash", "glam", "indexmap", "serde", @@ -745,9 +760,9 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "475f68c93e9cd5f17e9167635c8533a4f388f12d38245a202359e4c2721d87ba" +checksum = "90fa4e6b97fd2d582dd5ed96762a70d04505a477c833cf4f69d016dcd03d2d04" dependencies = [ "bevy_macro_utils", "indexmap", @@ -759,9 +774,9 @@ dependencies = [ [[package]] name = "bevy_tasks" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384eb04d80aa38664d69988fd30cbbe03e937ecb65c66aa6abe60ce0bca826aa" +checksum = "fa34e6b9b804851ec08a41c0346c859d82e2fa98c906d74b965ee61bbbb688e4" dependencies = [ "async-channel", "async-executor", @@ -773,14 +788,14 @@ dependencies = [ "derive_more", "futures-lite", "heapless", - "pin-project", + "web-task", ] [[package]] name = "bevy_time" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ef9af4e523195e561074cf60fbfad0f4cb8d1db504855fee3c4ce8896c7244" +checksum = "c21e3be2aa4426ea1e0a7036d3d1dbbded84c319459d9f3e2a616b33563372f2" dependencies = [ "bevy_app", "bevy_ecs", @@ -793,20 +808,22 @@ dependencies = [ [[package]] name = "bevy_utils" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111910cd7a4b1e6ce07eaaeb6f68a2c0ea0ca609ed0d0d506e3eb161101435b" +checksum = "3aceea6c7ffdd568f866d5ca4dfe50c33440c54f7bc230c13a9ba7ab0c91aed1" dependencies = [ + "async-channel", "bevy_platform", "disqualified", + "indexmap", "thread_local", ] [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -822,9 +839,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -840,15 +857,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -872,9 +889,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "castaway" @@ -887,9 +904,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.53" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -897,12 +914,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfb8" version = "0.9.1" @@ -926,9 +937,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures", @@ -937,9 +948,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "num-traits", "serde", @@ -957,9 +968,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -967,9 +978,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -979,18 +990,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -1000,30 +1011,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -1046,9 +1057,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -1081,9 +1092,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "convert_case" @@ -1161,18 +1172,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1180,33 +1191,33 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crypto-bigint" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ "cpubits", "ctutils", @@ -1237,20 +1248,19 @@ dependencies = [ [[package]] name = "crypto-primes" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" dependencies = [ "crypto-bigint", - "libm", "rand_core 0.10.1", ] [[package]] name = "ctrlc" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", "nix", @@ -1268,15 +1278,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -1322,16 +1332,16 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", ] [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags", "block2", @@ -1341,9 +1351,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1370,9 +1380,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "enum-as-inner" @@ -1394,15 +1404,25 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", "typeid", ] +[[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 = "event-listener" version = "5.4.1" @@ -1426,15 +1446,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -1455,11 +1475,13 @@ dependencies = [ [[package]] name = "flint-core" -version = "1.1.3" -source = "git+https://github.com/FlintTestMC/flint-core?tag=v1.1.3#8afddb21b049b41e4aed0f8099d5dc79febcd1d3" +version = "1.1.4" +source = "git+https://github.com/FlintTestMC/flint-core?rev=42857bf#42857bf5bce6ad78a630b0359a1e8eb00d726b2f" dependencies = [ "anyhow", + "base64", "colored", + "flate2", "rayon", "rustc-hash", "semver", @@ -1486,18 +1508,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1632,9 +1642,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1645,37 +1655,23 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", + "r-efi", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] name = "glam" -version = "0.30.9" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" dependencies = [ "serde_core", ] @@ -1689,15 +1685,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1747,8 +1734,8 @@ dependencies = [ "hickory-proto", "idna", "ipnet", - "jni 0.22.2", - "rand 0.10.1", + "jni", + "rand 0.10.2", "thiserror 2.0.18", "tinyvec", "tokio", @@ -1765,10 +1752,10 @@ dependencies = [ "data-encoding", "idna", "ipnet", - "jni 0.22.2", + "jni", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", "thiserror 2.0.18", "tinyvec", @@ -1788,12 +1775,12 @@ dependencies = [ "hickory-proto", "ipconfig", "ipnet", - "jni 0.22.2", + "jni", "moka", "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "smallvec", "system-configuration", @@ -1804,12 +1791,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1844,18 +1830,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1866,7 +1852,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1874,15 +1859,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -1890,14 +1874,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1906,7 +1889,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2", "tokio", "tower-service", "tracing", @@ -1914,12 +1897,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1927,9 +1911,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1940,9 +1924,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1954,15 +1938,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1974,15 +1958,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1993,12 +1977,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -2012,9 +1990,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2028,8 +2006,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -2043,35 +2019,26 @@ dependencies = [ [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" dependencies = [ "serde", ] -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2080,36 +2047,20 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.0", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cad2989693095826dade4735429d1c427e79df7cc4013a919d3a7719b884b" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", "thiserror 2.0.18", @@ -2119,9 +2070,9 @@ dependencies = [ [[package]] name = "jni-macros" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c3d1da60c95c98847b26b9d45f4360fee718b31de746df016d9cd6de916a7ef" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", @@ -2130,12 +2081,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "jni-sys" version = "0.4.1" @@ -2157,21 +2102,22 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2181,29 +2127,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2216,9 +2150,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2257,9 +2191,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "minecraft_folder_path" @@ -2279,9 +2213,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -2290,9 +2224,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.11" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2300,7 +2234,6 @@ dependencies = [ "equivalent", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", "uuid", @@ -2314,9 +2247,9 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags", "cfg-if", @@ -2351,7 +2284,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-complex", "num-integer", "num-iter", @@ -2361,9 +2294,19 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" dependencies = [ "num-integer", "num-traits", @@ -2399,11 +2342,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2414,7 +2356,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-integer", "num-traits", ] @@ -2430,9 +2372,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2445,9 +2387,9 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -2461,9 +2403,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "parking" @@ -2509,37 +2451,11 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -2561,26 +2477,32 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2605,16 +2527,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -2626,9 +2538,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -2637,7 +2549,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -2646,15 +2558,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.2", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -2668,33 +2581,27 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2709,33 +2616,23 @@ checksum = "59ffec9df464013295b499298811e6a3de31bf8128092135826517db12dee601" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -2749,39 +2646,35 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core 0.10.1", ] [[package]] -name = "rand_core" -version = "0.10.1" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" @@ -2814,9 +2707,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -2826,9 +2719,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -2837,15 +2730,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2881,9 +2774,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "ring" @@ -2893,7 +2786,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2925,15 +2818,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3e9243a1f8b312c5535c09de102cc061416515201b194ee4f0a9a76da20ebf4" dependencies = [ "num", - "rand 0.8.5", + "rand 0.8.6", "simple_asn1", ] [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2946,9 +2839,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -2960,9 +2853,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -2972,9 +2865,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -2982,13 +2875,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -3009,9 +2902,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3021,15 +2914,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3042,9 +2935,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3057,9 +2950,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -3070,9 +2963,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3080,9 +2973,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -3116,9 +3009,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3141,9 +3034,9 @@ dependencies = [ [[package]] name = "serdect" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ef0e35b322ddfaecbc60f34ab448e157e48531288ee49fafbb053696b8ffe2" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" dependencies = [ "base16ct", "serde", @@ -3182,16 +3075,17 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3207,15 +3101,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_cesu8" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e75ccd3404ef387c23d4cf34a30d49eab8f2aeb17e5ad575aacad434170daf" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" dependencies = [ "rustc_version", "simdutf8", @@ -3259,31 +3153,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80" dependencies = [ "chrono", - "num-bigint", + "num-bigint 0.4.8", "num-traits", "thiserror 1.0.69", ] [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smol_str" @@ -3296,19 +3190,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3316,15 +3200,16 @@ dependencies = [ [[package]] name = "socks5-impl" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "150816c2d954315f351129f438f851285e1ddb6d6ccc850ddd45c523d19abda0" +checksum = "378b73f34c3f2ad440310f40bf72574c76c3b72189f4d6c4180f3189884bf3a0" dependencies = [ "async-trait", "bytes", "percent-encoding", "thiserror 2.0.18", "tokio", + "url", ] [[package]] @@ -3372,9 +3257,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3479,9 +3364,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3489,9 +3374,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3514,7 +3399,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3555,18 +3440,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -3576,18 +3461,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3600,20 +3485,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3685,9 +3570,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3726,21 +3611,21 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -3756,9 +3641,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3780,11 +3665,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "md-5 0.10.6", "serde_core", @@ -3839,29 +3724,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3872,22 +3739,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3895,9 +3759,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3908,53 +3772,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" +name = "web-sys" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "web-sys" -version = "0.3.82" +name = "web-task" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" dependencies = [ + "async-task", + "cfg-if", "js-sys", "wasm-bindgen", ] @@ -3971,25 +3813,25 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] [[package]] name = "wgpu-types" -version = "27.0.1" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" dependencies = [ "bitflags", "bytemuck", "js-sys", "log", + "raw-window-handle", "serde", - "thiserror 2.0.18", "web-sys", ] @@ -4015,39 +3857,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-targets 0.42.2", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -4059,373 +3903,90 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.13" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4434,9 +3995,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4446,18 +4007,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -4466,18 +4027,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4487,15 +4048,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4504,9 +4065,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4515,9 +4076,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -4526,12 +4087,12 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" -version = "1.0.15" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 105053a..16c1027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,16 @@ edition = "2024" license = "MIT" [dependencies] -azalea = { git = "https://github.com/azalea-rs/azalea", rev = "5cefefe" } +azalea = { git = "https://github.com/azalea-rs/azalea", rev = "a8959f62" } tokio = { version = "1.48", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -anyhow = "1.0.102" +anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } colored = "3.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } parking_lot = "0.12" -flint-core = { git = "https://github.com/FlintTestMC/flint-core", tag = "v1.1.3" } +flint-core = { git = "https://github.com/FlintTestMC/flint-core", rev = "42857bf" } clap_complete = "4.5.65" rustc-hash = "2.1" diff --git a/FlintBenchmark b/FlintBenchmark index c19a090..2c41ecb 160000 --- a/FlintBenchmark +++ b/FlintBenchmark @@ -1 +1 @@ -Subproject commit c19a09050c1628e9786fe91a57c832e49ddc9a7f +Subproject commit 2c41ecb1a2913f38d48b827da65537f48b57229a diff --git a/README.md b/README.md index 24dfd6b..d70e441 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,17 @@ flintmc -s localhost:25565 -i See **[USAGE.md](USAGE.md)** for the full reference: all flags, output modes, interactive commands, test recording guide, and test format specification. +## Debugging and Visualization + +### Flint Visualizer Links +When a test fails in the default `Pretty` format, FlintCLI automatically emits a clickable link containing the compressed test spec and failure data. Clicking this link opens it in the **FlintVisualizer** tool for interactive 3D inspection. + +By default, the visualizer URL uses the hosted FlintViz application. You can customize this by setting the `FLINT_VIZ_URL` environment variable: + +```bash +FLINT_VIZ_URL="http://your-viz-host:5173" flintmc example_tests/ -s localhost:25565 +``` + ## License MIT diff --git a/USAGE.md b/USAGE.md index a1823a9..f17eb06 100644 --- a/USAGE.md +++ b/USAGE.md @@ -21,7 +21,7 @@ flintmc [OPTIONS] --server [PATH] | `--break-after-setup` | | Pause after test setup (cleanup phase) for manual inspection | | `--tag ` | `-t` | Filter tests by tag. Can be specified multiple times | | `--interactive` | `-i` | Enter interactive mode (listen for in-game chat commands) | -| `--action-delay ` | `-d` | Delay between actions in milliseconds (default: 100) | +| `--record ` | | Enter interactive mode and immediately start recording `NAME` | | `--verbose` | `-v` | Show detailed per-action output during execution | | `--quiet` | `-q` | Suppress the progress bar | | `--fail-fast` | | Stop after the first test failure | @@ -227,7 +227,12 @@ The recorder lets you create tests by performing actions in-game. The bot watche ``` The name can include `/` for subdirectories. Time is automatically frozen. -3. Place and break blocks in-game. The bot scans for changes in a 10-block radius around its position. + You can also start recording directly from the CLI: + ```bash + flintmc -s localhost:25565 --record redstone/my_test + ``` + +3. Place and break blocks in-game. The bot scans for changes in a fixed 10-block radius around the position where recording started. 4. When ready to advance a tick: ``` @@ -241,18 +246,24 @@ The recorder lets you create tests by performing actions in-game. The bot watche ``` Records the block at that position as an expected value. -6. To convert all detected changes in the current tick into assertions instead of placements: +6. Record a player interaction at the tracked player's current position and orientation: + ``` + !use [item] + ``` + This records a `tp` followed by an `interact`. If `item` is omitted, the test uses the player's active hand. + +7. To convert all detected changes in the current tick into assertions instead of placements: ``` !assert_changes ``` -7. Save the test: +8. Save the test: ``` !save ``` The JSON file is written to the tests directory. The test index is automatically reloaded so you can immediately run it with `!run`. -8. Or discard: +9. Or discard: ``` !cancel ``` @@ -264,13 +275,15 @@ The recorder lets you create tests by performing actions in-game. The bot watche | `!record [player]` | Start recording. Optional player name for position tracking | | `!tick` / `!next` | Snapshot changes and advance one game tick | | `!assert ` | Assert the block at the given coordinates | +| `!use [item]` | Record `tp` to the tracked player's current pose, then `interact` | | `!assert_changes` | Convert all detected block changes to assertions | | `!save` | Save the recording as a JSON test file | | `!cancel` | Discard the recording and unfreeze time | ### Tips -- The recorder auto-detects block placements and removals within scan range. +- The recorder auto-detects block placements and removals within the fixed scan range captured when recording starts. +- Use `!record [player]` to choose which player is used for the initial scan center and later `!use` pose capture. - Positions are stored relative to the first block changed (origin), so tests are portable. - The cleanup region is computed automatically from the bounding box of all affected blocks. - Saved tests are tagged with `recorded` so you can filter them: `flintmc -s ... -t recorded`. @@ -342,6 +355,20 @@ Tests are JSON files: { "at": 0, "do": "remove", "pos": [0, 64, 0] } ``` +**summon** -- summon an entity and assign a test-local alias: +```json +{ + "at": 0, + "do": "summon", + "entity_alias": "falling", + "entity_type": "minecraft:falling_block", + "pos": [1.5, 64, 1.5], + "nbt": "{NoGravity:1b}" +} +``` + +`nbt` is optional raw Minecraft summon NBT. FlintMC injects its internal alias tag into the final summon command. + **assert** -- check block type(s): ```json { @@ -358,6 +385,24 @@ Blocks can include state properties: { "pos": [0, 64, 0], "is": { "id": "minecraft:oak_fence", "properties": { "east": "true" } } } ``` +Assertions can also check summoned entities by alias: +```json +{ + "at": 1, + "do": "assert", + "checks": [ + { + "entity_alias": "falling", + "is": "minecraft:falling_block", + "pos": [1.5, 64, 1.5], + "position_tolerance": 0.5 + } + ] +} +``` + +`entity_alias` is resolved by the server implementation. FlintMC keeps this alias map in its Minecraft implementation and reserves `"player"` for the bot-backed player. + **assert_state** -- check a property across multiple ticks: ```json { diff --git a/src/bot.rs b/src/bot.rs index fe242ee..6821cf2 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,21 +1,64 @@ use anyhow::Result; +use azalea::app::{App, Plugin, Update}; +use azalea::ecs::schedule::IntoScheduleConfigs; use azalea::prelude::*; +use flint_core::test_spec::{GameMode, Item, PlayerSlot}; use parking_lot::RwLock; +use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use tokio::sync::mpsc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; // Constants for connection and timing const INIT_WAIT_ATTEMPTS: u32 = 50; const INIT_WAIT_DELAY_MS: u64 = 100; const GAME_STATE_WAIT_ATTEMPTS: u32 = 100; -const WORLD_SYNC_DELAY_MS: u64 = 500; +const STATE_SYNC_TIMEOUT_MS: u64 = 2_000; +const CHUNK_SYNC_TIMEOUT_MS: u64 = 10_000; +const STATE_SYNC_POLL_MS: u64 = 5; +// Must be configured during Event::Init, before Azalea allocates PartialWorld. +// Vanilla servers support at most 32 chunks and may clamp this request lower. +const CLIENT_VIEW_DISTANCE: u8 = 32; + +type ChatReceiver = std::sync::mpsc::Receiver<(Option, String)>; +type AckReceiver = std::sync::mpsc::Receiver; +type UpdateReceiver = std::sync::mpsc::Receiver<()>; + +struct TestBotRuntimePlugin { + update_tx: std::sync::mpsc::SyncSender<()>, +} + +impl Plugin for TestBotRuntimePlugin { + fn build(&self, app: &mut App) { + app.configure_sets( + azalea::core::tick::GameTick, + azalea::physics::PhysicsSystems.run_if(|| false), + ); + let update_tx = self.update_tx.clone(); + app.add_systems(Update, move || { + let _ = update_tx.try_send(()); + }); + } +} + +#[derive(Clone, Default)] +struct ActivePlayer { + owner: Option, + slots: HashMap, + selected_hotbar: Option, + position: Option<[f64; 3]>, + rotation: Option<[f32; 2]>, + game_mode: Option, +} #[derive(Clone, Component)] struct State { client_handle: Arc>>, in_game: Arc, - chat_tx: Option, String)>>, + chat_tx: Option, String)>>, + ack_tx: Option>, + world_ready_tx: Option>, + view_distance: Arc, + simulation_distance: Arc, } impl Default for State { @@ -24,15 +67,43 @@ impl Default for State { client_handle: Arc::new(RwLock::new(None)), in_game: Arc::new(AtomicBool::new(false)), chat_tx: None, + ack_tx: None, + world_ready_tx: None, + view_distance: Arc::new(AtomicU32::new(0)), + simulation_distance: Arc::new(AtomicU32::new(0)), } } } -#[derive(Default)] +#[derive(Clone)] pub struct TestBot { client: Option>>>, in_game: Option>, - chat_rx: Option, String)>>, + chat_rx: Option>>, + ack_rx: Option>>, + update_rx: Option>>, + next_inventory_owner: Arc, + next_command_ack: Arc, + active_player: Arc>, + view_distance: Arc, + simulation_distance: Arc, +} + +impl Default for TestBot { + fn default() -> Self { + Self { + client: None, + in_game: None, + chat_rx: None, + ack_rx: None, + update_rx: None, + next_inventory_owner: Arc::new(AtomicU64::new(1)), + next_command_ack: Arc::new(AtomicU64::new(1)), + active_player: Arc::new(parking_lot::Mutex::new(ActivePlayer::default())), + view_distance: Arc::new(AtomicU32::new(0)), + simulation_distance: Arc::new(AtomicU32::new(0)), + } + } } impl TestBot { @@ -48,20 +119,27 @@ impl TestBot { .map(|handle| handle.read()) } - pub async fn connect(&mut self, server: &str) -> Result<()> { + pub fn connect(&mut self, server: &str) -> Result<()> { let account = Account::offline("flintmc_testbot"); tracing::info!("Connecting to server: {}", server); // Create chat channel - let (chat_tx, chat_rx) = mpsc::unbounded_channel(); + let (chat_tx, chat_rx) = std::sync::mpsc::channel(); + let (ack_tx, ack_rx) = std::sync::mpsc::channel(); + let (update_tx, update_rx) = std::sync::mpsc::sync_channel(1); + let (world_ready_tx, world_ready_rx) = std::sync::mpsc::sync_channel(1); let state = State { chat_tx: Some(chat_tx), + ack_tx: Some(ack_tx), + world_ready_tx: Some(world_ready_tx), ..Default::default() }; let client_handle = state.client_handle.clone(); let in_game = state.in_game.clone(); + let view_distance = state.view_distance.clone(); + let simulation_distance = state.simulation_distance.clone(); // Spawn the bot in a background thread with LocalSet (required by new azalea version) let server_owned = server.to_string(); @@ -76,6 +154,10 @@ impl TestBot { async fn handler(bot: Client, event: Event, state: State) -> Result<()> { match event { Event::Init => { + bot.set_client_information(azalea::ClientInformation { + view_distance: CLIENT_VIEW_DISTANCE, + ..Default::default() + })?; *state.client_handle.write() = Some(bot.clone()); tracing::info!("Bot initialized"); } @@ -84,6 +166,12 @@ impl TestBot { state.in_game.store(true, Ordering::SeqCst); tracing::info!("Bot in game state"); } + Event::Spawn => { + if let Some(tx) = &state.world_ready_tx { + let _ = tx.try_send(()); + } + tracing::info!("Bot world spawned"); + } Event::Chat(m) => { // Extract the message content let message = m.message().to_string(); @@ -95,16 +183,56 @@ impl TestBot { None }; - if let Some(ref tx) = state.chat_tx { + if message.contains("__flintmc_ack_") { + if let Some(tx) = &state.ack_tx { + let _ = tx.send(message); + } + } else if let Some(ref tx) = state.chat_tx { let _ = tx.send((sender, message)); } } + Event::Packet(packet) => { + use azalea::protocol::packets::game::ClientboundGamePacket; + match &*packet { + ClientboundGamePacket::Login(packet) => { + let effective = + packet.chunk_radius.min(packet.simulation_distance); + bot.set_client_information(azalea::ClientInformation { + view_distance: effective.min(u8::MAX.into()) as u8, + ..Default::default() + })?; + state.view_distance.store(effective, Ordering::SeqCst); + state + .simulation_distance + .store(packet.simulation_distance, Ordering::SeqCst); + } + ClientboundGamePacket::SetChunkCacheRadius(packet) => { + state.view_distance.store(packet.radius, Ordering::SeqCst); + } + ClientboundGamePacket::SetSimulationDistance(packet) => { + state + .simulation_distance + .store(packet.simulation_distance, Ordering::SeqCst); + let effective = state + .view_distance + .load(Ordering::SeqCst) + .min(packet.simulation_distance); + bot.set_client_information(azalea::ClientInformation { + view_distance: effective.min(u8::MAX.into()) as u8, + ..Default::default() + })?; + state.view_distance.store(effective, Ordering::SeqCst); + } + _ => {} + } + } _ => {} } Ok(()) } let result = ClientBuilder::new() + .add_plugins(TestBotRuntimePlugin { update_tx }) .set_handler(handler) .set_state(state) .start(account, server_owned.as_str()) @@ -118,7 +246,7 @@ impl TestBot { // Wait for client to initialize for _ in 0..INIT_WAIT_ATTEMPTS { - tokio::time::sleep(tokio::time::Duration::from_millis(INIT_WAIT_DELAY_MS)).await; + std::thread::sleep(std::time::Duration::from_millis(INIT_WAIT_DELAY_MS)); if client_handle.read().is_some() { break; } @@ -131,7 +259,7 @@ impl TestBot { // Wait for bot to be in game state tracing::info!("Waiting for bot to enter game state..."); for _ in 0..GAME_STATE_WAIT_ATTEMPTS { - tokio::time::sleep(tokio::time::Duration::from_millis(INIT_WAIT_DELAY_MS)).await; + std::thread::sleep(std::time::Duration::from_millis(INIT_WAIT_DELAY_MS)); if in_game.load(Ordering::SeqCst) { break; } @@ -143,31 +271,119 @@ impl TestBot { self.client = Some(client_handle); self.in_game = Some(in_game); - self.chat_rx = Some(chat_rx); + self.chat_rx = Some(Arc::new(parking_lot::Mutex::new(chat_rx))); + self.ack_rx = Some(Arc::new(parking_lot::Mutex::new(ack_rx))); + self.update_rx = Some(Arc::new(parking_lot::Mutex::new(update_rx))); + self.view_distance = view_distance; + self.simulation_distance = simulation_distance; tracing::info!("Connected successfully and in game state"); - // Give a small amount of extra time for world data to sync - tokio::time::sleep(tokio::time::Duration::from_millis(WORLD_SYNC_DELAY_MS)).await; + world_ready_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .map_err(|_| anyhow::anyhow!("Bot world did not spawn within timeout"))?; + self.reset_to_test_origin()?; + + Ok(()) + } + + pub fn reset_to_test_origin(&self) -> Result<()> { + let client = { + let client_guard = self.get_client()?; + client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))? + .clone() + }; + let mut updates = client.get_update_broadcaster(); + self.send_command("execute in minecraft:overworld run setblock 0 63 0 minecraft:bedrock")?; + self.send_command("execute in minecraft:overworld run tp flintmc_testbot 0.5 64 0.5 0 0")?; + self.keep_airborne()?; + self.wait_until("Overworld test origin", || { + let in_overworld = client + .world_name() + .is_ok_and(|world| world.to_string() == "minecraft:overworld"); + let at_origin = client.position().is_ok_and(|position| { + (position.x - 0.5).abs() < 0.01 && (position.z - 0.5).abs() < 0.01 + }); + in_overworld && at_origin + }) + .map_err(|error| { + anyhow::anyhow!( + "{error}; current world={:?}, position={:?}", + client.world_name(), + client.position() + ) + })?; + let _ = updates.blocking_recv(); + let _ = updates.blocking_recv(); + *self.active_player.lock() = ActivePlayer::default(); + Ok(()) + } + /// Park the physical bot at the shared focus point and discard any active virtual + /// player ownership. The next player action will restore that player's full state. + pub fn park_at(&self, position: [f64; 3]) -> Result<()> { + self.keep_airborne()?; + self.teleport(position, Some([0.0, 0.0]))?; + *self.active_player.lock() = ActivePlayer::default(); Ok(()) } + pub fn keep_airborne(&self) -> Result<()> { + let client = { + let client_guard = self.get_client()?; + client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))? + .clone() + }; + client.query_self::<&mut azalea::entity::PlayerAbilities, _>(|mut abilities| { + abilities.can_fly = true; + abilities.flying = true; + })?; + client.write_packet( + azalea::protocol::packets::game::s_player_abilities::ServerboundPlayerAbilities { + is_flying: true, + }, + ); + // The acknowledgement packet is sent after the abilities packet on the same + // connection, so receiving it proves the server has processed flying=true. + self.send_command_synced("execute if entity flintmc_testbot run return 1")?; + Ok(()) + } + + pub fn effective_chunk_distance(&self) -> Result { + let view = self.view_distance.load(Ordering::SeqCst); + let simulation = self.simulation_distance.load(Ordering::SeqCst); + if view == 0 || simulation == 0 { + anyhow::bail!( + "server did not advertise view/simulation distance (view={view}, simulation={simulation})" + ); + } + Ok(view.min(simulation)) + } + + pub fn detected_distances(&self) -> (u32, u32) { + ( + self.view_distance.load(Ordering::SeqCst), + self.simulation_distance.load(Ordering::SeqCst), + ) + } + /// Wait for a chat message with timeout - pub async fn recv_chat_timeout( - &mut self, + pub fn recv_chat_timeout( + &self, timeout: std::time::Duration, ) -> Option<(Option, String)> { - if let Some(ref mut rx) = self.chat_rx { - tokio::time::timeout(timeout, rx.recv()) - .await - .ok() - .flatten() + if let Some(ref rx_mutex) = self.chat_rx { + let rx = rx_mutex.lock(); + rx.recv_timeout(timeout).ok() } else { None } } - pub async fn send_command(&self, command: &str) -> Result<()> { + pub fn send_command(&self, command: &str) -> Result<()> { let client_guard = self.get_client()?; let client = client_guard .as_ref() @@ -184,7 +400,350 @@ impl TestBot { Ok(()) } - pub async fn get_block(&self, pos: [i32; 3]) -> Result> { + /// Send a command and wait until the server has processed it. Commands from one + /// connection are ordered, so receiving the marker also acknowledges every command + /// sent before it without relying on an arbitrary delay. + pub fn send_command_synced(&self, command: &str) -> Result<()> { + self.send_command(command)?; + let id = self.next_command_ack.fetch_add(1, Ordering::Relaxed); + let marker = format!("__flintmc_ack_{id}__"); + self.send_command(&format!( + "tellraw flintmc_testbot {{\"text\":\"{marker}\"}}" + ))?; + + let deadline = + std::time::Instant::now() + std::time::Duration::from_millis(STATE_SYNC_TIMEOUT_MS); + while std::time::Instant::now() < deadline { + let Some(ack_rx) = &self.ack_rx else { + anyhow::bail!("command acknowledgement channel is unavailable"); + }; + if ack_rx + .lock() + .recv_timeout(std::time::Duration::from_millis(STATE_SYNC_POLL_MS)) + .is_ok_and(|message| message.contains(&marker)) + { + return Ok(()); + } + } + anyhow::bail!("timed out waiting for command acknowledgement: {command}") + } + + /// Fence server world changes against Azalea's packet processing. The marker is + /// emitted after earlier commands/ticks. After observing it, wait for a subsequent + /// Azalea ECS update so packet-driven world mutations are visible to readers. + pub fn sync_client_world(&self) -> Result<()> { + self.send_command_synced("execute if entity flintmc_testbot run return 1")?; + let Some(update_rx) = &self.update_rx else { + anyhow::bail!("Azalea update channel is unavailable"); + }; + let update_rx = update_rx.lock(); + while update_rx.try_recv().is_ok() {} + update_rx + .recv_timeout(std::time::Duration::from_millis(STATE_SYNC_TIMEOUT_MS)) + .map_err(|error| anyhow::anyhow!("failed waiting for Azalea world update: {error}"))?; + Ok(()) + } + + pub fn teleport(&self, pos: [f64; 3], rot: Option<[f32; 2]>) -> Result<()> { + let client = { + let client_guard = self.get_client()?; + client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))? + .clone() + }; + let mut updates = client.get_update_broadcaster(); + let command = match rot { + Some([yaw, pitch]) => format!( + "execute in minecraft:overworld run tp flintmc_testbot {} {} {} {} {}", + pos[0], pos[1], pos[2], yaw, pitch + ), + None => format!( + "execute in minecraft:overworld run tp flintmc_testbot {} {} {}", + pos[0], pos[1], pos[2] + ), + }; + self.send_command(&command)?; + + if let Some([yaw, pitch]) = rot { + client.set_direction(yaw, pitch)?; + } + + self.wait_until("teleport", || { + client + .world_name() + .is_ok_and(|world| world.to_string() == "minecraft:overworld") + && client.position().is_ok_and(|actual| { + (actual.x - pos[0]).abs() < 0.01 + && (actual.y - pos[1]).abs() < 0.75 + && (actual.z - pos[2]).abs() < 0.01 + }) + })?; + let _ = updates.blocking_recv(); + let _ = updates.blocking_recv(); + Ok(()) + } + + pub fn interact(&self) -> Result<()> { + let client = { + let client_guard = self.get_client()?; + client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))? + .clone() + }; + let mut updates = client.get_update_broadcaster(); + let mut ticks = client.get_tick_broadcaster(); + let block_hit = client + .hit_result()? + .as_block_hit_result_if_not_miss() + .cloned(); + if let Some(hit) = block_hit { + client.block_interact(hit.block_pos); + } else { + client.start_use_item(); + } + let _ = updates.blocking_recv(); + let _ = updates.blocking_recv(); + let _ = ticks.blocking_recv(); + let _ = ticks.blocking_recv(); + // The acknowledgement is sent after the interaction packet on the same + // connection. Waiting for it and a subsequent ECS update makes both world + // and inventory changes visible without requiring a visible state change. + self.sync_client_world() + } + + pub fn select_hotbar(&self, slot: u8) -> Result<()> { + if !(1..=9).contains(&slot) { + anyhow::bail!("hotbar slot must be in the range 1..=9"); + } + let client = { + let client_guard = self.get_client()?; + client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))? + .clone() + }; + let mut ticks = client.get_tick_broadcaster(); + client.set_selected_hotbar_slot(slot - 1); + self.wait_until("hotbar selection", || { + client + .selected_hotbar_slot() + .is_ok_and(|selected| selected == slot - 1) + })?; + let _ = ticks.blocking_recv(); + let _ = ticks.blocking_recv(); + Ok(()) + } + + pub fn allocate_inventory_owner(&self) -> u64 { + self.next_inventory_owner.fetch_add(1, Ordering::Relaxed) + } + + pub fn restore_player( + &self, + owner: u64, + slots: &HashMap, + selected_hotbar: u8, + position: Option<[f64; 3]>, + rotation: Option<[f32; 2]>, + game_mode: GameMode, + ) -> Result<()> { + let mut active = self.active_player.lock(); + if active.owner == Some(owner) + && active.slots == *slots + && active.selected_hotbar == Some(selected_hotbar) + && active.position == position + && active.rotation == rotation + && active.game_mode == Some(game_mode) + { + return Ok(()); + } + + for slot in all_player_slots() { + let current = active.slots.get(&slot); + let desired = slots.get(&slot); + if active.owner.is_none() || current != desired { + let slot_name = slot_to_minecraft_name(slot); + let command = match desired { + Some(item) => format!( + "item replace entity flintmc_testbot {} with {} {}", + slot_name, item.id, item.count + ), + None => format!("item replace entity flintmc_testbot {} with air", slot_name), + }; + self.send_command(&command)?; + } + } + + if active.selected_hotbar != Some(selected_hotbar) { + self.select_hotbar(selected_hotbar)?; + } + if active.game_mode != Some(game_mode) { + let mode = match game_mode { + GameMode::Survival => "survival", + GameMode::Creative => "creative", + GameMode::Adventure => "adventure", + GameMode::Spectator => "spectator", + }; + self.send_command_synced(&format!("gamemode {mode} flintmc_testbot"))?; + } + self.keep_airborne()?; + match position { + Some(pos) + if active.position != position + || active.rotation != rotation + || active.owner != Some(owner) => + { + self.teleport(pos, rotation)?; + } + None if active.position.is_some() || active.owner != Some(owner) => { + self.teleport([0.5, 64.0, 0.5], Some([0.0, 0.0]))?; + } + _ => {} + } + self.wait_for_inventory(slots)?; + active.owner = Some(owner); + active.slots = slots.clone(); + active.selected_hotbar = Some(selected_hotbar); + active.position = position; + active.rotation = rotation; + active.game_mode = Some(game_mode); + Ok(()) + } + + pub fn record_player( + &self, + owner: u64, + slots: &HashMap, + selected_hotbar: u8, + position: Option<[f64; 3]>, + rotation: Option<[f32; 2]>, + game_mode: GameMode, + ) { + let mut active = self.active_player.lock(); + active.owner = Some(owner); + active.slots = slots.clone(); + active.selected_hotbar = Some(selected_hotbar); + active.position = position; + active.rotation = rotation; + active.game_mode = Some(game_mode); + } + + pub fn wait_for_inventory(&self, slots: &HashMap) -> Result<()> { + self.wait_until("inventory synchronization", || { + let Ok(client_guard) = self.get_client() else { + return false; + }; + let Some(client) = client_guard.as_ref() else { + return false; + }; + let Ok(menu) = client.menu() else { + return false; + }; + let Some(player) = menu.try_as_player() else { + return false; + }; + + all_player_slots().into_iter().all(|slot| { + let actual = match slot { + PlayerSlot::Hotbar1 => &player.inventory[27], + PlayerSlot::Hotbar2 => &player.inventory[28], + PlayerSlot::Hotbar3 => &player.inventory[29], + PlayerSlot::Hotbar4 => &player.inventory[30], + PlayerSlot::Hotbar5 => &player.inventory[31], + PlayerSlot::Hotbar6 => &player.inventory[32], + PlayerSlot::Hotbar7 => &player.inventory[33], + PlayerSlot::Hotbar8 => &player.inventory[34], + PlayerSlot::Hotbar9 => &player.inventory[35], + PlayerSlot::OffHand => &player.offhand, + PlayerSlot::Helmet => &player.armor[0], + PlayerSlot::Chestplate => &player.armor[1], + PlayerSlot::Leggings => &player.armor[2], + PlayerSlot::Boots => &player.armor[3], + }; + match slots.get(&slot) { + Some(expected) => { + actual.kind().to_str() == normalized_item_id(&expected.id) + && actual.count() == i32::from(expected.count) + } + None => actual.is_empty(), + } + }) + }) + } + + /// Read a player inventory slot from the state reported by the server. + pub fn inventory_slot(&self, slot: PlayerSlot) -> Result> { + let client_guard = self.get_client()?; + let client = client_guard + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Bot not initialized"))?; + let menu = client.menu()?; + let player = menu + .try_as_player() + .ok_or_else(|| anyhow::anyhow!("Bot does not have a player inventory open"))?; + let actual = match slot { + PlayerSlot::Hotbar1 => &player.inventory[27], + PlayerSlot::Hotbar2 => &player.inventory[28], + PlayerSlot::Hotbar3 => &player.inventory[29], + PlayerSlot::Hotbar4 => &player.inventory[30], + PlayerSlot::Hotbar5 => &player.inventory[31], + PlayerSlot::Hotbar6 => &player.inventory[32], + PlayerSlot::Hotbar7 => &player.inventory[33], + PlayerSlot::Hotbar8 => &player.inventory[34], + PlayerSlot::Hotbar9 => &player.inventory[35], + PlayerSlot::OffHand => &player.offhand, + PlayerSlot::Helmet => &player.armor[0], + PlayerSlot::Chestplate => &player.armor[1], + PlayerSlot::Leggings => &player.armor[2], + PlayerSlot::Boots => &player.armor[3], + }; + if actual.is_empty() { + return Ok(None); + } + let count = u8::try_from(actual.count()) + .map_err(|_| anyhow::anyhow!("invalid inventory count: {}", actual.count()))?; + Ok(Some(Item::with_count(actual.kind().to_str(), count))) + } + + pub(crate) fn wait_until( + &self, + operation: &str, + predicate: impl FnMut() -> bool, + ) -> Result<()> { + self.wait_until_timeout( + operation, + std::time::Duration::from_millis(STATE_SYNC_TIMEOUT_MS), + predicate, + ) + } + + pub(crate) fn wait_for_block_chunk(&self, pos: [i32; 3]) -> Result<()> { + self.wait_until_timeout( + "target block chunk availability", + std::time::Duration::from_millis(CHUNK_SYNC_TIMEOUT_MS), + || self.get_block(pos).is_ok_and(|block| block.is_some()), + ) + } + + fn wait_until_timeout( + &self, + operation: &str, + timeout: std::time::Duration, + mut predicate: impl FnMut() -> bool, + ) -> Result<()> { + let deadline = std::time::Instant::now() + timeout; + while std::time::Instant::now() < deadline { + if predicate() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(STATE_SYNC_POLL_MS)); + } + anyhow::bail!("Timed out waiting for {operation}"); + } + + pub fn get_block(&self, pos: [i32; 3]) -> Result> { let client_guard = self.get_client()?; let client = client_guard .as_ref() @@ -220,3 +779,49 @@ impl TestBot { Ok([0, 0, 0]) } } + +pub fn slot_to_minecraft_name(slot: PlayerSlot) -> &'static str { + match slot { + PlayerSlot::Hotbar1 => "hotbar.0", + PlayerSlot::Hotbar2 => "hotbar.1", + PlayerSlot::Hotbar3 => "hotbar.2", + PlayerSlot::Hotbar4 => "hotbar.3", + PlayerSlot::Hotbar5 => "hotbar.4", + PlayerSlot::Hotbar6 => "hotbar.5", + PlayerSlot::Hotbar7 => "hotbar.6", + PlayerSlot::Hotbar8 => "hotbar.7", + PlayerSlot::Hotbar9 => "hotbar.8", + PlayerSlot::OffHand => "weapon.offhand", + PlayerSlot::Helmet => "armor.head", + PlayerSlot::Chestplate => "armor.chest", + PlayerSlot::Leggings => "armor.legs", + PlayerSlot::Boots => "armor.feet", + } +} + +fn all_player_slots() -> [PlayerSlot; 14] { + [ + PlayerSlot::Hotbar1, + PlayerSlot::Hotbar2, + PlayerSlot::Hotbar3, + PlayerSlot::Hotbar4, + PlayerSlot::Hotbar5, + PlayerSlot::Hotbar6, + PlayerSlot::Hotbar7, + PlayerSlot::Hotbar8, + PlayerSlot::Hotbar9, + PlayerSlot::OffHand, + PlayerSlot::Helmet, + PlayerSlot::Chestplate, + PlayerSlot::Leggings, + PlayerSlot::Boots, + ] +} + +fn normalized_item_id(id: &str) -> String { + if id.contains(':') { + id.to_string() + } else { + format!("minecraft:{id}") + } +} diff --git a/src/executor/actions.rs b/src/executor/actions.rs index 8d17f05..71e5f14 100644 --- a/src/executor/actions.rs +++ b/src/executor/actions.rs @@ -1,71 +1,29 @@ //! Test action execution - block placement, assertions, etc. -use crate::bot::TestBot; +use crate::executor::adapter::{MinecraftPlayer, MinecraftWorld}; +use crate::executor::block; use anyhow::Result; use colored::Colorize; -use flint_core::results::{ActionOutcome, AssertFailure, AssertPosition, InfoType}; -use flint_core::test_spec::AssertType::Block; -use flint_core::test_spec::{ActionType, BlockSpec, TimelineEntry}; - -use super::block::{block_matches, extract_block_id}; - -// Constants for action timing -pub const BLOCK_POLL_ATTEMPTS: u32 = 10; -pub const BLOCK_POLL_DELAY_MS: u64 = 50; -pub const PLACE_EACH_DELAY_MS: u64 = 10; - -/// Apply offset to a position -pub fn apply_offset(pos: [i32; 3], offset: [i32; 3]) -> [i32; 3] { - [pos[0] + offset[0], pos[1] + offset[1], pos[2] + offset[2]] -} - -/// Poll for a block at the given position with retries -/// This handles timing issues in CI environments where block updates may take longer -pub async fn poll_block_with_retry( - bot: &TestBot, - world_pos: [i32; 3], - expected_block: &str, -) -> Result> { - for attempt in 0..BLOCK_POLL_ATTEMPTS { - let block = bot.get_block(world_pos).await?; - - // Check if the block matches what we expect - if let Some(ref actual) = block - && block_matches(actual, expected_block) - { - return Ok(block); - } - - // If not the last attempt, wait before retrying - if attempt < BLOCK_POLL_ATTEMPTS - 1 { - tokio::time::sleep(tokio::time::Duration::from_millis(BLOCK_POLL_DELAY_MS)).await; - } - } - - // Return whatever we have after all retries - bot.get_block(world_pos).await -} +use flint_core::results::{ + ActionOutcome, AssertEntityFail, AssertFailure, AssertPosition, InfoType, +}; +use flint_core::test_spec::AssertType; +use flint_core::test_spec::{ActionType, Item, PlayerSlot, TimelineEntry}; +use flint_core::traits::{FlintPlayer, FlintWorld}; /// Execute a single test action /// Returns the outcome: Action (non-assertion), AssertPassed, or AssertFailed with details -pub async fn execute_action( - bot: &mut TestBot, +pub fn execute_action( + world: &mut MinecraftWorld, + player: &mut Option>, tick: u32, entry: &TimelineEntry, _value_idx: usize, - offset: [i32; 3], - action_delay_ms: u64, verbose: bool, ) -> Result { match &entry.action_type { ActionType::Place { pos, block } => { - let world_pos = apply_offset(*pos, offset); - let block_spec = block.to_command(); - let cmd = format!( - "setblock {} {} {} {}", - world_pos[0], world_pos[1], world_pos[2], block_spec - ); - bot.send_command(&cmd).await?; + world.set_block_checked(*pos, block)?; if verbose { println!( " {} Tick {}: place at [{}, {}, {}] = {}", @@ -74,22 +32,15 @@ pub async fn execute_action( pos[0], pos[1], pos[2], - block_spec.dimmed() + block.to_command().dimmed() ); } - tokio::time::sleep(tokio::time::Duration::from_millis(action_delay_ms)).await; Ok(ActionOutcome::Action) } ActionType::PlaceEach { blocks } => { for placement in blocks { - let world_pos = apply_offset(placement.pos, offset); - let block_spec = placement.block.to_command(); - let cmd = format!( - "setblock {} {} {} {}", - world_pos[0], world_pos[1], world_pos[2], block_spec - ); - bot.send_command(&cmd).await?; + world.set_block_checked(placement.pos, &placement.block)?; if verbose { println!( " {} Tick {}: place at [{}, {}, {}] = {}", @@ -98,17 +49,24 @@ pub async fn execute_action( placement.pos[0], placement.pos[1], placement.pos[2], - block_spec.dimmed() + placement.block.to_command().dimmed() ); } - tokio::time::sleep(tokio::time::Duration::from_millis(PLACE_EACH_DELAY_MS)).await; } Ok(ActionOutcome::Action) } ActionType::Fill { region, with } => { - let world_min = apply_offset(region[0], offset); - let world_max = apply_offset(region[1], offset); + let world_min = [ + region[0][0] + world.offset[0], + region[0][1] + world.offset[1], + region[0][2] + world.offset[2], + ]; + let world_max = [ + region[1][0] + world.offset[0], + region[1][1] + world.offset[1], + region[1][2] + world.offset[2], + ]; let block_spec = with.to_command(); let cmd = format!( "fill {} {} {} {} {} {} {}", @@ -120,7 +78,8 @@ pub async fn execute_action( world_max[2], block_spec ); - bot.send_command(&cmd).await?; + world.bot.send_command_synced(&cmd)?; + if verbose { println!( " {} Tick {}: fill [{},{},{}] to [{},{},{}] = {}", @@ -135,17 +94,15 @@ pub async fn execute_action( block_spec.dimmed() ); } - tokio::time::sleep(tokio::time::Duration::from_millis(action_delay_ms)).await; Ok(ActionOutcome::Action) } ActionType::Remove { pos } => { - let world_pos = apply_offset(*pos, offset); - let cmd = format!( - "setblock {} {} {} air", - world_pos[0], world_pos[1], world_pos[2] - ); - bot.send_command(&cmd).await?; + let air = flint_core::test_spec::Block { + id: "minecraft:air".to_string(), + properties: Default::default(), + }; + world.set_block_checked(*pos, &air)?; if verbose { println!( " {} Tick {}: remove at [{}, {}, {}]", @@ -156,103 +113,85 @@ pub async fn execute_action( pos[2] ); } - tokio::time::sleep(tokio::time::Duration::from_millis(action_delay_ms)).await; + Ok(ActionOutcome::Action) + } + + ActionType::Summon { + entity_alias, + entity_type, + pos, + nbt, + } => { + if verbose { + println!( + " {} Tick {}: summon {} as {} at [{}, {}, {}]", + "→".blue(), + tick, + entity_type, + entity_alias, + pos[0], + pos[1], + pos[2] + ); + } + world.summon_entity(entity_alias, entity_type, *pos, nbt.as_ref()); Ok(ActionOutcome::Action) } ActionType::Assert { checks } => { for check in checks { - let Block(check) = check else { - anyhow::bail!("TODO: AssertType::Inventory not yet implemented"); - }; - let BlockSpec::Single(expected_block) = &check.is else { - anyhow::bail!("TODO: BlockSpec::Multiple not yet implemented"); - }; - let world_pos = apply_offset(check.pos, offset); - - // Poll with retries to handle timing issues in CI environments - let actual_block = - poll_block_with_retry(bot, world_pos, &expected_block.id).await?; - - // Check block type - let matches = actual_block - .as_ref() - .is_some_and(|actual| block_matches(actual, &expected_block.id)); - - if !matches { - let actual_name = actual_block - .as_ref() - .map(|s| extract_block_id(s)) - .unwrap_or_else(|| "none".to_string()); - - if verbose { - println!( - " {} Tick {}: assert block at [{}, {}, {}] expected {}, got {}", - "✗".red().bold(), - tick, - check.pos[0], - check.pos[1], - check.pos[2], - expected_block.id.green(), - actual_name.red() - ); - } - - return Ok(ActionOutcome::AssertFailed(AssertFailure { - tick, - expected: InfoType::String(expected_block.id.clone()), - actual: InfoType::String(actual_name), - position: AssertPosition::from_array(check.pos), - error_message: "Block was different".to_string(), - execution_time_ms: None, - })); - } + match check { + AssertType::Block(check) => { + let expected_blocks = check.is.to_vec(); + let actual = world.get_block(check.pos); - // Check state properties if any are specified - if !expected_block.properties.is_empty() { - let actual_str = actual_block.as_ref().unwrap(); + // Helper function to check ID match (allowing for optional minecraft: prefix difference) + let check_id = |actual: &str, expected: &str| -> bool { + let actual_clean = actual.strip_prefix("minecraft:").unwrap_or(actual); + let expected_clean = + expected.strip_prefix("minecraft:").unwrap_or(expected); + actual_clean.to_lowercase() == expected_clean.to_lowercase() + }; - for (prop_name, expected_value) in &expected_block.properties { - // Check if the property value is in the block state string - let actual_lower = actual_str.to_lowercase(); - let prop_pattern = - format!("{}: {}", prop_name, expected_value).to_lowercase(); - let prop_pattern_quoted = - format!("{}: \"{}\"", prop_name, expected_value).to_lowercase(); - // Handle numeric values with underscore prefix (e.g., level: _0) - let prop_pattern_underscore = - format!("{}: _{}", prop_name, expected_value).to_lowercase(); + let mut matched_any = false; + for expected_block in &expected_blocks { + // Check block type + let matches_id = check_id(&actual.id, &expected_block.id); + let matches_props = + matches_id && block::properties_match(&actual, expected_block); - let prop_matches = actual_lower.contains(&prop_pattern) - || actual_lower.contains(&prop_pattern_quoted) - || actual_lower.contains(&prop_pattern_underscore); + if matches_id && matches_props { + matched_any = true; + break; + } + } - if !prop_matches { - // Try to extract the actual property value from the block state string - let actual_prop = extract_property_value(actual_str, prop_name) - .unwrap_or_else(|| "?".to_string()); + if !matched_any { + let first_expected = + expected_blocks.first().cloned().unwrap_or_else(|| { + flint_core::test_spec::Block { + id: "minecraft:air".to_string(), + properties: Default::default(), + } + }); if verbose { println!( - " {} Tick {}: assert block at [{}, {}, {}] state {} expected {}, got {}", + " {} Tick {}: assert block at [{}, {}, {}] expected {}, got {}", "✗".red().bold(), tick, check.pos[0], check.pos[1], check.pos[2], - prop_name.dimmed(), - expected_value.green(), - actual_prop.red() + first_expected.id.green(), + actual.id.red() ); } return Ok(ActionOutcome::AssertFailed(AssertFailure { tick, - expected: InfoType::String(format!( - "{}={}", - prop_name, expected_value - )), - actual: InfoType::String(format!("{}={}", prop_name, actual_prop)), + expected: InfoType::Blocks(expected_blocks), + actual: InfoType::Block(actual), position: AssertPosition::from_array(check.pos), error_message: "Block was different".to_string(), execution_time_ms: None, @@ -260,63 +199,188 @@ pub async fn execute_action( } if verbose { + let first_expected = + expected_blocks.first().cloned().unwrap_or_else(|| { + flint_core::test_spec::Block { + id: "minecraft:air".to_string(), + properties: Default::default(), + } + }); println!( - " {} Tick {}: assert block at [{}, {}, {}] state {} = {}", + " {} Tick {}: assert block at [{}, {}, {}] is {}", "✓".green(), tick, check.pos[0], check.pos[1], check.pos[2], - prop_name.dimmed(), - expected_value.dimmed() + first_expected.id.dimmed() + ); + } + } + AssertType::Inventory(check) => { + let actual = if let Some(p) = player { + if let Some(p) = p.as_any_mut().downcast_mut::() { + p.restore_inventory()?; + } + p.get_slot(check.slot, Vec::new()) + } else { + None + }; + + let match_ok = match (&check.is, &actual) { + (None, None) => true, + (Some(expected), Some(act)) => { + let actual_clean = + act.id.strip_prefix("minecraft:").unwrap_or(&act.id); + let expected_clean = expected + .id + .strip_prefix("minecraft:") + .unwrap_or(&expected.id); + let id_matches = + actual_clean.to_lowercase() == expected_clean.to_lowercase(); + let count_matches = act.count == expected.count; + id_matches && count_matches + } + _ => false, + }; + + if !match_ok { + if verbose { + println!( + " {} Tick {}: assert inventory slot {:?} expected {:?}, got {:?}", + "✗".red().bold(), + tick, + check.slot, + check.is, + actual + ); + } + return Ok(ActionOutcome::AssertFailed(AssertFailure { + tick, + expected: check + .is + .clone() + .map(InfoType::Item) + .unwrap_or_else(|| InfoType::String("empty".to_string())), + actual: actual + .clone() + .map(InfoType::Item) + .unwrap_or_else(|| InfoType::String("empty".to_string())), + position: AssertPosition::from_array([0, 0, 0]), + error_message: "Inventory slot content was different".to_string(), + execution_time_ms: None, + })); + } + + if verbose { + println!( + " {} Tick {}: assert inventory slot {:?} matches expected", + "✓".green(), + tick, + check.slot + ); + } + } + AssertType::Entity(check) => { + let requested_nbt = check + .nbt + .as_ref() + .map(|nbt| nbt.requested_paths()) + .unwrap_or_default(); + let actual = world.get_entity(&check.entity_alias, &requested_nbt); + if !flint_core::runner::entity_matches(&actual, check) { + return Ok(ActionOutcome::AssertFailed( + AssertEntityFail::new(tick, check, &actual).into(), + )); + } + + if verbose { + println!( + " {} Tick {}: assert entity {} matches expected", + "✓".green(), + tick, + check.entity_alias ); } } - } else if verbose { - println!( - " {} Tick {}: assert block at [{}, {}, {}] is {}", - "✓".green(), - tick, - check.pos[0], - check.pos[1], - check.pos[2], - expected_block.id.dimmed() - ); } } Ok(ActionOutcome::AssertPassed) } - ActionType::UseItemOn { .. } - | ActionType::SetSlot { .. } - | ActionType::SelectHotbar { .. } => { - anyhow::bail!( - "TODO: ActionType {:?} not yet implemented", - entry.action_type - ); + ActionType::Tp { + entity_alias, + pos, + rot, + } => { + if verbose { + println!( + " {} Tick {}: tp entity {} to [{}, {}, {}] with {:?}", + "→".blue(), + tick, + entity_alias, + pos[0], + pos[1], + pos[2], + rot + ); + } + if entity_alias == "player" { + let p = player.get_or_insert_with(|| world.create_player()); + let p = p + .as_any_mut() + .downcast_mut::() + .ok_or_else(|| anyhow::anyhow!("unsupported FlintPlayer implementation"))?; + p.teleport_checked(*pos, *rot)?; + } else { + world.teleport_entity(entity_alias, *pos, *rot); + } + Ok(ActionOutcome::Action) + } + + ActionType::Interact { item } => { + if verbose { + println!(" {} Tick {}: interact with {:?}", "→".blue(), tick, item); + } + let p = player + .as_mut() + .expect("interact requires an existing player"); + let p = p + .as_any_mut() + .downcast_mut::() + .ok_or_else(|| anyhow::anyhow!("unsupported FlintPlayer implementation"))?; + if let Some(item_id) = item { + let it = Item::new(item_id); + p.set_slot_checked(PlayerSlot::Hotbar1, Some(&it))?; + p.select_hotbar_checked(1)?; + } + p.interact_checked()?; + Ok(ActionOutcome::Action) } - } -} -/// Extract a property value from an Azalea block state debug string -/// Input: "BlockState(id: 6795, OakFence { east: false, north: true })", "east" -/// Output: Some("false") -fn extract_property_value(block_state_str: &str, prop_name: &str) -> Option { - let lower = block_state_str.to_lowercase(); - let prop_lower = prop_name.to_lowercase(); + ActionType::SetSlot { slot, item, count } => { + let p = player.get_or_insert_with(|| world.create_player()); + let p = p + .as_any_mut() + .downcast_mut::() + .ok_or_else(|| anyhow::anyhow!("unsupported FlintPlayer implementation"))?; + if let Some(item_id) = item { + let it = Item::with_count(item_id, *count); + p.set_slot_checked(*slot, Some(&it))?; + } else { + p.set_slot_checked(*slot, None)?; + } + Ok(ActionOutcome::Action) + } - // Look for "prop_name: value" pattern - let pattern = format!("{}: ", prop_lower); - if let Some(start) = lower.find(&pattern) { - let value_start = start + pattern.len(); - let rest = &block_state_str[value_start..]; - // Value ends at comma, space before }, or } - let end = rest.find([',', '}']).unwrap_or(rest.len()); - let value = rest[..end].trim().trim_matches('_'); - if !value.is_empty() { - return Some(value.to_string()); + ActionType::SelectHotbar { slot } => { + let p = player.get_or_insert_with(|| world.create_player()); + let p = p + .as_any_mut() + .downcast_mut::() + .ok_or_else(|| anyhow::anyhow!("unsupported FlintPlayer implementation"))?; + p.select_hotbar_checked(*slot)?; + Ok(ActionOutcome::Action) } } - - None } diff --git a/src/executor/adapter.rs b/src/executor/adapter.rs new file mode 100644 index 0000000..f670d7a --- /dev/null +++ b/src/executor/adapter.rs @@ -0,0 +1,581 @@ +use crate::bot::{TestBot, slot_to_minecraft_name}; +use crate::executor::block; +use crate::executor::tick; +use anyhow::Result; +use flint_core::BlockPos; +use flint_core::test_spec::{Block, EntityNbt, GameMode, Item, PlayerSlot}; +use flint_core::traits::{EntityState, FlintAdapter, FlintPlayer, FlintWorld, ServerInfo}; +use std::collections::HashMap; + +#[allow(dead_code)] +pub struct MinecraftAdapter { + bot: TestBot, +} + +#[allow(dead_code)] +impl MinecraftAdapter { + pub fn new(bot: TestBot) -> Self { + Self { bot } + } +} + +impl FlintAdapter for MinecraftAdapter { + fn create_test_world(&self) -> Box { + // Freeze time globally first when creating test world + let _ = self.bot.send_command_synced("tick freeze"); + + Box::new(MinecraftWorld { + bot: self.bot.clone(), + offset: [0, 0, 0], + current_tick: 0, + entities: HashMap::new(), + }) + } + + fn server_info(&self) -> ServerInfo { + ServerInfo { + minecraft_version: "1.21.8".to_string(), + } + } +} + +pub struct MinecraftWorld { + pub bot: TestBot, + pub offset: [i32; 3], + pub current_tick: u64, + pub(crate) entities: HashMap, +} + +#[derive(Debug, Clone)] +pub(crate) struct MinecraftEntity { + entity_type: String, + tag: String, +} + +impl MinecraftWorld { + fn world_pos(&self, pos: BlockPos) -> [i32; 3] { + [ + pos[0] + self.offset[0], + pos[1] + self.offset[1], + pos[2] + self.offset[2], + ] + } + + fn entity_tag(alias: &str) -> Option { + if alias == "player" + || alias.is_empty() + || alias + .chars() + .any(|c| !(c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')) + { + return None; + } + Some(format!("flintmc.entity.{alias}")) + } + + fn entity_selector(entity: &MinecraftEntity) -> String { + format!("@e[tag={},type={},limit=1]", entity.tag, entity.entity_type) + } + + pub(crate) fn set_block_checked(&mut self, pos: BlockPos, block: &Block) -> Result<()> { + let world_pos = self.world_pos(pos); + let block_spec = block.to_command(); + let cmd = format!( + "setblock {} {} {} {}", + world_pos[0], world_pos[1], world_pos[2], block_spec + ); + let expected = block.clone(); + self.bot.wait_for_block_chunk(world_pos)?; + self.bot.send_command(&cmd)?; + self.bot.sync_client_world()?; + self.bot.wait_until("block synchronization", || { + let Ok(Some(actual_block_str)) = self.bot.get_block(world_pos) else { + return false; + }; + let actual = block::make_block(&block::extract_block_id(&actual_block_str)); + actual.id == expected.id && block::properties_match(&actual, &expected) + }) + } +} + +impl Drop for MinecraftWorld { + fn drop(&mut self) { + for entity in self.entities.values() { + let _ = self + .bot + .send_command(&format!("kill @e[tag={}]", entity.tag)); + } + // Tick state is shared by the whole server, not owned by an individual + // test world. The executor unfreezes once after the complete batch. + } +} + +impl FlintWorld for MinecraftWorld { + fn do_tick(&mut self) { + let mut bot = self.bot.clone(); + let _ = tick::step_tick(&mut bot, false); + self.current_tick += 1; + } + + fn current_tick(&self) -> u64 { + self.current_tick + } + + fn get_block(&self, pos: BlockPos) -> Block { + let world_pos = self.world_pos(pos); + for _ in 0..10 { + if let Ok(Some(actual_block_str)) = self.bot.get_block(world_pos) { + let normalized_id = block::extract_block_id(&actual_block_str); + let block = block::make_block(&normalized_id); + if !normalized_id.is_empty() { + return block; + } + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + + Block { + id: "minecraft:air".to_string(), + properties: Default::default(), + } + } + + fn set_block(&mut self, pos: BlockPos, block: &Block) { + if let Err(error) = self.set_block_checked(pos, block) { + tracing::error!( + "Failed to set block at {:?} to {}: {}", + pos, + block.id, + error + ); + } + } + + fn summon_entity( + &mut self, + alias: &str, + entity_type: &str, + pos: [f64; 3], + nbt: Option<&EntityNbt>, + ) { + let Some(tag) = Self::entity_tag(alias) else { + tracing::error!("Invalid entity alias for summon: {}", alias); + return; + }; + if entity_type + .chars() + .any(|c| !(c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == ':' || c == '.')) + { + tracing::error!("Invalid entity type for summon: {}", entity_type); + return; + } + let world_pos = [ + pos[0] + f64::from(self.offset[0]), + pos[1] + f64::from(self.offset[1]), + pos[2] + f64::from(self.offset[2]), + ]; + let _ = self.bot.send_command(&format!("kill @e[tag={}]", tag)); + let nbt = summon_nbt_with_tag(nbt.map(EntityNbt::to_snbt).as_deref(), &tag); + let cmd = format!( + "summon {} {} {} {} {}", + entity_type, world_pos[0], world_pos[1], world_pos[2], nbt + ); + if let Err(error) = self.bot.send_command(&cmd) { + tracing::error!("Failed to summon entity alias {}: {}", alias, error); + return; + } + self.entities.insert( + alias.to_string(), + MinecraftEntity { + entity_type: entity_type.to_string(), + tag, + }, + ); + } + + fn teleport_entity(&mut self, alias: &str, pos: [f64; 3], rot: Option<[f32; 2]>) { + let Some(entity) = self.entities.get(alias) else { + tracing::error!("Cannot teleport unknown entity alias: {}", alias); + return; + }; + let selector = Self::entity_selector(entity); + let world_pos = [ + pos[0] + f64::from(self.offset[0]), + pos[1] + f64::from(self.offset[1]), + pos[2] + f64::from(self.offset[2]), + ]; + let cmd = match rot { + Some([yaw, pitch]) => format!( + "tp {} {} {} {} {} {}", + selector, world_pos[0], world_pos[1], world_pos[2], yaw, pitch + ), + None => format!( + "tp {} {} {} {}", + selector, world_pos[0], world_pos[1], world_pos[2] + ), + }; + if let Err(error) = self.bot.send_command(&cmd) { + tracing::error!("Failed to teleport entity alias {}: {}", alias, error); + } + } + + fn get_entity(&self, alias: &str, requested_nbt: &[String]) -> EntityState { + let Some(entity) = self.entities.get(alias) else { + return EntityState::default(); + }; + let selector = Self::entity_selector(entity); + let Ok(values) = query_entity_numbers(&self.bot, &selector, "Pos") else { + return EntityState { + exists: false, + entity_type: Some(entity.entity_type.clone()), + pos: None, + rot: None, + nbt: HashMap::new(), + }; + }; + let pos = values.get(..3).map(|pos| { + [ + pos[0] - f64::from(self.offset[0]), + pos[1] - f64::from(self.offset[1]), + pos[2] - f64::from(self.offset[2]), + ] + }); + let rot = query_entity_numbers(&self.bot, &selector, "Rotation") + .ok() + .and_then(|rot| rot.get(..2).map(|rot| [rot[0] as f32, rot[1] as f32])); + let mut nbt = HashMap::new(); + for path in requested_nbt { + if let Ok(value) = query_entity_data(&self.bot, &selector, path) { + nbt.insert(path.clone(), value); + } + } + EntityState { + exists: true, + entity_type: Some(entity.entity_type.clone()), + pos, + rot, + nbt, + } + } + + fn create_player(&mut self) -> Box { + Box::new(MinecraftPlayer { + bot: self.bot.clone(), + inventory_owner: self.bot.allocate_inventory_owner(), + selected_hotbar: 1, + inventory: std::collections::HashMap::new(), + position: None, + rotation: None, + offset: self.offset, + game_mode: GameMode::Creative, + }) + } +} + +fn query_entity_numbers(bot: &TestBot, selector: &str, path: &str) -> Result> { + let message = query_entity_data_message(bot, selector, path)?; + let values = parse_numbers_after_colon(&message); + if values.is_empty() { + anyhow::bail!("entity query returned no numbers: {message}"); + } + Ok(values) +} + +fn query_entity_data(bot: &TestBot, selector: &str, path: &str) -> Result { + let message = query_entity_data_message(bot, selector, path)?; + Ok(message + .split_once(':') + .map(|(_, value)| value.trim().to_string()) + .unwrap_or_else(|| message.trim().to_string())) +} + +fn query_entity_data_message(bot: &TestBot, selector: &str, path: &str) -> Result { + while bot + .recv_chat_timeout(std::time::Duration::from_millis( + tick::CHAT_DRAIN_TIMEOUT_MS, + )) + .is_some() + {} + + bot.send_command(&format!("data get entity {selector} {path}"))?; + let timeout = std::time::Duration::from_secs(3); + let started = std::time::Instant::now(); + + while started.elapsed() < timeout { + if let Some((_, message)) = + bot.recv_chat_timeout(std::time::Duration::from_millis(tick::CHAT_POLL_TIMEOUT_MS)) + { + if message.contains("No entity was found") || message.contains("Found no elements") { + anyhow::bail!("entity query failed: {message}"); + } + if message.contains(path) || message.contains("entity data") { + return Ok(message); + } + } + } + + anyhow::bail!("timed out querying entity {selector} {path}") +} + +fn parse_numbers_after_colon(message: &str) -> Vec { + let value_part = message + .split_once(':') + .map(|(_, value)| value) + .unwrap_or(message); + value_part + .split(|c: char| { + !(c.is_ascii_digit() || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') + }) + .filter_map(|part| { + if part.is_empty() || part == "-" || part == "+" || part == "." { + None + } else { + part.parse::().ok() + } + }) + .collect() +} + +fn summon_nbt_with_tag(nbt: Option<&str>, tag: &str) -> String { + match nbt.map(str::trim).filter(|nbt| !nbt.is_empty()) { + None => format!("{{Tags:[\"{tag}\"]}}"), + Some("{}") => format!("{{Tags:[\"{tag}\"]}}"), + Some(nbt) if nbt.starts_with('{') && nbt.ends_with('}') => { + let inner = &nbt[1..nbt.len() - 1]; + if inner.trim().is_empty() { + format!("{{Tags:[\"{tag}\"]}}") + } else { + format!("{{{inner},Tags:[\"{tag}\"]}}") + } + } + Some(nbt) => { + tracing::warn!("Invalid summon NBT '{}', using only FlintMC alias tag", nbt); + format!("{{Tags:[\"{tag}\"]}}") + } + } +} + +pub struct MinecraftPlayer { + bot: TestBot, + inventory_owner: u64, + selected_hotbar: u8, + inventory: std::collections::HashMap, + position: Option<[f64; 3]>, + rotation: Option<[f32; 2]>, + offset: [i32; 3], + game_mode: GameMode, +} + +impl FlintPlayer for MinecraftPlayer { + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + + fn set_slot(&mut self, slot: PlayerSlot, item: Option<&Item>) { + if let Err(error) = self.set_slot_checked(slot, item) { + tracing::error!("Failed to set player slot {:?}: {}", slot, error); + } + } + + fn get_slot(&self, slot: PlayerSlot, _requested_data: Vec) -> Option { + self.inventory.get(&slot).cloned() + } + + fn select_hotbar(&mut self, slot: u8) { + if let Err(error) = self.select_hotbar_checked(slot) { + tracing::error!("Failed to select hotbar slot {}: {}", slot, error); + } + } + + fn selected_hotbar(&self) -> u8 { + self.selected_hotbar + } + + fn teleport(&mut self, pos: [f64; 3], rot: Option<[f32; 2]>) { + if let Err(error) = self.teleport_checked(pos, rot) { + tracing::error!("Failed to teleport player: {}", error); + } + } + + fn interact(&mut self) { + if let Err(error) = self.interact_checked() { + tracing::error!("Failed to interact: {}", error); + } + } + + fn set_game_mode(&mut self, mode: GameMode) { + if let Err(error) = self.set_game_mode_checked(mode) { + tracing::error!("Failed to set game mode {:?}: {}", mode, error); + } + } +} + +impl MinecraftPlayer { + pub(crate) fn set_slot_checked(&mut self, slot: PlayerSlot, item: Option<&Item>) -> Result<()> { + self.restore_state_checked()?; + if let Some(item) = item { + self.inventory.insert(slot, item.clone()); + } else { + self.inventory.remove(&slot); + } + let slot_name = slot_to_minecraft_name(slot); + let command = match item { + Some(item) => format!( + "item replace entity flintmc_testbot {} with {} {}", + slot_name, item.id, item.count + ), + None => format!("item replace entity flintmc_testbot {} with air", slot_name), + }; + self.bot.send_command_synced(&command)?; + self.bot.wait_for_inventory(&self.inventory)?; + self.record_state(); + Ok(()) + } + + pub(crate) fn select_hotbar_checked(&mut self, slot: u8) -> Result<()> { + self.restore_state_checked()?; + self.bot.select_hotbar(slot)?; + self.selected_hotbar = slot; + self.record_state(); + Ok(()) + } + + pub(crate) fn teleport_checked( + &mut self, + pos: [f64; 3], + rotation: Option<[f32; 2]>, + ) -> Result<()> { + self.restore_state_checked()?; + let world_pos = [ + pos[0] + f64::from(self.offset[0]), + pos[1] + f64::from(self.offset[1]), + pos[2] + f64::from(self.offset[2]), + ]; + self.bot.teleport(world_pos, rotation)?; + self.position = Some(world_pos); + self.rotation = rotation; + self.record_state(); + Ok(()) + } + + pub(crate) fn interact_checked(&mut self) -> Result<()> { + self.restore_state_checked()?; + self.bot.interact()?; + if self.game_mode == GameMode::Survival || self.game_mode == GameMode::Adventure { + let slot = match self.selected_hotbar { + 1 => PlayerSlot::Hotbar1, + 2 => PlayerSlot::Hotbar2, + 3 => PlayerSlot::Hotbar3, + 4 => PlayerSlot::Hotbar4, + 5 => PlayerSlot::Hotbar5, + 6 => PlayerSlot::Hotbar6, + 7 => PlayerSlot::Hotbar7, + 8 => PlayerSlot::Hotbar8, + 9 => PlayerSlot::Hotbar9, + _ => anyhow::bail!("invalid selected hotbar slot: {}", self.selected_hotbar), + }; + let observed = self.bot.inventory_slot(slot)?; + reconcile_inventory_slot(&mut self.inventory, slot, observed); + } + self.bot.wait_for_inventory(&self.inventory)?; + self.record_state(); + Ok(()) + } + + pub(crate) fn set_game_mode_checked(&mut self, mode: GameMode) -> Result<()> { + self.restore_state_checked()?; + let mode_name = match mode { + GameMode::Survival => "survival", + GameMode::Creative => "creative", + GameMode::Adventure => "adventure", + GameMode::Spectator => "spectator", + }; + self.bot + .send_command_synced(&format!("gamemode {mode_name} flintmc_testbot"))?; + self.game_mode = mode; + self.record_state(); + Ok(()) + } + + pub(crate) fn restore_inventory(&mut self) -> Result<()> { + self.restore_state_checked() + } + + fn restore_state_checked(&mut self) -> Result<()> { + self.bot.restore_player( + self.inventory_owner, + &self.inventory, + self.selected_hotbar, + self.position, + self.rotation, + self.game_mode, + ) + } + + fn record_state(&self) { + self.bot.record_player( + self.inventory_owner, + &self.inventory, + self.selected_hotbar, + self.position, + self.rotation, + self.game_mode, + ); + } +} + +fn reconcile_inventory_slot( + inventory: &mut std::collections::HashMap, + slot: PlayerSlot, + observed: Option, +) { + match observed { + Some(item) => { + inventory.insert(slot, item); + } + None => { + inventory.remove(&slot); + } + } +} + +#[cfg(test)] +mod tests { + use super::reconcile_inventory_slot; + use flint_core::test_spec::{Item, PlayerSlot}; + use std::collections::HashMap; + + #[test] + fn interaction_does_not_consume_item_when_server_reports_unchanged_stack() { + let slot = PlayerSlot::Hotbar1; + let mut inventory = HashMap::from([(slot, Item::with_count("minecraft:stick", 2))]); + + reconcile_inventory_slot( + &mut inventory, + slot, + Some(Item::with_count("minecraft:stick", 2)), + ); + + assert_eq!( + inventory.get(&slot), + Some(&Item::with_count("minecraft:stick", 2)) + ); + } + + #[test] + fn interaction_uses_consumed_stack_reported_by_server() { + let slot = PlayerSlot::Hotbar1; + let mut inventory = HashMap::from([(slot, Item::with_count("minecraft:oak_sign", 2))]); + + reconcile_inventory_slot( + &mut inventory, + slot, + Some(Item::with_count("minecraft:oak_sign", 1)), + ); + + assert_eq!( + inventory.get(&slot), + Some(&Item::with_count("minecraft:oak_sign", 1)) + ); + } +} diff --git a/src/executor/block.rs b/src/executor/block.rs index ffa4769..7836279 100644 --- a/src/executor/block.rs +++ b/src/executor/block.rs @@ -3,6 +3,16 @@ use flint_core::test_spec::Block; use rustc_hash::FxHashMap; +/// Check that all expected block properties match an actual Azalea block state. +pub fn properties_match(actual: &Block, expected: &Block) -> bool { + expected.properties.iter().all(|(name, expected_value)| { + actual + .properties + .get(name) + .is_some_and(|actual_value| actual_value.eq_ignore_ascii_case(expected_value)) + }) +} + /// Extract block ID and properties from Azalea debug string /// Input: "BlockState(id: 6795, OakFence { east: false, ... })" /// Output: "minecraft:oak_fence[east=false,west=false]" @@ -69,11 +79,9 @@ pub fn extract_block_id(debug_str: &str) -> String { continue; } if let Some((k, v)) = part.split_once(':') { - pairs.push(format!( - "{}={}", - k.trim().to_lowercase(), - v.trim().to_lowercase() - )); + let key = k.trim().to_lowercase(); + let key = if key == "kind" { "type" } else { &key }; + pairs.push(format!("{}={}", key, v.trim().to_lowercase())); } } if !pairs.is_empty() { @@ -116,6 +124,7 @@ pub fn make_block(block_str: &str) -> Block { } /// Normalize block name for comparison (remove minecraft: prefix and underscores) +#[allow(dead_code)] pub fn normalize_block_name(name: &str) -> String { name.trim_start_matches("minecraft:") .to_lowercase() @@ -123,6 +132,7 @@ pub fn normalize_block_name(name: &str) -> String { } /// Check if actual block matches expected block name +#[allow(dead_code)] pub fn block_matches(actual: &str, expected: &str) -> bool { let actual_lower = actual.to_lowercase(); let expected_normalized = normalize_block_name(expected); @@ -149,6 +159,13 @@ mod tests { assert!(result.contains("north=true")); } + #[test] + fn test_extract_block_id_normalizes_generated_kind_property() { + let input = "BlockState(id: 1, OakSlab { kind: Double, waterlogged: false })"; + let result = extract_block_id(input); + assert_eq!(result, "minecraft:oak_slab[type=double,waterlogged=false]"); + } + #[test] fn test_make_block_simple() { let block = make_block("minecraft:stone"); @@ -169,4 +186,11 @@ mod tests { assert!(block_matches("minecraft:oak_fence", "oak_fence")); assert!(!block_matches("SpruceFence", "oak_fence")); } + + #[test] + fn properties_match_requires_exact_property_names() { + let actual = make_block("minecraft:oak_slab[type=bottom,waterlogged=false]"); + let expected = make_block("minecraft:oak_slab[type=bottom,waterlogged=false]"); + assert!(properties_match(&actual, &expected)); + } } diff --git a/src/executor/handlers.rs b/src/executor/handlers.rs index e3862b3..5ef4fbb 100644 --- a/src/executor/handlers.rs +++ b/src/executor/handlers.rs @@ -2,12 +2,10 @@ use anyhow::Result; use flint_core::loader::TestLoader; -use flint_core::spatial::calculate_test_offset_default; +use flint_core::spatial::pair_tests_with_offsets; use flint_core::test_spec::TestSpec; -use super::{ - COMMAND_DELAY_MS, DEFAULT_TESTS_DIR, TEST_RESULT_DELAY_MS, TestExecutor, block, recorder, -}; +use super::{DEFAULT_TESTS_DIR, TestExecutor, block, recorder, tick}; /// Parse command parts from a chat message /// Returns (command, args) if a valid command was found @@ -37,51 +35,37 @@ pub fn parse_command(message: &str) -> Option<(String, Vec)> { impl TestExecutor { // Command handlers - pub(super) async fn handle_help(&mut self) -> Result<()> { - self.bot.send_command("say Commands:").await?; + pub(super) fn handle_help(&mut self) -> Result<()> { + self.bot.send_command("say Commands:")?; self.bot - .send_command("say !search - Search tests by name") - .await?; + .send_command("say !search - Search tests by name")?; self.bot - .send_command("say !run [step] - Run a specific test") - .await?; + .send_command("say !run [step] - Run a specific test")?; + self.bot.send_command("say !run-all - Run all tests")?; self.bot - .send_command("say !run-all - Run all tests") - .await?; + .send_command("say !run-tags - Run tests with tags")?; + self.bot.send_command("say !list - List all tests")?; + self.bot.send_command("say !reload - Reload test files")?; self.bot - .send_command("say !run-tags - Run tests with tags") - .await?; - self.bot.send_command("say !list - List all tests").await?; + .send_command("say Recorder: !record , !tick/!next, !save, !cancel")?; self.bot - .send_command("say !reload - Reload test files") - .await?; - self.bot - .send_command("say Recorder: !record , !tick/!next, !save, !cancel") - .await?; - self.bot - .send_command("say Recorder actions: !assert , !assert_changes") - .await?; - self.bot - .send_command( - "say Recorder actions: !pos1 , !pos - Allow to use assert for a 3d area", - ) - .await?; - self.bot - .send_command("say Recorder actions: !sprint - ticks this ticks and asserts after each tick") - .await?; - self.bot - .send_command("say !stop - Exit interactive mode") - .await?; + .send_command("say Recorder actions: !assert , !assert_changes")?; + self.bot.send_command( + "say Recorder actions: !use [item] - record tp + interact at player pose", + )?; + self.bot.send_command( + "say Recorder actions: !pos1 , !pos - Allow to use assert for a 3d area", + )?; + self.bot.send_command( + "say Recorder actions: !sprint - ticks this ticks and asserts after each tick", + )?; + self.bot.send_command("say !stop - Exit interactive mode")?; Ok(()) } - pub(super) async fn handle_list( - &mut self, - all_test_files: &[std::path::PathBuf], - ) -> Result<()> { + pub(super) fn handle_list(&mut self, all_test_files: &[std::path::PathBuf]) -> Result<()> { self.bot - .send_command(&format!("say Found {} tests:", all_test_files.len())) - .await?; + .send_command(&format!("say Found {} tests:", all_test_files.len()))?; for test_file in all_test_files { if let Ok(test) = TestSpec::from_file(test_file, false) { let tags = if test.tags.is_empty() { @@ -90,15 +74,13 @@ impl TestExecutor { format!(" [{}]", test.tags.join(", ")) }; self.bot - .send_command(&format!("say - {}{}", test.name, tags)) - .await?; - tokio::time::sleep(tokio::time::Duration::from_millis(TEST_RESULT_DELAY_MS)).await; + .send_command_synced(&format!("say - {}{}", test.name, tags))?; } } Ok(()) } - pub(super) async fn handle_search( + pub(super) fn handle_search( &mut self, all_test_files: &[std::path::PathBuf], pattern: &str, @@ -115,25 +97,21 @@ impl TestExecutor { format!(" [{}]", test.tags.join(", ")) }; self.bot - .send_command(&format!("say - {}{}", test.name, tags)) - .await?; + .send_command_synced(&format!("say - {}{}", test.name, tags))?; found += 1; - tokio::time::sleep(tokio::time::Duration::from_millis(TEST_RESULT_DELAY_MS)).await; } } if found == 0 { self.bot - .send_command(&format!("say No tests matching '{}'", pattern)) - .await?; + .send_command(&format!("say No tests matching '{}'", pattern))?; } else { self.bot - .send_command(&format!("say Found {} matching tests", found)) - .await?; + .send_command(&format!("say Found {} matching tests", found))?; } Ok(()) } - pub(super) async fn handle_run( + pub(super) fn handle_run( &mut self, all_test_files: &[std::path::PathBuf], test_name: &str, @@ -166,71 +144,55 @@ impl TestExecutor { if let Some(test) = found_test { if step_mode { - self.bot - .send_command(&format!( - "say Running test: {} (step mode - type 's' or 'c')", - test.name - )) - .await?; + self.bot.send_command(&format!( + "say Running test: {} (step mode - type 's' or 'c')", + test.name + ))?; } else { self.bot - .send_command(&format!("say Running test: {}", test.name)) - .await?; + .send_command(&format!("say Running test: {}", test.name))?; } - let offset = calculate_test_offset_default(0, 1); - let tests_with_offsets = vec![(test, offset)]; - let output = self - .run_tests_parallel(&tests_with_offsets, step_mode) - .await?; + let tests_with_offsets = pair_tests_with_offsets(vec![test]); + let output = self.run_tests_parallel(&tests_with_offsets, step_mode)?; for result in &output.results { let status = if result.success { "PASS" } else { "FAIL" }; self.bot - .send_command(&format!("say [{}] {}", status, result.test_name)) - .await?; + .send_command(&format!("say [{}] {}", status, result.test_name))?; } } else { self.bot - .send_command(&format!("say Test '{}' not found", test_name)) - .await?; + .send_command(&format!("say Test '{}' not found", test_name))?; } Ok(()) } - pub(super) async fn handle_run_all( - &mut self, - all_test_files: &[std::path::PathBuf], - ) -> Result<()> { - self.bot - .send_command(&format!( - "say Running all {} tests...", - all_test_files.len() - )) - .await?; + pub(super) fn handle_run_all(&mut self, all_test_files: &[std::path::PathBuf]) -> Result<()> { + self.bot.send_command(&format!( + "say Running all {} tests...", + all_test_files.len() + ))?; - let mut tests_with_offsets = Vec::new(); - for (idx, test_file) in all_test_files.iter().enumerate() { + let mut specs = Vec::new(); + for test_file in all_test_files { if let Ok(test) = TestSpec::from_file(test_file, false) { - let offset = calculate_test_offset_default(idx, all_test_files.len()); - tests_with_offsets.push((test, offset)); + specs.push(test); } } - - let output = self.run_tests_parallel(&tests_with_offsets, false).await?; + let tests_with_offsets = pair_tests_with_offsets(specs); + let output = self.run_tests_parallel(&tests_with_offsets, false)?; let passed = output.results.iter().filter(|r| r.success).count(); let failed = output.results.len() - passed; - self.bot - .send_command(&format!( - "say Results: {} passed, {} failed", - passed, failed - )) - .await?; + self.bot.send_command(&format!( + "say Results: {} passed, {} failed", + passed, failed + ))?; Ok(()) } - pub(super) async fn handle_run_tags( + pub(super) fn handle_run_tags( &mut self, test_loader: &TestLoader, tags: &[String], @@ -239,43 +201,37 @@ impl TestExecutor { if test_files.is_empty() { self.bot - .send_command(&format!("say No tests found with tags: {:?}", tags)) - .await?; + .send_command(&format!("say No tests found with tags: {:?}", tags))?; return Ok(()); } - self.bot - .send_command(&format!( - "say Running {} tests with tags {:?}...", - test_files.len(), - tags - )) - .await?; + self.bot.send_command(&format!( + "say Running {} tests with tags {:?}...", + test_files.len(), + tags + ))?; - let mut tests_with_offsets = Vec::new(); - for (idx, test_file) in test_files.iter().enumerate() { + let mut specs = Vec::new(); + for test_file in &test_files { if let Ok(test) = TestSpec::from_file(test_file, false) { - let offset = calculate_test_offset_default(idx, test_files.len()); - tests_with_offsets.push((test, offset)); + specs.push(test); } } - - let output = self.run_tests_parallel(&tests_with_offsets, false).await?; + let tests_with_offsets = pair_tests_with_offsets(specs); + let output = self.run_tests_parallel(&tests_with_offsets, false)?; let passed = output.results.iter().filter(|r| r.success).count(); let failed = output.results.len() - passed; - self.bot - .send_command(&format!( - "say Results: {} passed, {} failed", - passed, failed - )) - .await?; + self.bot.send_command(&format!( + "say Results: {} passed, {} failed", + passed, failed + ))?; Ok(()) } // Recorder command handlers - pub(super) async fn handle_record_start( + pub(super) fn handle_record_start( &mut self, test_name: &str, _test_loader: &TestLoader, @@ -283,8 +239,7 @@ impl TestExecutor { ) -> Result<()> { if self.recorder.is_some() { self.bot - .send_command("say Recording already in progress. Use !save or !cancel first.") - .await?; + .send_command("say Recording already in progress. Use !save or !cancel first.")?; return Ok(()); } @@ -293,14 +248,18 @@ impl TestExecutor { // Default to @p if nothing works recorder_state.player_name = player_name.or_else(|| Some("@p".to_string())); - // Get bot position to set scan center - let scan_center = match self.bot.get_position() { - Ok(pos) => pos, + // Get tracked player position to set scan center. + let scan_center = match self.query_record_player_pose(&recorder_state) { + Ok((pos, _)) => [ + pos[0].floor() as i32, + pos[1].floor() as i32, + pos[2].floor() as i32, + ], Err(_) => { - self.bot - .send_command("say Warning: Could not get bot position, using spawn") - .await?; - [0, 64, 0] + self.bot.send_command( + "say Warning: Could not get player position, using bot position", + )?; + self.bot.get_position().unwrap_or([0, 64, 0]) } }; @@ -308,64 +267,53 @@ impl TestExecutor { recorder_state.scan_radius = 10; // 10 block radius for scanning // Take initial snapshot of blocks - let initial_blocks = self - .scan_blocks_around(scan_center, recorder_state.scan_radius) - .await?; + let initial_blocks = self.scan_blocks_around(scan_center, recorder_state.scan_radius)?; recorder_state.snapshot = initial_blocks; self.recorder = Some(recorder_state); // Freeze time for controlled recording - self.bot.send_command("tick freeze").await?; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + self.bot.send_command_synced("tick freeze")?; self.bot - .send_command(&format!("say Recording started: {}", test_name)) - .await?; + .send_command(&format!("say Recording started: {}", test_name))?; self.bot - .send_command("say Time frozen. Block changes will be detected automatically!") - .await?; + .send_command("say Time frozen. Block changes will be detected automatically!")?; self.bot - .send_command( - "say Commands: !assert (add check), !tick (step game tick), !save, !cancel", - ) - .await?; + .send_command("say Commands: !assert, !use, !tick, !save, !cancel")?; Ok(()) } - pub(super) async fn handle_record_tick(&mut self) -> Result<()> { + pub(super) fn handle_record_tick(&mut self) -> Result<()> { // Check if recorder exists first if self.recorder.is_none() { self.bot - .send_command("say No recording in progress. Use !record to start.") - .await?; + .send_command("say No recording in progress. Use !record to start.")?; return Ok(()); } let current_tick = self.recorder.as_ref().unwrap().current_tick; // Snapshot before advancing tick to capture all changes - self.handle_record_snapshot().await?; + self.handle_record_snapshot()?; // Step the game tick - self.bot.send_command("tick step").await?; - self.delay().await; + tick::step_tick(&mut self.bot, false)?; // Now advance our recording tick counter let recorder = self.require_recorder().unwrap(); recorder.next_tick(); let new_tick = recorder.current_tick; - self.bot - .send_command(&format!( - "say Stepped game tick, now recording tick {} (was {})", - new_tick, current_tick - )) - .await?; + self.bot.send_command(&format!( + "say Stepped game tick, now recording tick {} (was {})", + new_tick, current_tick + ))?; Ok(()) } + pub(super) fn handle_pos1(&mut self, args: &[String]) { if args.is_empty() { self.pos1 = None; @@ -377,13 +325,12 @@ impl TestExecutor { self.pos1 = Some([x, y, z]); } - pub(super) async fn handle_record_assert(&mut self, args: &[String]) -> Result<()> { + pub(super) fn handle_record_assert(&mut self, args: &[String]) -> Result<()> { let _recorder = match self.recorder.as_mut() { Some(r) => r, None => { self.bot - .send_command("say No recording in progress. Use !record to start.") - .await?; + .send_command("say No recording in progress. Use !record to start.")?; return Ok(()); } }; @@ -414,76 +361,99 @@ impl TestExecutor { } // Get block at position for pos in blocks { - if let Some(block_str) = self.bot.get_block(pos).await? { + if let Some(block_str) = self.bot.get_block(pos)? { let block_id = block::extract_block_id(&block_str); let recorder = self.recorder.as_mut().unwrap(); recorder.add_assertion(pos, &block_id); - self.bot - .send_command(&format!( - "say Added assert at [{}, {}, {}] = {}", - pos[0], pos[1], pos[2], block_id - )) - .await?; + self.bot.send_command(&format!( + "say Added assert at [{}, {}, {}] = {}", + pos[0], pos[1], pos[2], block_id + ))?; } else { - self.bot - .send_command(&format!( - "say No block found at [{}, {}, {}]", - pos[0], pos[1], pos[2] - )) - .await?; + self.bot.send_command(&format!( + "say No block found at [{}, {}, {}]", + pos[0], pos[1], pos[2] + ))?; } } Ok(()) } - pub(super) async fn handle_record_assert_changes(&mut self) -> Result<()> { + pub(super) fn handle_record_assert_changes(&mut self) -> Result<()> { let Some(recorder) = self.require_recorder() else { - self.bot - .send_command("say No recording in progress.") - .await?; + self.bot.send_command("say No recording in progress.")?; return Ok(()); }; let count = recorder.convert_actions_to_asserts(); - self.bot - .send_command(&format!( - "say Converted {} actions to assertions for this tick.", - count - )) - .await?; + self.bot.send_command(&format!( + "say Converted {} actions to assertions for this tick.", + count + ))?; Ok(()) } - pub(super) async fn handle_record_save(&mut self) -> Result { - let Some(recorder) = self.recorder.take() else { + pub(super) fn handle_record_use(&mut self, args: &[String]) -> Result<()> { + if self.recorder.is_none() { self.bot - .send_command("say No recording in progress.") - .await?; + .send_command("say No recording in progress. Use !record to start.")?; + return Ok(()); + } + + let item = args.first().cloned(); + if args.len() > 1 { + self.bot.send_command("say Usage: !use [item]")?; + return Ok(()); + } + + self.handle_record_snapshot()?; + + let (pos, rot) = { + let recorder = self.recorder.as_ref().unwrap(); + self.query_record_player_pose(recorder)? + }; + + let recorder = self.recorder.as_mut().unwrap(); + recorder.record_use(pos, Some(rot), item.clone()); + + self.bot.send_command(&format!( + "say Recorded use at [{:.2}, {:.2}, {:.2}] rot [{:.1}, {:.1}]{}", + pos[0], + pos[1], + pos[2], + rot[0], + rot[1], + item.as_ref() + .map(|item| format!(" with {item}")) + .unwrap_or_default() + ))?; + Ok(()) + } + + pub(super) fn handle_record_save(&mut self) -> Result { + let Some(recorder) = self.recorder.take() else { + self.bot.send_command("say No recording in progress.")?; return Ok(false); }; // Check if there's anything to save if recorder.timeline.is_empty() { self.bot - .send_command("say Warning: No actions recorded! Test will be empty.") - .await?; + .send_command("say Warning: No actions recorded! Test will be empty.")?; } match recorder.save() { Ok(path) => { - self.bot - .send_command(&format!( - "say Test saved to: {}", - path.file_name().unwrap_or_default().to_string_lossy() - )) - .await?; + self.bot.send_command(&format!( + "say Test saved to: {}", + path.file_name().unwrap_or_default().to_string_lossy() + ))?; println!("Test saved to: {}", path.display()); // Print execution commands self.bot - .send_command(&format!("say To execute: !run {}", recorder.test_name)) - .await?; + .send_command(&format!("say To execute: !run {}", recorder.test_name))?; println!( "To execute this test locally:\ncargo run -- --server localhost:25565 {}", recorder.test_name @@ -491,39 +461,34 @@ impl TestExecutor { } Err(e) => { self.bot - .send_command(&format!("say Failed to save test: {}", e)) - .await?; + .send_command(&format!("say Failed to save test: {}", e))?; eprintln!("Failed to save: {}", e); return Err(e); } } // Unfreeze time after recording - self.bot.send_command("tick unfreeze").await?; + self.bot.send_command("tick unfreeze")?; Ok(true) } - pub(super) async fn handle_record_snapshot(&mut self) -> Result<()> { - let recorder = match self.recorder.as_mut() { + pub(super) fn handle_record_snapshot(&mut self) -> Result<()> { + let recorder = match self.recorder.as_ref() { Some(r) => r, None => { - self.bot - .send_command("say No recording in progress.") - .await?; + self.bot.send_command("say No recording in progress.")?; return Ok(()); } }; - let scan_center = recorder.scan_center.unwrap_or([0, 64, 0]); let scan_radius = recorder.scan_radius; + let scan_center = recorder.scan_center.unwrap_or([0, 64, 0]); - self.bot - .send_command("say Scanning for block changes...") - .await?; + self.bot.send_command("say Scanning for block changes...")?; // Scan current blocks - let current_blocks = self.scan_blocks_around(scan_center, scan_radius).await?; + let current_blocks = self.scan_blocks_around(scan_center, scan_radius)?; // Compare with initial snapshot and record differences let mut changes = 0; @@ -562,37 +527,123 @@ impl TestExecutor { .unwrap_or(true) { // Was a block, now is air - let recorder = self.recorder.as_mut().unwrap(); recorder.record_remove(*pos); changes += 1; } } self.bot - .send_command(&format!("say Found {} block changes", changes)) - .await?; + .send_command(&format!("say Found {} block changes", changes))?; Ok(()) } - pub(super) async fn handle_record_cancel(&mut self) -> Result<()> { + pub(super) fn handle_record_cancel(&mut self) -> Result<()> { if self.recorder.take().is_some() { // Unfreeze time after cancelling - self.bot.send_command("tick unfreeze").await?; - self.bot.send_command("say Recording cancelled.").await?; + self.bot.send_command("tick unfreeze")?; + self.bot.send_command("say Recording cancelled.")?; } else { - self.bot - .send_command("say No recording in progress.") - .await?; + self.bot.send_command("say No recording in progress.")?; } Ok(()) } - pub(super) async fn handle_record_sprint(&mut self, ticks: u32) -> Result<()> { + pub(super) fn handle_record_sprint(&mut self, ticks: u32) -> Result<()> { for _ in 0..ticks { - self.handle_record_tick().await?; - self.handle_record_assert(&self.last_assert_pos.clone()) - .await?; + self.handle_record_tick()?; + self.handle_record_assert(&self.last_assert_pos.clone())?; } Ok(()) } + + fn query_record_player_pose( + &self, + recorder: &recorder::RecorderState, + ) -> Result<([f64; 3], [f32; 2])> { + let target = recorder.player_name.as_deref().unwrap_or("@p"); + let pos = self.query_entity_vec3(target, "Pos")?; + let rot = self.query_entity_vec2_f32(target, "Rotation")?; + Ok((pos, rot)) + } + + fn query_entity_vec3(&self, target: &str, path: &str) -> Result<[f64; 3]> { + let values = self.query_entity_numbers(target, path)?; + if values.len() < 3 { + anyhow::bail!("entity {path} query returned fewer than 3 values"); + } + Ok([values[0], values[1], values[2]]) + } + + fn query_entity_vec2_f32(&self, target: &str, path: &str) -> Result<[f32; 2]> { + let values = self.query_entity_numbers(target, path)?; + if values.len() < 2 { + anyhow::bail!("entity {path} query returned fewer than 2 values"); + } + Ok([values[0] as f32, values[1] as f32]) + } + + fn query_entity_numbers(&self, target: &str, path: &str) -> Result> { + validate_entity_target(target)?; + while self + .bot + .recv_chat_timeout(std::time::Duration::from_millis( + tick::CHAT_DRAIN_TIMEOUT_MS, + )) + .is_some() + {} + self.bot + .send_command(&format!("data get entity {target} {path}"))?; + + let timeout = std::time::Duration::from_secs(3); + let started = std::time::Instant::now(); + while started.elapsed() < timeout { + if let Some((_, message)) = self + .bot + .recv_chat_timeout(std::time::Duration::from_millis(tick::CHAT_POLL_TIMEOUT_MS)) + { + if message.contains(path) || message.contains("entity data") { + let values = parse_numbers_after_colon(&message); + if !values.is_empty() { + return Ok(values); + } + } + if message.contains("No entity was found") || message.contains("Found no elements") + { + anyhow::bail!("failed to query entity {target} {path}: {message}"); + } + } + } + + anyhow::bail!("timed out querying entity {target} {path}") + } +} + +fn validate_entity_target(target: &str) -> Result<()> { + if target.is_empty() + || target + .chars() + .any(|c| c.is_whitespace() || c == '/' || c == ';') + { + anyhow::bail!("invalid player/entity target for recording: {target}"); + } + Ok(()) +} + +fn parse_numbers_after_colon(message: &str) -> Vec { + let value_part = message + .split_once(':') + .map(|(_, value)| value) + .unwrap_or(message); + value_part + .split(|c: char| { + !(c.is_ascii_digit() || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E') + }) + .filter_map(|part| { + if part.is_empty() || part == "-" || part == "+" || part == "." { + None + } else { + part.parse::().ok() + } + }) + .collect() } diff --git a/src/executor/mod.rs b/src/executor/mod.rs index f0790bc..d1da8e0 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -1,6 +1,7 @@ //! Test executor module - core test orchestration mod actions; +pub mod adapter; mod block; mod events; mod handlers; @@ -8,18 +9,17 @@ mod recorder; mod tick; use crate::bot::TestBot; +use adapter::MinecraftWorld; use anyhow::Result; use colored::Colorize; use flint_core::loader::TestLoader; use flint_core::results::{ActionOutcome, AssertFailure, AssertPosition, TestResult}; use flint_core::test_spec::{ActionType, AssertType, TestSpec, TimelineEntry}; use flint_core::timeline::TimelineAggregate; +use flint_core::traits::{FlintPlayer, FlintWorld}; use std::io::Write; -pub use tick::{COMMAND_DELAY_MS, MIN_RETRY_DELAY_MS}; // Timing constants -const CLEANUP_DELAY_MS: u64 = 200; -const TEST_RESULT_DELAY_MS: u64 = 50; const DEFAULT_TESTS_DIR: &str = "FlintBenchmark/tests"; // Progress bar constants @@ -33,8 +33,7 @@ pub struct TestRunOutput { } pub struct TestExecutor { - bot: TestBot, - action_delay_ms: u64, + pub bot: TestBot, recorder: Option, verbose: bool, quiet: bool, @@ -43,13 +42,13 @@ pub struct TestExecutor { last_assert_pos: Vec, events_path: Option, events: Option, + enable_breakpoints: bool, } impl Default for TestExecutor { fn default() -> Self { Self { bot: TestBot::new(), - action_delay_ms: COMMAND_DELAY_MS, recorder: None, verbose: false, quiet: false, @@ -58,6 +57,7 @@ impl Default for TestExecutor { last_assert_pos: vec![], events_path: None, events: None, + enable_breakpoints: true, } } } @@ -67,10 +67,6 @@ impl TestExecutor { Self::default() } - pub fn set_action_delay(&mut self, delay_ms: u64) { - self.action_delay_ms = delay_ms; - } - pub fn set_verbose(&mut self, verbose: bool) { self.verbose = verbose; } @@ -83,14 +79,27 @@ impl TestExecutor { self.fail_fast = fail_fast; } - /// Enable JSONL event emission. The writer is opened lazily once the - /// world offset for the (single) test is known. + pub fn set_enable_breakpoints(&mut self, enable: bool) { + self.enable_breakpoints = enable; + } + + /// Enable JSONL event emission. pub fn set_events_path(&mut self, path: std::path::PathBuf) { self.events_path = Some(path); } - pub async fn connect(&mut self, server: &str) -> Result<()> { - self.bot.connect(server).await + pub fn connect(&mut self, server: &str) -> Result<()> { + self.bot.connect(server) + } + + /// Start a recording session before or during interactive mode. + pub fn start_recording( + &mut self, + test_name: &str, + test_loader: &TestLoader, + player_name: Option, + ) -> Result<()> { + self.handle_record_start(test_name, test_loader, player_name) } /// Helper to get a mutable reference to the recorder, or return an error @@ -98,28 +107,47 @@ impl TestExecutor { self.recorder.as_mut() } - /// Helper to apply the standard command delay - async fn delay(&self) { - tokio::time::sleep(tokio::time::Duration::from_millis(self.action_delay_ms)).await; + /// Helper to apply offset to a position + fn apply_offset(&self, pos: [i32; 3], offset: [i32; 3]) -> [i32; 3] { + [pos[0] + offset[0], pos[1] + offset[1], pos[2] + offset[2]] + } + + fn forceload_regions( + &self, + tests_with_offsets: &[(TestSpec, [i32; 3])], + add: bool, + ) -> Result<()> { + for (test, offset) in tests_with_offsets { + let region = test.cleanup_region(); + let min = self.apply_offset(region[0], *offset); + let max = self.apply_offset(region[1], *offset); + let cx0 = min[0].div_euclid(16); + let cz0 = min[2].div_euclid(16); + let cx1 = max[0].div_euclid(16); + let cz1 = max[2].div_euclid(16); + let verb = if add { "add" } else { "remove" }; + let cmd = format!("forceload {verb} {cx0} {cz0} {cx1} {cz1}"); + self.bot.send_command_synced(&cmd)?; + } + Ok(()) } /// Interactive mode: listen for chat commands and execute them - pub async fn interactive_mode(&mut self, test_loader: &mut TestLoader) -> Result<()> { + pub fn interactive_mode(&mut self, test_loader: &mut TestLoader) -> Result<()> { // Interactive mode always uses verbose output self.verbose = true; // Send help message to chat (without ! to avoid self-triggering) self.bot - .send_command("say FlintMC Interactive Mode active") - .await?; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; - self.bot.send_command("say Type: help, search, run, run-all, run-tags, list, reload, stop (prefix with !)").await?; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + .send_command_synced("say FlintMC Interactive Mode active")?; + self.bot.send_command_synced( + "say Type: help, search, run, run-all, run-tags, list, reload, stop (prefix with !)", + )?; - // Drain any messages (including our own welcome messages) - tick::drain_chat_messages(&mut self.bot).await; + // Drain any messages + tick::drain_chat_messages(&mut self.bot); - // Collect all tests upfront (mutable to allow reload) + // Collect all tests upfront let mut all_test_files = test_loader.collect_all_test_files()?; loop { @@ -127,7 +155,6 @@ impl TestExecutor { if let Some((sender, message)) = self .bot .recv_chat_timeout(std::time::Duration::from_millis(tick::CHAT_POLL_TIMEOUT_MS)) - .await { let Some((command, args)) = handlers::parse_command(&message) else { continue; @@ -135,29 +162,26 @@ impl TestExecutor { match command.as_str() { "!help" => { - self.handle_help().await?; + self.handle_help()?; } "!list" => { - self.handle_list(&all_test_files).await?; + self.handle_list(&all_test_files)?; } "!search" => { if args.is_empty() { - self.bot - .send_command("say Usage: !search ") - .await?; + self.bot.send_command("say Usage: !search ")?; continue; } let pattern = args.join(" "); - self.handle_search(&all_test_files, &pattern).await?; + self.handle_search(&all_test_files, &pattern)?; } "!run" => { if args.is_empty() { self.bot - .send_command("say Usage: !run [step]") - .await?; + .send_command("say Usage: !run [step]")?; continue; } @@ -169,73 +193,68 @@ impl TestExecutor { (args.join(" "), false) }; - self.handle_run(&all_test_files, &test_name, step_mode) - .await?; + self.handle_run(&all_test_files, &test_name, step_mode)?; } "!run-all" => { - self.handle_run_all(&all_test_files).await?; + self.handle_run_all(&all_test_files)?; } "!run-tags" => { if args.is_empty() { self.bot - .send_command("say Usage: !run-tags ") - .await?; + .send_command("say Usage: !run-tags ")?; continue; } let tags: Vec = args[0].split(',').map(|s| s.trim().to_string()).collect(); - self.handle_run_tags(test_loader, &tags).await?; + self.handle_run_tags(test_loader, &tags)?; } "!stop" => { self.bot - .send_command("say Exiting interactive mode. Goodbye!") - .await?; + .send_command("say Exiting interactive mode. Goodbye!")?; return Ok(()); } "!reload" => { test_loader.verify_and_rebuild_index()?; all_test_files = test_loader.collect_all_test_files()?; - self.bot - .send_command(&format!("say Reloaded {} tests", all_test_files.len())) - .await?; + self.bot.send_command(&format!( + "say Reloaded {} tests", + all_test_files.len() + ))?; } // Recorder commands "!record" => { if args.is_empty() { self.bot - .send_command("say Usage: !record [player_name]") - .await?; - self.bot - .send_command( - "say Example: !record my_test or !record fence/fence_connect", - ) - .await?; + .send_command("say Usage: !record [player_name]")?; + self.bot.send_command( + "say Example: !record my_test or !record fence/fence_connect", + )?; continue; } let test_name = args[0].clone(); - // If player name not provided, use sender if available let player_name = args.get(1).cloned().or_else(|| sender.clone()); - self.handle_record_start(&test_name, test_loader, player_name) - .await?; + self.handle_record_start(&test_name, test_loader, player_name)?; } "!assert_changes" => { - self.handle_record_assert_changes().await?; + self.handle_record_assert_changes()?; + } + + "!use" => { + self.handle_record_use(&args)?; } "!tick" | "!next" => { - self.handle_record_tick().await?; + self.handle_record_tick()?; } "!pos1" | "!pos" => { if (!args.is_empty() && args.len() < 3) || args.len() > 3 { - self.bot - .send_command("say Usage: !assert ") - .await?; + self.bot.send_command("say Usage: !assert ")?; continue; } self.handle_pos1(&args); @@ -243,40 +262,35 @@ impl TestExecutor { "!assert" => { if args.len() < 3 { - self.bot - .send_command("say Usage: !assert ") - .await?; + self.bot.send_command("say Usage: !assert ")?; continue; } self.last_assert_pos = args.clone(); - self.handle_record_assert(&args).await?; + self.handle_record_assert(&args)?; } "!sprint" => { if args.len() != 1 { - self.bot.send_command("say Usage: !sprint ").await?; - self.bot - .send_command( - "say: please be assert before a start state of a block/region", - ) - .await?; + self.bot.send_command("say Usage: !sprint ")?; + self.bot.send_command( + "say: please be assert before a start state of a block/region", + )?; continue; } let ticks = args[0].parse::().unwrap_or(1); if ticks == 0 { self.bot - .send_command("say Sprint ticks must be greater than 0") - .await?; + .send_command("say Sprint ticks must be greater than 0")?; continue; } if self.last_assert_pos.is_empty() { - self.bot.send_command("say Please assert a position first, which should be used for each string (can be also a 3d area)").await?; + self.bot.send_command("say Please assert a position first, which should be used for each string (can be also a 3d area)")?; continue; } - self.handle_record_sprint(ticks).await?; + self.handle_record_sprint(ticks)?; } "!save" => { - if self.handle_record_save().await? { + if self.handle_record_save()? { // Reload tests after successful save test_loader.verify_and_rebuild_index()?; all_test_files = test_loader.collect_all_test_files()?; @@ -284,17 +298,15 @@ impl TestExecutor { } "!cancel" => { - self.handle_record_cancel().await?; + self.handle_record_cancel()?; } _ => { if command.starts_with('!') { - self.bot - .send_command(&format!( - "say Unknown command: {}. Type !help for commands.", - command - )) - .await?; + self.bot.send_command(&format!( + "say Unknown command: {}. Type !help for commands.", + command + ))?; } } } @@ -303,7 +315,7 @@ impl TestExecutor { } /// Scan blocks in a cube around a center point (ignores air) - async fn scan_blocks_around( + fn scan_blocks_around( &self, center: [i32; 3], radius: i32, @@ -314,9 +326,8 @@ impl TestExecutor { for y in (center[1] - radius).max(-64)..=(center[1] + radius).min(319) { for z in (center[2] - radius)..=(center[2] + radius) { let pos = [x, y, z]; - if let Ok(Some(block)) = self.bot.get_block(pos).await { + if let Ok(Some(block)) = self.bot.get_block(pos) { let block_id = block::extract_block_id(&block); - // Ignore air blocks if !block_id.to_lowercase().contains("air") { blocks.insert(pos, block_id); } @@ -329,8 +340,7 @@ impl TestExecutor { } /// Scan blocks within an AABB (inclusive bounds, in world coordinates). - /// Air is omitted; clamped to vanilla world height limits. - async fn scan_region( + fn scan_region( &self, min: [i32; 3], max: [i32; 3], @@ -340,7 +350,7 @@ impl TestExecutor { for y in min[1].max(-64)..=max[1].min(319) { for z in min[2]..=max[2] { let pos = [x, y, z]; - if let Ok(Some(block)) = self.bot.get_block(pos).await { + if let Ok(Some(block)) = self.bot.get_block(pos) { let block_id = block::extract_block_id(&block); if !block_id.to_lowercase().contains("air") { blocks.insert(pos, block_id); @@ -353,7 +363,7 @@ impl TestExecutor { } /// Run tests in parallel with merged timeline - pub async fn run_tests_parallel( + pub fn run_tests_parallel( &mut self, tests_with_offsets: &[(TestSpec, [i32; 3])], break_after_setup: bool, @@ -368,8 +378,7 @@ impl TestExecutor { ); } - // Open the events writer if --emit-events was passed. Single-test only; - // parallel-grid runs would interleave coordinates from multiple tests. + // Open the events writer if --emit-events was passed. if let Some(events_path) = self.events_path.clone() { if tests_with_offsets.len() != 1 { anyhow::bail!( @@ -384,6 +393,25 @@ impl TestExecutor { // Build global merged timeline using flint-core let aggregate = TimelineAggregate::from_tests(tests_with_offsets); + // The stable focus point is the center of the complete parallel layout. Parking + // here keeps every test as close as possible to the client's loaded chunks. + let (layout_min, layout_max) = tests_with_offsets.iter().fold( + ([i32::MAX; 3], [i32::MIN; 3]), + |(mut min, mut max), (test, offset)| { + let region = test.cleanup_region(); + for axis in 0..3 { + min[axis] = min[axis].min(region[0][axis] + offset[axis]); + max[axis] = max[axis].max(region[1][axis] + offset[axis]); + } + (min, max) + }, + ); + let layout_center = [ + f64::from((layout_min[0] + layout_max[0]).div_euclid(2)) + 0.5, + 64.0, + f64::from((layout_min[2] + layout_max[2]).div_euclid(2)) + 0.5, + ]; + if verbose { println!(" Global timeline: {} ticks", aggregate.max_tick); println!( @@ -411,19 +439,19 @@ impl TestExecutor { } for (test, offset) in tests_with_offsets.iter() { let region = test.cleanup_region(); - let world_min = actions::apply_offset(region[0], *offset); - let world_max = actions::apply_offset(region[1], *offset); + let world_min = self.apply_offset(region[0], *offset); + let world_max = self.apply_offset(region[1], *offset); let cmd = format!( "fill {} {} {} {} {} {} air", world_min[0], world_min[1], world_min[2], world_max[0], world_max[1], world_max[2] ); - self.bot.send_command(&cmd).await?; + self.bot.send_command_synced(&cmd)?; } - tokio::time::sleep(tokio::time::Duration::from_millis(CLEANUP_DELAY_MS)).await; + + self.forceload_regions(tests_with_offsets, true)?; // Freeze time globally - self.bot.send_command("tick freeze").await?; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + self.bot.send_command_synced("tick freeze")?; // Break after setup if requested let mut stepping_mode = false; @@ -431,19 +459,16 @@ impl TestExecutor { let should_continue = tick::wait_for_step( &mut self.bot, "After test setup (cleanup complete, time frozen)", - ) - .await?; + )?; stepping_mode = !should_continue; } // Emit `run_started` and pre-compute the scan AABB in world coords. - // We pass world coords to the writer; it subtracts the offset so the - // emitted event is in test-local space. let scan_bounds: Option<([i32; 3], [i32; 3])> = if self.events.is_some() { let (test, offset) = &tests_with_offsets[0]; let region = test.cleanup_region(); - let world_min = actions::apply_offset(region[0], *offset); - let world_max = actions::apply_offset(region[1], *offset); + let world_min = self.apply_offset(region[0], *offset); + let world_max = self.apply_offset(region[1], *offset); if let Some(events) = self.events.as_mut() { events.run_started(&test.name, [world_min, world_max])?; } @@ -473,17 +498,48 @@ impl TestExecutor { let show_progress = !verbose && !self.quiet; let fail_fast = self.fail_fast; + // Initialize per-test worlds and players using the trait model + let mut worlds: Vec = tests_with_offsets + .iter() + .map(|(_test, offset)| MinecraftWorld { + bot: self.bot.clone(), + offset: *offset, + current_tick: 0, + entities: std::collections::HashMap::new(), + }) + .collect(); + + let mut players: Vec>> = + Vec::with_capacity(tests_with_offsets.len()); + for (idx, (spec, _offset)) in tests_with_offsets.iter().enumerate() { + let Some(player_config) = spec.setup.as_ref().and_then(|setup| setup.player.as_ref()) + else { + players.push(None); + continue; + }; + let mut player = worlds[idx].create_player(); + let minecraft_player = player + .as_any_mut() + .downcast_mut::() + .ok_or_else(|| anyhow::anyhow!("unsupported FlintPlayer implementation"))?; + for (slot, item) in &player_config.inventory { + minecraft_player.set_slot_checked(*slot, Some(item))?; + } + minecraft_player.select_hotbar_checked(player_config.selected_hotbar)?; + minecraft_player.set_game_mode_checked(player_config.game_mode)?; + players.push(Some(player)); + } + // Execute merged timeline let mut current_tick = 0; while current_tick <= aggregate.max_tick { if let Some(entries) = aggregate.timeline.get(¤t_tick) { for (test_idx, entry, value_idx) in entries { - let (test, offset) = &tests_with_offsets[*test_idx]; + let (test, _) = &tests_with_offsets[*test_idx]; + let world = &mut worlds[*test_idx]; + let player = &mut players[*test_idx]; - match self - .execute_action(current_tick, entry, *value_idx, *offset) - .await - { + match self.execute_action(world, player, current_tick, entry, *value_idx) { Ok(ActionOutcome::AssertPassed) => { test_results[*test_idx].0 += 1; if let Some(events) = self.events.as_mut() @@ -579,8 +635,8 @@ impl TestExecutor { ); } let region = test.cleanup_region(); - let world_min = actions::apply_offset(region[0], *offset); - let world_max = actions::apply_offset(region[1], *offset); + let world_min = self.apply_offset(region[0], *offset); + let world_max = self.apply_offset(region[1], *offset); let cmd = format!( "fill {} {} {} {} {} {} air", world_min[0], @@ -590,37 +646,35 @@ impl TestExecutor { world_max[1], world_max[2] ); - self.bot.send_command(&cmd).await?; + self.bot.send_command_synced(&cmd)?; tests_cleaned[test_idx] = true; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + players[test_idx] = None; + self.bot.park_at(layout_center)?; } } // Check for breakpoint - if aggregate.breakpoints.contains(¤t_tick) || stepping_mode { + if (self.enable_breakpoints && aggregate.breakpoints.contains(¤t_tick)) + || stepping_mode + { let should_continue = tick::wait_for_step( &mut self.bot, &format!("End of tick {} (before step to next tick)", current_tick), - ) - .await?; + )?; stepping_mode = !should_continue; } - // Advance to next tick. With event emission enabled we force - // single-step mode so we can scan and diff after every tick — - // sprint elides intermediate states the visualizer needs. + // Advance to next tick. if let Some((scan_min, scan_max)) = scan_bounds { - tick::step_tick(&mut self.bot, verbose).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(CLEANUP_DELAY_MS)).await; - let world_blocks = self.scan_region(scan_min, scan_max).await?; + tick::step_tick(&mut self.bot, verbose)?; + let world_blocks = self.scan_region(scan_min, scan_max)?; if let Some(events) = self.events.as_mut() { events.emit_tick(current_tick, world_blocks)?; } current_tick += 1; } else if current_tick < aggregate.max_tick { if stepping_mode { - tick::step_tick(&mut self.bot, verbose).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(CLEANUP_DELAY_MS)).await; + tick::step_tick(&mut self.bot, verbose)?; current_tick += 1; } else { let next_event_tick = aggregate @@ -633,23 +687,25 @@ impl TestExecutor { aggregate.max_tick - current_tick }; - let sprint_time_ms = if ticks_to_sprint == 1 { - tick::step_tick(&mut self.bot, verbose).await? + if ticks_to_sprint == 1 { + tick::step_tick(&mut self.bot, verbose)? } else if ticks_to_sprint > 1 { - tick::sprint_ticks(&mut self.bot, ticks_to_sprint, verbose).await? + tick::sprint_ticks(&mut self.bot, ticks_to_sprint, verbose)? } else { 0 }; - let retry_delay = sprint_time_ms.max(MIN_RETRY_DELAY_MS); - tokio::time::sleep(tokio::time::Duration::from_millis(retry_delay)).await; - current_tick += ticks_to_sprint; } } else { current_tick += 1; } + // Update tick counts in the FlintWorld adapter instances + for world in &mut worlds { + world.current_tick = current_tick as u64; + } + // Update progress bar in non-verbose mode if show_progress { print_progress_bar(current_tick.min(aggregate.max_tick), aggregate.max_tick); @@ -661,18 +717,14 @@ impl TestExecutor { println!(); } - // Emit run_completed (totals are summed across tests, but event mode - // is gated to a single test so this collapses to its results). + // Emit run_completed if let Some(events) = self.events.as_mut() { let asserts_passed: u32 = test_results.iter().map(|(p, _)| *p as u32).sum(); let asserts_failed: u32 = test_results.iter().map(|(_, f)| *f as u32).sum(); events.run_completed(asserts_passed, asserts_failed)?; } - // Unfreeze time - self.bot.send_command("tick unfreeze").await?; - - // Clean up remaining tests + // Clean up remaining tests while their chunks are still force-loaded. for test_idx in 0..tests_with_offsets.len() { if !tests_cleaned[test_idx] { let (test, offset) = &tests_with_offsets[test_idx]; @@ -684,8 +736,8 @@ impl TestExecutor { ); } let region = test.cleanup_region(); - let world_min = actions::apply_offset(region[0], *offset); - let world_max = actions::apply_offset(region[1], *offset); + let world_min = self.apply_offset(region[0], *offset); + let world_max = self.apply_offset(region[1], *offset); let cmd = format!( "fill {} {} {} {} {} {} air", world_min[0], @@ -695,12 +747,17 @@ impl TestExecutor { world_max[1], world_max[2] ); - self.bot.send_command(&cmd).await?; + self.bot.send_command_synced(&cmd)?; tests_cleaned[test_idx] = true; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + players[test_idx] = None; + self.bot.park_at(layout_center)?; } } + // Release the chunks only after cleanup has completed, then resume time. + self.forceload_regions(tests_with_offsets, false)?; + self.bot.send_command("tick unfreeze")?; + // Build results let results: Vec = tests_with_offsets .iter() @@ -747,19 +804,15 @@ impl TestExecutor { results.len(), total_failed ); - self.bot.send_command(&format!("say {}", summary)).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(COMMAND_DELAY_MS)).await; + self.bot.send_command_synced(&format!("say {}", summary))?; // Send individual test results to chat for result in &results { let status = if result.success { "PASS" } else { "FAIL" }; let msg = format!("say [{}] {}", status, result.test_name); - self.bot.send_command(&msg).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(TEST_RESULT_DELAY_MS)).await; + self.bot.send_command_synced(&msg)?; } - tokio::time::sleep(tokio::time::Duration::from_millis(CLEANUP_DELAY_MS)).await; - // Collect failure details let failures: Vec<(String, AssertFailure)> = tests_with_offsets .iter() @@ -771,26 +824,22 @@ impl TestExecutor { }) .collect(); + // A virtual player may have left the shared bot inside a test region. Park the + // physical bot at the layout center after every run, including playerless runs. + self.bot.park_at(layout_center)?; + Ok(TestRunOutput { results, failures }) } - async fn execute_action( + fn execute_action( &mut self, + world: &mut MinecraftWorld, + player: &mut Option>, tick: u32, entry: &TimelineEntry, value_idx: usize, - offset: [i32; 3], ) -> Result { - actions::execute_action( - &mut self.bot, - tick, - entry, - value_idx, - offset, - self.action_delay_ms, - self.verbose, - ) - .await + actions::execute_action(world, player, tick, entry, value_idx, self.verbose) } } diff --git a/src/executor/recorder/actions.rs b/src/executor/recorder/actions.rs index 5e357f0..282056d 100644 --- a/src/executor/recorder/actions.rs +++ b/src/executor/recorder/actions.rs @@ -1,15 +1,30 @@ //! Recorded action types for the test recorder /// A recorded action in the timeline -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum RecordedAction { - Place { pos: [i32; 3], block: String }, - Remove { pos: [i32; 3] }, - Assert { pos: [i32; 3], block: String }, + Place { + pos: [i32; 3], + block: String, + }, + Remove { + pos: [i32; 3], + }, + Assert { + pos: [i32; 3], + block: String, + }, + Tp { + pos: [f64; 3], + rot: Option<[f32; 2]>, + }, + Interact { + item: Option, + }, } /// A step in the recorded timeline -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct TimelineStep { pub tick: u32, pub actions: Vec, diff --git a/src/executor/recorder/state.rs b/src/executor/recorder/state.rs index b6ccb9d..427ebd2 100644 --- a/src/executor/recorder/state.rs +++ b/src/executor/recorder/state.rs @@ -119,6 +119,20 @@ impl RecorderState { }); } + /// Convert world position to local floating-point position for player actions. + #[must_use] + pub fn to_local_f64(&self, world_pos: [f64; 3]) -> [f64; 3] { + if let Some(origin) = self.origin { + [ + world_pos[0] - f64::from(origin[0]), + world_pos[1] - f64::from(origin[1]), + world_pos[2] - f64::from(origin[2]), + ] + } else { + world_pos + } + } + /// Record a block placement pub fn record_place(&mut self, world_pos: [i32; 3], block: &str) { // Set origin on first placement @@ -176,6 +190,28 @@ impl RecorderState { }); } + /// Record a player use action at an exact world position and rotation. + pub fn record_use(&mut self, world_pos: [f64; 3], rot: Option<[f32; 2]>, item: Option) { + let block_pos = [ + world_pos[0].floor() as i32, + world_pos[1].floor() as i32, + world_pos[2].floor() as i32, + ]; + if self.origin.is_none() { + self.set_origin(block_pos); + } + + let local_pos = self.to_local_f64(world_pos); + self.bounds.expand(self.to_local(block_pos)); + + let step = self.get_or_create_current_step(); + step.actions.push(RecordedAction::Tp { + pos: local_pos, + rot, + }); + step.actions.push(RecordedAction::Interact { item }); + } + /// Convert all Place/Remove actions in the current tick to Assertions pub fn convert_actions_to_asserts(&mut self) -> usize { let mut converted_count = 0; @@ -204,6 +240,10 @@ impl RecorderState { assert_action @ RecordedAction::Assert { .. } => { new_actions.push(assert_action); } + player_action @ (RecordedAction::Tp { .. } + | RecordedAction::Interact { .. }) => { + new_actions.push(player_action); + } } } @@ -231,10 +271,38 @@ impl RecorderState { let mut timeline_entries: Vec = Vec::new(); for step in &self.timeline { - // Group actions by type for this tick let mut placements: Vec = Vec::new(); let mut checks: Vec = Vec::new(); + let flush_placements = + |timeline_entries: &mut Vec, + placements: &mut Vec| { + if !placements.is_empty() { + timeline_entries.push(TimelineEntry { + at: TickSpec::Single(step.tick), + action_type: ActionType::PlaceEach { + blocks: std::mem::take(placements), + }, + }); + } + }; + + let flush_checks = |timeline_entries: &mut Vec, + checks: &mut Vec| { + if !checks.is_empty() { + let assert_checks = std::mem::take(checks) + .into_iter() + .map(AssertType::Block) + .collect(); + timeline_entries.push(TimelineEntry { + at: TickSpec::Single(step.tick), + action_type: ActionType::Assert { + checks: assert_checks, + }, + }); + } + }; + for action in &step.actions { match action { RecordedAction::Place { pos, block } => { @@ -255,31 +323,35 @@ impl RecorderState { is: BlockSpec::Single(make_block(block)), }); } + RecordedAction::Tp { pos, rot } => { + flush_placements(&mut timeline_entries, &mut placements); + flush_checks(&mut timeline_entries, &mut checks); + timeline_entries.push(TimelineEntry { + at: TickSpec::Single(step.tick), + action_type: ActionType::Tp { + entity_alias: "player".to_string(), + pos: *pos, + rot: *rot, + }, + }); + } + RecordedAction::Interact { item } => { + flush_placements(&mut timeline_entries, &mut placements); + flush_checks(&mut timeline_entries, &mut checks); + timeline_entries.push(TimelineEntry { + at: TickSpec::Single(step.tick), + action_type: ActionType::Interact { item: item.clone() }, + }); + } } } - // Emit place_each if there are placements - if !placements.is_empty() { - timeline_entries.push(TimelineEntry { - at: TickSpec::Single(step.tick), - action_type: ActionType::PlaceEach { blocks: placements }, - }); - } - - // Emit assert if there are checks - if !checks.is_empty() { - let assert_checks = checks.into_iter().map(AssertType::Block).collect(); - timeline_entries.push(TimelineEntry { - at: TickSpec::Single(step.tick), - action_type: ActionType::Assert { - checks: assert_checks, - }, - }); - } + flush_placements(&mut timeline_entries, &mut placements); + flush_checks(&mut timeline_entries, &mut checks); } TestSpec { - flint_version: None, + flint_version: Some("1.0.0".to_string()), name: self.test_name.replace('/', "_"), description: Some(format!("Recorded test: {}", self.test_name)), tags: vec!["recorded".to_string()], diff --git a/src/executor/recorder/tests.rs b/src/executor/recorder/tests.rs index 13cac10..c3f3ea4 100644 --- a/src/executor/recorder/tests.rs +++ b/src/executor/recorder/tests.rs @@ -2,6 +2,7 @@ use super::bounding_box::BoundingBox; use super::state::RecorderState; +use flint_core::test_spec::ActionType; #[test] fn test_bounding_box() { @@ -26,3 +27,37 @@ fn test_local_position() { assert_eq!(recorder.to_local([100, 64, 200]), [0, 0, 0]); assert_eq!(recorder.to_local([105, 65, 198]), [5, 1, -2]); } + +#[test] +fn test_record_use_emits_tp_then_interact() { + let mut recorder = RecorderState::new("test", std::path::Path::new("/tmp")); + recorder.set_origin([100, 64, 200]); + recorder.record_use( + [101.5, 64.0, 198.25], + Some([90.0, 15.0]), + Some("minecraft:bone_meal".to_string()), + ); + + let spec = recorder.generate_test_spec(); + assert_eq!(spec.timeline.len(), 2); + + match &spec.timeline[0].action_type { + ActionType::Tp { + entity_alias, + pos, + rot, + } => { + assert_eq!(entity_alias, "player"); + assert_eq!(*pos, [1.5, 0.0, -1.75]); + assert_eq!(*rot, Some([90.0, 15.0])); + } + other => panic!("expected tp, got {other:?}"), + } + + match &spec.timeline[1].action_type { + ActionType::Interact { item } => { + assert_eq!(item.as_deref(), Some("minecraft:bone_meal")); + } + other => panic!("expected interact, got {other:?}"), + } +} diff --git a/src/executor/tick.rs b/src/executor/tick.rs index 21be387..75f76f4 100644 --- a/src/executor/tick.rs +++ b/src/executor/tick.rs @@ -7,18 +7,15 @@ use colored::Colorize; // Constants for tick timing pub const CHAT_DRAIN_TIMEOUT_MS: u64 = 10; pub const CHAT_POLL_TIMEOUT_MS: u64 = 100; -pub const COMMAND_DELAY_MS: u64 = 100; pub const GAMETIME_QUERY_TIMEOUT_SECS: u64 = 5; pub const TICK_STEP_TIMEOUT_SECS: u64 = 5; -pub const TICK_STEP_POLL_MS: u64 = 50; pub const SPRINT_TIMEOUT_SECS: u64 = 30; pub const MIN_RETRY_DELAY_MS: u64 = 200; /// Drain old chat messages from the bot's queue -pub async fn drain_chat_messages(bot: &mut TestBot) { +pub fn drain_chat_messages(bot: &mut TestBot) { while bot .recv_chat_timeout(std::time::Duration::from_millis(CHAT_DRAIN_TIMEOUT_MS)) - .await .is_some() { // Discard old messages @@ -26,7 +23,7 @@ pub async fn drain_chat_messages(bot: &mut TestBot) { } /// Returns true to continue, false to step to next tick only -pub async fn wait_for_step(bot: &mut TestBot, reason: &str) -> Result { +pub fn wait_for_step(bot: &mut TestBot, reason: &str) -> Result { println!( "\n{} {} {}", "⏸".yellow().bold(), @@ -41,17 +38,15 @@ pub async fn wait_for_step(bot: &mut TestBot, reason: &str) -> Result { ); // Send chat message to inform player - bot.send_command("say Waiting for step/continue (s = step, c = continue)") - .await?; + bot.send_command("say Waiting for step/continue (s = step, c = continue)")?; // First, drain any old messages from the chat queue - drain_chat_messages(bot).await; + drain_chat_messages(bot); // Now wait for a fresh chat command loop { - if let Some((_, message)) = bot - .recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) - .await + if let Some((_, message)) = + bot.recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) { // Skip messages from the bot itself (contains "Waiting for step/continue") if message.contains("Waiting for step/continue") { @@ -84,21 +79,20 @@ pub async fn wait_for_step(bot: &mut TestBot, reason: &str) -> Result { /// Query the current game time from the server /// Returns the game time in ticks -pub async fn query_gametime(bot: &mut TestBot) -> Result { +pub fn query_gametime(bot: &mut TestBot) -> Result { // Clear any pending chat messages - drain_chat_messages(bot).await; + drain_chat_messages(bot); // Send the time query command - bot.send_command("time query gametime").await?; + bot.send_command("time query gametime")?; // Wait for response: "The time is " let timeout = std::time::Duration::from_secs(GAMETIME_QUERY_TIMEOUT_SECS); let start = std::time::Instant::now(); while start.elapsed() < timeout { - if let Some((_, message)) = bot - .recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) - .await + if let Some((_, message)) = + bot.recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) { // Look for "The time is" message if message.contains("The game time is") { @@ -122,21 +116,24 @@ pub async fn query_gametime(bot: &mut TestBot) -> Result { /// Step a single tick using /tick step and verify completion /// Returns the time taken in ms -pub async fn step_tick(bot: &mut TestBot, verbose: bool) -> Result { - let before = query_gametime(bot).await?; +pub fn step_tick(bot: &mut TestBot, verbose: bool) -> Result { + let before = query_gametime(bot)?; let start = std::time::Instant::now(); - bot.send_command("tick step").await?; + // First acknowledge that the server processed the command, then verify the + // scheduled tick itself completed by observing game time advance. + bot.send_command_synced("tick step")?; - // Wait for the tick to actually complete by polling gametime + // Each game-time query blocks until its server response arrives, so no polling + // delay is needed between verification attempts. let timeout = std::time::Duration::from_secs(TICK_STEP_TIMEOUT_SECS); let poll_start = std::time::Instant::now(); loop { - tokio::time::sleep(std::time::Duration::from_millis(TICK_STEP_POLL_MS)).await; - let after = query_gametime(bot).await?; + let after = query_gametime(bot)?; if after > before { + bot.sync_client_world()?; let elapsed = start.elapsed().as_millis() as u64; if verbose { println!( @@ -159,17 +156,16 @@ pub async fn step_tick(bot: &mut TestBot, verbose: bool) -> Result { /// Sprint ticks and capture the time taken from server output /// Returns the ms per tick from the server's sprint completion message /// NOTE: Accounts for Minecraft's off-by-one bug where "tick sprint N" executes N+1 ticks -pub async fn sprint_ticks(bot: &mut TestBot, ticks: u32, verbose: bool) -> Result { +pub fn sprint_ticks(bot: &mut TestBot, ticks: u32, verbose: bool) -> Result { // Clear any pending chat messages - drain_chat_messages(bot).await; + drain_chat_messages(bot); // Account for Minecraft's off-by-one bug: "tick sprint N" executes N+1 ticks // So to execute `ticks` ticks, we request ticks-1 let ticks_to_request = ticks - 1; // Send the sprint command - bot.send_command(&format!("tick sprint {}", ticks_to_request)) - .await?; + bot.send_command(&format!("tick sprint {}", ticks_to_request))?; // Wait for the "Sprint completed" message // Server message format: "Sprint completed with X ticks per second, or Y ms per tick" @@ -177,12 +173,12 @@ pub async fn sprint_ticks(bot: &mut TestBot, ticks: u32, verbose: bool) -> Resul let start = std::time::Instant::now(); while start.elapsed() < timeout { - if let Some((_, message)) = bot - .recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) - .await + if let Some((_, message)) = + bot.recv_chat_timeout(std::time::Duration::from_millis(CHAT_POLL_TIMEOUT_MS)) { // Look for "Sprint completed" message if message.contains("Sprint completed") { + bot.sync_client_world()?; // Try to extract ms per tick // Format: "... or X ms per tick" if let Some(ms_part) = message.split("or ").nth(1) @@ -214,13 +210,5 @@ pub async fn sprint_ticks(bot: &mut TestBot, ticks: u32, verbose: bool) -> Resul } } - // Timeout - return default - if verbose { - println!( - " {} Sprint {} ticks (no completion message received)", - "⚡".dimmed(), - ticks - ); - } - Ok(MIN_RETRY_DELAY_MS) + anyhow::bail!("Sprint verification timeout: no completion message for {ticks} ticks") } diff --git a/src/main.rs b/src/main.rs index 5c23343..0d29c26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod bot; mod executor; +mod spatial_batch; use anyhow::{Context, Result}; use clap::{CommandFactory, Parser, ValueEnum}; @@ -9,8 +10,9 @@ use flint_core::format; use flint_core::format::{format_number, print_concise_summary, print_test_summary}; use flint_core::loader::TestLoader; use flint_core::results::AssertFailure; -use flint_core::spatial::calculate_test_offset_default; +use flint_core::spatial::calculate_test_offsets_for_batch_default; use flint_core::test_spec::{ActionType, TestSpec}; +use spatial_batch::split_tests_by_simulation_distance; use std::path::Path; use std::path::PathBuf; use std::time::Instant; @@ -32,7 +34,6 @@ enum OutputFormat { // Constants const CHUNK_SIZE: usize = 100; -const GRID_SIZE: usize = 10; // Tests are arranged in a 10x10 grid const SEPARATOR_WIDTH: usize = 60; /// Print a separator line @@ -43,14 +44,12 @@ fn print_separator() { /// Print chunk header fn print_chunk_header(chunk_idx: usize, total_chunks: usize, chunk_len: usize) { println!( - "{} {} Chunk {}/{} ({} tests in {}x{} grid)", + "{} {} Chunk {}/{} ({} tests)", "═".repeat(SEPARATOR_WIDTH).dimmed(), "→".blue().bold(), chunk_idx + 1, total_chunks, chunk_len, - GRID_SIZE, - GRID_SIZE ); print_separator(); println!(); @@ -86,9 +85,9 @@ struct Args { #[arg(short = 'i', long)] interactive: bool, - /// Delay in milliseconds between each action (default: 100) - #[arg(short = 'd', long = "action-delay", default_value = "100")] - action_delay: u64, + /// Start interactive mode and immediately begin recording a test with this name + #[arg(long, value_name = "NAME")] + record: Option, /// Verbose output: show all per-action details during test execution #[arg(short, long)] @@ -125,8 +124,7 @@ struct Args { emit_events: Option, } -#[tokio::main] -async fn main() -> Result<()> { +fn main() -> Result<()> { // Setup logging tracing_subscriber::fmt() .with_writer(std::io::stderr) @@ -186,8 +184,10 @@ async fn main() -> Result<()> { .context("Failed to collect test files")? }; - // In interactive mode, we don't require tests to be found initially - if test_files.is_empty() && !args.interactive { + let interactive_mode = args.interactive || args.record.is_some(); + + // In interactive/recording mode, we don't require tests to be found initially + if test_files.is_empty() && !interactive_mode { let location = if !args.tags.is_empty() { format!("with tags: {:?}", args.tags) } else if let Some(ref path) = args.path { @@ -199,7 +199,7 @@ async fn main() -> Result<()> { std::process::exit(1); } - if verbose && !args.interactive { + if verbose && !interactive_mode { println!("Found {} test file(s)\n", test_files.len()); } @@ -243,10 +243,12 @@ async fn main() -> Result<()> { chunk.len() ); } - for (test_index, test_file) in chunk.iter().enumerate() { + for test_file in chunk.iter() { match TestSpec::from_file(test_file, false) { Ok(test) => { - let offset = calculate_test_offset_default(test_index, chunk.len()); + let offset = + calculate_test_offsets_for_batch_default(std::slice::from_ref(&test)) + [0]; let max_tick = test.max_tick(); let assertions = test .timeline @@ -295,11 +297,10 @@ async fn main() -> Result<()> { // Connect to server let mut executor = executor::TestExecutor::new(); - // Set action delay - executor.set_action_delay(args.action_delay); executor.set_verbose(args.verbose); executor.set_quiet(args.quiet || !matches!(args.format, OutputFormat::Pretty)); executor.set_fail_fast(args.fail_fast); + executor.set_enable_breakpoints(interactive_mode); if let Some(events_path) = args.emit_events.clone() { if test_files.len() != 1 { eprintln!( @@ -312,16 +313,8 @@ async fn main() -> Result<()> { executor.set_events_path(events_path); } - if verbose && args.action_delay != 100 { - println!( - "{} Action delay set to {} ms", - "→".yellow(), - args.action_delay - ); - } - // Interactive mode: enter command loop - if args.interactive { + if interactive_mode { println!( "{} Interactive mode enabled - listening for chat commands", "→".yellow().bold() @@ -330,19 +323,31 @@ async fn main() -> Result<()> { println!(" During tests: type 's' to step, 'c' to continue\n"); println!("{} Connecting to {}...", "→".blue(), server); - executor.connect(server).await?; + executor.connect(server)?; println!("{} Connected successfully\n", "✓".green()); - executor.interactive_mode(&mut test_loader).await?; + if let Some(record_name) = args.record.as_deref() { + executor.start_recording(record_name, &test_loader, None)?; + } + + executor.interactive_mode(&mut test_loader)?; return Ok(()); } if verbose { println!("{} Connecting to {}...", "→".blue(), server); } - executor.connect(server).await?; + executor.connect(server)?; + let effective_chunk_distance = executor.bot.effective_chunk_distance()?; + let (view_distance, simulation_distance) = executor.bot.detected_distances(); if verbose { - println!("{} Connected successfully\n", "✓".green()); + println!( + "{} Connected successfully (view: {}, simulation: {}, effective: {})\n", + "✓".green(), + view_distance, + simulation_distance, + effective_chunk_distance + ); } // Load all tests and run in chunks @@ -359,8 +364,8 @@ async fn main() -> Result<()> { CHUNK_SIZE ); println!( - " Each chunk uses a {}x{} grid around spawn\n", - GRID_SIZE, GRID_SIZE + " Each chunk is laid out from cleanup regions with {} block padding\n", + 8 ); } else { eprintln!("Running {} tests...", format_number(total_tests)); @@ -369,6 +374,7 @@ async fn main() -> Result<()> { let start_time = Instant::now(); let mut all_results = Vec::new(); let mut all_failures: Vec<(String, AssertFailure)> = Vec::new(); + let mut test_specs_map = std::collections::HashMap::new(); for (chunk_idx, chunk) in chunks.iter().enumerate() { if verbose { @@ -376,22 +382,12 @@ async fn main() -> Result<()> { } let mut tests_with_offsets = Vec::new(); - for (test_index, test_file) in chunk.iter().enumerate() { + let mut chunk_specs = Vec::new(); + for test_file in chunk.iter() { match TestSpec::from_file(test_file, false) { Ok(test) => { - // Calculate offset within this chunk (10x10 grid) - let offset = calculate_test_offset_default(test_index, chunk.len()); - if verbose { - println!( - " {} Grid position: {} (offset: [{}, {}, {}])", - "→".blue(), - format!("[{}/{}]", test_index + 1, chunk.len()).dimmed(), - offset[0], - offset[1], - offset[2] - ); - } - tests_with_offsets.push((test, offset)); + test_specs_map.insert(test.name.clone(), (test.clone(), test_file.clone())); + chunk_specs.push(test); } Err(e) => { eprintln!( @@ -405,17 +401,65 @@ async fn main() -> Result<()> { } } - if verbose { - println!(); + let sim_batches = split_tests_by_simulation_distance(chunk_specs, effective_chunk_distance); + + if verbose && sim_batches.len() > 1 { + println!( + " {} Split into {} parallel batch(es) for simulation-distance={}\n", + "→".blue(), + sim_batches.len(), + effective_chunk_distance + ); } - // Run this chunk of tests in parallel using merged timeline - let output = executor - .run_tests_parallel(&tests_with_offsets, args.break_after_setup) - .await?; + for (sim_batch_idx, sim_batch) in sim_batches.iter().enumerate() { + if verbose && sim_batches.len() > 1 { + println!( + " {} Simulation batch {}/{} ({} tests)", + "→".blue(), + sim_batch_idx + 1, + sim_batches.len(), + sim_batch.len() + ); + } + + executor.bot.reset_to_test_origin()?; + let offsets = calculate_test_offsets_for_batch_default(sim_batch); + let bot_position = executor.bot.get_position()?; + tests_with_offsets.clear(); + for (test_index, (test, offset)) in sim_batch.iter().cloned().zip(offsets).enumerate() { + let offset = [ + offset[0] + bot_position[0], + offset[1], + offset[2] + bot_position[2], + ]; + if verbose { + println!( + " {} Test {} (offset: [{}, {}, {}])", + "→".blue(), + format!("[{}/{}]", test_index + 1, sim_batch.len()).dimmed(), + offset[0], + offset[1], + offset[2] + ); + } + tests_with_offsets.push((test, offset)); + } + + if verbose { + println!(); + } - all_results.extend(output.results); - all_failures.extend(output.failures); + let output = + executor.run_tests_parallel(&tests_with_offsets, args.break_after_setup)?; + + all_results.extend(output.results); + all_failures.extend(output.failures); + + if args.fail_fast && !all_failures.is_empty() { + break; + } + } if args.fail_fast && !all_failures.is_empty() { break; @@ -448,6 +492,28 @@ async fn main() -> Result<()> { } if all_results.iter().any(|r| !r.success) { + if matches!(args.format, OutputFormat::Pretty) && !all_failures.is_empty() { + println!("{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); + println!("{}", "Flint Visualizer Links:".cyan().bold()); + for (test_name, failure) in &all_failures { + if let Some((spec, path)) = test_specs_map.get(test_name) { + let payload = flint_core::viz_link::FailurePayload::new( + spec.clone(), + Some(path.clone()), + vec![failure.clone()], + failure.tick, + ); + let base_url = std::env::var("FLINT_VIZ_URL") + .unwrap_or_else(|_| "https://flinttestmc.github.io/FlintViz/#".to_string()); + if let Ok(url) = flint_core::viz_link::failure_url(&payload, &base_url) { + println!(" [Visualizer Link for {}]:", test_name.bold()); + println!(" {}", url.underline().blue()); + } + } + } + println!("{}", "═".repeat(SEPARATOR_WIDTH).dimmed()); + println!(); + } std::process::exit(1); } diff --git a/src/spatial_batch.rs b/src/spatial_batch.rs new file mode 100644 index 0000000..f25b369 --- /dev/null +++ b/src/spatial_batch.rs @@ -0,0 +1,146 @@ +//! Batch layout helpers: simulation-distance limits and test focus positions. + +use flint_core::spatial::calculate_test_offsets_for_batch_default; +use flint_core::test_spec::TestSpec; + +/// Blocks from world origin (0, 0) that can still be simulated when the bot stands at +/// the layout center. Reserves two chunks of margin for the player and chunk edges. +pub fn simulation_radius_blocks(simulation_distance: u32) -> i32 { + (simulation_distance.saturating_sub(2) as i32) * 16 +} + +/// Maximum Chebyshev distance from origin for any corner of any test region in a batch. +pub fn max_extent_from_origin(tests: &[(TestSpec, [i32; 3])]) -> i32 { + let mut max = 0; + for (test, offset) in tests { + let region = test.cleanup_region(); + for corner in [region[0], region[1]] { + let wx = offset[0] + corner[0]; + let wz = offset[2] + corner[2]; + max = max.max(wx.abs().max(wz.abs())); + } + } + max +} + +/// Split a batch so every sub-batch fits within the server's simulation distance from the +/// layout center (origin). Tests stay parallel within each sub-batch. +pub fn split_tests_by_simulation_distance( + tests: Vec, + simulation_distance: u32, +) -> Vec> { + if tests.is_empty() { + return Vec::new(); + } + + let max_radius = simulation_radius_blocks(simulation_distance); + let mut batches: Vec> = Vec::new(); + let mut current: Vec = Vec::new(); + + for test in tests { + current.push(test); + let offsets = calculate_test_offsets_for_batch_default(¤t); + let paired: Vec<(TestSpec, [i32; 3])> = current.iter().cloned().zip(offsets).collect(); + + if max_extent_from_origin(&paired) > max_radius && current.len() > 1 { + let overflow = current.pop().expect("current has at least two tests"); + batches.push(current); + current = vec![overflow]; + } + } + + if !current.is_empty() { + batches.push(current); + } + + batches +} + +#[cfg(test)] +mod tests { + use super::*; + use flint_core::test_spec::ActionType; + use flint_core::test_spec::{CleanupSpec, SetupSpec}; + + fn test_spec(name: &str, region: [[i32; 3]; 2]) -> TestSpec { + TestSpec { + flint_version: None, + name: name.to_string(), + description: None, + tags: vec![], + minecraft_ids: vec![], + dependencies: vec![], + setup: Some(SetupSpec { + cleanup: Some(CleanupSpec { region }), + player: None, + }), + timeline: vec![], + breakpoints: vec![], + } + } + + #[test] + fn simulation_radius_reserves_margin() { + assert_eq!(simulation_radius_blocks(10), 128); + assert_eq!(simulation_radius_blocks(12), 160); + } + + #[test] + fn split_keeps_small_batch_intact() { + let tests = vec![ + test_spec("a", [[0, 0, 0], [5, 0, 5]]), + test_spec("b", [[0, 0, 0], [5, 0, 5]]), + ]; + let batches = split_tests_by_simulation_distance(tests, 32); + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].len(), 2); + } + + #[test] + fn split_breaks_wide_batch_for_low_sim_distance() { + let tests: Vec<_> = (0..20) + .map(|i| test_spec(&format!("t{i}"), [[-20, 0, -20], [20, 0, 20]])) + .collect(); + let batches = split_tests_by_simulation_distance(tests, 6); + assert!(batches.len() > 1); + assert_eq!(batches.iter().map(|b| b.len()).sum::(), 20); + } + + #[test] + fn split_keeps_player_timelines_in_parallel_batch() { + let mut first = test_spec("first", [[0, 0, 0], [1, 1, 1]]); + first.timeline.push(flint_core::test_spec::TimelineEntry { + at: flint_core::test_spec::TickSpec::Single(0), + action_type: ActionType::Interact { item: None }, + }); + let mut second = test_spec("second", [[0, 0, 0], [1, 1, 1]]); + second.timeline.push(flint_core::test_spec::TimelineEntry { + at: flint_core::test_spec::TickSpec::Single(0), + action_type: ActionType::Tp { + entity_alias: "player".to_string(), + pos: [0.0, 0.0, 0.0], + rot: None, + }, + }); + + let batches = split_tests_by_simulation_distance(vec![first, second], 32); + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].len(), 2); + } + + #[test] + fn player_timeline_stays_with_block_timelines() { + let block_before = test_spec("before", [[0, 0, 0], [1, 1, 1]]); + let mut player = test_spec("player", [[0, 0, 0], [1, 1, 1]]); + player.timeline.push(flint_core::test_spec::TimelineEntry { + at: flint_core::test_spec::TickSpec::Single(0), + action_type: ActionType::Interact { item: None }, + }); + let block_after = test_spec("after", [[0, 0, 0], [1, 1, 1]]); + + let batches = + split_tests_by_simulation_distance(vec![block_before, player, block_after], 32); + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].len(), 3); + } +}