diff --git a/client/src-tauri/Cargo.lock b/client/src-tauri/Cargo.lock index 576901d2c7..7b53144279 100644 --- a/client/src-tauri/Cargo.lock +++ b/client/src-tauri/Cargo.lock @@ -25,9 +25,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.3" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.1" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -210,9 +210,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -462,7 +462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -501,7 +501,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -512,7 +512,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -544,7 +544,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -565,7 +565,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -619,7 +619,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -642,7 +642,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -653,7 +653,7 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", "cssparser", - "foldhash 0.2.0", + "foldhash", "html5ever", "precomputed-hash", "selectors", @@ -851,12 +851,6 @@ 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" @@ -881,7 +875,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -939,7 +933,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1104,15 +1098,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", ] [[package]] @@ -1181,7 +1173,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1260,7 +1252,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1269,15 +1261,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[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.17.1" @@ -1525,12 +1508,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 = "ident_case" version = "1.0.1" @@ -1702,7 +1679,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1730,14 +1707,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", @@ -1783,12 +1760,6 @@ 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 = "libappindicator" version = "0.9.0" @@ -1944,9 +1915,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -2045,7 +2016,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2451,7 +2422,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2544,16 +2515,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2733,7 +2694,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2991,7 +2952,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3091,7 +3052,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3102,7 +3063,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3126,7 +3087,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3176,7 +3137,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3198,7 +3159,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3443,7 +3404,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3475,9 +3436,9 @@ dependencies = [ [[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", @@ -3501,7 +3462,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3565,7 +3526,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3587,9 +3548,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" dependencies = [ "anyhow", "bytes", @@ -3638,9 +3599,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -3659,9 +3620,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -3675,7 +3636,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.117", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -3686,23 +3647,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -3780,9 +3741,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", @@ -3805,9 +3766,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", @@ -3831,9 +3792,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", @@ -3885,7 +3846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3927,7 +3888,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3938,7 +3899,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4227,7 +4188,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4271,9 +4232,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -4362,12 +4323,6 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -4417,7 +4372,7 @@ version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4488,27 +4443,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[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", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -4519,9 +4465,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.73" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -4529,9 +4475,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4539,48 +4485,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" 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 2.14.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -4594,23 +4518,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4628,9 +4540,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf", "phf_codegen", @@ -4684,9 +4596,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -4713,7 +4625,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4840,7 +4752,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4851,7 +4763,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5195,100 +5107,12 @@ dependencies = [ "windows-sys 0.59.0", ] -[[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" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "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 0.5.0", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "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 2.0.117", - "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 2.13.0", - "indexmap 2.14.0", - "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 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -5389,7 +5213,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -5410,7 +5234,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5430,15 +5254,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[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" @@ -5470,7 +5294,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/crates/engine/src/ai_support/candidates.rs b/crates/engine/src/ai_support/candidates.rs index fb61aa59de..34a8a1afe4 100644 --- a/crates/engine/src/ai_support/candidates.rs +++ b/crates/engine/src/ai_support/candidates.rs @@ -4968,6 +4968,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/costs.rs b/crates/engine/src/game/costs.rs index 96f0eacad4..8a6e6f88fe 100644 --- a/crates/engine/src/game/costs.rs +++ b/crates/engine/src/game/costs.rs @@ -688,6 +688,7 @@ fn pay_ability_cost_inner( track_exiled_by_source: true, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: true, }; return Ok(PaymentOutcome::Paused { diff --git a/crates/engine/src/game/effects/blight.rs b/crates/engine/src/game/effects/blight.rs index de5cebef99..ef1ae96878 100644 --- a/crates/engine/src/game/effects/blight.rs +++ b/crates/engine/src/game/effects/blight.rs @@ -76,6 +76,7 @@ pub fn resolve( // CR 708.2a: Blight places -1/-1 counters; no face-down entry. face_down_profile: None, count_param: count, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/effects/bounce.rs b/crates/engine/src/game/effects/bounce.rs index 19b7a5c9f8..2f9d2f9fee 100644 --- a/crates/engine/src/game/effects/bounce.rs +++ b/crates/engine/src/game/effects/bounce.rs @@ -233,6 +233,7 @@ pub fn resolve( // CR 708.2a: bounce returns cards face up; no face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; return Ok(()); @@ -322,6 +323,7 @@ pub fn resolve( // CR 708.2a: bounce returns cards face up; no face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; return Ok(()); @@ -494,6 +496,7 @@ pub fn resolve_all( // CR 708.2a: bounce returns cards face up; no face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; return Ok(()); diff --git a/crates/engine/src/game/effects/cast_from_zone.rs b/crates/engine/src/game/effects/cast_from_zone.rs index 77179efce9..d92a97cc11 100644 --- a/crates/engine/src/game/effects/cast_from_zone.rs +++ b/crates/engine/src/game/effects/cast_from_zone.rs @@ -63,6 +63,7 @@ fn open_private_zone_cast_selection( // CR 708.2a: cast-from-zone selection is not a face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; Ok(()) diff --git a/crates/engine/src/game/effects/change_zone.rs b/crates/engine/src/game/effects/change_zone.rs index ea0aa1af8d..e84579fc11 100644 --- a/crates/engine/src/game/effects/change_zone.rs +++ b/crates/engine/src/game/effects/change_zone.rs @@ -3,7 +3,7 @@ use rand::Rng; use crate::game::zones; use crate::types::ability::{ ControllerRef, Duration, Effect, EffectError, EffectKind, FilterProp, LibraryPosition, - ResolvedAbility, TargetChoiceTiming, TargetFilter, TargetSelectionMode, TypedFilter, + ResolvedAbility, TargetChoiceTiming, TargetFilter, TargetRef, TargetSelectionMode, TypedFilter, }; #[cfg(test)] use crate::types::ability::{EffectScope, TapStateChange}; @@ -212,19 +212,49 @@ pub fn resolve( let mut origin = origin; + let parsed_target = match &ability.effect { + Effect::ChangeZone { target, .. } => target.clone(), + _ => TargetFilter::Any, + }; // CR 603.7: Resolve the `TrackedSetId(0)` sentinel emitted by the parser // for "from among the milled cards" / "X cards revealed this way" // continuations to the most recent non-empty tracked set. Done up front so // every downstream path (interactive scan, `matches_target_filter`, // `tracked_set_member_zones`) sees the bound id — `matches_target_filter` // looks the set up by exact id and would otherwise miss the sentinel. - let target_filter: TargetFilter = match &ability.effect { - Effect::ChangeZone { target, .. } => { - crate::game::targeting::resolve_tracked_set_sentinel(state, target.clone()) + let mut effective_target_filter = + crate::game::targeting::resolve_tracked_set_sentinel(state, parsed_target); + // CR 608.2c: After a dig that already routed ParentTarget to hand, a chained + // "exile one of them" must pick from the remaining looked-at cards in the + // tracked set — not re-exile the card already in hand (Expressive Iteration). + let mut exile_tracked_set_library_only = false; + if let Effect::ChangeZone { + destination: Zone::Exile, + .. + } = &ability.effect + { + if matches!(effective_target_filter, TargetFilter::ParentTarget) { + if let Some(parent) = ability.targets.iter().find_map(|t| match t { + TargetRef::Object(id) => Some(*id), + _ => None, + }) { + if state + .objects + .get(&parent) + .is_some_and(|obj| obj.zone == Zone::Hand) + { + exile_tracked_set_library_only = true; + effective_target_filter = crate::game::targeting::resolve_tracked_set_sentinel( + state, + TargetFilter::TrackedSet { + id: crate::types::identifiers::TrackedSetId(0), + }, + ); + } + } } - _ => TargetFilter::Any, - }; - let target_filter = &target_filter; + } + let target_filter = &effective_target_filter; if origin.is_none() && matches!(target_filter, TargetFilter::TriggeringSource) { origin = state .current_trigger_event @@ -256,6 +286,19 @@ pub fn resolve( targeted_objects, target_filter, ); + let targeted_objects: Vec = if exile_tracked_set_library_only { + targeted_objects + .into_iter() + .filter(|id| { + state + .objects + .get(id) + .is_some_and(|obj| obj.zone == Zone::Library) + }) + .collect() + } else { + targeted_objects + }; if targeted_objects.is_empty() { // CR 115.6: "Up to one target" — if the player chose zero targets during @@ -338,13 +381,17 @@ pub fn resolve( // there is no fixed `InZone` constraint to extract — so derive the scan // zone from the members' actual zone rather than defaulting to the // battlefield. - let scan_zone = origin - .or_else(|| target_filter.extract_in_zone()) - .or_else(|| { - tracked_set_member_zones(state, target_filter) - .and_then(|zones| zones.into_iter().next()) - }) - .unwrap_or(Zone::Battlefield); + let scan_zone = if exile_tracked_set_library_only { + Zone::Library + } else { + origin + .or_else(|| target_filter.extract_in_zone()) + .or_else(|| { + tracked_set_member_zones(state, target_filter) + .and_then(|zones| zones.into_iter().next()) + }) + .unwrap_or(Zone::Battlefield) + }; // Filter-controller override is primary here: when a filter like // "creature you control" needs "you" to resolve to the *target* player // (not the caster), we pass `filter_controller` explicitly. Use @@ -555,6 +602,7 @@ pub fn resolve( // resolves the choice. face_down_profile: face_down_profile.clone(), count_param: 0, + library_position: None, is_cost_payment: false, }; // EffectResolved is emitted by the EffectZoneChoice handler after the player chooses @@ -6522,6 +6570,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/effects/dig.rs b/crates/engine/src/game/effects/dig.rs index 18d78c122f..ee51320051 100644 --- a/crates/engine/src/game/effects/dig.rs +++ b/crates/engine/src/game/effects/dig.rs @@ -594,7 +594,9 @@ mod tests { ); } - // A fresh tracked set must have been inserted with exactly the kept cards. + // A fresh tracked set must publish the kept/revealed selection so + // downstream TrackedSetFiltered routing (Zimone land/creature split) + // resolves against the cards the player chose to keep. let tracked_id = TrackedSetId(next_id_before); let set = state .tracked_object_sets diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 97a222a51d..b474262217 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -7916,6 +7916,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; @@ -12141,6 +12142,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; state.pending_continuation = @@ -12178,6 +12180,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }, GameAction::SelectCards { @@ -18267,4 +18270,142 @@ mod tests { mid-chain blessing check before the condition is evaluated" ); } + + /// CR 601.2a + CR 608.2c (issue #1162): Expressive Iteration looks at + /// three cards, keeps one in hand, then must still reach the bottom/exile + /// tail on the other looked-at cards. + #[test] + fn expressive_iteration_dig_chain_reaches_library_bottom_and_exile() { + use crate::game::engine; + use crate::types::ability::CastingPermission; + use crate::types::ability::Duration; + use crate::types::actions::GameAction; + + let mut state = GameState::new_two_player(42); + let source = create_object( + &mut state, + CardId(100), + PlayerId(0), + "Expressive Iteration".to_string(), + Zone::Stack, + ); + let card_a = create_object( + &mut state, + CardId(1), + PlayerId(0), + "Card A".to_string(), + Zone::Library, + ); + let card_b = create_object( + &mut state, + CardId(2), + PlayerId(0), + "Card B".to_string(), + Zone::Library, + ); + let card_c = create_object( + &mut state, + CardId(3), + PlayerId(0), + "Card C".to_string(), + Zone::Library, + ); + state.players[0].library = vec![card_a, card_b, card_c].into(); + + let def = crate::parser::oracle_effect::parse_effect_chain( + "Look at the top three cards of your library. Put one of them into your hand, put one of them on the bottom of your library, and exile one of them. You may play the exiled card this turn.", + AbilityKind::Spell, + ); + let ability = + crate::game::ability_utils::build_resolved_from_def(&def, source, PlayerId(0)); + let mut events = Vec::new(); + resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); + + assert!( + matches!(state.waiting_for, WaitingFor::DigChoice { .. }), + "expected initial dig choice, got {:?}", + state.waiting_for + ); + + engine::apply_as_current( + &mut state, + GameAction::SelectCards { + cards: vec![card_a], + }, + ) + .unwrap(); + + let tracked: Vec<_> = state + .tracked_object_sets + .get( + &state + .chain_tracked_set_id + .expect("dig tail must publish a tracked set"), + ) + .expect("tracked set must exist") + .clone(); + assert_eq!( + tracked, + vec![card_b, card_c], + "dig must publish only the unkept looked-at cards" + ); + + let WaitingFor::EffectZoneChoice { + cards: eligible, + effect_kind, + .. + } = state.waiting_for.clone() + else { + panic!( + "expected bottom-of-library choice after keeping to hand, got {:?}", + state.waiting_for + ); + }; + assert_eq!( + effect_kind, + crate::types::ability::EffectKind::PutAtLibraryPosition + ); + assert_eq!( + eligible, + vec![card_b, card_c], + "bottom choice must be among unkept library cards" + ); + + engine::apply_as_current( + &mut state, + GameAction::SelectCards { + cards: vec![card_b], + }, + ) + .unwrap(); + + assert_eq!(state.objects[&card_a].zone, Zone::Hand); + assert_eq!(state.objects[&card_b].zone, Zone::Library); + assert_eq!(state.objects[&card_c].zone, Zone::Exile); + assert!( + state.players[0].library.back() == Some(&card_b), + "card B must be on the bottom of the library" + ); + assert!( + !state.objects[&card_b] + .casting_permissions + .iter() + .any(|p| matches!(p, CastingPermission::PlayFromExile { .. })), + "bottomed card must not receive play permission" + ); + assert!( + state.objects[&card_c] + .casting_permissions + .iter() + .any(|p| matches!( + p, + CastingPermission::PlayFromExile { + duration: Duration::UntilEndOfTurn, + granted_to: PlayerId(0), + .. + } + )), + "exiled card must receive play-this-turn permission" + ); + } } diff --git a/crates/engine/src/game/effects/put_on_top.rs b/crates/engine/src/game/effects/put_on_top.rs index 04b651bf0f..e84b2084b0 100644 --- a/crates/engine/src/game/effects/put_on_top.rs +++ b/crates/engine/src/game/effects/put_on_top.rs @@ -7,6 +7,7 @@ use crate::types::ability::{ }; use crate::types::events::GameEvent; use crate::types::game_state::{GameState, WaitingFor}; +use crate::types::identifiers::ObjectId; use crate::types::zones::Zone; /// Place target card at a specific position in its owner's library. Unlike @@ -43,8 +44,13 @@ pub fn resolve( // This is the post-#323 SelfRef short-circuit applied uniformly. let effective_targets = crate::game::targeting::resolved_targets(ability, &target_filter, state); - let mut collected_targets = - crate::game::effects::effect_object_targets(&target_filter, &effective_targets); + // CR 608.2c: `effect_object_targets` forwards `ability.targets` verbatim + // for non-slot filters. A dig hand-keep binds `ParentTarget` on the exile + // tail but must not pre-fill a `TrackedSet` bottom pick with the kept card. + let mut collected_targets = match &target_filter { + TargetFilter::TrackedSet { .. } | TargetFilter::TrackedSetFiltered { .. } => Vec::new(), + _ => crate::game::effects::effect_object_targets(&target_filter, &effective_targets), + }; if collected_targets.is_empty() && matches!(target_filter, TargetFilter::ExiledBySource) { let ctx = crate::game::filter::FilterContext::from_ability(ability); collected_targets = state @@ -57,6 +63,45 @@ pub fn resolve( .map(|(id, _)| *id) .collect(); } + if collected_targets.is_empty() + && matches!( + target_filter, + TargetFilter::TrackedSet { .. } | TargetFilter::TrackedSetFiltered { .. } + ) + { + // CR 608.2c + CR 401.2: Tracked-set continuations after a dig keep may + // still reference cards left in the library (Expressive Iteration's + // "put one on the bottom" step). Only those library members are legal + // picks — cards already routed to hand must not re-enter this choice. + // Resolve sentinel/explicit tracked-set identity first, then apply the + // filter predicate instead of blindly reading `chain_tracked_set_id`. + let effective_filter = + crate::game::targeting::resolve_tracked_set_sentinel(state, target_filter.clone()); + let ctx = crate::game::filter::FilterContext::from_ability(ability); + let candidate_ids: Vec = match &effective_filter { + TargetFilter::TrackedSet { id } | TargetFilter::TrackedSetFiltered { id, .. } => state + .tracked_object_sets + .get(id) + .cloned() + .unwrap_or_default(), + _ => state.objects.keys().copied().collect(), + }; + collected_targets = candidate_ids + .into_iter() + .filter(|id| { + state + .objects + .get(id) + .is_some_and(|obj| obj.zone == Zone::Library) + && crate::game::filter::matches_target_filter( + state, + *id, + &effective_filter, + &ctx, + ) + }) + .collect(); + } // CR 115.1 + CR 400.2: When the filter specifies a private zone (hand/library) // and no targets were pre-selected during casting (because the Oracle text does @@ -64,6 +109,24 @@ pub fn resolve( // This covers Brainstorm ("put two cards from your hand on top of your library") // and similar cards where the player chooses during resolution. let expected = resolve_quantity_with_targets(state, &count_expr, ability).max(0) as usize; + let expected = if expected == 0 + && matches!( + position, + LibraryPosition::Bottom | LibraryPosition::NthFromTop { .. } + ) + && matches!( + target_filter, + TargetFilter::TrackedSet { .. } | TargetFilter::TrackedSetFiltered { .. } + ) + && !collected_targets.is_empty() + { + // Parser placeholder `count: 0` on dig-tail bottom steps means "one of + // the tracked looked-at cards", not "all of them". + 1 + } else { + expected + }; + if collected_targets.is_empty() { if expected == 0 { events.push(GameEvent::EffectResolved { @@ -114,6 +177,7 @@ pub fn resolve( // CR 708.2a: library-position selection is not a face-down entry. face_down_profile: None, count_param: 0, + library_position: Some(position.clone()), is_cost_payment: false, }; return Ok(()); @@ -138,6 +202,33 @@ pub fn resolve( )); } + // CR 115.1 + CR 608.2c: When more tracked-set library candidates exist + // than the placement count allows, prompt before auto-picking the first. + if collected_targets.len() > expected && expected > 0 { + state.waiting_for = WaitingFor::EffectZoneChoice { + player: ability.controller, + cards: collected_targets, + count: expected, + min_count: 0, + up_to: false, + source_id: ability.source_id, + effect_kind: EffectKind::PutAtLibraryPosition, + zone: Zone::Library, + destination: None, + enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enter_transformed: false, + enters_under_player: None, + enters_attacking: false, + owner_library: false, + track_exiled_by_source: false, + face_down_profile: None, + count_param: 0, + library_position: Some(position.clone()), + is_cost_payment: false, + }; + return Ok(()); + } + // `count` carries the cardinality of the placement. For multi-card // placement, CR 401.4 lets the owner arrange cards put into the same // library position. The runtime uses target/selection order for "in any @@ -708,4 +799,97 @@ mod tests { other => panic!("Expected EffectZoneChoice, got {:?}", other), } } + + /// CR 608.2c (issue #1162): filtered tracked-set library-position + /// continuations must honor `TrackedSetFiltered`, not every library member + /// in the chain set. + #[test] + fn tracked_set_filtered_library_bottom_honors_inner_filter() { + use crate::types::ability::TypeFilter; + use crate::types::card_type::CoreType; + use crate::types::identifiers::TrackedSetId; + + let mut state = GameState::new_two_player(42); + let creature = create_object( + &mut state, + CardId(10), + PlayerId(0), + "Bear".to_string(), + Zone::Library, + ); + let instant_a = create_object( + &mut state, + CardId(11), + PlayerId(0), + "Bolt A".to_string(), + Zone::Library, + ); + let instant_b = create_object( + &mut state, + CardId(12), + PlayerId(0), + "Bolt B".to_string(), + Zone::Library, + ); + state + .objects + .get_mut(&creature) + .unwrap() + .card_types + .core_types = vec![CoreType::Creature]; + state + .objects + .get_mut(&instant_a) + .unwrap() + .card_types + .core_types = vec![CoreType::Instant]; + state + .objects + .get_mut(&instant_b) + .unwrap() + .card_types + .core_types = vec![CoreType::Instant]; + state.players[0].library = vec![creature, instant_a, instant_b].into(); + + let set_id = TrackedSetId(7); + state + .tracked_object_sets + .insert(set_id, vec![creature, instant_a, instant_b]); + state.chain_tracked_set_id = Some(set_id); + + let ability = ResolvedAbility::new( + Effect::PutAtLibraryPosition { + target: TargetFilter::TrackedSetFiltered { + id: set_id, + filter: Box::new(TargetFilter::Typed( + crate::types::ability::TypedFilter::new(TypeFilter::Instant), + )), + caused_by: None, + }, + count: QuantityExpr::Fixed { value: 1 }, + position: LibraryPosition::Bottom, + }, + vec![], + ObjectId(100), + PlayerId(0), + ); + + let mut events = vec![]; + resolve(&mut state, &ability, &mut events).unwrap(); + + match &state.waiting_for { + WaitingFor::EffectZoneChoice { cards, .. } => { + assert_eq!( + cards, + &vec![instant_a, instant_b], + "only instant members of the tracked set may be offered" + ); + assert!( + !cards.contains(&creature), + "creature must not be offered for instant-only filter" + ); + } + other => panic!("expected EffectZoneChoice, got {other:?}"), + } + } } diff --git a/crates/engine/src/game/effects/sacrifice.rs b/crates/engine/src/game/effects/sacrifice.rs index aba9db9fc2..fb657d038f 100644 --- a/crates/engine/src/game/effects/sacrifice.rs +++ b/crates/engine/src/game/effects/sacrifice.rs @@ -311,6 +311,7 @@ pub fn resolve( // CR 708.2a: sacrifice selection is not a face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/effects/tap_untap.rs b/crates/engine/src/game/effects/tap_untap.rs index cc660da8f5..26cc87c243 100644 --- a/crates/engine/src/game/effects/tap_untap.rs +++ b/crates/engine/src/game/effects/tap_untap.rs @@ -249,6 +249,7 @@ fn prompt_resolution_tap_untap_choice( // CR 708.2a: tap/untap selection is not a face-down entry. face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; true diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index ecc0e19822..39e4d1e481 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -17984,6 +17984,7 @@ Echo—Discard a card. (At the beginning of your upkeep, if this came under your track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; state.pending_continuation = Some(crate::types::game_state::PendingContinuation::new( @@ -18054,6 +18055,7 @@ Echo—Discard a card. (At the beginning of your upkeep, if this came under your track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; @@ -18100,6 +18102,7 @@ Echo—Discard a card. (At the beginning of your upkeep, if this came under your track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/engine_resolution_choices.rs b/crates/engine/src/game/engine_resolution_choices.rs index 686e4b2c85..f022649f47 100644 --- a/crates/engine/src/game/engine_resolution_choices.rs +++ b/crates/engine/src/game/engine_resolution_choices.rs @@ -135,6 +135,28 @@ pub(super) fn handles(waiting_for: &WaitingFor) -> bool { ) } +/// CR 608.2c: Expressive Iteration-style dig tails chain a +/// `PutAtLibraryPosition { TrackedSet }` step before exiling from the same +/// looked-at pile. Those continuations publish and route via the **unkept** +/// looked-at cards; generic reveal/keep continuations (Zimone land split) +/// bind only the kept/revealed subset. +fn dig_continuation_needs_full_looked_at_tracked_set(ability: &ResolvedAbility) -> bool { + let mut current = Some(ability); + while let Some(sub) = current { + if matches!( + &sub.effect, + Effect::PutAtLibraryPosition { + target: crate::types::ability::TargetFilter::TrackedSet { .. }, + .. + } + ) { + return true; + } + current = sub.sub_ability.as_deref(); + } + false +} + /// CR 701.20e / CR 701.23a + CR 401.4: Move the "rest" partition of an /// interactive selection (Dig's unkept cards, a search-split's non-primary /// cards) to a concrete destination zone. `Library` routes to the bottom of the @@ -1523,24 +1545,40 @@ pub(super) fn handle_resolution_choice( } } } - // CR 701.20b + CR 608.2c: Publish the kept (revealed) cards as a - // tracked set so downstream sub_abilities can route them by type - // via `TargetFilter::TrackedSetFiltered`. Used by Zimone's - // Experiment — "Put all land cards revealed this way onto the - // battlefield tapped and put all creature cards revealed this way - // into your hand" consume this set. Use a fresh tracked set so a - // parent effect's empty pre-choice publish cannot keep the chain - // sentinel bound to the wrong set. - effects::publish_fresh_tracked_set(state, kept.clone()); + // CR 701.20b + CR 608.2c: Publish a tracked set for downstream + // sub_abilities. Reveal/keep continuations (Zimone land split) bind + // the kept subset; Expressive Iteration's bottom/exile tail binds the + // unkept looked-at pile when its continuation chains + // `PutAtLibraryPosition { TrackedSet }`. + let publish_set = + if kept.is_empty() { + Vec::new() + } else if state.pending_continuation.as_ref().is_some_and(|cont| { + dig_continuation_needs_full_looked_at_tracked_set(&cont.chain) + }) { + // Expressive Iteration-style bottom/exile tail: downstream + // `TrackedSet` steps address the unkept looked-at pile only. + unkept.clone() + } else { + kept.clone() + }; + effects::publish_fresh_tracked_set(state, publish_set); // None => Graveyard; map to a concrete zone so the rest mover // (shared with the search-split partition) has a single Zone. - route_rest_partition( - state, - &unkept, - rest_destination.unwrap_or(Zone::Graveyard), - events, - ); + // When a continuation owns the unkept pile (Expressive Iteration + // bottom/exile tail), do not pre-route here. + if state.pending_continuation.is_none() { + route_rest_partition( + state, + &unkept, + rest_destination.unwrap_or(Zone::Graveyard), + events, + ); + } if let Some(cont) = state.pending_continuation.as_mut() { + // CR 608.2c: ParentTarget continuations (Hideaway conceal, dig + // conditionals on the kept card) bind to the kept selection. + // Hand/bottom/exile tails route via TrackedSetFiltered instead. cont.chain.targets = kept.iter().map(|&id| TargetRef::Object(id)).collect(); cont.chain.context.optional_effect_performed = !kept.is_empty(); } @@ -2441,6 +2479,7 @@ pub(super) fn handle_resolution_choice( track_exiled_by_source, face_down_profile, count_param, + library_position, is_cost_payment, }, GameAction::SelectCards { cards: chosen }, @@ -2718,12 +2757,27 @@ pub(super) fn handle_resolution_choice( // CR 115.1: Resolution-time selection for PutAtLibraryPosition // from a private zone (e.g. Brainstorm's "put two cards from // your hand on top of your library"). Cards are placed in - // selection order (first chosen = top). - EffectKind::PutAtLibraryPosition => { - for &card_id in chosen.iter().rev() { - super::zones::move_to_library_at_index(state, card_id, Some(0), events); + // selection order (first chosen = top). Expressive Iteration's + // tracked-set bottom step chains an exile `ParentTarget` tail — + // detect that continuation shape to honor bottom placement. + EffectKind::PutAtLibraryPosition => match library_position { + Some(LibraryPosition::Bottom) => { + for &card_id in &chosen { + super::zones::move_to_library_position(state, card_id, false, events); + } } - } + Some(LibraryPosition::NthFromTop { n }) => { + let index = Some(n.saturating_sub(1) as usize); + for &card_id in &chosen { + super::zones::move_to_library_at_index(state, card_id, index, events); + } + } + _ => { + for &card_id in chosen.iter().rev() { + super::zones::move_to_library_at_index(state, card_id, Some(0), events); + } + } + }, // CR 601.2c + CR 115.1: Resolution-time hand pick for // `CastFromZone` (Electrodominance, Baral's Expertise). EffectKind::CastFromZone => { @@ -2955,6 +3009,26 @@ pub(super) fn handle_resolution_choice( _ => None, }) .collect() + } else if matches!(effect_kind, EffectKind::PutAtLibraryPosition) + && matches!(library_position, Some(LibraryPosition::Bottom)) + && state.pending_continuation.is_some() + { + // CR 608.2c: Expressive Iteration's bottom pick narrows the + // tracked set to the remaining looked-at library cards so the + // chained exile step cannot re-select the bottomed card. + state + .chain_tracked_set_id + .and_then(|id| state.tracked_object_sets.get(&id).cloned()) + .unwrap_or_default() + .into_iter() + .filter(|id| !chosen.contains(id)) + .filter(|id| { + state + .objects + .get(id) + .is_some_and(|obj| obj.zone == Zone::Library) + }) + .collect() } else { chosen.clone() }; diff --git a/crates/engine/src/game/replacement.rs b/crates/engine/src/game/replacement.rs index 1b4bea64b5..9f190c7ab5 100644 --- a/crates/engine/src/game/replacement.rs +++ b/crates/engine/src/game/replacement.rs @@ -737,6 +737,7 @@ fn pay_replacement_may_cost( && matches!( state.waiting_for, WaitingFor::EffectZoneChoice { + library_position: None, is_cost_payment: true, .. } diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index 1757330eab..78c4f5d20a 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -19345,6 +19345,7 @@ pub mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; diff --git a/crates/engine/src/game/visibility.rs b/crates/engine/src/game/visibility.rs index 91c7a2db0c..090b2c996a 100644 --- a/crates/engine/src/game/visibility.rs +++ b/crates/engine/src/game/visibility.rs @@ -526,6 +526,7 @@ pub fn filter_state_for_viewer(state: &GameState, viewer: PlayerId) -> GameState track_exiled_by_source, ref face_down_profile, count_param, + library_position: None, is_cost_payment: _, } = state.waiting_for { @@ -550,6 +551,7 @@ pub fn filter_state_for_viewer(state: &GameState, viewer: PlayerId) -> GameState // not private hand info — pass them through the redaction. face_down_profile: face_down_profile.clone(), count_param, + library_position: None, is_cost_payment: false, }; } diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 5e84e23bc6..e403d0e65b 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -2741,7 +2741,12 @@ pub(super) fn apply_clause_continuation( .. } = &mut *previous.effect { - *destination = Some(Zone::Library); + // Preserve an explicit kept destination (Hand, Battlefield, etc.) + // from an earlier "put one into your hand" clause; only default + // destination to Library for reveal-only digs. + if destination.is_none() { + *destination = Some(Zone::Library); + } *rest_destination = Some(Zone::Library); } let put_def = AbilityDefinition::new( diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 77ad87e40c..4f2967273b 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -3119,6 +3119,11 @@ pub enum WaitingFor { /// Zero for all non-blight EffectZoneChoice uses. #[serde(default)] count_param: u32, + /// CR 401.4: Explicit library placement for resolution-time + /// `PutAtLibraryPosition` choices. `None` = top (Brainstorm); `Some` + /// preserves bottom/nth placement across the choice round-trip. + #[serde(default, skip_serializing_if = "Option::is_none")] + library_position: Option, /// CR 118.3: When true, this choice is for a cost payment (e.g., exile cost) /// rather than effect resolution. Cost-payment choices require special /// handling for exile-link tracking (push_exiled_with_source_this_turn). @@ -8198,6 +8203,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, })); variants.push(Box::new(WaitingFor::DefilerPayment { @@ -8447,6 +8453,7 @@ mod tests { track_exiled_by_source: false, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: false, }; let json = serde_json::to_string(&wf).unwrap(); @@ -8549,6 +8556,7 @@ mod tests { ward: None, }), count_param: 0, + library_position: None, is_cost_payment: false, }; let json = serde_json::to_string(&wf).expect("serialize"); diff --git a/crates/engine/tests/integration/mimeoplasm_interactive_exile.rs b/crates/engine/tests/integration/mimeoplasm_interactive_exile.rs index e1fb576ce3..53d2648bd7 100644 --- a/crates/engine/tests/integration/mimeoplasm_interactive_exile.rs +++ b/crates/engine/tests/integration/mimeoplasm_interactive_exile.rs @@ -483,6 +483,7 @@ fn paycost_arm_exiles_cards_via_apply_as_current() { track_exiled_by_source: true, face_down_profile: None, count_param: 0, + library_position: None, is_cost_payment: true, }; } diff --git a/crates/server-core/src/filter.rs b/crates/server-core/src/filter.rs index 7ae7628cb6..91a4bc6e00 100644 --- a/crates/server-core/src/filter.rs +++ b/crates/server-core/src/filter.rs @@ -344,6 +344,7 @@ mod tests { face_down_profile: None, count_param: 0, is_cost_payment: false, + library_position: None, }; let filtered = filter_state_for_player(&state, PlayerId(1));