diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 78bc369..3131a29 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,13 +10,13 @@ jobs: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: nightly-2025-12-28 override: true - uses: actions-rs/cargo@v1 with: command: check - toolchain: nightly + toolchain: nightly-2025-12-28 diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index 4349927..0000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Clippy check - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - - run: rustup component add clippy - - uses: actions-rs/cargo@v1 - with: - command: clippy - toolchain: nightly - # For now I will allow unused here - args: -- -D warnings -A unused diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index cae854c..eb3345a 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -10,7 +10,7 @@ jobs: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 34a80e5..614a6f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: name: Test Suite runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.gitignore b/.gitignore index 8c907ba..e03cb19 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ dhat-heap.json samply.json vtune-results profile.json.gz -dist \ No newline at end of file +dist +result \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index abac346..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "gruntfuggly.todo-tree", - "eamodio.gitlens", - "spencerwmiles.vscode-task-buttons" - ] -} diff --git a/Cargo.lock b/Cargo.lock index cbbc526..85580c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -159,12 +168,6 @@ dependencies = [ "equator", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-activity" version = "0.6.0" @@ -172,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.11.0", "cc", "cesu8", "jni", @@ -203,9 +206,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -233,12 +236,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -247,7 +244,17 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", +] + +[[package]] +name = "args" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b7432c65177b8d5c032d56e020dd8d407e939468479fc8c300e2d93e6d970b" +dependencies = [ + "getopts", + "log", ] [[package]] @@ -269,34 +276,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] -name = "ash" -version = "0.38.0+1.3.281" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" dependencies = [ - "libloading", + "stable_deref_trait", ] [[package]] -name = "ashpd" -version = "0.11.0" +name = "ash" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", + "libloading", ] [[package]] @@ -325,9 +319,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", @@ -337,17 +331,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.6.0" @@ -361,33 +344,22 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - [[package]] name = "async-process" version = "2.5.0" @@ -403,7 +375,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -414,7 +386,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -429,7 +401,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.2", + "rustix 1.1.3", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -449,7 +421,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -528,7 +500,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -544,7 +516,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.108", + "syn", ] [[package]] @@ -553,6 +525,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" version = "0.2.5" @@ -569,9 +561,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec", ] @@ -664,9 +656,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitcode" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74" +checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d" dependencies = [ "arrayvec", "bitcode_derive", @@ -677,13 +669,13 @@ dependencies = [ [[package]] name = "bitcode_derive" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e" +checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -694,18 +686,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "bitvec" @@ -766,12 +761,6 @@ dependencies = [ "piper", ] -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" - [[package]] name = "built" version = "0.8.0" @@ -784,15 +773,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" [[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", ] @@ -805,7 +794,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -822,9 +811,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calloop" @@ -832,7 +821,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "polling", "rustix 0.38.44", @@ -842,13 +831,13 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "tracing", ] @@ -871,17 +860,17 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", - "rustix 1.1.2", + "calloop 0.14.4", + "rustix 1.1.3", "wayland-backend", "wayland-client", ] [[package]] name = "cc" -version = "1.2.44" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -895,16 +884,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -926,43 +905,17 @@ dependencies = [ "libc", ] -[[package]] -name = "charts-rs" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64caa6454be69fcf2555a9692729d118d26ee8c3b281c355e600707c19b39109" -dependencies = [ - "ahash", - "arc-swap", - "charts-rs-derive", - "fontdue", - "once_cell", - "regex", - "resvg", - "serde", - "serde_json", - "snafu", - "substring", -] - -[[package]] -name = "charts-rs-derive" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a22d9bb806d0d341a234556bee1481d8877a78c1a718d9a92aa4e9f542766c" -dependencies = [ - "quote", - "syn 2.0.108", -] - [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -981,7 +934,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1015,11 +968,11 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1041,6 +994,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1092,12 +1064,12 @@ dependencies = [ ] [[package]] -name = "core_maths" -version = "0.1.1" +name = "core2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" dependencies = [ - "libm", + "memchr", ] [[package]] @@ -1157,9 +1129,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1171,17 +1143,11 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] @@ -1194,7 +1160,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1256,7 +1222,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -1270,7 +1236,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1311,19 +1277,21 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bdf37f8d5bd9aa7f753573fdda9cf7343afa73dd28d7bfe9593bd9798fc07e" dependencies = [ "bytemuck", "color-hex", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "emath", "serde", ] [[package]] name = "eframe" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d1c15e7bd136b309bd3487e6ffe5f668b354cd9768636a836dd738ac90eb0b" dependencies = [ "ahash", "bytemuck", @@ -1349,7 +1317,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "web-time 1.1.0", + "web-time", "wgpu", "winapi", "windows-sys 0.59.0", @@ -1358,48 +1326,51 @@ dependencies = [ [[package]] name = "egui" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5d0306cd61ca75e29682926d71f2390160247f135965242e904a636f51c0dc" dependencies = [ "accesskit", "ahash", - "bitflags 2.10.0", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "bitflags 2.11.0", + "emath", "epaint", "log", "nohash-hasher", "profiling", "ron 0.10.1", "serde", + "smallvec", "unicode-segmentation", ] [[package]] name = "egui-show-info" version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b75935b7bb7710de4fc4900a35fa9" +source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577ff72e0c7eb21cafe4850c60ac2" dependencies = [ "bimap", "egui", "enum-map", "parking_lot 0.12.5", - "petgraph 0.8.2", + "petgraph", ] [[package]] name = "egui-show-info-derive" version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b75935b7bb7710de4fc4900a35fa9" +source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577ff72e0c7eb21cafe4850c60ac2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "egui-wgpu" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12eca13293f8eba27a32aaaa1c765bfbf31acd43e8d30d5881dcbe5e99ca0c7" dependencies = [ "ahash", "bytemuck", @@ -1410,15 +1381,16 @@ dependencies = [ "profiling", "thiserror 1.0.69", "type-map", - "web-time 1.1.0", + "web-time", "wgpu", "winit", ] [[package]] name = "egui-winit" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95d0a91f9cb0dc2e732d49c2d521ac8948e1f0b758f306fb7b14d6f5db3927f" dependencies = [ "accesskit_winit", "ahash", @@ -1429,16 +1401,16 @@ dependencies = [ "profiling", "raw-window-handle", "smithay-clipboard", - "web-time 1.1.0", + "web-time", "webbrowser", "winit", ] [[package]] name = "egui_extras" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" +checksum = "dddbceddf39805fc6c62b1f7f9c05e23590b40844dc9ed89c6dc6dbc886e3e3b" dependencies = [ "ahash", "egui", @@ -1451,8 +1423,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7037813341727937f9e22f78d912f3e29bc3c46e2f40a9e82bb51cbf5e4cfb" dependencies = [ "ahash", "bytemuck", @@ -1468,24 +1441,26 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.25.1" -source = "git+https://github.com/BloodStainedCrow/egui_graphs?branch=tree_layout#32aeb647d0ebd65ff0105274397ce4c4258d9e87" +version = "0.28.0" +source = "git+https://github.com/BloodStainedCrow/egui_graphs?branch=tree_layout#5e4e3191d6d17e264660d78b6f47363d16bd325b" dependencies = [ "egui", - "petgraph 0.8.2", + "getrandom 0.2.17", + "instant", + "petgraph", "rand 0.9.2", "serde", ] [[package]] name = "egui_plot" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ae092b46ea532f6c69d3e71036fb3b688fd00fd09c2a1e43d17051a8ae43e6" +checksum = "524318041a8ea90c81c738e8985f8ad9e3f9bed636b03c2ff37b218113ed5121" dependencies = [ "ahash", "egui", - "emath 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", + "emath", ] [[package]] @@ -1496,14 +1471,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emath" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" - -[[package]] -name = "emath" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +checksum = "45fd7bc25f769a3c198fe1cf183124bf4de3bd62ef7b4f1eaf6b08711a3af8db" dependencies = [ "bytemuck", "serde", @@ -1523,9 +1493,9 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enum-map" @@ -1545,7 +1515,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1566,7 +1536,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1577,19 +1547,20 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "epaint" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63adcea970b7a13094fe97a36ab9307c35a750f9e24bf00bb7ef3de573e0fddb" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "emath", "epaint_default_fonts", "log", "nohash-hasher", @@ -1600,8 +1571,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1537accc50c9cab5a272c39300bdd0dd5dca210f6e5e8d70be048df9596e7ca2" [[package]] name = "equator" @@ -1620,7 +1592,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1645,15 +1617,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "euclid" -version = "0.22.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" -dependencies = [ - "num-traits", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -1677,9 +1640,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -1692,16 +1655,19 @@ dependencies = [ [[package]] name = "factory" -version = "0.2.0" +version = "0.3.0" dependencies = [ + "args", "base64 0.22.1", "bimap", "bincode 2.0.1", "bitcode", "bitvec", - "built 0.8.0", + "built", "bytemuck", - "charts-rs", + "chrono", + "console_error_panic_hook", + "convert_case", "dhat", "directories", "ecolor", @@ -1713,30 +1679,30 @@ dependencies = [ "egui_graphs", "egui_plot", "enum-map", - "fixed-buffer", + "fixedbitset", "flate2", "fork", - "genawaiter", "get-size2", - "getrandom 0.2.16", "getrandom 0.3.4", "hex", "image", "interprocess", - "itertools 0.14.0", + "itertools", "libc", "log", "memoffset", "mimalloc", "noise", + "open", "parking_lot 0.12.5", - "petgraph 0.8.2", + "petgraph", "postcard", "profiling", "proptest", "puffin", "puffin_egui", - "rand 0.8.5", + "rand 0.9.2", + "rand_xoshiro", "rayon", "recycle_vec", "rfd", @@ -1747,14 +1713,13 @@ dependencies = [ "serde_path_to_error", "sha2", "simple_logger", - "smallvec", "spin_sleep_util", - "stable-vec", "static_assertions", "strum 0.27.2", "take_mut", "thin-dst", "tilelib", + "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", @@ -1785,7 +1750,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1799,21 +1764,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "fixed-buffer" -version = "1.0.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e394ec858f7f07a580a2e63c6da5c4632cd7503da034d6a545c75516a2df91" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -1823,21 +1776,15 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "fnv" version = "1.0.7" @@ -1850,38 +1797,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "slotmap", - "tinyvec", - "ttf-parser 0.25.1", -] - -[[package]] -name = "fontdue" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" -dependencies = [ - "hashbrown 0.15.5", - "ttf-parser 0.21.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -1900,7 +1815,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -1935,9 +1850,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1950,9 +1865,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1960,15 +1875,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1977,9 +1892,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1996,26 +1911,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -2025,9 +1940,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2037,45 +1952,14 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] -[[package]] -name = "genawaiter" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" -dependencies = [ - "genawaiter-macro", - "genawaiter-proc-macro", - "proc-macro-hack", -] - -[[package]] -name = "genawaiter-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" - -[[package]] -name = "genawaiter-proc-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" -dependencies = [ - "proc-macro-error", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2083,20 +1967,20 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f" +checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" dependencies = [ "attribute-derive", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "get-size2" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9" +checksum = "49cf31a6d70300cf81461098f7797571362387ef4bf85d32ac47eaa59b3a5a1a" dependencies = [ "get-size-derive2", ] @@ -2107,15 +1991,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "windows-link 0.2.1", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[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", @@ -2139,16 +2032,29 @@ dependencies = [ ] [[package]] -name = "gif" -version = "0.13.3" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ - "color_quant", - "weezl", -] - -[[package]] + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2156,11 +2062,11 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -2180,9 +2086,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.30.9" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" +checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" [[package]] name = "glob" @@ -2208,7 +2114,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "cgl", "dispatch2", @@ -2274,7 +2180,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-alloc-types", ] @@ -2284,7 +2190,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2305,7 +2211,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2316,7 +2222,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2346,17 +2252,15 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", "rayon", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heapless" @@ -2398,9 +2302,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2468,9 +2372,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -2482,9 +2386,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2501,6 +2405,12 @@ 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" @@ -2524,9 +2434,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2536,14 +2446,14 @@ dependencies = [ "image-webp", "moxcms", "num-traits", - "png 0.18.0", + "png", "qoi", "ravif", "rayon", "rgb", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", ] [[package]] @@ -2556,12 +2466,6 @@ dependencies = [ "quick-error 2.0.1", ] -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - [[package]] name = "imgref" version = "1.12.0" @@ -2570,12 +2474,12 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "rayon", "serde", "serde_core", @@ -2588,6 +2492,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2598,7 +2505,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -2609,9 +2516,9 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "interprocess" -version = "2.2.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +checksum = "53bf2b0e0785c5394a7392f66d7c4fb9c653633c29b27a932280da3cb344c66a" dependencies = [ "doctest-file", "libc", @@ -2621,21 +2528,22 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.10.5" +name = "is-docker" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ - "either", + "once_cell", ] [[package]] -name = "itertools" -version = "0.12.1" +name = "is-wsl" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" dependencies = [ - "either", + "is-docker", + "once_cell", ] [[package]] @@ -2649,9 +2557,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" @@ -2687,9 +2595,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2712,23 +2620,18 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - [[package]] name = "lazy_static" 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 = "lebe" version = "0.5.3" @@ -2737,15 +2640,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -2753,9 +2656,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -2775,9 +2678,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" @@ -2791,29 +2694,20 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.5.18", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", + "redox_syscall 0.7.1", ] [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -2857,9 +2751,18 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "log-once" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "6d8a05e3879b317b1b6dbf353e5bba7062bedcc59815267bb23eaa0c576cebf0" +dependencies = [ + "log", +] [[package]] name = "loop9" @@ -2894,7 +2797,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -2920,15 +2823,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -2948,7 +2851,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types", "foreign-types", @@ -3002,9 +2905,9 @@ checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -3018,7 +2921,7 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "codespan-reporting", "half", @@ -3031,7 +2934,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] @@ -3047,7 +2950,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -3086,25 +2989,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "no-std-compat" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df270209a7f04d62459240d890ecb792714d5db12c92937823574a09930276b4" - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -3149,9 +3033,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -3161,7 +3045,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -3213,7 +3097,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -3265,7 +3149,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3281,7 +3165,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", "objc2 0.6.3", "objc2-core-foundation", @@ -3295,7 +3179,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3319,7 +3203,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3331,7 +3215,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2 0.6.3", ] @@ -3342,7 +3226,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2 0.6.3", "objc2-core-foundation", @@ -3385,7 +3269,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "dispatch", "libc", @@ -3398,7 +3282,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3409,7 +3293,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3432,7 +3316,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3444,7 +3328,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3467,7 +3351,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -3499,7 +3383,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3521,6 +3405,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3529,10 +3424,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.48" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" dependencies = [ + "libc", "libredox", ] @@ -3561,7 +3457,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ - "ttf-parser 0.25.1", + "ttf-parser", ] [[package]] @@ -3611,10 +3507,8 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "backtrace", "cfg-if", "libc", - "petgraph 0.6.5", "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", @@ -3627,27 +3521,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "pastey" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] -name = "petgraph" -version = "0.6.5" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" version = "0.8.2" source = "git+https://github.com/BloodStainedCrow/petgraph?branch=stable_graph_node_weights_mut_indexed#a2146326d17db154ac872a4d029fc1a4e546cc84" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap", "rayon", @@ -3685,7 +3581,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.108", + "syn", "unicase", ] @@ -3699,12 +3595,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.10" @@ -3722,7 +3612,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -3756,24 +3646,11 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -3790,7 +3667,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -3802,9 +3679,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[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 = "postcard" @@ -3850,46 +3727,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.7", -] - -[[package]] -name = "proc-macro-error" -version = "0.4.12" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "proc-macro-error-attr", "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", + "syn", ] [[package]] -name = "proc-macro-error-attr" -version = "0.4.12" +name = "proc-macro-crate" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "syn-mid", - "version_check", + "toml_edit", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -3903,9 +3758,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3927,21 +3782,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "num-traits", "rand 0.9.2", - "rand_chacha 0.9.0", + "rand_chacha", "rand_xorshift 0.4.0", "regex-syntax", "rusty-fork", @@ -3951,44 +3806,44 @@ dependencies = [ [[package]] name = "puffin" -version = "0.19.2" -source = "git+https://github.com/BloodStainedCrow/puffin#420f296797ecf2acdc7156bf65e2b9a2d8e06c8d" +version = "0.19.1" +source = "git+https://github.com/EmbarkStudios/puffin#c5276b9d5264af37a9c9fb2655990a3a0b720a0b" dependencies = [ "anyhow", "bincode 1.3.3", "byteorder", "cfg-if", - "itertools 0.10.5", + "itertools", "js-sys", "lz4_flex", - "once_cell", "parking_lot 0.12.5", "serde", - "web-time 0.2.4", + "web-time", ] [[package]] name = "puffin_egui" -version = "0.29.1" -source = "git+https://github.com/BloodStainedCrow/puffin#420f296797ecf2acdc7156bf65e2b9a2d8e06c8d" +version = "0.29.0" +source = "git+https://github.com/EmbarkStudios/puffin#c5276b9d5264af37a9c9fb2655990a3a0b720a0b" dependencies = [ "egui", "egui_extras", "indexmap", + "log", + "log-once", "natord", - "once_cell", "parking_lot 0.12.5", "puffin", "time", "vec1", - "web-time 0.2.4", + "web-time", ] [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -4016,28 +3871,19 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quote" -version = "1.0.41" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -4061,7 +3907,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -4082,8 +3928,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -4093,18 +3937,8 @@ 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_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core 0.9.5", ] [[package]] @@ -4114,7 +3948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4122,15 +3956,12 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4150,7 +3981,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", ] [[package]] @@ -4161,19 +4001,21 @@ checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", - "built 0.7.7", + "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools", "libc", "libfuzzer-sys", "log", @@ -4182,23 +4024,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -4271,7 +4111,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -4280,16 +4129,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4299,9 +4148,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4310,9 +4159,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "relative-path" @@ -4326,42 +4175,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - [[package]] name = "rfd" -version = "0.15.4" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" dependencies = [ - "ashpd", "block2 0.6.2", "dispatch2", "js-sys", + "libc", "log", "objc2 0.6.3", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", + "percent-encoding", "pollster", "raw-window-handle", - "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4369,9 +4207,6 @@ name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] [[package]] name = "ron" @@ -4380,7 +4215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "serde_derive", ] @@ -4392,18 +4227,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "serde_derive", "unicode-ident", ] -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rstest" version = "0.25.0" @@ -4430,15 +4259,15 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.108", + "syn", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -4467,7 +4296,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4476,11 +4305,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4505,30 +4334,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser 0.25.1", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -4596,20 +4401,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4631,16 +4436,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", + "syn", ] [[package]] @@ -4662,18 +4458,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[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", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -4686,9 +4483,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291bee647ce7310b0ea721bfd7e0525517b4468eb7c7e15eb8bd774343179702" +checksum = "c7038d0e96661bf9ce647e1a6f6ef6d6f3663f66d9bf741abf14ba4876071c17" dependencies = [ "colored", "log", @@ -4696,32 +4493,23 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[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", ] @@ -4731,9 +4519,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smithay-client-toolkit" @@ -4741,7 +4526,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -4766,15 +4551,15 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", + "bitflags 2.11.0", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", "memmap2", - "rustix 1.1.2", - "thiserror 2.0.17", + "rustix 1.1.3", + "thiserror 2.0.18", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -4807,27 +4592,6 @@ dependencies = [ "serde", ] -[[package]] -name = "snafu" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "spin" version = "0.9.8" @@ -4861,16 +4625,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "stable-vec" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1dff32a2ce087283bec878419027cebd888760d8760b2941ad0843531dc9ec8" -dependencies = [ - "no-std-compat", + "bitflags 2.11.0", ] [[package]] @@ -4890,9 +4645,6 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] [[package]] name = "strum" @@ -4922,7 +4674,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.108", + "syn", ] [[package]] @@ -4934,61 +4686,20 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", -] - -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg", -] - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo", - "siphasher", + "syn", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "syn" -version = "2.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-mid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -4997,20 +4708,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "syn", ] [[package]] @@ -5025,22 +4723,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" -version = "3.23.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -5070,11 +4762,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5085,18 +4777,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -5116,19 +4808,19 @@ dependencies = [ "half", "quick-error 2.0.1", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] name = "tilelib" -version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/tilelib.git#ddacebc889c872eb4146604e0f4cde1caf2c20d5" +version = "0.2.0" +source = "git+https://github.com/BloodStainedCrow/tilelib.git#a79c22ac7dd51dacb7c5e80e4fcd8ed3448a5f05" dependencies = [ "bytemuck", "egui", "egui-wgpu", "image", - "itertools 0.14.0", + "itertools", "log", "pollster", "spin_sleep_util", @@ -5138,9 +4830,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -5148,22 +4840,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5180,7 +4872,6 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png 0.17.16", "tiny-skia-path", ] @@ -5205,90 +4896,41 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime 0.7.3", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5298,38 +4940,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] [[package]] name = "type-map" @@ -5365,45 +4998,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-script" -version = "0.5.7" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -5411,18 +5014,18 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unty" version = "0.0.4" @@ -5431,9 +5034,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[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", @@ -5441,39 +5044,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5482,12 +5052,12 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -5514,12 +5084,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -5559,18 +5123,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -5581,11 +5154,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5594,9 +5168,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5604,26 +5178,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" 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 = "wasm-timer" version = "0.2.5" @@ -5639,15 +5235,27 @@ 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.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.2", + "rustix 1.1.3", "scoped-tls", "smallvec", "wayland-sys", @@ -5655,12 +5263,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.2", + "bitflags 2.11.0", + "rustix 1.1.3", "wayland-backend", "wayland-scanner", ] @@ -5671,29 +5279,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5705,7 +5313,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5714,11 +5322,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5727,11 +5335,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5740,11 +5348,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5753,20 +5361,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -5776,19 +5384,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5806,9 +5404,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.1", "jni", @@ -5822,9 +5420,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" @@ -5833,7 +5431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -5863,7 +5461,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -5877,7 +5475,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-wasm", @@ -5932,7 +5530,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "bytemuck", "cfg-if", @@ -5961,7 +5559,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -5975,11 +5573,11 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "js-sys", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-sys", ] @@ -6110,7 +5708,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -6121,7 +5719,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -6132,7 +5730,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -6143,7 +5741,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -6472,7 +6070,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -6508,7 +6106,7 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time 1.1.0", + "web-time", "windows-sys 0.52.0", "x11-dl", "x11rb", @@ -6517,18 +6115,100 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +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 = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +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 2.11.0", + "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" @@ -6567,7 +6247,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "x11rb-protocol", ] @@ -6589,7 +6269,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dlib", "log", "once_cell", @@ -6609,10 +6289,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] -name = "xmlwriter" -version = "0.1.0" +name = "y4m" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" [[package]] name = "yoke" @@ -6633,15 +6313,15 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", "synstructure", ] [[package]] name = "zbus" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast", "async-executor", @@ -6657,8 +6337,9 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix 1.1.3", "serde", "serde_repr", "tracing", @@ -6673,9 +6354,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", "zvariant", @@ -6683,13 +6364,13 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", "zbus-lockstep", "zbus_xml", "zvariant", @@ -6697,14 +6378,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn", "zbus_names", "zvariant", "zvariant_utils", @@ -6712,47 +6393,45 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow", "zvariant", ] [[package]] name = "zbus_xml" -version = "5.0.2" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" dependencies = [ - "quick-xml 0.36.2", + "quick-xml", "serde", - "static_assertions", "zbus_names", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] @@ -6772,7 +6451,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", "synstructure", ] @@ -6806,14 +6485,20 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn", ] [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zune-core" @@ -6821,6 +6506,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -6836,19 +6527,27 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", ] [[package]] name = "zvariant" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", "serde", - "url", "winnow", "zvariant_derive", "zvariant_utils", @@ -6856,26 +6555,26 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.108", + "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 9b0d06b..8507b2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,8 @@ # cargo-features = ["codegen-backend"] [package] name = "factory" -version = "0.2.0" +version = "0.3.0" edition = "2024" -rust-version = "1.85" build = "build.rs" @@ -15,65 +14,69 @@ image = { version = "0.25.5", optional = true } log = "0.4.25" simple_logger = {version = "5.0.0", optional = true } rayon = "1.10.0" -serde = { version = "1.0.217", features = ["derive"], default-features = false } +serde = { version = "1.0.217", features = ["derive", "rc"] } directories = "6.0.0" ron = "0.8.1" take_mut = "0.2.2" static_assertions = "1.1.0" itertools = "0.14.0" -genawaiter = "0.99.1" petgraph = { version = "0.8.2", features = ["rayon", "serde", "serde-1", "serde_derive"] } sha2 = "0.10.8" hex = "0.4.3" postcard = { version = "1.1.1", features = ["use-std"] } -charts-rs = { version = "0.3.20", features = ["resvg"] } strum = { version = "0.27.1", features = ["derive"] } +# explicitly disable atomic feature, so that bitvecs do not use atomic instructions. very important for performance! bitvec = { version = "1.0.1", features = ["alloc", "serde", "std"], default-features = false } bimap = { version = "0.6.3", features = ["serde", "std"], default-features = false } -eframe = { version = "0.31.1", features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "wgpu"], optional = true, default-features = false } +eframe = { version = "0.32", features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "wgpu"], optional = true, default-features = false } wgpu = { version = "25.0.2", features = ["webgl"] } -egui_extras = { version = "0.31.1", optional = true } -egui_plot = { version = "0.32.1", optional = true } -rand = "0.8.5" +egui_extras = { version = "0.32", optional = true } +egui_plot = { version = "0.33", optional = true } +rand = "0.9.0" bitcode = { version = "0.6.6", features = ["serde"] } -egui = { version = "0.31.1", features = ["bytemuck", "serde"], optional = true } +egui = { version = "0.32", features = ["bytemuck", "serde"], optional = true } flate2 = { version = "1.1.1", features = ["zlib-rs"] } -rstest = "0.25.0" -parking_lot = { version = "0.12.3", features = ["serde", "deadlock_detection"] } +parking_lot = { version = "0.12.3", features = ["serde"] } profiling = { version = "1.0.16" } -puffin_egui = { version = "0.29", optional = true } -puffin = { version = "0.19", features = ["web"] } -dhat = "0.3.3" +puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } +puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } +dhat = {version = "0.3.3", optional = true } noise = { version = "0.9.0", features = ["std"] } -rfd = { version = "0.15.3", optional = true } -egui_graphs = { version = "0.25.1", optional = true } +rfd = { version = "0.17", optional = true } +egui_graphs = { version = "0.28", optional = true } serde_path_to_error = "0.1.17" get-size2 = { version = "0.7.1", features = ["derive"], optional = true } egui-show-info = { git = "https://github.com/BloodStainedCrow/egui-show-info", features = ["petgraph", "parking_lot", "enum-map", "bimap"], optional = true } egui-show-info-derive = { git = "https://github.com/BloodStainedCrow/egui-show-info", optional = true } bytemuck = "1.23.1" -# ph = "0.9.6" memoffset = "0.9.1" -smallvec = { version = "1.15.1", features = ["serde"] } -ecolor = { version = "0.31.1", features = ["color-hex"] } +ecolor = { version = "0.32", features = ["color-hex"] } getrandom = { version = "0.3.3", features = ["wasm_js"] } -getrandom_old = { version = "0.2.16", features = ["js"], package = "getrandom" } wasm-bindgen = "0.2.104" wasm-bindgen-futures = "0.4.54" wasm-timer = "0.2.5" bincode = { version = "2.0.1", features = ["serde"] } thin-dst = "1.1.0" -stable-vec = "0.4.1" recycle_vec = "1.1.2" -fork = "0.3.0" -libc = { version = "0.2.177", default-features = false } -interprocess = "2.2.3" -fixed-buffer = "1.0.2" base64 = "0.22.1" mimalloc = { version = "0.1.48", features = ["v3"] } rustc-hash = "2.1.1" +chrono = { version = "0.4.42", features = ["serde"] } +rand_xoshiro = "0.7.0" +url = "2.5.7" +args = "2.2.0" +console_error_panic_hook = "0.1.7" +fixedbitset = "0.5.7" +convert_case = "0.11.0" + +# These are all the dependencies which do not work on wasm +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +libc = { version = "0.2.177", default-features = false } +interprocess = { version = "2.2.3" } +fork = "0.3.0" +open = "5.3.3" [build-dependencies] built = {version = "0.8", features= ["git2", "chrono"]} @@ -81,14 +84,11 @@ built = {version = "0.8", features= ["git2", "chrono"]} [dev-dependencies] winit = "0.30.12" proptest = "1.4.0" +rstest = "0.25.0" [patch.crates-io] -puffin_egui = { git = "https://github.com/BloodStainedCrow/puffin" } -puffin = { git = "https://github.com/BloodStainedCrow/puffin" } -egui = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -eframe = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -ecolor = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -egui-wgpu = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } +puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } +puffin = { git = "https://github.com/EmbarkStudios/puffin" } egui_graphs = { git = "https://github.com/BloodStainedCrow/egui_graphs", branch = "tree_layout" } petgraph = { git = "https://github.com/BloodStainedCrow/petgraph", branch = "stable_graph_node_weights_mut_indexed" } @@ -106,7 +106,6 @@ match_same_arms = { level = "deny", priority = -1 } redundant_closure_for_method_calls = { level = "allow", priority = 1 } suboptimal_flops = { level = "allow", priority = 1 } module_name_repetitions = { level = "allow", priority = 1 } -# lto = true [profile.release-with-debug] inherits = "release" @@ -130,11 +129,14 @@ incremental = true # codegen-units = 1 [features] -default = ["profiler", "graphics", "client", "logging"] -# Use Krastorio2 graphics. Since I have not properly added licensing information, I currently do not push them, therefore this feature is broken +default = ["profiler", "graphics", "client", "logging", "replay"] +# Use Krastorio2 graphics. graphics = [] # dhat-rs memory profiling (https://docs.rs/dhat/latest/dhat/) -dhat-heap = [] +dhat-heap = [ "dhat" ] profiler = ["profiling/profile-with-puffin"] client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:puffin_egui", "dep:egui_graphs", "dep:egui-show-info", "dep:egui-show-info-derive", "dep:tilelib", "dep:image", "dep:rfd", "dep:get-size2"] logging = ["simple_logger"] +debug-stat-gathering = [] +assembler-craft-tracking = [] +replay = [] diff --git a/README.md b/README.md index c19a723..46ce074 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,37 @@ # What is this? -This project is an academic recreation of the factory game [Factorio](https://www.factorio.com/) taking additional ideas from [Dyson Sphere Program](https://store.steampowered.com/app/1366540/Dyson_Sphere_Program/). +This project is an academic recreation of the factory game [Factorio](https://www.factorio.com/). -I created it as an exercise to see how far I could optimize the basic concepts and algorithms of the genre in terms of performance, while allowing myself minor changes to the games' rules. +I created it as an exercise to see how far I could optimize the basic mechanics and algorithms of the genre in terms of performance. Another goal that emerged along the way, was learning about the way modern CPUs actually work. -# Roadmap -Currently I adding beacons and thinking about how to efficiently add logistics bots. Then I want to build a comprehensive suit of benchmark test to show if/by how much I was able to improve performance. - # Why did you start? -I was playing the above games and started being unable to expand due to performance issues. So in my hubris I declared: "How hard can it be?". - -## TODOS -- ~~Place Power Production~~ -- ~~Blueprints so I can actually do perf tests~~ -- ~~Permanently running replay system, so I can easily recreate crashes~~ -- ~~Test harness for replays, to ensure they do not crash~~ -- ~~Automatic insertion limit~~ -- ~~Assembler Module Support~~ -- ~~World listener support (i.e. update whenever something changes in the world, for power, beacons and inserters)~~ -- Lazy Terrain Generation -- ~~Assembler Module Frontend~~ -- ~~Assembler Power Consumption Modifier Support~~ -- ~~Beacons~~ -- ~~FIX Beacon Flicker due to lowering power consumption when beacons are unpowered~~ -- ~~Storage Storage Inserters~~ -- ~~Science Consumption in Labs~~ -- ~~Inserter connections to labs~~ -- ~~Debug inserters~~ -- ~~Production Graphs~~ -- ~~Liquids~~ -- ~~Map View~~ -- ~~Technology~~ -- Mining Drills -- ~~Underground belts~~ -- Fix Underground Pipe connection breaking/overlap -- Place Steam Turbines -- ~~Splitters~~ -- Allow Belts of different types to connect to one another -- Decide if I want beacons to match factorio behaviour or keep the hard switch on/off -- ~~Ore Generation~~ -- Add tile requirements for buildings/recipes (for offshore pump) -- Bots -- MAYBE: A canonical version of the simulation that can be used for diff testing (and as some weird documentation of the mechanics I suppose) \ No newline at end of file +I was playing Factorio and started being unable to expand due to performance issues. So in my hubris I declared: "How hard can it be?". +I hope that the next person with this thought can find this project, and build on the ideas I have had and continue to push the performance. + +# Current State +Most logic for the basic building blocks of factories are working. This allowed me to recreate a Factorio base, giving me a point for performance comparison. +I was able to run a base comprised of 60 copies of [this](https://factoriobox.1au.us/map/view/2824bc1566bd95b5825baf3bd2eb8fa32de8397526464f5a0327bcb82d64ebf8/#1/nauvis/15/2942/1158/0/447) Factorio Megabase by Smurphy (a single of which Factorio runs at ~40 UPS) at 60 UPS on my machine (Intel 12400F CPU with DDR4 3600 MT/s RAM). + +### Megabase (40k SPM) with Solar +![A 40k SPM megabase with its solar array](mega.png) +### Gigabase (60x Megabase, 2.4M SPM) +![A 2.4M SPM Gigabase comprised of 60 megabases running at 60UPS](giga.png) + +### Machines Producing Stuff +![Furnaces smelting steel](steel.png) +![Production of advanced circuits using direct insertion and belts](red_chips.png) + +# Running it +If you only want to try it out, a web based build is available on [my website](https://aschhoff.de/projects). The performance of running in the browser is not amazing (mainly due to the browser being limited to a single thread), but it should still be able to run a 40k SPM Megabase without issue. +WASM being limited to 4GB of RAM also limits the size of the factories. + +For the best experience, I advise using the native build. +It should run on Linux, Windows, MacOS as WASM (Though only Linux and WASM are tested regularly). Assuming you have [rust and cargo](https://rust-lang.org), just `cargo run --release`. On NixOS the included `flake.nix` contains the program packaged for nix, or a devshell with my VSCodium based development environment. + +# How it works +I am currently working on a writeup, explaining how I designed this project and what decisions lead me to this level of performance, and which decisions I regret and might change in the future. +This writeup will be available [here](https://aschhoff.de/blog/factory-game) as soon as it is done. + +# Attributions +I am not affiliated with Wube and Factorio in any way. I am just a fan, being nerdsniped by their excellent game. +All graphics used with the `graphics` feature are from the Factorio Mod [Krastorio 2 Assets](https://codeberg.org/raiguard/Krastorio2Assets). \ No newline at end of file diff --git a/codium.nix b/codium.nix deleted file mode 100644 index 99b1a4d..0000000 --- a/codium.nix +++ /dev/null @@ -1,28 +0,0 @@ -let - nix-vscode-extensions.url = "github:nix-community/nix-vscode-extensions"; - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/1750f3c1c89488e2ffdd47cab9d05454dddfb734.tar.gz")) { }; - addr2linePkg = pkgs.callPackage ./addr2line-rs/default.nix {}; -in -pkgs.mkShell { - buildInputs = [ - ] ++ (with pkgs; [ - bacon - - (vscode-with-extensions.override { - vscode = vscodium; - vscodeExtensions = with vscode-extensions; [ - rust-lang.rust-analyzer - vadimcn.vscode-lldb - gruntfuggly.todo-tree - a5huynh.vscode-ron - ]; - }) - - addr2linePkg - ]); - RUST_BACKTRACE = 1; - - shellHook = '' - export PATH=${addr2linePkg}/bin:$PATH - ''; -} \ No newline at end of file diff --git a/crash_replays/001.rep.ron b/crash_replays/001.rep.ron new file mode 100644 index 0000000..eb24d74 --- /dev/null +++ b/crash_replays/001.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:77,action:Position(player:"0",pos:(1600.0,1599.5834))),(timestamp:78,action:Position(player:"0",pos:(1600.0,1599.1667))),(timestamp:79,action:Position(player:"0",pos:(1600.0,1598.7501))),(timestamp:80,action:Position(player:"0",pos:(1600.0,1598.3335))),(timestamp:81,action:Position(player:"0",pos:(1600.0,1597.9169))),(timestamp:82,action:Position(player:"0",pos:(1600.0,1597.5002))),(timestamp:83,action:Position(player:"0",pos:(1600.0,1597.0836))),(timestamp:84,action:Position(player:"0",pos:(1600.0,1596.667))),(timestamp:85,action:Position(player:"0",pos:(1600.0,1596.2504))),(timestamp:86,action:Position(player:"0",pos:(1600.0,1595.8337))),(timestamp:87,action:Position(player:"0",pos:(1600.0,1595.4171))),(timestamp:88,action:Position(player:"0",pos:(1600.0,1595.0005))),(timestamp:89,action:Position(player:"0",pos:(1600.0,1594.5839))),(timestamp:90,action:Position(player:"0",pos:(1600.0,1594.1672))),(timestamp:91,action:Position(player:"0",pos:(1600.4166,1593.7506))),(timestamp:92,action:Position(player:"0",pos:(1600.8333,1593.334))),(timestamp:93,action:Position(player:"0",pos:(1601.2499,1592.9174))),(timestamp:94,action:Position(player:"0",pos:(1601.6665,1592.5007))),(timestamp:95,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:96,action:Position(player:"0",pos:(1602.4998,1591.6675))),(timestamp:97,action:Position(player:"0",pos:(1602.9164,1591.2509))),(timestamp:98,action:Position(player:"0",pos:(1603.333,1590.8342))),(timestamp:99,action:Position(player:"0",pos:(1603.7496,1590.4176))),(timestamp:100,action:Position(player:"0",pos:(1604.1663,1590.001))),(timestamp:101,action:Position(player:"0",pos:(1604.5829,1590.001))),(timestamp:102,action:Position(player:"0",pos:(1604.9995,1590.001))),(timestamp:109,action:Position(player:"0",pos:(1604.9995,1590.4176))),(timestamp:110,action:Position(player:"0",pos:(1604.9995,1590.8342))),(timestamp:111,action:Position(player:"0",pos:(1604.9995,1591.2509))),(timestamp:112,action:Position(player:"0",pos:(1604.5829,1591.6675))),(timestamp:113,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:114,action:Position(player:"0",pos:(1603.7496,1592.5007))),(timestamp:115,action:Position(player:"0",pos:(1603.333,1592.9174))),(timestamp:116,action:Position(player:"0",pos:(1602.9164,1593.334))),(timestamp:117,action:Position(player:"0",pos:(1602.4998,1593.7506))),(timestamp:118,action:Position(player:"0",pos:(1602.0831,1594.1672))),(timestamp:119,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:120,action:Position(player:"0",pos:(1601.2499,1595.0005))),(timestamp:121,action:Position(player:"0",pos:(1600.8333,1595.4171))),(timestamp:122,action:Position(player:"0",pos:(1600.4166,1595.8337))),(timestamp:123,action:Position(player:"0",pos:(1600.0,1596.2504))),(timestamp:124,action:Position(player:"0",pos:(1599.5834,1596.667))),(timestamp:125,action:Position(player:"0",pos:(1599.1667,1597.0836))),(timestamp:126,action:Position(player:"0",pos:(1598.7501,1597.5002))),(timestamp:127,action:Position(player:"0",pos:(1598.3335,1597.9169))),(timestamp:128,action:Position(player:"0",pos:(1597.9169,1598.3335))),(timestamp:129,action:Position(player:"0",pos:(1597.5002,1598.7501))),(timestamp:130,action:Position(player:"0",pos:(1597.0836,1598.7501))),(timestamp:131,action:Position(player:"0",pos:(1596.667,1598.7501))),(timestamp:132,action:Position(player:"0",pos:(1596.2504,1598.7501))),(timestamp:133,action:Position(player:"0",pos:(1595.8337,1598.7501))),(timestamp:134,action:Position(player:"0",pos:(1595.4171,1598.7501))),(timestamp:135,action:Position(player:"0",pos:(1595.0005,1598.7501))),(timestamp:164,action:Position(player:"0",pos:(1595.0005,1598.3335))),(timestamp:165,action:Position(player:"0",pos:(1595.0005,1597.9169))),(timestamp:166,action:Position(player:"0",pos:(1595.0005,1597.5002))),(timestamp:167,action:Position(player:"0",pos:(1595.0005,1597.0836))),(timestamp:168,action:Position(player:"0",pos:(1595.0005,1596.667))),(timestamp:169,action:Position(player:"0",pos:(1595.0005,1596.2504))),(timestamp:170,action:Position(player:"0",pos:(1595.0005,1595.8337))),(timestamp:171,action:Position(player:"0",pos:(1595.0005,1595.4171))),(timestamp:172,action:Position(player:"0",pos:(1595.4171,1595.0005))),(timestamp:173,action:Position(player:"0",pos:(1595.8337,1594.5839))),(timestamp:174,action:Position(player:"0",pos:(1596.2504,1594.1672))),(timestamp:175,action:Position(player:"0",pos:(1596.667,1593.7506))),(timestamp:176,action:Position(player:"0",pos:(1597.0836,1593.334))),(timestamp:177,action:Position(player:"0",pos:(1597.5002,1592.9174))),(timestamp:178,action:Position(player:"0",pos:(1597.9169,1592.5007))),(timestamp:179,action:Position(player:"0",pos:(1598.3335,1592.0841))),(timestamp:180,action:Position(player:"0",pos:(1598.7501,1591.6675))),(timestamp:181,action:Position(player:"0",pos:(1599.1667,1591.2509))),(timestamp:182,action:Position(player:"0",pos:(1599.5834,1590.8342))),(timestamp:183,action:Position(player:"0",pos:(1600.0,1590.4176))),(timestamp:184,action:Position(player:"0",pos:(1600.4166,1590.001))),(timestamp:185,action:Position(player:"0",pos:(1600.8333,1589.5844))),(timestamp:186,action:Position(player:"0",pos:(1601.2499,1589.1677))),(timestamp:187,action:Position(player:"0",pos:(1601.6665,1588.7511))),(timestamp:188,action:Position(player:"0",pos:(1602.0831,1588.3345))),(timestamp:189,action:Position(player:"0",pos:(1602.4998,1587.9178))),(timestamp:190,action:Position(player:"0",pos:(1602.9164,1587.5012))),(timestamp:191,action:Position(player:"0",pos:(1603.333,1587.0846))),(timestamp:192,action:Position(player:"0",pos:(1603.7496,1586.668))),(timestamp:193,action:Position(player:"0",pos:(1604.1663,1586.2513))),(timestamp:194,action:Position(player:"0",pos:(1604.5829,1585.8347))),(timestamp:195,action:Position(player:"0",pos:(1604.9995,1585.4181))),(timestamp:196,action:Position(player:"0",pos:(1605.4161,1585.0015))),(timestamp:197,action:Position(player:"0",pos:(1605.8328,1584.5848))),(timestamp:198,action:Position(player:"0",pos:(1606.2494,1584.1682))),(timestamp:199,action:Position(player:"0",pos:(1606.666,1583.7516))),(timestamp:200,action:Position(player:"0",pos:(1607.0826,1583.335))),(timestamp:201,action:Position(player:"0",pos:(1607.4993,1582.9183))),(timestamp:202,action:Position(player:"0",pos:(1607.9159,1582.5017))),(timestamp:203,action:Position(player:"0",pos:(1608.3325,1582.0851))),(timestamp:204,action:Position(player:"0",pos:(1608.7491,1581.6685))),(timestamp:205,action:Position(player:"0",pos:(1609.1658,1581.2518))),(timestamp:206,action:Position(player:"0",pos:(1609.1658,1580.8352))),(timestamp:207,action:Position(player:"0",pos:(1608.7491,1580.8352))),(timestamp:208,action:Position(player:"0",pos:(1608.3325,1580.8352))),(timestamp:209,action:Position(player:"0",pos:(1607.9159,1580.8352))),(timestamp:210,action:Position(player:"0",pos:(1607.4993,1580.8352))),(timestamp:211,action:Position(player:"0",pos:(1607.0826,1580.8352))),(timestamp:212,action:Position(player:"0",pos:(1606.666,1580.8352))),(timestamp:213,action:Position(player:"0",pos:(1606.2494,1580.8352))),(timestamp:214,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1578),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:214,action:Position(player:"0",pos:(1605.8328,1580.8352))),(timestamp:215,action:Position(player:"0",pos:(1605.4161,1580.8352))),(timestamp:216,action:Position(player:"0",pos:(1604.9995,1580.8352))),(timestamp:217,action:Position(player:"0",pos:(1604.5829,1580.8352))),(timestamp:218,action:Position(player:"0",pos:(1604.1663,1580.8352))),(timestamp:219,action:Position(player:"0",pos:(1603.7496,1580.8352))),(timestamp:220,action:Position(player:"0",pos:(1603.333,1580.8352))),(timestamp:221,action:Position(player:"0",pos:(1602.9164,1580.8352))),(timestamp:222,action:Position(player:"0",pos:(1602.4998,1580.8352))),(timestamp:223,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1581),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:223,action:Position(player:"0",pos:(1602.0831,1580.8352))),(timestamp:224,action:Position(player:"0",pos:(1601.6665,1580.8352))),(timestamp:225,action:Position(player:"0",pos:(1601.2499,1580.8352))),(timestamp:226,action:Position(player:"0",pos:(1600.8333,1580.8352))),(timestamp:227,action:Position(player:"0",pos:(1600.4166,1580.8352))),(timestamp:228,action:Position(player:"0",pos:(1600.0,1580.8352))),(timestamp:229,action:Position(player:"0",pos:(1599.5834,1580.8352))),(timestamp:230,action:Position(player:"0",pos:(1599.1667,1580.8352))),(timestamp:231,action:Position(player:"0",pos:(1598.7501,1580.8352))),(timestamp:232,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1582),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:232,action:Position(player:"0",pos:(1598.3335,1580.8352))),(timestamp:240,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1580),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:251,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1577),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:396,action:SetRecipe(pos:(x:1598,y:1581),recipe:"factory_game::iron_ore_generation")),(timestamp:445,action:Position(player:"0",pos:(1598.7501,1580.8352))),(timestamp:446,action:Position(player:"0",pos:(1599.1667,1580.8352))),(timestamp:447,action:Position(player:"0",pos:(1599.5834,1580.8352))),(timestamp:448,action:Position(player:"0",pos:(1600.0,1580.8352))),(timestamp:449,action:Position(player:"0",pos:(1600.4166,1580.8352))),(timestamp:450,action:Position(player:"0",pos:(1600.8333,1580.8352))),(timestamp:451,action:Position(player:"0",pos:(1601.2499,1580.8352))),(timestamp:452,action:Position(player:"0",pos:(1601.6665,1580.8352))),(timestamp:453,action:Position(player:"0",pos:(1602.0831,1580.8352))),(timestamp:454,action:Position(player:"0",pos:(1602.4998,1580.8352))),(timestamp:455,action:Position(player:"0",pos:(1602.9164,1580.8352))),(timestamp:456,action:Position(player:"0",pos:(1603.333,1580.8352))),(timestamp:576,action:SetRecipe(pos:(x:1603,y:1578),recipe:"factory_game::iron_ore_generation")),(timestamp:625,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1582),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:667,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1582),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:697,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1583),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:788,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1584),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:901,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1580),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1039,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1579),ty:"factory_game::inserter",rotation:West,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1110,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1120,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1130,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1138,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt())))],current_timestep:1139,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/002.rep.ron b/crash_replays/002.rep.ron new file mode 100644 index 0000000..702e5e3 --- /dev/null +++ b/crash_replays/002.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:36,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:37,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:38,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:39,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:40,action:Position(player:"0",pos:(1597.9169,1600.0))),(timestamp:156,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:164,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:174,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:183,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:192,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:200,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:210,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:219,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:228,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:240,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:326,action:Remove(pos:(x:1597,y:1597))),(timestamp:396,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:487,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:533,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1598),ty:"factory_game::inserter",rotation:South,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:574,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1599),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:780,action:SetRecipe(pos:(x:1600,y:1599),recipe:"factory_game::iron_ore_generation")),(timestamp:844,action:PlaceEntity(force:false,info:(pos:(x:1594,y:1599),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:938,action:SetRecipe(pos:(x:1594,y:1599),recipe:"factory_game::iron_ore_generation")),(timestamp:1000,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1600),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1230,action:Remove(pos:(x:1595,y:1598))),(timestamp:1407,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1430,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:1431,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:1432,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:1433,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:1434,action:Position(player:"0",pos:(1600.0,1600.0))),(timestamp:1435,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:1436,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:1437,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:1438,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:1532,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1602),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:1572,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:1573,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:1574,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:1575,action:Position(player:"0",pos:(1600.0,1600.0))),(timestamp:1576,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:1577,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:1578,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:1579,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:1580,action:Position(player:"0",pos:(1597.9169,1600.0))),(timestamp:1888,action:Remove(pos:(x:1600,y:1598))),(timestamp:1968,action:Remove(pos:(x:1595,y:1598))),(timestamp:2026,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:2067,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1598),ty:"factory_game::inserter",rotation:South,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:2209,action:Remove(pos:(x:1598,y:1602))),(timestamp:2339,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1602),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:2379,action:Position(player:"0",pos:(1597.9169,1600.4166))),(timestamp:2380,action:Position(player:"0",pos:(1597.9169,1600.8333))),(timestamp:2381,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2382,action:Position(player:"0",pos:(1597.9169,1601.6665))),(timestamp:2383,action:Position(player:"0",pos:(1597.9169,1602.0831))),(timestamp:2384,action:Position(player:"0",pos:(1597.9169,1602.4998))),(timestamp:2385,action:Position(player:"0",pos:(1597.9169,1602.9164))),(timestamp:2386,action:Position(player:"0",pos:(1597.9169,1603.333))),(timestamp:2387,action:Position(player:"0",pos:(1597.9169,1603.7496))),(timestamp:2388,action:Position(player:"0",pos:(1597.9169,1604.1663))),(timestamp:2389,action:Position(player:"0",pos:(1597.9169,1604.5829))),(timestamp:2390,action:Position(player:"0",pos:(1597.9169,1604.9995))),(timestamp:2391,action:Position(player:"0",pos:(1597.9169,1605.4161))),(timestamp:2392,action:Position(player:"0",pos:(1597.9169,1605.8328))),(timestamp:2393,action:Position(player:"0",pos:(1597.5002,1606.2494))),(timestamp:2394,action:Position(player:"0",pos:(1597.0836,1606.666))),(timestamp:2395,action:Position(player:"0",pos:(1597.0836,1607.0826))),(timestamp:2396,action:Position(player:"0",pos:(1597.0836,1607.4993))),(timestamp:2397,action:Position(player:"0",pos:(1597.0836,1607.9159))),(timestamp:2398,action:Position(player:"0",pos:(1597.0836,1608.3325))),(timestamp:2399,action:Position(player:"0",pos:(1597.0836,1608.7491))),(timestamp:2400,action:Position(player:"0",pos:(1597.0836,1609.1658))),(timestamp:2401,action:Position(player:"0",pos:(1597.0836,1609.5824))),(timestamp:2402,action:Position(player:"0",pos:(1597.0836,1609.999))),(timestamp:2403,action:Position(player:"0",pos:(1597.0836,1610.4156))),(timestamp:2404,action:Position(player:"0",pos:(1597.0836,1610.8323))),(timestamp:2406,action:Position(player:"0",pos:(1597.0836,1611.2489))),(timestamp:2407,action:Position(player:"0",pos:(1597.0836,1611.6655))),(timestamp:2423,action:PlaceEntity(force:false,info:(pos:(x:1591,y:1617),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2434,action:PlaceEntity(force:false,info:(pos:(x:1592,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2444,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1622),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2453,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2462,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1619),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2471,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1618),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2481,action:PlaceEntity(force:false,info:(pos:(x:1593,y:1619),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2490,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2499,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1620),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2506,action:Position(player:"0",pos:(1597.0836,1611.2489))),(timestamp:2507,action:Position(player:"0",pos:(1597.0836,1610.8323))),(timestamp:2508,action:Position(player:"0",pos:(1597.0836,1610.4156))),(timestamp:2509,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1617),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2509,action:Position(player:"0",pos:(1597.0836,1609.999))),(timestamp:2510,action:Position(player:"0",pos:(1597.0836,1609.5824))),(timestamp:2511,action:Position(player:"0",pos:(1597.0836,1609.1658))),(timestamp:2512,action:Position(player:"0",pos:(1597.0836,1608.7491))),(timestamp:2513,action:Position(player:"0",pos:(1597.0836,1608.3325))),(timestamp:2519,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1616),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2530,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1616),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2539,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1614),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2549,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1611),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2559,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1610),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2577,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1606),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole())))],current_timestep:2578,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/003.rep.ron b/crash_replays/003.rep.ron new file mode 100644 index 0000000..cc3c476 --- /dev/null +++ b/crash_replays/003.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:139,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:152,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:153,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:154,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:155,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:156,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:157,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:158,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:159,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:183,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:184,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:185,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:232,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:279,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:337,action:PlaceEntity(force:false,info:(pos:(x:1612,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:367,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:368,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:369,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:370,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:371,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:372,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:373,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:474,action:Remove(pos:(x:1607,y:1598))),(timestamp:560,action:Remove(pos:(x:1602,y:1598))),(timestamp:819,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1600),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:839,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:840,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:841,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:842,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:843,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:844,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:845,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:846,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:847,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:848,action:Position(player:"0",pos:(1605.8328,1600.0))),(timestamp:871,action:PlaceEntity(force:false,info:(pos:(x:1612,y:1600),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:994,action:SetRecipe(pos:(x:1612,y:1600),recipe:"factory_game::iron_ore_generation")),(timestamp:1105,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1178,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1293,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:1294,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:1295,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:1296,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:1297,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:1298,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:1354,action:Remove(pos:(x:1602,y:1598))),(timestamp:1429,action:Remove(pos:(x:1607,y:1598))),(timestamp:1480,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:1481,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:1482,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:1483,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:1484,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:1485,action:Position(player:"0",pos:(1605.8328,1600.0))),(timestamp:1486,action:Position(player:"0",pos:(1606.2494,1600.0))),(timestamp:1487,action:Position(player:"0",pos:(1606.666,1600.0))),(timestamp:1502,action:Position(player:"0",pos:(1606.666,1599.5834))),(timestamp:1503,action:Position(player:"0",pos:(1606.666,1599.1667))),(timestamp:1504,action:Position(player:"0",pos:(1606.666,1598.7501))),(timestamp:1505,action:Position(player:"0",pos:(1606.666,1598.3335))),(timestamp:1506,action:Position(player:"0",pos:(1606.666,1597.9169))),(timestamp:1507,action:Position(player:"0",pos:(1606.666,1597.5002))),(timestamp:1508,action:Position(player:"0",pos:(1606.666,1597.0836))),(timestamp:1509,action:Position(player:"0",pos:(1606.666,1596.667))),(timestamp:1510,action:Position(player:"0",pos:(1606.666,1596.2504))),(timestamp:1511,action:Position(player:"0",pos:(1606.666,1595.8337))),(timestamp:1512,action:Position(player:"0",pos:(1606.666,1595.4171))),(timestamp:1513,action:Position(player:"0",pos:(1606.666,1595.0005))),(timestamp:1607,action:Position(player:"0",pos:(1606.666,1594.5839))),(timestamp:1608,action:Position(player:"0",pos:(1606.666,1594.1672))),(timestamp:1609,action:Position(player:"0",pos:(1606.666,1593.7506))),(timestamp:1610,action:Position(player:"0",pos:(1606.666,1593.334))),(timestamp:1611,action:Position(player:"0",pos:(1606.666,1592.9174))),(timestamp:1612,action:Position(player:"0",pos:(1606.666,1592.5007))),(timestamp:1613,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:1614,action:Position(player:"0",pos:(1606.666,1591.6675))),(timestamp:1681,action:Position(player:"0",pos:(1606.2494,1591.6675))),(timestamp:1682,action:Position(player:"0",pos:(1605.8328,1591.6675))),(timestamp:1683,action:Position(player:"0",pos:(1605.4161,1591.6675))),(timestamp:1684,action:Position(player:"0",pos:(1604.9995,1591.6675))),(timestamp:1685,action:Position(player:"0",pos:(1604.5829,1591.6675))),(timestamp:1686,action:Position(player:"0",pos:(1604.1663,1591.6675))),(timestamp:1687,action:Position(player:"0",pos:(1603.7496,1591.6675))),(timestamp:1688,action:Position(player:"0",pos:(1603.333,1591.6675))),(timestamp:1689,action:Position(player:"0",pos:(1602.9164,1591.6675))),(timestamp:1690,action:Position(player:"0",pos:(1602.4998,1591.6675))),(timestamp:1691,action:Position(player:"0",pos:(1602.0831,1591.6675))),(timestamp:1692,action:Position(player:"0",pos:(1601.6665,1591.6675))),(timestamp:1871,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1921,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1938,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:1939,action:Position(player:"0",pos:(1601.6665,1592.5007))),(timestamp:1940,action:Position(player:"0",pos:(1601.6665,1592.9174))),(timestamp:1941,action:Position(player:"0",pos:(1601.6665,1593.334))),(timestamp:1942,action:Position(player:"0",pos:(1601.6665,1593.7506))),(timestamp:1943,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:1944,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:1945,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:1946,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:1978,action:Position(player:"0",pos:(1602.0831,1595.4171))),(timestamp:1979,action:Position(player:"0",pos:(1602.4998,1595.4171))),(timestamp:1980,action:Position(player:"0",pos:(1602.9164,1595.4171))),(timestamp:1981,action:Position(player:"0",pos:(1603.333,1595.4171))),(timestamp:1982,action:Position(player:"0",pos:(1603.7496,1595.4171))),(timestamp:1983,action:Position(player:"0",pos:(1604.1663,1595.4171))),(timestamp:1984,action:Position(player:"0",pos:(1604.5829,1595.4171))),(timestamp:1985,action:Position(player:"0",pos:(1604.9995,1595.4171))),(timestamp:1986,action:Position(player:"0",pos:(1605.4161,1595.4171))),(timestamp:1987,action:Position(player:"0",pos:(1605.8328,1595.4171))),(timestamp:1988,action:Position(player:"0",pos:(1606.2494,1595.4171))),(timestamp:1989,action:Position(player:"0",pos:(1606.666,1595.4171))),(timestamp:1990,action:Position(player:"0",pos:(1607.0826,1595.4171))),(timestamp:1991,action:Position(player:"0",pos:(1607.4993,1595.4171))),(timestamp:1992,action:Position(player:"0",pos:(1607.9159,1595.4171))),(timestamp:1993,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:1994,action:Position(player:"0",pos:(1608.7491,1595.4171))),(timestamp:1995,action:Position(player:"0",pos:(1609.1658,1595.4171))),(timestamp:1996,action:Position(player:"0",pos:(1609.5824,1595.4171))),(timestamp:1997,action:Position(player:"0",pos:(1609.999,1595.4171))),(timestamp:1998,action:Position(player:"0",pos:(1610.4156,1595.4171))),(timestamp:1999,action:Position(player:"0",pos:(1610.8323,1595.4171))),(timestamp:2000,action:Position(player:"0",pos:(1611.2489,1595.4171))),(timestamp:2001,action:Position(player:"0",pos:(1611.6655,1595.4171))),(timestamp:2002,action:Position(player:"0",pos:(1612.0822,1595.4171))),(timestamp:2003,action:Position(player:"0",pos:(1612.4988,1595.4171))),(timestamp:2004,action:Position(player:"0",pos:(1612.9154,1595.4171))),(timestamp:2005,action:Position(player:"0",pos:(1613.332,1595.4171))),(timestamp:2006,action:Position(player:"0",pos:(1613.7487,1595.4171))),(timestamp:2070,action:Position(player:"0",pos:(1613.7487,1595.0005))),(timestamp:2071,action:Position(player:"0",pos:(1613.7487,1594.5839))),(timestamp:2072,action:Position(player:"0",pos:(1613.332,1594.1672))),(timestamp:2073,action:Position(player:"0",pos:(1612.9154,1593.7506))),(timestamp:2074,action:Position(player:"0",pos:(1612.4988,1593.334))),(timestamp:2075,action:Position(player:"0",pos:(1612.0822,1592.9174))),(timestamp:2076,action:Position(player:"0",pos:(1611.6655,1592.5007))),(timestamp:2077,action:Position(player:"0",pos:(1611.2489,1592.0841))),(timestamp:2078,action:Position(player:"0",pos:(1610.8323,1592.0841))),(timestamp:2079,action:Position(player:"0",pos:(1610.4156,1592.0841))),(timestamp:2080,action:Position(player:"0",pos:(1609.999,1592.0841))),(timestamp:2081,action:Position(player:"0",pos:(1609.5824,1592.0841))),(timestamp:2082,action:Position(player:"0",pos:(1609.1658,1592.0841))),(timestamp:2083,action:Position(player:"0",pos:(1608.7491,1592.0841))),(timestamp:2084,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:2085,action:Position(player:"0",pos:(1607.9159,1592.0841))),(timestamp:2086,action:Position(player:"0",pos:(1607.4993,1592.0841))),(timestamp:2087,action:Position(player:"0",pos:(1607.0826,1592.0841))),(timestamp:2088,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:2089,action:Position(player:"0",pos:(1606.2494,1592.0841))),(timestamp:2090,action:Position(player:"0",pos:(1605.8328,1592.0841))),(timestamp:2091,action:Position(player:"0",pos:(1605.4161,1592.0841))),(timestamp:2092,action:Position(player:"0",pos:(1604.9995,1592.0841))),(timestamp:2093,action:Position(player:"0",pos:(1604.5829,1592.0841))),(timestamp:2094,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:2095,action:Position(player:"0",pos:(1603.7496,1592.0841))),(timestamp:2096,action:Position(player:"0",pos:(1603.333,1592.0841))),(timestamp:2097,action:Position(player:"0",pos:(1602.9164,1592.0841))),(timestamp:2098,action:Position(player:"0",pos:(1602.4998,1592.0841))),(timestamp:2099,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:2100,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:2101,action:Position(player:"0",pos:(1601.2499,1592.0841))),(timestamp:2102,action:Position(player:"0",pos:(1600.8333,1592.0841))),(timestamp:2103,action:Position(player:"0",pos:(1600.4166,1592.0841))),(timestamp:2104,action:Position(player:"0",pos:(1600.0,1592.0841))),(timestamp:2124,action:Position(player:"0",pos:(1600.4166,1592.0841))),(timestamp:2125,action:Position(player:"0",pos:(1600.8333,1592.0841))),(timestamp:2126,action:Position(player:"0",pos:(1601.2499,1592.0841))),(timestamp:2127,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:2128,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:2129,action:Position(player:"0",pos:(1602.4998,1592.0841))),(timestamp:2130,action:Position(player:"0",pos:(1602.9164,1592.0841))),(timestamp:2131,action:Position(player:"0",pos:(1603.333,1592.0841))),(timestamp:2132,action:Position(player:"0",pos:(1603.7496,1592.0841))),(timestamp:2133,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:2134,action:Position(player:"0",pos:(1604.5829,1592.0841))),(timestamp:2135,action:Position(player:"0",pos:(1604.9995,1592.0841))),(timestamp:2136,action:Position(player:"0",pos:(1605.4161,1592.0841))),(timestamp:2137,action:Position(player:"0",pos:(1605.8328,1592.0841))),(timestamp:2138,action:Position(player:"0",pos:(1606.2494,1592.0841))),(timestamp:2139,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:2140,action:Position(player:"0",pos:(1607.0826,1592.0841))),(timestamp:2141,action:Position(player:"0",pos:(1607.4993,1592.0841))),(timestamp:2142,action:Position(player:"0",pos:(1607.9159,1592.0841))),(timestamp:2143,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:2144,action:Position(player:"0",pos:(1608.7491,1592.0841))),(timestamp:2145,action:Position(player:"0",pos:(1609.1658,1592.0841))),(timestamp:2146,action:Position(player:"0",pos:(1609.5824,1592.0841))),(timestamp:2147,action:Position(player:"0",pos:(1609.999,1592.0841))),(timestamp:2148,action:Position(player:"0",pos:(1610.4156,1592.0841))),(timestamp:2149,action:Position(player:"0",pos:(1610.8323,1592.0841))),(timestamp:2150,action:Position(player:"0",pos:(1611.2489,1592.0841))),(timestamp:2151,action:Position(player:"0",pos:(1611.6655,1592.0841))),(timestamp:2152,action:Position(player:"0",pos:(1612.0822,1592.0841))),(timestamp:2153,action:Position(player:"0",pos:(1612.4988,1592.0841))),(timestamp:2154,action:Position(player:"0",pos:(1612.9154,1592.0841))),(timestamp:2155,action:Position(player:"0",pos:(1613.332,1592.0841))),(timestamp:2156,action:Position(player:"0",pos:(1613.7487,1592.0841))),(timestamp:2157,action:Position(player:"0",pos:(1614.1653,1592.0841))),(timestamp:2158,action:Position(player:"0",pos:(1614.5819,1592.0841))),(timestamp:2159,action:Position(player:"0",pos:(1614.9985,1592.0841))),(timestamp:2160,action:Position(player:"0",pos:(1615.4152,1592.0841))),(timestamp:2161,action:Position(player:"0",pos:(1615.8318,1592.0841))),(timestamp:2162,action:Position(player:"0",pos:(1616.2484,1592.0841))),(timestamp:2163,action:Position(player:"0",pos:(1616.665,1592.0841))),(timestamp:2164,action:Position(player:"0",pos:(1617.0817,1592.0841))),(timestamp:2165,action:Position(player:"0",pos:(1617.4983,1592.0841))),(timestamp:2166,action:Position(player:"0",pos:(1617.9149,1592.0841))),(timestamp:2167,action:Position(player:"0",pos:(1618.3315,1592.0841))),(timestamp:2168,action:Position(player:"0",pos:(1618.7482,1592.0841))),(timestamp:2169,action:Position(player:"0",pos:(1619.1648,1592.0841))),(timestamp:2170,action:Position(player:"0",pos:(1619.5814,1592.0841))),(timestamp:2171,action:Position(player:"0",pos:(1619.998,1592.0841))),(timestamp:2172,action:Position(player:"0",pos:(1620.4147,1592.0841))),(timestamp:2215,action:Position(player:"0",pos:(1619.998,1592.5007))),(timestamp:2216,action:Position(player:"0",pos:(1619.5814,1592.9174))),(timestamp:2217,action:Position(player:"0",pos:(1619.1648,1593.334))),(timestamp:2218,action:Position(player:"0",pos:(1618.7482,1593.7506))),(timestamp:2219,action:Position(player:"0",pos:(1618.3315,1594.1672))),(timestamp:2220,action:Position(player:"0",pos:(1617.9149,1594.5839))),(timestamp:2221,action:Position(player:"0",pos:(1617.4983,1595.0005))),(timestamp:2222,action:Position(player:"0",pos:(1617.0817,1595.4171))),(timestamp:2223,action:Position(player:"0",pos:(1616.665,1595.8337))),(timestamp:2224,action:Position(player:"0",pos:(1616.2484,1596.2504))),(timestamp:2225,action:Position(player:"0",pos:(1615.8318,1596.667))),(timestamp:2226,action:Position(player:"0",pos:(1615.4152,1597.0836))),(timestamp:2227,action:Position(player:"0",pos:(1614.9985,1597.5002))),(timestamp:2228,action:Position(player:"0",pos:(1614.5819,1597.9169))),(timestamp:2229,action:Position(player:"0",pos:(1614.1653,1598.3335))),(timestamp:2230,action:Position(player:"0",pos:(1613.7487,1598.7501))),(timestamp:2231,action:Position(player:"0",pos:(1613.332,1599.1667))),(timestamp:2232,action:Position(player:"0",pos:(1612.9154,1599.5834))),(timestamp:2233,action:Position(player:"0",pos:(1612.4988,1600.0))),(timestamp:2234,action:Position(player:"0",pos:(1612.0822,1600.4166))),(timestamp:2235,action:Position(player:"0",pos:(1611.6655,1600.8333))),(timestamp:2236,action:Position(player:"0",pos:(1611.2489,1600.8333))),(timestamp:2237,action:Position(player:"0",pos:(1610.8323,1600.8333))),(timestamp:2238,action:Position(player:"0",pos:(1610.4156,1600.8333))),(timestamp:2239,action:Position(player:"0",pos:(1609.999,1600.8333))),(timestamp:2240,action:Position(player:"0",pos:(1609.5824,1600.8333))),(timestamp:2241,action:Position(player:"0",pos:(1609.1658,1600.8333))),(timestamp:2242,action:Position(player:"0",pos:(1608.7491,1600.8333))),(timestamp:2243,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:2244,action:Position(player:"0",pos:(1607.9159,1600.8333))),(timestamp:2245,action:Position(player:"0",pos:(1607.4993,1600.8333))),(timestamp:2246,action:Position(player:"0",pos:(1607.0826,1600.8333))),(timestamp:2247,action:Position(player:"0",pos:(1606.666,1600.8333))),(timestamp:2248,action:Position(player:"0",pos:(1606.2494,1600.8333))),(timestamp:2249,action:Position(player:"0",pos:(1605.8328,1600.8333))),(timestamp:2250,action:Position(player:"0",pos:(1605.4161,1600.8333))),(timestamp:2251,action:Position(player:"0",pos:(1604.9995,1600.8333))),(timestamp:2252,action:Position(player:"0",pos:(1604.5829,1600.8333))),(timestamp:2253,action:Position(player:"0",pos:(1604.1663,1600.8333))),(timestamp:2254,action:Position(player:"0",pos:(1603.7496,1600.8333))),(timestamp:2255,action:Position(player:"0",pos:(1603.333,1600.8333))),(timestamp:2256,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:2257,action:Position(player:"0",pos:(1602.4998,1600.8333))),(timestamp:2258,action:Position(player:"0",pos:(1602.0831,1600.8333))),(timestamp:2259,action:Position(player:"0",pos:(1601.6665,1600.8333))),(timestamp:2312,action:Position(player:"0",pos:(1601.6665,1600.4166))),(timestamp:2313,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:2314,action:Position(player:"0",pos:(1601.6665,1599.5834))),(timestamp:2315,action:Position(player:"0",pos:(1601.6665,1599.1667))),(timestamp:2316,action:Position(player:"0",pos:(1601.6665,1598.7501))),(timestamp:2317,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:2318,action:Position(player:"0",pos:(1601.6665,1597.9169))),(timestamp:2319,action:Position(player:"0",pos:(1601.6665,1597.5002))),(timestamp:2320,action:Position(player:"0",pos:(1601.6665,1597.0836))),(timestamp:2321,action:Position(player:"0",pos:(1601.6665,1596.667))),(timestamp:2322,action:Position(player:"0",pos:(1601.6665,1596.2504))),(timestamp:2323,action:Position(player:"0",pos:(1601.6665,1595.8337))),(timestamp:2324,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:2325,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:2326,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:2327,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:2328,action:Position(player:"0",pos:(1601.6665,1593.7506))),(timestamp:2357,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:2358,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:2359,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:2360,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:2361,action:Position(player:"0",pos:(1601.6665,1595.8337))),(timestamp:2362,action:Position(player:"0",pos:(1601.6665,1596.2504))),(timestamp:2363,action:Position(player:"0",pos:(1601.6665,1596.667))),(timestamp:2364,action:Position(player:"0",pos:(1601.6665,1597.0836))),(timestamp:2365,action:Position(player:"0",pos:(1601.6665,1597.5002))),(timestamp:2366,action:Position(player:"0",pos:(1601.6665,1597.9169))),(timestamp:2367,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:2368,action:Position(player:"0",pos:(1601.6665,1598.7501))),(timestamp:2369,action:Position(player:"0",pos:(1601.6665,1599.1667))),(timestamp:2370,action:Position(player:"0",pos:(1601.6665,1599.5834))),(timestamp:2371,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:2372,action:Position(player:"0",pos:(1601.6665,1600.4166))),(timestamp:2373,action:Position(player:"0",pos:(1601.6665,1600.8333))),(timestamp:2374,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2429,action:Remove(pos:(x:1597,y:1600))),(timestamp:2452,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2453,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2454,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2455,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2456,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2457,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2458,action:Position(player:"0",pos:(1604.5829,1601.2499))),(timestamp:2459,action:Position(player:"0",pos:(1604.9995,1601.2499))),(timestamp:2460,action:Position(player:"0",pos:(1605.4161,1601.2499))),(timestamp:2461,action:Position(player:"0",pos:(1605.8328,1601.2499))),(timestamp:2462,action:Position(player:"0",pos:(1606.2494,1601.2499))),(timestamp:2463,action:Position(player:"0",pos:(1606.666,1601.2499))),(timestamp:2464,action:Position(player:"0",pos:(1607.0826,1601.2499))),(timestamp:2465,action:Position(player:"0",pos:(1607.4993,1601.2499))),(timestamp:2466,action:Position(player:"0",pos:(1607.9159,1601.2499))),(timestamp:2467,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:2564,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1600),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:2591,action:Position(player:"0",pos:(1607.9159,1601.2499))),(timestamp:2592,action:Position(player:"0",pos:(1607.4993,1601.2499))),(timestamp:2593,action:Position(player:"0",pos:(1607.0826,1601.2499))),(timestamp:2594,action:Position(player:"0",pos:(1606.666,1601.2499))),(timestamp:2595,action:Position(player:"0",pos:(1606.2494,1601.2499))),(timestamp:2596,action:Position(player:"0",pos:(1605.8328,1601.2499))),(timestamp:2597,action:Position(player:"0",pos:(1605.4161,1601.2499))),(timestamp:2598,action:Position(player:"0",pos:(1604.9995,1601.2499))),(timestamp:2599,action:Position(player:"0",pos:(1604.5829,1601.2499))),(timestamp:2600,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2601,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2602,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2603,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2604,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2605,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2606,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2607,action:Position(player:"0",pos:(1601.2499,1601.2499))),(timestamp:2608,action:Position(player:"0",pos:(1600.8333,1601.2499))),(timestamp:2609,action:Position(player:"0",pos:(1600.4166,1601.2499))),(timestamp:2610,action:Position(player:"0",pos:(1600.0,1601.2499))),(timestamp:2611,action:Position(player:"0",pos:(1599.5834,1601.2499))),(timestamp:2612,action:Position(player:"0",pos:(1599.1667,1601.2499))),(timestamp:2613,action:Position(player:"0",pos:(1598.7501,1601.2499))),(timestamp:2614,action:Position(player:"0",pos:(1598.3335,1601.2499))),(timestamp:2615,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2616,action:Position(player:"0",pos:(1597.5002,1601.2499))),(timestamp:2617,action:Position(player:"0",pos:(1597.0836,1601.2499))),(timestamp:2618,action:Position(player:"0",pos:(1596.667,1601.2499))),(timestamp:2619,action:Position(player:"0",pos:(1596.2504,1601.2499))),(timestamp:2620,action:Position(player:"0",pos:(1595.8337,1601.2499))),(timestamp:2621,action:Position(player:"0",pos:(1595.4171,1601.2499))),(timestamp:2622,action:Position(player:"0",pos:(1595.0005,1601.2499))),(timestamp:2623,action:Position(player:"0",pos:(1594.5839,1601.2499))),(timestamp:2624,action:Position(player:"0",pos:(1594.1672,1601.2499))),(timestamp:2625,action:Position(player:"0",pos:(1593.7506,1601.2499))),(timestamp:2626,action:Position(player:"0",pos:(1593.334,1601.2499))),(timestamp:2627,action:Position(player:"0",pos:(1592.9174,1601.2499))),(timestamp:2628,action:Position(player:"0",pos:(1592.5007,1601.2499))),(timestamp:2629,action:Position(player:"0",pos:(1592.0841,1601.2499))),(timestamp:2630,action:Position(player:"0",pos:(1591.6675,1601.2499))),(timestamp:2631,action:Position(player:"0",pos:(1591.2509,1601.2499))),(timestamp:2632,action:Position(player:"0",pos:(1590.8342,1601.2499))),(timestamp:2636,action:Position(player:"0",pos:(1591.2509,1601.2499))),(timestamp:2637,action:Position(player:"0",pos:(1591.6675,1601.2499))),(timestamp:2638,action:Position(player:"0",pos:(1592.0841,1601.2499))),(timestamp:2639,action:Position(player:"0",pos:(1592.5007,1601.2499))),(timestamp:2640,action:Position(player:"0",pos:(1592.9174,1601.2499))),(timestamp:2641,action:Position(player:"0",pos:(1593.334,1601.2499))),(timestamp:2642,action:Position(player:"0",pos:(1593.7506,1601.2499))),(timestamp:2643,action:Position(player:"0",pos:(1594.1672,1601.2499))),(timestamp:2644,action:Position(player:"0",pos:(1594.5839,1601.2499))),(timestamp:2645,action:Position(player:"0",pos:(1595.0005,1601.2499))),(timestamp:2646,action:Position(player:"0",pos:(1595.4171,1601.2499))),(timestamp:2647,action:Position(player:"0",pos:(1595.8337,1601.2499))),(timestamp:2648,action:Position(player:"0",pos:(1596.2504,1601.2499))),(timestamp:2649,action:Position(player:"0",pos:(1596.667,1601.2499))),(timestamp:2650,action:Position(player:"0",pos:(1597.0836,1601.2499))),(timestamp:2651,action:Position(player:"0",pos:(1597.5002,1601.2499))),(timestamp:2652,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2653,action:Position(player:"0",pos:(1598.3335,1601.2499))),(timestamp:2654,action:Position(player:"0",pos:(1598.7501,1601.2499))),(timestamp:2655,action:Position(player:"0",pos:(1599.1667,1601.2499))),(timestamp:2656,action:Position(player:"0",pos:(1599.5834,1601.2499))),(timestamp:2657,action:Position(player:"0",pos:(1600.0,1601.2499))),(timestamp:2658,action:Position(player:"0",pos:(1600.4166,1601.2499))),(timestamp:2659,action:Position(player:"0",pos:(1600.8333,1601.2499))),(timestamp:2660,action:Position(player:"0",pos:(1601.2499,1601.2499))),(timestamp:2661,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2662,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2663,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2719,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2720,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2721,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2722,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2723,action:Position(player:"0",pos:(1604.5829,1600.8333))),(timestamp:2724,action:Position(player:"0",pos:(1604.9995,1600.4166))),(timestamp:2725,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:2726,action:Position(player:"0",pos:(1605.8328,1599.5834))),(timestamp:2727,action:Position(player:"0",pos:(1606.2494,1599.1667))),(timestamp:2728,action:Position(player:"0",pos:(1606.666,1598.7501))),(timestamp:2729,action:Position(player:"0",pos:(1607.0826,1598.3335))),(timestamp:2730,action:Position(player:"0",pos:(1607.4993,1597.9169))),(timestamp:2731,action:Position(player:"0",pos:(1607.9159,1597.5002))),(timestamp:2732,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:2733,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:2734,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:2735,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:2776,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:2777,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:2778,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:2779,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:2780,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:2781,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:2850,action:PlaceEntity(force:false,info:(pos:(x:1605,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:2902,action:PlaceEntity(force:false,info:(pos:(x:1610,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:3159,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:3248,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:3249,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:3250,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:3251,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:3252,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:3253,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:3254,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:3255,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:3256,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:3257,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:3258,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:3259,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:3260,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:3261,action:Position(player:"0",pos:(1608.3325,1592.5007))),(timestamp:3262,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:3263,action:Position(player:"0",pos:(1608.3325,1591.6675))),(timestamp:3264,action:Position(player:"0",pos:(1608.3325,1591.2509))),(timestamp:3265,action:Position(player:"0",pos:(1608.3325,1590.8342))),(timestamp:3913,action:Position(player:"0",pos:(1608.3325,1590.4176))),(timestamp:3914,action:Position(player:"0",pos:(1608.3325,1590.001))),(timestamp:3915,action:Position(player:"0",pos:(1608.3325,1589.5844))),(timestamp:3916,action:Position(player:"0",pos:(1608.3325,1589.1677))),(timestamp:3917,action:Position(player:"0",pos:(1608.3325,1588.7511))),(timestamp:3918,action:Position(player:"0",pos:(1608.3325,1588.3345))),(timestamp:3919,action:Position(player:"0",pos:(1608.3325,1587.9178))),(timestamp:3920,action:Position(player:"0",pos:(1608.3325,1587.5012))),(timestamp:3921,action:Position(player:"0",pos:(1608.3325,1587.0846))),(timestamp:3922,action:Position(player:"0",pos:(1608.3325,1586.668))),(timestamp:3923,action:Position(player:"0",pos:(1608.3325,1586.2513))),(timestamp:3924,action:Position(player:"0",pos:(1608.3325,1585.8347))),(timestamp:3925,action:Position(player:"0",pos:(1608.3325,1585.4181))),(timestamp:3926,action:Position(player:"0",pos:(1608.3325,1585.0015))),(timestamp:3937,action:Position(player:"0",pos:(1608.3325,1585.4181))),(timestamp:3938,action:Position(player:"0",pos:(1608.3325,1585.8347))),(timestamp:3939,action:Position(player:"0",pos:(1608.3325,1586.2513))),(timestamp:3940,action:Position(player:"0",pos:(1608.3325,1586.668))),(timestamp:3941,action:Position(player:"0",pos:(1608.3325,1587.0846))),(timestamp:3942,action:Position(player:"0",pos:(1608.3325,1587.5012))),(timestamp:3943,action:Position(player:"0",pos:(1608.3325,1587.9178))),(timestamp:3944,action:Position(player:"0",pos:(1608.3325,1588.3345))),(timestamp:3945,action:Position(player:"0",pos:(1608.3325,1588.7511))),(timestamp:3946,action:Position(player:"0",pos:(1608.3325,1589.1677))),(timestamp:3947,action:Position(player:"0",pos:(1608.3325,1589.5844))),(timestamp:3948,action:Position(player:"0",pos:(1608.3325,1590.001))),(timestamp:3949,action:Position(player:"0",pos:(1608.3325,1590.4176))),(timestamp:3950,action:Position(player:"0",pos:(1608.3325,1590.8342))),(timestamp:3951,action:Position(player:"0",pos:(1608.3325,1591.2509))),(timestamp:3952,action:Position(player:"0",pos:(1608.3325,1591.6675))),(timestamp:3953,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:3954,action:Position(player:"0",pos:(1608.3325,1592.5007))),(timestamp:3955,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:3956,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:3957,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:3958,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:3959,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:3960,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:3961,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:3962,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:3963,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:3964,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:3965,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:3966,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:3967,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:3968,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:3969,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:3970,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:3971,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:3972,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:3973,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:3974,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:3975,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:3976,action:Position(player:"0",pos:(1608.3325,1601.6665))),(timestamp:3977,action:Position(player:"0",pos:(1608.3325,1602.0831))),(timestamp:3978,action:Position(player:"0",pos:(1608.3325,1602.4998))),(timestamp:3979,action:Position(player:"0",pos:(1608.3325,1602.9164))),(timestamp:3980,action:Position(player:"0",pos:(1608.3325,1603.333))),(timestamp:3981,action:Position(player:"0",pos:(1608.3325,1603.7496))),(timestamp:3996,action:Position(player:"0",pos:(1608.3325,1603.333))),(timestamp:3997,action:Position(player:"0",pos:(1608.3325,1602.9164))),(timestamp:3998,action:Position(player:"0",pos:(1608.3325,1602.4998))),(timestamp:3999,action:Position(player:"0",pos:(1608.3325,1602.0831))),(timestamp:4000,action:Position(player:"0",pos:(1608.3325,1601.6665))),(timestamp:4001,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:4002,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4003,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4004,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4005,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4006,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4007,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4008,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4009,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:4010,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:4011,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:4012,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:4013,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:4014,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:4015,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:4016,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:4017,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:4018,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:4019,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:4020,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:4021,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:4047,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:4048,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:4049,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:4050,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:4051,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:4052,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:4053,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:4054,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:4055,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:4056,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:4057,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:4058,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:4059,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4060,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4061,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4062,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4063,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4064,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4065,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4066,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:4086,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4087,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4088,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4089,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4090,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4091,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4092,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4158,action:Remove(pos:(x:1605,y:1595)))],current_timestep:4159,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/004.rep.ron b/crash_replays/004.rep.ron new file mode 100644 index 0000000..90d4b22 --- /dev/null +++ b/crash_replays/004.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"7c292fc",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:107,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1598),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:198,action:Position(player:"0",pos:(1600.4166,1599.5834))),(timestamp:199,action:Position(player:"0",pos:(1600.8333,1599.1667))),(timestamp:200,action:Position(player:"0",pos:(1601.2499,1598.7501))),(timestamp:201,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:202,action:Position(player:"0",pos:(1602.0831,1597.9169))),(timestamp:203,action:Position(player:"0",pos:(1602.4998,1597.5002))),(timestamp:204,action:Position(player:"0",pos:(1602.9164,1597.0836))),(timestamp:387,action:SetRecipe(pos:(x:1598,y:1598),recipe:"factory_game::iron_ore_generation")),(timestamp:514,action:Position(player:"0",pos:(1602.9164,1597.5002))),(timestamp:515,action:Position(player:"0",pos:(1602.9164,1597.9169))),(timestamp:516,action:Position(player:"0",pos:(1602.9164,1598.3335))),(timestamp:517,action:Position(player:"0",pos:(1602.9164,1598.7501))),(timestamp:518,action:Position(player:"0",pos:(1602.9164,1599.1667))),(timestamp:519,action:Position(player:"0",pos:(1602.9164,1599.5834))),(timestamp:520,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:521,action:Position(player:"0",pos:(1602.9164,1600.4166))),(timestamp:522,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:523,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:524,action:Position(player:"0",pos:(1602.9164,1601.6665))),(timestamp:525,action:Position(player:"0",pos:(1602.9164,1602.0831))),(timestamp:526,action:Position(player:"0",pos:(1602.9164,1602.4998))),(timestamp:569,action:Remove(pos:(x:1599,y:1599))),(timestamp:593,action:Position(player:"0",pos:(1602.9164,1602.0831))),(timestamp:594,action:Position(player:"0",pos:(1602.9164,1601.6665))),(timestamp:595,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:596,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:597,action:Position(player:"0",pos:(1602.9164,1600.4166))),(timestamp:598,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:919,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:989,action:PlaceEntity(force:false,info:(pos:(x:1604,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:1033,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:1057,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:1125,action:Remove(pos:(x:1600,y:1597))),(timestamp:1184,action:Remove(pos:(x:1603,y:1597)))],current_timestep:1185,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/dummy.rep b/crash_replays/dummy.rep deleted file mode 100644 index e69de29..0000000 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7d36536 --- /dev/null +++ b/flake.lock @@ -0,0 +1,99 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1770419512, + "narHash": "sha256-o8Vcdz6B6bkiGUYkZqFwH3Pv1JwZyXht3dMtS7RchIo=", + "owner": "ipetkov", + "repo": "crane", + "rev": "2510f2cbc3ccd237f700bb213756a8f35c32d8d7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1770447430, + "narHash": "sha256-smrRbWhvJF6BATB6pXbD8Cp04HRrVcYQkXqOhUF81nk=", + "owner": "nix-community", + "repo": "fenix", + "rev": "e1b28f6ca0d1722edceec1f2f3501558988d1aed", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-codium": { + "locked": { + "lastModified": 1762943920, + "narHash": "sha256-ITeH8GBpQTw9457ICZBddQEBjlXMmilML067q0e6vqY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "fenix": "fenix", + "nixpkgs": "nixpkgs", + "nixpkgs-codium": "nixpkgs-codium" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1770290336, + "narHash": "sha256-rJ79U68ZLjCSg1Qq+63aBXi//W7blaKiYq9NnfeTboA=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "d2a00da09293267e5be2efb216698762929d7140", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7a2fa2d --- /dev/null +++ b/flake.nix @@ -0,0 +1,157 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-codium.url = "github:nixos/nixpkgs?ref=91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + crane.url = "github:ipetkov/crane"; + }; + + inputs.self.lfs = true; + outputs = { self, nixpkgs, nixpkgs-codium, fenix, crane }: let + inherit (nixpkgs) lib; + pkgs = nixpkgs.legacyPackages."x86_64-linux"; + pkgs-codium = nixpkgs-codium.legacyPackages."x86_64-linux"; + fenixLib = fenix.packages."x86_64-linux"; + + toolchain_sha = "sha256-dXoddWaPL6UtPscTpxMUMBDL83jFtqeDtmH/+bXBs3E="; + + rustToolchain = fenixLib.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = toolchain_sha; + }; + + wasmToolchain = fenixLib.combine [ + (fenixLib.targets.wasm32-unknown-unknown.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = toolchain_sha; + }) + rustToolchain + ]; + + neededPackages = with pkgs; [ + wayland + xorg.libX11 + xorg.libXcursor + xorg.libXrandr + xorg.libXi + libxkbcommon + + openssl + + vulkan-headers vulkan-loader + ]; + + built_overrides = { + # info for built + # BUILT_OVERRIDE_factory_GIT_DIRTY = if self.revDirty then "true" else "false"; + BUILT_OVERRIDE_factory_GIT_HEAD_REF = self.ref or null; + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH = self.rev or null; + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH_SHORT = self.revShort or null; + SOURCE_DATE_EPOCH = self.lastModified; + }; + + client_package_for_target = { + target, toolchain + }: ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain toolchain).buildPackage ({ + name = "factory"; + + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH_SHORT = "BLUB"; + + CARGO_BUILD_TARGET = target; + meta = { + homepage = "https://www.github.com/BloodStainedCrow/FactoryGame/"; + maintainers = with lib.maintainers; [ BloodStainedCrow ]; + mainProgram = "factory"; + }; + src = ./.; + + buildInputs = neededPackages; + nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; + cargoHash = "sha256-83+1Y486PUHM9+uyFw+yJ9bNMlMbN/fc8cYRzKmDdb8="; + # cargoLock.lockFile = ./Cargo.lock; + doCheck = false; + + postInstall = '' + wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" + ''; + } // built_overrides); + + client_package = client_package_for_target { target = "x86_64-unknown-linux-gnu"; toolchain = rustToolchain; }; + in { + + + devShells."x86_64-linux".codium = pkgs.mkShell { + buildInputs = with pkgs; [ + bashInteractive + rustToolchain + + perf + samply + bacon + + (vscode-with-extensions.override { + vscode = pkgs-codium.vscodium; + vscodeExtensions = with pkgs-codium.vscode-extensions; [ + rust-lang.rust-analyzer + vadimcn.vscode-lldb + gruntfuggly.todo-tree + a5huynh.vscode-ron + ]; + }) + ] ++ neededPackages; + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}"; + + shellHook = '' + export SHELL="${pkgs.bashInteractive}/bin/bash" + ''; + + + # env.RUST_SRC_PATH = "${rustToolchain.rust-src}"; + }; + + packages."x86_64-linux".default = client_package; + packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" "-F logging" ]; }); + + "wasm" = (client_package_for_target { target = "wasm32-unknown-unknown"; toolchain = wasmToolchain; }).overrideAttrs ( oldAttrs: { + nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.wabt pkgs.binaryen ]; + postInstall = '' + mkdir -p $out/lib + ls -lR $out + wasm-strip $out/bin/factory.wasm -o $out/lib/factory.wasm + # TODO: Use wasm-opt + # wasm-opt $out/lib/factory.wasm -o $out/lib/factory.wasm -O4 + rm -r $out/bin + wasm-validate $out/lib/factory.wasm + ''; + + # We cannot run tests on a wasm binary + doCheck = false; + }); + + "trunk" = ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain wasmToolchain).buildTrunkPackage ({ + nativeBuildInputs = [ pkgs.lld ]; + CARGO_BUILD_TARGET = "wasm32-unknown-unknown"; + src = ./.; + + wasm-bindgen-cli = pkgs.buildWasmBindgenCli rec { + src = pkgs.fetchCrate { + pname = "wasm-bindgen-cli"; + version = "0.2.108"; + hash = "sha256-UsuxILm1G6PkmVw0I/JF12CRltAfCJQFOaT4hFwvR8E="; + # hash = lib.fakeHash; + }; + + cargoDeps = pkgs.rustPlatform.fetchCargoVendor { + inherit src; + inherit (src) pname version; + hash = "sha256-iqQiWbsKlLBiJFeqIYiXo3cqxGLSjNM8SOWXGM9u43E="; + # hash = lib.fakeHash; + }; + }; + } // built_overrides); + + }; +} diff --git a/giga.png b/giga.png new file mode 100644 index 0000000..dcb6fcd Binary files /dev/null and b/giga.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..03dca5c --- /dev/null +++ b/index.html @@ -0,0 +1,137 @@ + + + + + + + + + + FactoryGame + + + + + + + + + + + + + + + + + + + +
+ +

+ Loading… +

+
+
+ + + + + + + + + diff --git a/mega.png b/mega.png new file mode 100644 index 0000000..0a08f03 Binary files /dev/null and b/mega.png differ diff --git a/proptest-regressions/inserter/mod.txt b/proptest-regressions/inserter/mod.txt index 12d51b2..f1f3c08 100644 --- a/proptest-regressions/inserter/mod.txt +++ b/proptest-regressions/inserter/mod.txt @@ -6,3 +6,4 @@ # everyone who runs the test benefits from these saved cases. cc a27a3dc53d7764e25eaa4974387a987dd428e75859b930d82490208fccf4a1a1 # shrinks to (item, num_grids, storage) = (Item { id: 0 }, 1, Lab { grid: 0, index: 0 }) cc da0b9b95353896ab4b3c74e271842d68b773f18aa829b4f174a035584bd104e6 # shrinks to (item, num_grids, storage) = (Item { id: 0 }, 1, Static { index: 0, static_id: Chest }) +cc 4bfd9265944575f3527bc20200aad3fb6292511435a7723bc421b76e052de433 # shrinks to (item, _num_grids, storage) = (Item { id: 0 }, 1, Assembler { grid: 0, recipe_idx_with_this_item: 1, index: 0 }) diff --git a/proptest-regressions/test_world_harness/mod.txt b/proptest-regressions/test_world_harness/mod.txt new file mode 100644 index 0000000..3fc0d91 --- /dev/null +++ b/proptest-regressions/test_world_harness/mod.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 7beba775c95a401c87549909d3f4975ebab4cca640f413eccaa0801c9aaa77e1 # shrinks to pos = Position { x: 1600, y: 1600 }, zoom_level = 0.0, camera_pos = (0.0, 25.080559) diff --git a/red_chips.png b/red_chips.png new file mode 100644 index 0000000..ef23aae Binary files /dev/null and b/red_chips.png differ diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9fe5ede..ed45212 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2025-05-27" +channel = "nightly-2025-12-28" components = ["clippy", "rustfmt"] diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 6bcf3da..0000000 --- a/shell.nix +++ /dev/null @@ -1,28 +0,0 @@ -let - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/929116e316068c7318c54eb4d827f7d9756d5e9c.tar.gz")) { overlays = [ ]; }; - buildInputs = [ - ] ++ (with pkgs; [ - rustup - - pkg-config - - # perf for cargo-flamegraph - linuxPackages_latest.perf - - wayland - xorg.libX11 - xorg.libXcursor - xorg.libXrandr - xorg.libXi - libxkbcommon - - openssl - - vulkan-headers vulkan-loader - ]); -in -pkgs.mkShell { - inherit buildInputs; - RUST_BACKTRACE = 1; - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath buildInputs)}"; -} diff --git a/src/app_state.rs b/src/app_state.rs index c819a19..c9537b2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,14 +1,26 @@ +use crate::assembler::simd::Conn; use crate::belt::BeltTileId; use crate::belt::belt::Belt; +use crate::blueprint::BlueprintAction; +use crate::blueprint::BlueprintPlaceEntity; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; use crate::data::AllowedFluidDirection; use crate::frontend::action::belt_placement::FakeGameState; use crate::frontend::action::place_entity::PlaceEntityInfo; +use crate::frontend::world::tile::CHUNK_SIZE; +use crate::frontend::world::tile::ModuleSlotDedupIndex; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; +use crate::frontend::world::tile::PlayerInfo; +use crate::get_const_string; use crate::inserter::InserterStateInfo; +use crate::inserter::WaitlistSearchSide; +use crate::inserter::belt_storage_inserter; +use crate::inserter::belt_storage_movement_list::BeltStorageInserterInMovement; +use crate::inserter::belt_storage_movement_list::List; use crate::inserter::storage_storage_with_buckets_indirect::BucketedStorageStorageInserterStore; +use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::inserter::storage_storage_with_buckets_indirect::InserterIdentifier; use crate::item::ITEMCOUNTTYPE; use crate::join_many::join; @@ -21,6 +33,11 @@ use crate::par_generation::BoundingBox; use crate::par_generation::ParGenerateInfo; use crate::par_generation::par_generate; use crate::power::Watt; +use crate::progress_info::ProgressInfo; +use crate::replays::GenerationInformation; +use crate::replays::ProgramInformation; +#[cfg(feature = "client")] +use crate::saving::loading::SaveFileList; #[cfg(feature = "client")] use crate::{Input, LoadedGame}; use crate::{ @@ -52,9 +69,7 @@ use crate::{ statistics::{ GenStatistics, Timeline, consumption::ConsumptionInfo, production::ProductionInfo, }, - storage_list::{ - SingleItemStorages, full_to_by_item, grid_size, num_recipes, sizes, storages_by_item, - }, + storage_list::{SingleItemStorages, full_to_by_item, grid_size, sizes, storages_by_item}, }; use crate::{ item::Indexable, @@ -67,8 +82,9 @@ use flate2::bufread::ZlibDecoder; use get_size2::GetSize; use itertools::Itertools; use log::error; -use log::{info, trace, warn}; +use log::{info, warn}; use petgraph::graph::NodeIndex; +use rayon::iter::IntoParallelRefIterator; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use std::collections::BTreeMap; use std::io::BufReader; @@ -94,12 +110,17 @@ use crate::get_size::Mutex; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct AuxillaryData { + pub game_name: String, + pub(crate) gen_info: (ProgramInformation, GenerationInformation), + pub current_tick: u64, pub statistics: GenStatistics, pub update_round_trip_times: Timeline, pub update_times: Timeline, + + #[cfg_attr(feature = "client", get_size(ignore))] #[serde(skip)] last_update_time: Option, @@ -107,9 +128,13 @@ pub struct AuxillaryData { } impl AuxillaryData { pub fn new( + name: String, + gen_info: GenerationInformation, data_store: &DataStore, ) -> Self { AuxillaryData { + game_name: name, + gen_info: (ProgramInformation::new(data_store), gen_info), current_tick: 0, statistics: GenStatistics::new(data_store), update_times: Timeline::new(false, data_store), @@ -123,7 +148,7 @@ impl AuxillaryData { } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GameState { pub world: Mutex>, pub simulation_state: Mutex>, @@ -151,15 +176,21 @@ impl<'a> AddAssign<&'a UpdateTime> for UpdateTime { impl GameState { #[must_use] - pub fn new(data_store: &DataStore) -> Self { + pub fn new( + name: String, + gen_info: GenerationInformation, + data_store: &DataStore, + ) -> Self { Self { world: Mutex::new(World::default()), simulation_state: Mutex::new(SimulationState::new(data_store)), - aux_data: Mutex::new(AuxillaryData::new(data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, gen_info, data_store)), } } fn new_with_world_area( + name: String, + gen_info: GenerationInformation, top_left: Position, bottom_right: Position, data_store: &DataStore, @@ -167,67 +198,35 @@ impl GameState, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, - Position { x: 3500, y: 31000 }, - data_store, - ); - - const BP_STRING: &'static str = include_str!("../test_blueprints/red_sci.bp"); - let bp: Blueprint = ron::de::from_str(BP_STRING).unwrap(); - // let file = File::open("test_blueprints/red_sci.bp").unwrap(); - // let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - puffin::set_scopes_on(false); - let y_range = (1590..3000).step_by(7); - let x_range = (1590..3000).step_by(20); - - let total = y_range.size_hint().0 * x_range.size_hint().0; - - let mut current = 0; - - for y_pos in y_range { - for x_pos in x_range.clone() { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - - bp.apply(Position { x: x_pos, y: y_pos }, &mut ret, data_store); - } - } - puffin::set_scopes_on(true); - - ret - } - #[must_use] pub fn new_with_megabase( + name: String, + gen_info: GenerationInformation, use_solar_field: bool, - progress: Arc, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { + const X_OFFS: i32 = -1_800; + // TODO: Increase size to fit solar field let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, + name, + gen_info, + Position { + x: X_OFFS, + y: -6_000, + }, Position { - x: 16_000, - y: 12_000, + x: 17_000 + X_OFFS, + y: 6_000, }, data_store, ); - let mut file = - File::open("test_blueprints/murphy/megabase_new_blueprint_format.bp").unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); + let s = get_const_string!("test_blueprints/murphy/megabase_new_blueprint_format.bp"); let bp: BlueprintString = BlueprintString(s); let mut bp: Blueprint = bp.try_into().expect("Blueprint String Invalid"); bp.optimize(); @@ -238,13 +237,18 @@ impl GameState GameState GameState GameState, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { let width = count.isqrt(); @@ -332,7 +346,7 @@ impl GameState GameState GameState GameState, - data_store: &DataStore, - ) -> Self { - Self::new_with_beacon_red_green_production_many_grids(progress, data_store) - } - - #[must_use] - pub fn new_with_beacon_red_green_production_many_grids( - progress: Arc, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, - Position { x: 44000, y: 44000 }, - data_store, - ); - - const BP_STRING: &'static str = - include_str!("../test_blueprints/red_and_green_with_clocking.bp"); - let bp: Blueprint = ron::de::from_str(BP_STRING).unwrap(); - // let file = File::open("test_blueprints/red_and_green_with_clocking.bp").unwrap(); - // let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - puffin::set_scopes_on(false); - let y_range = (0..40_000).step_by(4_000); - let x_range = (0..40_000).step_by(4_000); - - let total = y_range.size_hint().0 * x_range.size_hint().0; - - let mut current = 0; - - for y_start in y_range { - for x_start in x_range.clone() { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - for y_pos in (1590..4000).step_by(40) { - for x_pos in (1590..4000).step_by(50) { - bp.apply( - Position { - x: x_start + x_pos, - y: y_start + y_pos, - }, - &mut ret, - data_store, - ); - } - } - } - } - puffin::set_scopes_on(true); - - ret - } - - #[must_use] - pub fn new_with_beacon_belt_production( - progress: Arc, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, - Position { x: 10000, y: 20000 }, - data_store, - ); - - let file = File::open("test_blueprints/red_sci_with_beacons_and_belts.bp").unwrap(); - let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - let y_range = (0..20_000).step_by(6_000); - - let total = y_range.size_hint().0; - - let mut current = 0; - - puffin::set_scopes_on(false); - for y_start in y_range { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - for y_pos in (1590..6000).step_by(10) { - for x_pos in (1590..4000).step_by(60) { - bp.apply( - Position { - x: x_pos, - y: y_start + y_pos, - }, - &mut ret, - data_store, - ); - } - } - } - puffin::set_scopes_on(true); - - ret - } - #[must_use] pub fn new_with_lots_of_belts( + name: String, + gen_info: GenerationInformation, progress: Arc, data_store: &DataStore, ) -> Self { let mut ret = GameState::new_with_world_area( + name, + gen_info, Position { x: 0, y: 0 }, Position { - x: 5_000, - y: 250_000, + x: 1_000, + y: 250_000_00, }, data_store, ); - let mut file = File::open("test_blueprints/lots_of_belts.bp").unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); + let s = get_const_string!("test_blueprints/iron_generation_test.bp"); let bp: BlueprintString = BlueprintString(s); let mut bp: Blueprint = Blueprint::try_from(bp).unwrap(); bp.optimize(); @@ -492,9 +411,9 @@ impl GameState GameState, - progress: Arc, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { // TODO: Correct size let mut ret = GameState::new_with_world_area( + name, + gen_info, Position { x: 0, y: 0 }, Position { x: 60000, y: 60000 }, data_store, @@ -530,22 +453,74 @@ impl GameState, + ) -> Self { + progress.push_stage(1.0, Some("Generating Chunks".to_string())); + // This is the maximum thickness the player can generate while at the edge of the world + // Factorio uses 20 chunks, but their chunks are twice as large in each dimension + const CHUNK_THICKNESS: i32 = 40; + + // TODO: Correct size + let ret = GameState::new_with_world_area( + name, + gen_info, + Position { + x: -1_000_000, + y: -1_000_000, + }, + Position { + x: 1_000_000, + y: (-1_000_000 + CHUNK_THICKNESS * 16), + }, + data_store, + ); + + let left = (-1_000_000..=(-1_000_000 + CHUNK_THICKNESS * 16)) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(x, y)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(left); + + let right = ((1_000_000 - CHUNK_THICKNESS * 16)..=1_000_000) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(x, y)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(right); + + let bottom = ((1_000_000 - CHUNK_THICKNESS * 16)..=1_000_000) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(y, x)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(bottom); + + progress.pop_stage(); + + ret + } + pub fn add_solar_field( &mut self, pos: Position, amount: Watt, height_in_tiles: Option, - progress: Arc, + progress: ProgressInfo, data_store: &DataStore, ) { - let mut file = File::open("test_blueprints/solar_tile.bp").unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); + let s = get_const_string!("test_blueprints/solar_tile.bp"); let bp: BlueprintString = BlueprintString(s); let mut bp: Blueprint = Blueprint::try_from(bp).unwrap(); - bp.optimize(); - let bp = bp.get_reusable(false, data_store); - let bp = bp.optimize(); + let poles = bp.extract_if(|action| { + matches!( + action, + BlueprintAction::PlaceEntity(BlueprintPlaceEntity::PowerPole { .. }) + ) + }); + let rest = bp; let bp_count = amount.0.div_ceil((Watt(42_000) * 104).0); @@ -558,38 +533,44 @@ impl GameState, data_store: &DataStore, ) -> Self { - let mut ret = GameState::new(data_store); + let mut ret = GameState::new(name, gen_info, data_store); let red = File::open("test_blueprints/eight_beacon_red_sci_with_storage.bp").unwrap(); let red: Blueprint = ron::de::from_reader(red).unwrap(); @@ -605,10 +586,14 @@ impl GameState, bp_path: impl AsRef, ) -> Self { let mut ret = GameState::new_with_world_area( + name, + gen_info, Position { x: 0, y: 0 }, Position { x: 32000, @@ -653,13 +638,30 @@ pub struct Factory { pub power_grids: PowerGridStorage, pub belts: BeltStore, pub storage_storage_inserters: StorageStorageInserterStore, - pub belt_storage_inserters: Box<[BTreeMap< - u16, - ( - crate::inserter::belt_storage_pure_buckets::BucketedStorageStorageInserterStoreFrontend, - crate::inserter::belt_storage_pure_buckets::BucketedStorageStorageInserterStore, - ), - >]>, + pub belt_storage_inserters: Box< + [( + ( + List< + { belt_storage_inserter::Dir::BeltToStorage }, + { belt_storage_inserter::Dir::BeltToStorage }, + >, + List< + { belt_storage_inserter::Dir::StorageToBelt }, + { belt_storage_inserter::Dir::BeltToStorage }, + >, + ), + ( + List< + { belt_storage_inserter::Dir::BeltToStorage }, + { belt_storage_inserter::Dir::StorageToBelt }, + >, + List< + { belt_storage_inserter::Dir::StorageToBelt }, + { belt_storage_inserter::Dir::StorageToBelt }, + >, + ), + )], + >, pub chests: FullChestStore, pub fluid_store: FluidSystemStore, @@ -672,7 +674,7 @@ pub struct Factory { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct StorageStorageInserterStore { - pub inserters: Box<[BTreeMap]>, + pub inserters: Box<[BTreeMap, (BucketedStorageStorageInserterStore,)>]>, } impl StorageStorageInserterStore { @@ -740,14 +742,16 @@ impl StorageStorageInserterStore { pub fn add_ins( &mut self, item: Item, - movetime: u16, + movetime: NonZero, start: Storage, dest: Storage, hand_size: ITEMCOUNTTYPE, data_store: &DataStore, ) -> InserterIdentifier { - let source = FakeUnionStorage::from_storage_with_statics_at_zero(item, start, data_store); - let dest = FakeUnionStorage::from_storage_with_statics_at_zero(item, dest, data_store); + let source = + FakeUnionStorage::from_storage_with_statics_at_zero(item, start, data_store).unwrap(); + let dest = + FakeUnionStorage::from_storage_with_statics_at_zero(item, dest, data_store).unwrap(); let id: InserterIdentifier = self.inserters[item.into_usize()] .entry(movetime) @@ -761,7 +765,7 @@ impl StorageStorageInserterStore { pub fn get_inserter( &self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, current_tick: u32, ) -> InserterStateInfo { @@ -775,50 +779,75 @@ impl StorageStorageInserterStore { pub fn remove_ins( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, - ) { + ) -> Result<(), WaitlistSearchSide> { let inserter = self.inserters[item.into_usize()] .get_mut(&movetime) .unwrap() .0 .remove_inserter(id); - // TODO: Handle what happens with the items + + match inserter { + Ok(_) => { + // TODO: Handle what happens with the items + Ok(()) + }, + Err(side) => Err(side), + } } #[must_use] pub fn change_movetime( &mut self, item: Item, - old_movetime: u16, - new_movetime: u16, + old_movetime: NonZero, + new_movetime: NonZero, id: InserterIdentifier, - ) -> InserterIdentifier { + ) -> Result< + InserterIdentifier, + ( + WaitlistSearchSide, + impl FnMut(FakeUnionStorage, FakeUnionStorage, ITEMCOUNTTYPE) -> InserterIdentifier, + ), + > { // FIXME: This does not preserve the inserter state at all! - let inserter = self.inserters[item.into_usize()] + let inner_id = match self.inserters[item.into_usize()] .get_mut(&old_movetime) .unwrap() .0 - .remove_inserter(id); - - let inner_id: InserterIdentifier = self.inserters[item.into_usize()] - .entry(new_movetime) - .or_insert_with(|| (BucketedStorageStorageInserterStore::new(new_movetime),)) - .0 - .add_inserter( - inserter.storage_id_in, - inserter.storage_id_out, - inserter.max_hand_size, - ); + .remove_inserter(id) + { + Ok(inserter) => self.inserters[item.into_usize()] + .entry(new_movetime) + .or_insert_with(|| (BucketedStorageStorageInserterStore::new(new_movetime),)) + .0 + .add_inserter( + inserter.storage_id_in, + inserter.storage_id_out, + inserter.max_hand_size, + ), + Err(wait_list_side) => { + return Err((wait_list_side, move |source, dest, max_hand_size| { + self.inserters[item.into_usize()] + .entry(new_movetime) + .or_insert_with( + || (BucketedStorageStorageInserterStore::new(new_movetime),), + ) + .0 + .add_inserter(source, dest, max_hand_size) + })); + }, + }; - inner_id + Ok(inner_id) } #[profiling::function] pub fn update_inserter_src( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, new_src: Storage, data_store: &DataStore, @@ -829,7 +858,8 @@ impl StorageStorageInserterStore { .0 .update_inserter_src( id, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store) + .unwrap(), ) } @@ -837,7 +867,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_dest( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, new_dest: Storage, data_store: &DataStore, @@ -848,7 +878,8 @@ impl StorageStorageInserterStore { .0 .update_inserter_dest( id, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store) + .unwrap(), ) } @@ -856,7 +887,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_src_if_equal( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, old_src: FakeUnionStorage, new_src: Storage, @@ -869,7 +900,8 @@ impl StorageStorageInserterStore { .update_inserter_src_if_equal( id, old_src, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store) + .unwrap(), ) } @@ -877,7 +909,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_dest_if_equal( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, old_dest: FakeUnionStorage, new_dest: Storage, @@ -890,7 +922,8 @@ impl StorageStorageInserterStore { .update_inserter_dest_if_equal( id, old_dest, - FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store), + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store) + .unwrap(), ) } } @@ -907,7 +940,7 @@ impl Factory Factory Factory Factory Factory= 120 { - belt.update(sushi_splitters); - } - belt.update_inserters(item_storages, grid_size); + profiling::scope!("belt_storage_exit.update"); + belt_storage_exit_outgoing.update( + item_id, + grid_size, + &mut belt_storage_reinsertion_incoming, + item_storages, + ); + storage_belt_exit_incoming.update( + item_id, + grid_size, + &mut storage_belt_reinsertion_outgoing, + item_storages, + ); } - } - - { - profiling::scope!("Update PurePure Inserters"); - for ( - ins, - ((source, source_pos), (dest, dest_pos), cooldown, filter), - ) in pure_to_pure_inserters.iter_mut().flatten() { - let [mut source_loc, mut dest_loc] = if *source == *dest { - assert_ne!( - source_pos, dest_pos, - "An inserter cannot take and drop off on the same tile" + if data_store.item_is_fluid[item_id] { + profiling::scope!( + "FluidSystem Update", + format!( + "Item: {}", + data_store.item_display_names[item_id] + ) + .as_str() ); - // We are taking and placing onto the same belt - let belt = &mut belt_store.belts[*source]; - - belt.get_two([(*source_pos).into(), (*dest_pos).into()]).map(|v| *v) - } else { - let [inp, out] = belt_store - .belts - .get_disjoint_mut([*source, *dest]) - .unwrap(); - - [*inp.get(*source_pos), *out.get(*dest_pos)] - }; - - if *cooldown == 0 { - ins.update_instant(&mut source_loc,&mut dest_loc); + for fluid_system in fluid_store { + // FIXME: Switch to holes + if let Some(fluid_system) = fluid_system { + update_fluid_system( + item_id, + &mut fluid_system.hot_data, + item_storages, + grid_size, + ); + } + } } else { - ins.update( - &mut source_loc, - &mut dest_loc, - *cooldown, - // FIXME: - 1, - (), - |_| { - filter - .map(|filter_item| filter_item == item) - .unwrap_or(true) - }, + profiling::scope!( + "StorageStorage Inserter Update", + format!( + "Item: {}", + data_store.item_display_names[item_id] + ) + .as_str() ); - } - - { - profiling::scope!("Update update_first_free_pos"); - if !source_loc { - let _: Option<_> = belt_store.belts[*source] - .remove_item(*source_pos); - } - - if dest_loc { - let _ = belt_store.belts[*dest] - .try_insert_item(*dest_pos, item); + for (ins_store,) in + storage_storage_inserter_stores.values_mut() + { + profiling::scope!( + "StorageStorage Inserter Update", + format!("Movetime: {}", ins_store.movetime) + .as_str() + ); + ins_store.update( + item_id, + item_storages, + grid_size, + current_tick, + ); } } } } } - { - - let item = Item { - id: item_id.try_into().unwrap(), - }; - - let grid_size = grid_size(item, data_store); - let num_recipes = num_recipes(item, data_store); - - if data_store.item_is_fluid[item_id] { - profiling::scope!( - "FluidSystem Update", - format!("Item: {}", data_store.item_display_names[item_id]) - .as_str() - ); - for fluid_system in fluid_store { - // FIXME: Switch to holes - if let Some(fluid_system) = fluid_system { - update_fluid_system(&mut fluid_system.hot_data, item_storages, grid_size); - - } - } - } else { - profiling::scope!( - "StorageStorage Inserter Update", - format!("Item: {}", data_store.item_display_names[item_id]) - .as_str() - ); - for (ins_store,) in storage_storage_inserter_stores.values_mut() - { - profiling::scope!( - "StorageStorage Inserter Update", - format!("Movetime: {}", ins_store.movetime).as_str() - ); - ins_store.update( - item_id, - item_storages, - grid_size, - current_tick, - ); - } - } - } - let pure_update_time = pure_update_start.elapsed(); *avg_time = *avg_time @@ -1182,17 +1196,24 @@ impl Factory, + }, + LoadSaveMenu { + save_files: SaveFileList, + }, + NewGameMenu { + new_game_name: String, gigabase_size: u16, }, Ingame, Loading { start_time: Instant, - /// WARNING: This is a f64! - progress: Arc, + progress: ProgressInfo, game_state_receiver: Receiver<(LoadedGame, Arc, Sender)>, + current_message: String, }, } @@ -1246,14 +1267,42 @@ impl GameState { - game_state.simulation_state.tech_state.current_technology = *tech; + ActionType::SpawnPlayer {} => { + log::info!("Player joined"); + game_state.world.players.push(PlayerInfo::default()); + }, + + ActionType::AddResearchToQueue { tech } => { + // TODO: Do not allow adding techs which are done? + if !game_state + .simulation_state + .tech_state + .research_queue + .contains(tech) + { + game_state + .simulation_state + .tech_state + .research_queue + .push(*tech); + + // TODO: Do I want a max length for the queue? + }; + }, + ActionType::RemoveResearchFromQueue { tech } => { + game_state + .simulation_state + .tech_state + .research_queue + .retain(|queued_tech| queued_tech != tech); }, ActionType::CheatUnlockTechnology { tech } => { - if game_state.simulation_state.tech_state.current_technology == Some(*tech) { - game_state.simulation_state.tech_state.current_technology = None; - } + game_state + .simulation_state + .tech_state + .research_queue + .retain(|queued_tech| queued_tech != tech); game_state .simulation_state .tech_state @@ -1340,10 +1389,7 @@ impl GameState { if let Some((item, index)) = item { let removed_items = @@ -1364,70 +1410,184 @@ impl GameState { - game_state - .world - .get_entity_at_mut(*pos, data_store) - .map(|e| match e { + let e = game_state.world.get_entity_at_mut(*pos, data_store); + + if let Some(e) = e { + match e { Entity::Inserter { ty, user_movetime, info, + pos: inserter_pos, + direction, .. - } => { - match info { - InserterInfo::NotAttached {} => {}, - InserterInfo::Attached { info } => match info { - AttachedInserter::BeltStorage { id, belt_pos } => { - game_state - .simulation_state - .factory - .belts - .change_inserter_movetime( - *id, - *belt_pos, - new_movetime.map(|v| v.into()).unwrap_or( - data_store.inserter_infos[*ty as usize] - .swing_time_ticks, - ), - ); - }, - AttachedInserter::BeltBelt { item, inserter } => todo!(), - AttachedInserter::StorageStorage { item, inserter } => { - let old_movetime = - user_movetime.map(|v| v.into()).unwrap_or( - data_store.inserter_infos[*ty as usize] - .swing_time_ticks, - ); - - let new_movetime = + } => match info { + InserterInfo::NotAttached {} => {}, + InserterInfo::Attached { info } => match info { + AttachedInserter::BeltStorage { id, belt_pos } => { + game_state + .simulation_state + .factory + .belts + .change_inserter_movetime( + *id, + *belt_pos, new_movetime.map(|v| v.into()).unwrap_or( data_store.inserter_infos[*ty as usize] .swing_time_ticks, - ); + ), + ); + }, + AttachedInserter::BeltBelt { .. } => todo!(), + AttachedInserter::StorageStorage { item, inserter } => { + let old_movetime = user_movetime.unwrap_or( + data_store.inserter_infos[*ty as usize] + .swing_time_ticks, + ); - if old_movetime != new_movetime { - let new_id = game_state - .simulation_state - .factory - .storage_storage_inserters - .change_movetime( - *item, - old_movetime.into(), - new_movetime.into(), - *inserter, - ); + let new_movetime = + new_movetime.map(|v| v.into()).unwrap_or( + data_store.inserter_infos[*ty as usize] + .swing_time_ticks, + ); - *inserter = new_id; - } - }, + if old_movetime != new_movetime { + let new_id = match game_state + .simulation_state + .factory + .storage_storage_inserters + .change_movetime( + *item, + old_movetime.into(), + new_movetime.into(), + *inserter, + ) { + Ok(new_id) => new_id, + Err((search_side, mut handle_fn)) => { + match search_side { + WaitlistSearchSide::Source => { + let start_pos = data_store + .inserter_start_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at( + start_pos, data_store, + ) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, + .. + } => { + let removed = + game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores + .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); + let Conn::Storage { + storage_id_in, + storage_id_out, + .. + } = removed.conn + else { + unreachable!() + }; + handle_fn( + storage_id_in, + storage_id_out, + removed.max_hand.into(), + ) + }, + + e => unreachable!("{e:?}"), + } + }, + WaitlistSearchSide::Dest => { + let end_pos = data_store + .inserter_end_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at(end_pos, data_store) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, + .. + } => { + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores + .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); + let Conn::Storage { + index, + storage_id_in, + storage_id_out, + } = removed.conn + else { + unreachable!() + }; + handle_fn( + storage_id_in, + storage_id_out, + removed.max_hand.into(), + ) + }, + + e => unreachable!("{e:?}"), + } + }, + } + }, + }; + + let Some(Entity::Inserter { + user_movetime, + info: + InserterInfo::Attached { + info: + AttachedInserter::StorageStorage { + inserter, + .. + }, + }, + .. + }) = game_state + .world + .get_entity_at_mut(*pos, data_store) + else { + unreachable!(); + }; + *user_movetime = Some(new_movetime.try_into().unwrap()); + *inserter = new_id; + } }, - } - *user_movetime = *new_movetime; + }, }, _ => { warn!("Tried to set Inserter Settings on non inserter"); }, - }); + } + } else { + warn!("Tried to set Inserter Settings on empty tile"); + } }, ActionType::PlaceEntity(place_entity_info) => { let force = place_entity_info.force; @@ -1478,7 +1638,7 @@ impl GameState GameState GameState GameState = game_state + let connection_candidates = game_state .world .get_power_poles_which_could_connect_to_pole_at( pole_pos, @@ -1620,42 +1785,47 @@ impl GameState { // Handle storage updates for storage_update in storage_updates { - let mut entity_size = None; - game_state.world.get_entity_at_mut(storage_update.position, data_store).map(|e| { - match (e, storage_update.new_pg_entity.clone()) { - (Entity::Assembler { ty, pos: _, info: AssemblerInfo::Powered { id, pole_position: _, weak_index: _ }, modules: _, rotation }, crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index }) => { - entity_size = Some(data_store.assembler_info[usize::from(*ty)].size(*rotation)); + let entity = game_state.world.get_entity_at_mut(storage_update.position, data_store).expect(&format!("Did not find entity at {:?} for storage update", storage_update.position)); + + let entity_size = match (entity, storage_update.new_pg_entity.clone()) { + (Entity::Assembler { + ty, + pos: _, + info: AssemblerInfo::Powered { id, pole_position: _, weak_index: _ }, modules: _, rotation }, + crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index }) => { assert_eq!(id.recipe, recipe); id.grid = storage_update.new_grid; id.assembler_index = index; // FIXME: Store and update the weak_index + + data_store.assembler_info[usize::from(*ty)].size(*rotation) }, (Entity::Lab { pos: _, ty, modules: _, pole_position: Some((_pole_pos, _weak_idx, lab_store_index)) }, crate::power::power_grid::PowerGridEntity::Lab { ty: _, index: new_idx }) => { - entity_size = Some(data_store.lab_info[usize::from(*ty)].size); *lab_store_index = new_idx; // The weak index stays the same since it it still connected to the same power pole + + data_store.lab_info[usize::from(*ty)].size } (_, _) => todo!("Handler storage_update {storage_update:?}") - } - }); + }; // FIXME: Rotation - let e_size = entity_size.unwrap(); + let e_size = entity_size; let inserter_range = data_store.max_inserter_search_range; @@ -1698,11 +1868,11 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store).unwrap(); + }; game_state.simulation_state.factory.belts.update_belt_storage_inserter_src(*id, *belt_pos, game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); }, AttachedInserter::BeltBelt { .. } => { @@ -1712,11 +1882,11 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(*item, data_store).unwrap(); + }; let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); @@ -1736,11 +1906,11 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store).unwrap(); + }; game_state.simulation_state.factory.belts.update_belt_storage_inserter_dest(*id, *belt_pos, game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); }, AttachedInserter::BeltBelt { .. } => { @@ -1750,11 +1920,11 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(*item, data_store).unwrap(); + }; let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); let new_id = game_state.simulation_state.factory.storage_storage_inserters.update_inserter_dest(*item, movetime.into(), *inserter, new_storage, data_store); @@ -1765,34 +1935,30 @@ impl GameState { + Entity::FluidTank { pos, .. } => { let id: FluidSystemId<_> = game_state.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; if let Some(fluid) = id.fluid { - let storage = match storage_update.new_pg_entity { - crate::power::power_grid::PowerGridEntity::Assembler { ty, recipe, index } => Storage::Assembler { grid: storage_update.old_grid, recipe_idx_with_this_item: recipe.id, index }, - crate::power::power_grid::PowerGridEntity::Lab { ty, index } => Storage::Lab { grid: storage_update.old_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + let storage = match storage_update.old_pg_entity { + crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index } => Storage::Assembler { grid: storage_update.old_grid, recipe_idx_with_this_item: recipe.id, index }, + crate::power::power_grid::PowerGridEntity::Lab { ty: _, index } => Storage::Lab { grid: storage_update.old_grid, index }, + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), }; - dbg!(storage); - let Some(translated_storage) = storage.translate(fluid, data_store) else { + let Ok(old_storage) = FakeUnionStorage::from_storage_with_statics_at_zero(fluid, storage, data_store) else { return ControlFlow::Continue(()); }; - dbg!(translated_storage); - let old_storage = FakeUnionStorage::from_storage_with_statics_at_zero(fluid, translated_storage, data_store); - dbg!(old_storage); let new_storage = match storage_update.new_pg_entity { - crate::power::power_grid::PowerGridEntity::Assembler { ty, recipe, index } => Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, - crate::power::power_grid::PowerGridEntity::Lab { ty, index } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index } => Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, + crate::power::power_grid::PowerGridEntity::Lab { ty: _, index } => Storage::Lab { grid: storage_update.new_grid, index }, + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), - }.translate(fluid, data_store).unwrap(); - game_state.simulation_state.factory.fluid_store.update_fluid_conn_if_needed(*pos, old_storage, FakeUnionStorage::from_storage_with_statics_at_zero(fluid, new_storage, data_store)); + }; + game_state.simulation_state.factory.fluid_store.update_fluid_conn_if_needed(*pos, old_storage, FakeUnionStorage::from_storage_with_statics_at_zero(fluid, new_storage, data_store).unwrap()); } }, @@ -2079,10 +2245,11 @@ impl GameState GameState GameState ()>, @@ -2534,10 +2699,10 @@ impl GameState GameState GameState { - // TODO: - // todo!(); + // TODO: Currently I insert modules into beacons on placement instead of doing this. }, _ => { warn!( @@ -2704,10 +2868,10 @@ impl GameState GameState { game_state.world.ore_lookup.add_ore(*pos, *ore, *amount); + game_state + .world + .map_updates + .get_or_insert_default() + .push(*pos); }, } @@ -2802,22 +2971,308 @@ impl GameState, Vec<_>) = simulation_state + .factory + .belt_storage_inserters + .iter_mut() + .map(|belt_storage_inserter_lists| { + let (belt_to_storage_flow, storage_to_belt_flow) = belt_storage_inserter_lists; + + let (outgoing_belt_to_storage, incoming_belt_to_storage) = belt_to_storage_flow; + let (incoming_storage_to_belt, outgoing_storage_to_belt) = storage_to_belt_flow; + + let (belt_storage_exit_outgoing, belt_storage_reinsertion_outgoing) = + outgoing_belt_to_storage.get(); + let (belt_storage_exit_incoming, belt_storage_reinsertion_incoming) = + incoming_belt_to_storage.get(); + + let (storage_belt_exit_outgoing, storage_belt_reinsertion_outgoing) = + outgoing_storage_to_belt.get(); + let (storage_belt_exit_incoming, storage_belt_reinsertion_incoming) = + incoming_storage_to_belt.get(); + + ( + ( + belt_storage_exit_incoming, + belt_storage_reinsertion_outgoing, + storage_belt_exit_outgoing, + storage_belt_reinsertion_incoming, + ), + ( + belt_storage_exit_outgoing, + belt_storage_reinsertion_incoming, + storage_belt_reinsertion_outgoing, + storage_belt_exit_incoming, + ), ) - }, - || { - simulation_state.factory.power_grids.update( - &simulation_state.tech_state, - aux_data.current_tick as u32, - data_store, + }) + .unzip(); + + let mut ret = None; + + { + // profiling::scope!("Power Grid, Chest, Mining Drill Stage"); + + rayon::scope(|scope| { + simulation_state + .factory + .belts + .inner + .smart_belts + .iter_mut() + .zip( + simulation_state + .factory + .belts + .inner + .belt_belt_inserters + .pure_to_pure_inserters + .iter_mut(), ) - } - ); + .zip(belt_stuff) + .zip(simulation_state.factory.item_times.iter_mut()) + .enumerate() + // Queue long running items first, so we do not end up waiting for a long running item at the end, which we could have started early on + // This alone is ~10% faster on a test game I have + .sorted_unstable_by_key(|&(_, (_, &mut avg_time))| -(avg_time * 1000.0) as i32) + .for_each( + |( + item_id, + (((belt_store, pure_to_pure_inserters), belt_stuff), _avg_time), + )| scope.spawn(move |_| { + profiling::scope!( + "Pure Belt Update", + format!("Item: {}", data_store.item_display_names[item_id]).as_str() + ); + + let item = Item { + id: item_id.try_into().unwrap(), + }; + + let (belt_storage_exit_incoming, mut belt_storage_reinsertion_outgoing, storage_belt_exit_outgoing, mut storage_belt_reinsertion_incoming) = belt_stuff; + + + { + profiling::scope!("storage_belt_exit.update"); + // TODO: Rust-analyzer does not understand the const generics here, and thinks I am calling a different function + // than rustc. This does compile fine. + belt_storage_exit_incoming.update( + &mut belt_storage_reinsertion_outgoing, + belt_store.belts.as_mut_slice(), + ); + storage_belt_exit_outgoing.update( + &mut storage_belt_reinsertion_incoming, + belt_store.belts.as_mut_slice(), + ); + } + { + profiling::scope!( + "Update Belts", + format!("Count: {}", belt_store.belts.len()) + ); + // Belt update in parallel + // TODO: This is significantly better in parallel, for reasons I do not fully understand. + // The profiler indicates that belt updates are not a huge part of update times + let reinsertion = belt_store + .belts + .par_iter_mut() + .zip(belt_store.belt_ty.par_iter()) + .enumerate().filter_map(|(self_index, (belt, ty))| { + // Only update belts, which have moved according to their timer + // This is what makes some types of belts different speed from others + (update_timers[usize::from(*ty)] >= 120).then_some((self_index, belt)) + }).map(|(self_index, belt)| { + // Update a belt + belt.update(sushi_splitters); + belt.update_inserters_lazy().into_iter().flatten().zip(iter::repeat(self_index)) + }) + .fold(|| vec![], |mut v, reinsertions| { + v.extend(reinsertions); + v + }) + .collect_vec_list().into_iter().flatten().flatten(); + + // Do the reinsertion sequentially + for (ins, self_index) in reinsertion { + let self_index = self_index as u32; + let in_movement = BeltStorageInserterInMovement { + movetime: ins.movetime, + storage: ins.storage, + belt: self_index, + belt_pos: ins.belt_pos, + max_hand_size: ins.max_hand_size, + current_hand: ins.current_hand, + }; + if ins.outgoing { + belt_storage_reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); + } else { + storage_belt_reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); + } + } + } + + { + profiling::scope!("Update PurePure Inserters"); + for (ins, ((source, source_pos), (dest, dest_pos), cooldown, filter)) in + pure_to_pure_inserters.iter_mut().flatten() + { + let [mut source_loc, mut dest_loc] = if *source == *dest { + assert_ne!( + source_pos, dest_pos, + "An inserter cannot take and drop off on the same tile" + ); + // We are taking and placing onto the same belt + let belt = &mut belt_store.belts[*source]; + + belt.get_two([(*source_pos).into(), (*dest_pos).into()]) + .map(|v| *v) + } else { + let [inp, out] = + belt_store.belts.get_disjoint_mut([*source, *dest]).unwrap(); + + [*inp.get(*source_pos), *out.get(*dest_pos)] + }; + + if *cooldown == 0 { + ins.update_instant(&mut source_loc, &mut dest_loc); + } else { + ins.update( + &mut source_loc, + &mut dest_loc, + *cooldown, + // FIXME: + 1, + (), + |_| { + filter + .map(|filter_item| filter_item == item) + .unwrap_or(true) + }, + ); + } + + { + profiling::scope!("Update update_first_free_pos"); + if !source_loc { + let _: Option<_> = + belt_store.belts[*source].remove_item(*source_pos); + } + + if dest_loc { + let _ = + belt_store.belts[*dest].try_insert_item(*dest_pos, item); + } + } + } + } + }) + ); + + scope.spawn(|_| { + ret = Some(join!( + || simulation_state.factory.chests.update(data_store), + || { + simulation_state.factory.ore_store.update( + &simulation_state.tech_state.mining_productivity_by_item, + data_store, + ) + }, + || { + simulation_state.factory.power_grids.update( + &simulation_state.tech_state, + aux_data.current_tick as u32, + &mut simulation_state.factory.storage_storage_inserters, + assembler_stuff.as_mut_slice(), + data_store, + ) + } + )); + }); + }) + }; + + let (reinsertions, mining_production_by_item, (tech_progress, recipe_tick_info, lab_info)) = + ret.unwrap(); + + { + profiling::scope!("Chest inserter waitlist reinsertion"); + reinsertions + .zip( + simulation_state + .factory + .storage_storage_inserters + .inserters + .par_iter_mut() + .zip( + simulation_state + .factory + .belt_storage_inserters + .par_iter_mut(), + ), + ) + .for_each(|(reinsertions, (store, belt_storage))| { + for inserter in reinsertions { + match inserter.conn { + crate::assembler::simd::Conn::Storage { + index, + storage_id_in, + storage_id_out, + } => { + let store = store.get_mut(&inserter.movetime).unwrap(); + let ins = InserterBucketData { + storage_id_in, + storage_id_out, + index, + current_hand: inserter.current_hand, + max_hand_size: inserter.max_hand, + }; + if inserter.current_hand == 0 { + store.0.reinsert_empty(ins); + } else { + store.0.reinsert_empty(ins); + } + }, + crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_storage, + self_is_source, + } => { + let ((_a, belt_storage), (_c, storage_belt)) = belt_storage; + + if self_is_source { + storage_belt.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: inserter.max_hand, + movetime: inserter.movetime.try_into().unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } else { + belt_storage.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: 0, + movetime: inserter.movetime.try_into().unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } + }, + } + } + }); + } aux_data.statistics.append_single_set_of_samples(( ProductionInfo::from_recipe_info_and_per_item( @@ -2848,6 +3303,16 @@ impl GameState impl Strategy>> { Just(vec![ ActionType::PlaceEntity(PlaceEntityInfo { @@ -3207,7 +3670,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..1_000, &DATA_STORE)) { - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3216,7 +3679,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash_after(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..100, &DATA_STORE), time in 0usize..10) { - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3230,7 +3693,7 @@ mod tests { } #[test] - fn test_beacons_always_effect(actions in beacon_test_val().prop_shuffle()) { + fn test_beacons_always_effect(actions in beacon_test_val().prop_shuffle(), ticks in 60usize..120) { prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { @@ -3246,11 +3709,12 @@ mod tests { // .. // }))); - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); Blueprint { actions: actions.into_iter().map(|a| BlueprintAction::from_with_datastore(&a, &*DATA_STORE)).collect() }.apply(false, Position { x: 0, y: 0 }, &mut game_state, &DATA_STORE); - for _ in 0usize..10 { + // Currently Beacons are only able to update every 60 ticks + for _ in 0usize..ticks { GameState::update( &mut *game_state.simulation_state.lock(), &mut *game_state.aux_data.lock(), @@ -3259,20 +3723,25 @@ mod tests { } let world = game_state.world.lock(); - let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, .. }) = world.get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE) else { + let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, modules, .. }) = world.get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE) else { unreachable!("{:?}", game_state.world.lock().get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE)); }; + let modules = &world.module_slot_dedup_table[*modules as usize]; + let id = *id; - std::mem::drop(world); + // std::mem::drop(world); - prop_assume!(game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); + prop_assert!(game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); let info = game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].get_assembler_info(id, &DATA_STORE); - prop_assert!((info.power_consumption_mod - 0.7).abs() < 1.0e-6, "power_consumption_mod: {:?}", info.power_consumption_mod); + let module_power_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].power_mod as f32 / 20.0).sum::(); + let module_speed_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].speed_mod as f32 / 20.0).sum::(); + let module_prod_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].prod_mod as f32 / 100.0).sum::(); + prop_assert!((info.power_consumption_mod - (0.7 + module_power_effect)).abs() < 1.0e-6, "power_consumption_mod: {:?}, wanted: {}", info.power_consumption_mod, (0.7 + module_power_effect)); prop_assert!((info.base_speed - 1.25).abs() < 1.0e-6, "base_speed: {:?}", info.base_speed); - prop_assert!((info.prod_mod - 0.0).abs() < 1.0e-6, "prod_mod: {:?}", info.prod_mod); - prop_assert!((info.speed_mod - (0.5)).abs() < 1.0e-6, "speed_mod: {:?}", info.speed_mod); + prop_assert!((info.prod_mod - (0.0 + module_prod_effect)).abs() < 1.0e-6, "prod_mod: {:?}", info.prod_mod); + prop_assert!((info.speed_mod - (0.5 + module_speed_effect)).abs() < 1.0e-6, "speed_mod: {:?}, wanted: {}", info.speed_mod, (0.5 + module_speed_effect)); prop_assert_eq!(info.base_power_consumption, Watt(375_000), "base_power_consumption: {:?}", info.base_power_consumption); } @@ -3324,132 +3793,132 @@ mod tests { // } } - #[bench] - fn bench_single_inserter(b: &mut Bencher) { - let mut game_state = GameState::new(&DATA_STORE); - - let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::PowerPole { - pos: Position { x: 0, y: 5 }, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::SolarPanel { - pos: Position { x: 0, y: 2 }, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Assembler { - pos: Position { x: 0, y: 6 }, - ty: 0, - rotation: Dir::North, - }, - ), - })]); - - rep.append_actions(vec![ActionType::SetRecipe( - crate::frontend::action::set_recipe::SetRecipeInfo { - pos: Position { x: 0, y: 6 }, - recipe: Recipe { id: 0 }, - }, - )]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Belt { - pos: Position { x: 1, y: 4 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Belt { - pos: Position { x: 2, y: 4 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Inserter { - ty: 0, - pos: Position { x: 1, y: 5 }, - dir: crate::frontend::world::tile::Dir::North, - filter: None, - user_movetime: None, - }, - ), - })]); - - let blueprint = Blueprint::from_replay(&rep); - - blueprint.apply( - false, - Position { x: 1600, y: 1600 }, - &mut game_state, - &DATA_STORE, - ); - - dbg!( - &game_state - .world - .lock() - .get_chunk_for_tile(Position { x: 1600, y: 1600 }) - ); - - dbg!(game_state.aux_data.lock().current_tick); - - for _ in 0..600 { - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - &DATA_STORE, - ); - } + // #[bench] + // fn bench_single_inserter(b: &mut Bencher) { + // let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); + + // let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::PowerPole { + // pos: Position { x: 0, y: 5 }, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::SolarPanel { + // pos: Position { x: 0, y: 2 }, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Assembler { + // pos: Position { x: 0, y: 6 }, + // ty: 0, + // rotation: Dir::North, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::SetRecipe( + // crate::frontend::action::set_recipe::SetRecipeInfo { + // pos: Position { x: 0, y: 6 }, + // recipe: Recipe { id: 0 }, + // }, + // )]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Belt { + // pos: Position { x: 1, y: 4 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Belt { + // pos: Position { x: 2, y: 4 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Inserter { + // ty: 0, + // pos: Position { x: 1, y: 5 }, + // dir: crate::frontend::world::tile::Dir::North, + // filter: None, + // user_movetime: None, + // }, + // ), + // })]); + + // let blueprint = Blueprint::from_replay(&rep); + + // blueprint.apply( + // false, + // Position { x: 1600, y: 1600 }, + // &mut game_state, + // &DATA_STORE, + // ); - b.iter(|| { - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - &DATA_STORE, - ); - }); + // dbg!( + // &game_state + // .world + // .lock() + // .get_chunk_for_tile(Position { x: 1600, y: 1600 }) + // ); - dbg!(game_state.aux_data.lock().current_tick); - - assert!( - game_state - .aux_data - .lock() - .statistics - .production - .total - .as_ref() - .unwrap() - .items_produced[0] - > 0 - ); - } + // dbg!(game_state.aux_data.lock().current_tick); + + // for _ in 0..600 { + // GameState::update( + // &mut *game_state.simulation_state.lock(), + // &mut *game_state.aux_data.lock(), + // &DATA_STORE, + // ); + // } + + // b.iter(|| { + // GameState::update( + // &mut *game_state.simulation_state.lock(), + // &mut *game_state.aux_data.lock(), + // &DATA_STORE, + // ); + // }); + + // dbg!(game_state.aux_data.lock().current_tick); + + // assert!( + // game_state + // .aux_data + // .lock() + // .statistics + // .production + // .total + // .as_ref() + // .unwrap() + // .items_produced[0] + // > 0 + // ); + // } } diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index a92f38b..a710611 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -1,17 +1,18 @@ use std::cmp::{max, min}; -use std::{array, iter, u8}; +use std::{array, iter, u8, u16}; use itertools::Itertools; use log::warn; +use crate::assembler::arrays; +use crate::assembler::simd::{InserterReinsertionInfo, InserterWaitList}; use crate::assembler::{PowerUsageInfo, TIMERTYPE}; use crate::data::{DataStore, ItemRecipeDir}; use crate::frontend::world::Position; use crate::item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Recipe}; use crate::power::Watt; use crate::power::power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridIdentifier}; - -use crate::assembler::arrays; +use crate::storage_list::MaxInsertionLimit; use crate::WeakIdxTrait; @@ -29,6 +30,7 @@ pub struct MultiAssemblerStore< const NUM_INGS: usize, const NUM_OUTPUTS: usize, > { + current_power_tick: u32, recipe: Recipe, num_by_types: Vec, @@ -81,21 +83,14 @@ impl recipe_outs: &[ITEMCOUNTTYPE; NUM_OUTPUTS], power_subticks_per_craft: TIMERTYPE, - power_subtick_overshoot: u8, + ticks_passed: u16, ) -> (NextUpdateInfo, bool, bool, bool) { - let ticks_per_main: u16 = - power_subticks_per_craft * 20 / u16::from(data.combined_speed_mod); + let ticks_per_main: u16 = (power_subticks_per_craft as u32 * 20 + / u32::from(data.combined_speed_mod)) + .try_into() + .unwrap(); - let tick_till_main_done: u16 = ticks_per_main - data.timer; - - let power_subticks_passed = if data.bonus_productivity > 0 { - let ticks_per_prod: u16 = ticks_per_main * 100 / u16::from(data.bonus_productivity); - let tick_till_prod_done: u16 = ticks_per_prod - data.prod_timer; - - min(tick_till_main_done, tick_till_prod_done) + u16::from(power_subtick_overshoot) - } else { - tick_till_main_done + u16::from(power_subtick_overshoot) - }; + let power_subticks_passed = ticks_passed; let (is_idle, main_produced, prod_produced) = Self::apply_subticks( &mut ings, @@ -108,7 +103,7 @@ impl ); let next_update = if !is_idle { - Self::get_next_update_info_running(power_subticks_per_craft, data) + Self::get_next_update_info_running(ticks_per_main, data) } else { // We are waiting for ingredients or space // Check again next tick @@ -129,8 +124,10 @@ impl power_subticks_per_craft: TIMERTYPE, num_subticks: TIMERTYPE, ) -> (bool, bool, bool) { - let ticks_per_main: u16 = - power_subticks_per_craft * 20 / u16::from(data.combined_speed_mod); + let ticks_per_main: u16 = (power_subticks_per_craft as u32 * 20 + / u32::from(data.combined_speed_mod)) + .try_into() + .unwrap(); if ings .into_iter() @@ -145,10 +142,13 @@ impl data.prod_timer += num_subticks; let (main_done, prod_done) = ( - (data.timer >= ticks_per_main).then_some(data.timer - ticks_per_main), + data.timer.checked_sub(ticks_per_main), if data.bonus_productivity > 0 { - let ticks_per_prod: u16 = ticks_per_main * 100 / u16::from(data.bonus_productivity); - (data.prod_timer >= ticks_per_prod).then_some(data.prod_timer - ticks_per_prod) + let ticks_per_prod: u16 = (u32::from(ticks_per_main) * 100 + / u32::from(data.bonus_productivity)) + .try_into() + .unwrap(); + data.prod_timer.checked_sub(ticks_per_prod) } else { None }, @@ -163,8 +163,8 @@ impl for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { debug_assert!(**ing >= *recipe); } - data.timer += num_subticks; - data.prod_timer += num_subticks; + // data.timer += num_subticks; + // data.prod_timer += num_subticks; (true, false, false) }, (None, Some(prod_overshoot)) => { @@ -175,13 +175,14 @@ impl } data.prod_timer = prod_overshoot; - data.timer += num_subticks; + // data.timer += num_subticks; (true, false, true) } else { // No space (The last tick, the timer will not increase) - data.timer += num_subticks - prod_overshoot - 1; - data.prod_timer += num_subticks - prod_overshoot - 1; + // data.timer += num_subticks - prod_overshoot - 1; + // data.prod_timer += num_subticks - prod_overshoot - 1; + data.prod_timer -= prod_overshoot + 1; // Since prod_timer is right below ticks_per_prod we will check again next tick by get_next_update_info_running @@ -202,13 +203,15 @@ impl } data.timer = main_overshoot; - data.prod_timer += num_subticks; + // data.prod_timer += num_subticks; (true, true, false) } else { // No space (The last tick, the timer will not increase) - data.timer += num_subticks - main_overshoot - 1; - data.prod_timer = num_subticks - main_overshoot - 1; + // data.timer += num_subticks - main_overshoot - 1; + // data.prod_timer = num_subticks - main_overshoot - 1; + data.timer -= main_overshoot + 1; + data.prod_timer -= main_overshoot + 1; // Since prod_timer is right below ticks_per_prod we will check again next tick by get_next_update_info_running @@ -217,38 +220,57 @@ impl } }, (Some(main_overshoot), Some(prod_overshoot)) => { - let first_overshoot = max(main_overshoot, prod_overshoot); - - let (idle, main_done, prod_done) = Self::apply_subticks( - ings, - outs, - data, - recipe_ings, - recipe_outs, - power_subticks_per_craft, - num_subticks - first_overshoot, - ); + let main_is_first_done = main_overshoot < prod_overshoot; - let rest = min(main_overshoot, prod_overshoot); - - let (second_idle, second_main_done, second_prod_done) = Self::apply_subticks( - ings, - outs, - data, - recipe_ings, - recipe_outs, - power_subticks_per_craft, - rest, - ); + if check_space(outs) { + for (out, recipe) in outs.into_iter().zip(recipe_outs) { + **out += *recipe; + } + if main_is_first_done { + for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { + debug_assert!(**ing >= *recipe); + **ing -= *recipe; + } + } + if check_space(outs) { + for (out, recipe) in outs.into_iter().zip(recipe_outs) { + **out += *recipe; + } + if !main_is_first_done { + for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { + debug_assert!(**ing >= *recipe); + **ing -= *recipe; + } + } + + data.timer = main_overshoot; + data.prod_timer = prod_overshoot; + + (true, true, true) + } else { + if main_is_first_done { + data.timer = main_overshoot; + data.prod_timer -= (prod_overshoot - main_overshoot) + 1; + } else { + data.prod_timer = prod_overshoot; + data.timer -= (main_overshoot - prod_overshoot) + 1; + } + + (false, main_is_first_done, !main_is_first_done) + } + } else { + let overshoot = max(main_overshoot, prod_overshoot); + data.timer -= overshoot + 1; + data.prod_timer -= overshoot + 1; - debug_assert!(!(main_done && second_main_done)); - debug_assert!(!(prod_done && second_prod_done)); + (false, false, false) + } - ( - !idle || !second_idle, - main_done && second_main_done, - prod_done && second_prod_done, - ) + // ( + // !idle || !second_idle, + // main_done && second_main_done, + // prod_done && second_prod_done, + // ) }, }; @@ -259,12 +281,14 @@ impl power_subticks_per_main: u16, data: &AssemblerDataStruct, ) -> NextUpdateInfo { - let tick_till_main_done: u16 = power_subticks_per_main - data.timer; + let tick_till_main_done: u16 = power_subticks_per_main.checked_sub(data.timer).unwrap(); let when = if data.bonus_productivity > 0 { - let subticks_per_prod: u16 = - power_subticks_per_main * 100 / u16::from(data.bonus_productivity); - let tick_till_prod_done: u16 = subticks_per_prod - data.prod_timer; + let subticks_per_prod: u16 = (u32::from(power_subticks_per_main) * 100 + / u32::from(data.bonus_productivity)) + .try_into() + .unwrap(); + let tick_till_prod_done: u16 = subticks_per_prod.checked_sub(data.prod_timer).unwrap(); min(tick_till_main_done, tick_till_prod_done) } else { @@ -272,6 +296,7 @@ impl }; NextUpdateInfo { when } + // NextUpdateInfo { when: 1 } } } @@ -286,6 +311,8 @@ struct AssemblerDataStruct { bonus_productivity: u8, combined_speed_mod: u8, ty: u8, + + last_update_time: u32, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] @@ -303,6 +330,7 @@ impl( recipe: Recipe, + _grid: PowerGridIdentifier, data_store: &DataStore, ) -> Self { let ticks_per_craft = data_store.recipe_timers[recipe.into_usize()]; @@ -319,12 +347,13 @@ impl( + &mut self, + _new_grid_id: PowerGridIdentifier, + _data_store: &DataStore, + ) { + // Do nothing + } + fn get_recipe(&self) -> Recipe { self.recipe } @@ -358,6 +395,41 @@ impl 0 { + if let Ok(ticks_per_prod) = u16::try_from( + ticks_per_main as u32 * 100 + / u32::from(self.hot_data[index as usize].bonus_productivity), + ) { + f32::from(self.hot_data[index as usize].prod_timer + ticks_passed) + / f32::from(ticks_per_prod) + } else { + // TODO: This is bad, since productivity no longer works here + 0.0 + } + } else { + 0.0 + }; + super::AssemblerOnclickInfo { inputs: self .ings @@ -393,10 +465,9 @@ impl, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait, { + self.current_power_tick += u32::from(power_mult); let recipe = self.get_recipe(); assert!(power_mult <= MAX_POWER_MULT); - #[cfg(debug_assertions)] - { - assert!( - self.buckets - .waiting_for_update - .iter() - .flat_map(|v| v.iter()) - .all_unique(), - "Some Assembler in multiple update buckets" - ); - assert_eq!( - self.hot_data.len(), - self.buckets - .waiting_for_update - .iter() - .map(|v| v.len()) - .sum::(), - "Some Assembler are not being updated!" - ); - } + // #[cfg(debug_assertions)] + // { + // assert!( + // self.buckets + // .waiting_for_update + // .iter() + // .flat_map(|v| v.iter()) + // .all_unique(), + // "Some Assembler in multiple update buckets" + // ); + // assert_eq!( + // self.hot_data.len(), + // self.buckets + // .waiting_for_update + // .iter() + // .map(|v| v.len()) + // .sum::(), + // "Some Assembler are not being updated!" + // ); + // } let (ing_idx, out_idx) = recipe_lookup[recipe.into_usize()]; @@ -553,7 +641,9 @@ impl 0 { + // dbg!(&power_used); + } ( PowerUsageInfo::ByType( @@ -625,6 +736,7 @@ impl ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], - [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], + [&mut [u8]; NUM_INGS], + [(&mut [InserterWaitList], &mut [u8]); NUM_INGS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], - ) { ( - ( - self.ings_max_insert.each_mut().map(|b| &**b), - self.ings.each_mut().map(|b| &mut **b), - ), - self.outputs.each_mut().map(|b| &mut **b), - ) + [&mut [u8]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [u8]); NUM_OUTPUTS], + ), + ) { + // ( + // ( + // self.ings_max_insert.each_mut().map(|b| match b.get(0) { + // Some(v) => MaxInsertionLimit::Global(*v), + // None => PANIC_ON_INSERT, + // }), + // self.ings.each_mut().map(|b| &mut **b), + // ), + // self.outputs.each_mut().map(|b| &mut **b), + // ) + todo!() } fn modify_modifiers( @@ -726,6 +846,7 @@ impl usize { + self.hot_data.len() - self.holes.len() + } + + fn remove_wait_list_inserter( + &mut self, + _index: u32, + _item: crate::item::Item, + _info: crate::chest::WaitingInserterRemovalInfo, + _data_store: &DataStore, + ) -> super::simd::InserterReinsertionInfo { + unreachable!() + } } diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 6c35fa2..5626cbd 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -1,6 +1,8 @@ use std::{array, marker::PhantomData, simd::Simd, u8}; +use crate::assembler::simd::{InserterReinsertionInfo, InserterWaitList}; use crate::frontend::world::tile::ModuleTy; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, frontend::world::{Position, tile::AssemblerID}, @@ -62,6 +64,9 @@ pub struct AssemblerOnclickInfo { pub prod_mod: f32, pub power_consumption_mod: f32, pub base_power_consumption: Watt, + + #[cfg(feature = "assembler-craft-tracking")] + pub times_craft_finished: u32, } impl< @@ -90,69 +95,72 @@ impl< > { #[must_use] - pub fn new(data_store: &DataStore) -> Self { + pub fn new( + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let assemblers_0_1 = data_store .ing_out_num_to_recipe .get(&(0, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_1_1 = data_store .ing_out_num_to_recipe .get(&(1, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_1 = data_store .ing_out_num_to_recipe .get(&(2, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_2 = data_store .ing_out_num_to_recipe .get(&(2, 2)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_3 = data_store .ing_out_num_to_recipe .get(&(2, 3)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_3_1 = data_store .ing_out_num_to_recipe .get(&(3, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_4_1 = data_store .ing_out_num_to_recipe .get(&(4, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_5_1 = data_store .ing_out_num_to_recipe .get(&(5, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_6_1 = data_store .ing_out_num_to_recipe .get(&(6, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); Self { @@ -282,6 +290,54 @@ impl< ) } + pub fn set_grid( + &mut self, + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + let Self { + assemblers_0_1, + assemblers_1_1, + assemblers_2_1, + assemblers_2_2, + assemblers_2_3, + assemblers_3_1, + assemblers_4_1, + assemblers_5_1, + assemblers_6_1, + recipe: _, + } = self; + + for ass in assemblers_0_1 { + ass.set_grid_id(grid_id, data_store); + } + + for ass in assemblers_1_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_2 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_3 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_3_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_4_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_5_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_6_1 { + ass.set_grid_id(grid_id, data_store); + } + } + pub fn get_info( &self, assembler_id: AssemblerID, @@ -505,6 +561,172 @@ impl< _ => unreachable!(), } } + + pub fn remove_wait_list_inserter( + &mut self, + assembler_id: AssemblerID, + item: Item, + info: crate::chest::WaitingInserterRemovalInfo, + data_store: &DataStore, + ) -> simd::InserterReinsertionInfo { + let recipe_id = assembler_id.recipe.id.into(); + + match ( + data_store.recipe_num_ing_lookup[recipe_id], + data_store.recipe_num_out_lookup[recipe_id], + ) { + (0, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_0_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_0_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + (1, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_1_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_1_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + (2, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (2, 2) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (2, 3) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (3, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (4, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (5, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + (6, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) + }, + + _ => unreachable!(), + } + } + + pub fn num_assemblers(&self) -> usize { + let Self { + assemblers_0_1, + assemblers_1_1, + assemblers_2_1, + assemblers_2_2, + assemblers_2_3, + assemblers_3_1, + assemblers_4_1, + assemblers_5_1, + assemblers_6_1, + recipe: _, + } = self; + + assemblers_0_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_1_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_2 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_2_3 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_3_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_4_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_5_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + + assemblers_6_1 + .iter() + .map(|store| store.num_assemblers()) + .sum::() + } } // FIXME: @@ -561,6 +783,7 @@ impl Iterator for ZipArray { } } +#[derive(Debug)] pub enum PowerUsageInfo { ByType(Vec), Combined(Watt), @@ -583,8 +806,14 @@ pub trait MultiAssemblerStore< { fn new( recipe: Recipe, + grid: PowerGridIdentifier, data_store: &DataStore, ) -> Self; + fn set_grid_id( + &mut self, + new_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ); fn get_recipe(&self) -> Recipe; fn get_info( &self, @@ -612,7 +841,12 @@ pub trait MultiAssemblerStore< recipe_maximums: &[[ITEMCOUNTTYPE; NUM_OUTPUTS]], times: &[TIMERTYPE], data_store: &DataStore, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait; @@ -620,10 +854,14 @@ pub trait MultiAssemblerStore< &mut self, ) -> ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_INGS], + ), + ( + [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_OUTPUTS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ); fn modify_modifiers( @@ -799,6 +1037,16 @@ pub trait MultiAssemblerStore< ret } + + fn num_assemblers(&self) -> usize; + + fn remove_wait_list_inserter( + &mut self, + index: u32, + item: Item, + info: crate::chest::WaitingInserterRemovalInfo, + data_store: &DataStore, + ) -> simd::InserterReinsertionInfo; } pub mod arrays { diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index f3066a3..461bc24 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -1,9 +1,13 @@ +use crate::belt::belt::BeltLenType; use std::{ - array, i32, + array, + cmp::min, + i32, + num::NonZero, ops::{Add, Sub}, simd::{ Simd, - cmp::SimdPartialOrd, + cmp::{SimdPartialEq, SimdPartialOrd}, num::{SimdInt, SimdUint}, }, u8, @@ -13,13 +17,16 @@ use crate::{ MASKTYPE, SIMDTYPE, data::{AssemblerInfo, DataStore, ItemRecipeDir}, frontend::world::Position, - item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Recipe, WeakIdxTrait}, + inserter::{FakeUnionStorage, Storage, storage_storage_with_buckets_indirect::InserterId}, + item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Item, Recipe, WeakIdxTrait}, power::{ Watt, power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridEntity, PowerGridIdentifier}, }, + storage_list::{MaxInsertionLimit, PANIC_ON_INSERT}, }; use itertools::{Either, Itertools}; +use static_assertions::const_assert; use super::{AssemblerOnclickInfo, PowerUsageInfo, Simdtype, TIMERTYPE, arrays}; @@ -28,9 +35,115 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; +const WAITLIST_LEN: usize = 3; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[repr(align(64))] +pub struct InserterWaitList { + pub(crate) inserters: [Option; WAITLIST_LEN], +} + +const_assert!(std::mem::size_of::() <= 64); + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Inserter { + // item: u8, + // TODO: This is not needed for assemblers, just for chests. + pub self_is_source: bool, + // We track the hand here so we avoid having to reinsert them each time the assembler produces anything + // This does mean we can only fit 3 Inserters per cacheline :/ + // This is fixed by the item arena optimization + pub current_hand: ITEMCOUNTTYPE, + pub max_hand: NonZero, + pub movetime: u16, + pub(crate) index: InserterId, + pub other: FakeUnionStorage, +} + +const_assert!(std::mem::size_of::>() <= 20); +const_assert!(std::mem::size_of::() <= 20); + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct InserterWithBelts { + pub(crate) current_hand: ITEMCOUNTTYPE, + pub(crate) max_hand: ITEMCOUNTTYPE, + + pub(crate) rest: InserterWithBeltsEnum, + pub(crate) movetime: NonZero, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum InserterWithBeltsEnum { + StorageStorage { + // TODO: This is not needed for assemblers, just for chests. + self_is_source: bool, + index: InserterId, + + other: FakeUnionStorage, + }, + BeltStorage { + self_is_source: bool, + belt_id: u32, + belt_pos: u16, + }, +} + +#[derive(Debug)] +pub struct InserterReinsertionInfo { + pub movetime: NonZero, + pub item: Item, + pub current_hand: ITEMCOUNTTYPE, + pub max_hand: ITEMCOUNTTYPE, + + pub(crate) conn: Conn, +} + +#[derive(Debug)] +pub enum Conn { + Storage { + index: InserterId, + storage_id_in: FakeUnionStorage, + storage_id_out: FakeUnionStorage, + }, + Belt { + belt_id: u32, + belt_pos: BeltLenType, + self_storage: FakeUnionStorage, + self_is_source: bool, + }, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +struct InternalInserterReinsertionInfo { + pub movetime: NonZero, + pub item: u8, + pub max_hand: ITEMCOUNTTYPE, + pub self_index: u32, + + pub(crate) rest: Rest, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +enum Rest { + Storage { + index: InserterId, + other: FakeUnionStorage, + }, + Belt { + belt_id: u32, + belt_pos: BeltLenType, + self_is_source: bool, + }, +} + // FIXME: We store the same slice length n times! // TODO: Don´t clump update data and data for adding/removing assemblers together! - // FIXME: Using Boxed slices here is probably the main contributor to the time usage for building large power grids, since this means reallocation whenever we add assemblers! #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -75,10 +188,32 @@ pub struct MultiAssemblerStore< timers: Box<[TIMERTYPE]>, prod_timers: Box<[TIMERTYPE]>, + // FIXME: For me to be able to add inserters to waitlists during parallel/per item updates, + // We would need a list of waitlists per ING and OUTPUT item + // This is a pretty big commitment in terms of memory + #[serde(with = "arrays")] + waitlists_ings: [Box<[InserterWaitList]>; NUM_INGS], + #[serde(with = "arrays")] + waitlists_ings_needed: [Box<[ITEMCOUNTTYPE]>; NUM_INGS], + #[serde(with = "arrays")] + waitlists_outputs: [Box<[InserterWaitList]>; NUM_OUTPUTS], + #[serde(with = "arrays")] + waitlists_outputs_needed: [Box<[ITEMCOUNTTYPE]>; NUM_OUTPUTS], holes: Vec, + + #[cfg(feature = "assembler-craft-tracking")] + number_of_crafts_finished: Vec, + positions: Box<[Position]>, types: Box<[u8]>, len: usize, + + #[serde(skip)] + inserter_waitlist_output_vec: Vec, + #[serde(with = "arrays")] + self_fake_union_ing: [FakeUnionStorage; NUM_INGS], + #[serde(with = "arrays")] + self_fake_union_out: [FakeUnionStorage; NUM_OUTPUTS], } // TODO: Maybe also add a defragmentation routine to mend the ineffeciencies left by deconstruction large amounts of assemblers @@ -201,6 +336,12 @@ impl times: &[TIMERTYPE], power_list: &[AssemblerInfo], ) -> (Watt, u32, u32) { + // FIXME: This could technically not be enough if enough items are produced in a single tick + // self.inserter_waitlist_output_vec.reserve( + // (self.len * 4 * (NUM_INGS + NUM_OUTPUTS)) + // .saturating_sub(self.inserter_waitlist_output_vec.len()), + // ); + let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; let our_ings: &[ITEMCOUNTTYPE; NUM_INGS] = &recipe_ings[ing_idx]; @@ -240,7 +381,7 @@ impl let (timers, overlap) = self.timers.as_chunks_mut::<{ SIMDTYPE::LEN }>(); // FIXME: - // assert!(overlap.is_empty()); + assert!(overlap.is_empty()); for ( index, @@ -256,6 +397,50 @@ impl )) .enumerate() { + // let timer = SIMDTYPE::from_array(*timer_arr); + // let stopped_empty = dbg!(timer).simd_eq(SIMDTYPE::splat(0)); + // let stopped_full = timer.simd_eq(SIMDTYPE::splat(TIMERTYPE::MAX)); + + // let speed_mod = Simd::::from_array(*speed_mod); + // let increase: SIMDTYPE = (power_level_recipe_increase.cast::() + // * speed_mod.cast::() + // / Simd::::splat(20)) + // .cast(); + // let new_timer_assuming_free_run = timer + increase; + // let timer_mask_assuming_free_run: MASKTYPE = new_timer_assuming_free_run.simd_lt(timer); + // let progress = (new_timer_assuming_free_run.sub(timer)).cast::(); + // let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // let bonus_prod = Simd::::from_array(*bonus_prod); + // let new_prod_timer_assuming_free_run: SIMDTYPE = prod_timer.add(SimdUint::cast::( + // progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + // )); + // let prod_timer_mask: MASKTYPE = if bonus_prod + // .simd_gt(Simd::::splat(0)) + // .any() + // { + // let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // // This needs be calculated in u32 to prevent overflows in intermediate values + // let progress = (new_timer_assuming_free_run.sub(timer)).cast::(); + // let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( + // progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + // )); + + // *prod_timer_arr = new_prod_timer.to_array(); + + // new_prod_timer.simd_lt(prod_timer) + // } else { + // MASKTYPE::splat(false) + // }; + // if !stopped_empty.any() + // && !stopped_full.any() + // && !timer_mask_assuming_free_run.any() + // && !prod_timer_mask.any() + // { + // *timer_arr = new_timer_assuming_free_run.to_array(); + // *prod_timer_arr = new_prod_timer_assuming_free_run.to_array(); + // continue; + // } + let index = index * 16; // ~~Remove the items from the ings at the start of the crafting process~~ // We will do this as part of the frontend ui! @@ -306,10 +491,13 @@ impl let new_timer = space_mask.select(new_timer_output_space, new_timer_output_full); + if timer == new_timer { + continue; + } + // Power calculation // We use power if any work was done - let uses_power = - ing_mask & (space_mask | timer.simd_lt(SIMDTYPE::splat(TIMERTYPE::MAX))); + let uses_power = timer.simd_ne(new_timer); if self.single_type.is_some() { power_const_type += u32::from( uses_power @@ -327,50 +515,245 @@ impl ); } - if timer == new_timer { - continue; - } - let timer_mask: MASKTYPE = new_timer.simd_lt(timer); - // if we have enough items for another craft keep the wrapped value (This improves the accuracy), else clamp it to 0 let new_timer = (!timer_mask | ing_mask_for_two_crafts).select(new_timer, SIMDTYPE::splat(0)); - - let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + *timer_arr = new_timer.to_array(); let bonus_prod = Simd::::from_array(*bonus_prod); - // This needs be calculated in u32 to prevent overflows in intermediate values - let progress = (new_timer.sub(timer)).cast::(); - let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( - progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), - )); - - let prod_timer_mask: MASKTYPE = new_prod_timer.simd_lt(prod_timer); + let prod_timer_mask: MASKTYPE = if bonus_prod + .simd_gt(Simd::::splat(0)) + .any() + { + let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // This needs be calculated in u32 to prevent overflows in intermediate values + let progress = (new_timer.sub(timer)).cast::(); + let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( + progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + )); + + *prod_timer_arr = new_prod_timer.to_array(); + + new_prod_timer.simd_lt(prod_timer) + } else { + MASKTYPE::splat(false) + }; - *timer_arr = new_timer.to_array(); - *prod_timer_arr = new_prod_timer.to_array(); if timer_mask.any() || prod_timer_mask.any() { - for i in 0..NUM_OUTPUTS { - let our_outputs = SIMDTYPE::splat(our_outputs[i].into()); - let outputs: Simd = - Simd::::from_slice(&self.outputs[i][index..]); - let outputs = outputs.cast(); - let int_output = timer_mask.select(outputs + our_outputs, outputs); - self.outputs[i][index..(index + 16)].copy_from_slice( - (prod_timer_mask.select(int_output + our_outputs, int_output)) - .cast() - .as_array(), - ); + // for i in 0..NUM_OUTPUTS { + // let our_outputs = SIMDTYPE::splat(our_outputs[i].into()); + // let outputs: Simd = + // Simd::::from_slice(&self.outputs[i][index..]); + // let outputs = outputs.cast(); + // let int_output = timer_mask.select(outputs + our_outputs, outputs); + // self.outputs[i][index..(index + 16)].copy_from_slice( + // (prod_timer_mask.select(int_output + our_outputs, int_output)) + // .cast() + // .as_array(), + // ); + // } + + // FIXME: We are missing production if main and prod tick are at the same time! + for (i, (has_produced_base, has_produced_prod)) in timer_mask + .to_array() + .into_iter() + .zip(prod_timer_mask.to_array().into_iter()) + .enumerate() + { + if has_produced_base || has_produced_prod { + let final_idx = index + i; + #[cfg(feature = "assembler-craft-tracking")] + { + self.number_of_crafts_finished[final_idx] += + u32::from(has_produced_base) + u32::from(has_produced_prod); + } + + let mut items = our_outputs.clone(); + + for (item, (out, items_to_distribute)) in self + .waitlists_outputs + .iter_mut() + .zip(&mut items) + .enumerate() + { + if *items_to_distribute + self.outputs[item][final_idx] + >= min( + self.waitlists_outputs_needed[item][final_idx], + our_maximums[item], + ) + { + *items_to_distribute += self.outputs[item][final_idx]; + self.outputs[item][final_idx] = 0; + for idx in 0..WAITLIST_LEN { + let ins = &mut out[final_idx].inserters[idx]; + if let Some(v) = ins { + let amount_taken_by_this_inserter = min( + *items_to_distribute, + ITEMCOUNTTYPE::from(v.max_hand) - v.current_hand, + ); + if v.current_hand + amount_taken_by_this_inserter + == ITEMCOUNTTYPE::from(v.max_hand) + { + let ins = ins.take().unwrap(); + for move_left_idx in idx..WAITLIST_LEN { + out[final_idx].inserters[move_left_idx] = out + [final_idx] + .inserters + .get_mut(move_left_idx + 1) + .map(|v| v.take()) + .unwrap_or(None); + } + let () = + self.inserter_waitlist_output_vec + .push(InternalInserterReinsertionInfo { + movetime: ins.movetime.into(), + item: (NUM_INGS + item) as u8, + max_hand: ins.max_hand.into(), + self_index: final_idx as u32, + rest: match ins.rest { + InserterWithBeltsEnum::StorageStorage { + self_is_source: _, + index, + other, + } => Rest::Storage { index, other }, + InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => Rest::Belt { + belt_id, + belt_pos, + self_is_source, + }, + }, + }) + else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}", + self.inserter_waitlist_output_vec.capacity() + ); + }; + self.waitlists_outputs_needed[item][final_idx] = + out[final_idx].inserters[0] + .as_ref() + .map(|ins| ins.max_hand - ins.current_hand) + .unwrap_or(ITEMCOUNTTYPE::MAX); + } else { + v.current_hand += amount_taken_by_this_inserter; + } + *items_to_distribute -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_distribute == 0 { + break; + } + } + } + } + + if *items_to_distribute > 0 { + self.outputs[item][final_idx] += *items_to_distribute; + } + } + } } } if timer_mask.any() { - for i in 0..NUM_INGS { - let ings: Simd = Simd::::from_slice(&self.ings[i][index..]); - let ings = ings.cast(); - let our_ings = SIMDTYPE::splat(our_ings[i].into()); - self.ings[i][index..(index + 16)].copy_from_slice( - timer_mask.select(ings - our_ings, ings).cast().as_array(), - ); + // for i in 0..NUM_INGS { + // let ings: Simd = Simd::::from_slice(&self.ings[i][index..]); + // let ings = ings.cast(); + // let our_ings = SIMDTYPE::splat(our_ings[i].into()); + // self.ings[i][index..(index + 16)].copy_from_slice( + // timer_mask.select(ings - our_ings, ings).cast().as_array(), + // ); + // } + + let has_consumed = timer_mask; + for (i, has_consumed) in has_consumed.to_array().into_iter().enumerate() { + if has_consumed { + let final_idx = index + i; + let mut items = our_ings.clone(); + + for (item, (ing, items_to_drain)) in + self.waitlists_ings.iter_mut().zip(&mut items).enumerate() + { + if *items_to_drain + + (self.ings_max_insert[item][final_idx] + - self.ings[item][final_idx]) + >= self.waitlists_ings_needed[item][final_idx] + { + *items_to_drain += self.ings_max_insert[item][final_idx] + - self.ings[item][final_idx]; + self.ings[item][final_idx] = self.ings_max_insert[item][final_idx]; + + for idx in 0..WAITLIST_LEN { + let ins = &mut ing[final_idx].inserters[idx]; + if let Some(v) = ins { + let amount_taken_by_this_inserter = + min(*items_to_drain, v.current_hand); + if v.current_hand - amount_taken_by_this_inserter == 0 { + let ins = ins.take().unwrap(); + for move_left_idx in idx..WAITLIST_LEN { + ing[final_idx].inserters[move_left_idx] = ing + [final_idx] + .inserters + .get_mut(move_left_idx + 1) + .map(|v| v.take()) + .unwrap_or(None); + } + let () = + self.inserter_waitlist_output_vec + .push(InternalInserterReinsertionInfo { + movetime: ins.movetime.into(), + item: item as u8, + max_hand: ins.max_hand.into(), + self_index: final_idx as u32, + rest: match ins.rest { + InserterWithBeltsEnum::StorageStorage { + self_is_source: _, + index, + other, + } => Rest::Storage { index, other }, + InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => Rest::Belt { + belt_id, + belt_pos, + self_is_source, + }, + }, + }) + else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}.", + self.inserter_waitlist_output_vec.capacity() + ); + }; + self.waitlists_ings_needed[item][final_idx] = + ing[final_idx].inserters[0] + .as_ref() + .map(|ins| ins.current_hand) + .unwrap_or(ITEMCOUNTTYPE::MAX); + } else { + v.current_hand -= amount_taken_by_this_inserter; + } + *items_to_drain -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_drain == 0 { + break; + } + } + } + } + + if *items_to_drain > 0 { + self.ings[item][final_idx] -= *items_to_drain; + } + } + } } } times_ings_used += (timer_mask.to_int().reduce_sum() * -1) as u32; @@ -434,7 +817,8 @@ impl( recipe: Recipe, - _data_store: &DataStore, + grid: PowerGridIdentifier, + data_store: &DataStore, ) -> Self { Self { recipe, @@ -458,13 +842,96 @@ impl( + &mut self, + new_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + self.self_fake_union_ing = { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()][index]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }; + self.self_fake_union_out = { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + [index + NUM_INGS]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }; + } + fn modify_modifiers( &mut self, index: u32, @@ -526,7 +993,7 @@ impl; NUM_INGS] = self @@ -673,13 +1144,7 @@ impl| v.into_vec()); + for (new, other) in new_waitlists_outputs_needed + .iter_mut() + .zip(other.waitlists_outputs_needed) + { + new.extend(other); + } let updates = IntoIterator::into_iter(other.positions) .take(other.len) .zip(other.types) .enumerate() .take(other.len) - .filter(move |(i, _)| !other.holes.contains(i)) .enumerate() + .filter(move |(_offs, (i, _))| !other.holes.contains(i)) .map(move |(new_index_offs, (old_index, (pos, ty)))| { assert!(new_index_offs <= old_index); IndexUpdateInfo { @@ -938,8 +1396,63 @@ impl other.inserter_waitlist_output_vec.capacity() + { + self.inserter_waitlist_output_vec + } else { + other.inserter_waitlist_output_vec + }, + + self_fake_union_ing: { + array::from_fn(|index| { + let item = + data_store.recipe_item_index_to_item[self.recipe.into_usize()][index]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }, + self_fake_union_out: { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + [index + NUM_INGS]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }, + + #[cfg(feature = "assembler-craft-tracking")] + number_of_crafts_finished: { + self.number_of_crafts_finished + .extend(other.number_of_crafts_finished); + self.number_of_crafts_finished + }, }; #[cfg(debug_assertions)] @@ -964,7 +1477,12 @@ impl, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait, { @@ -978,24 +1496,131 @@ impl Conn::Storage { + index, + storage_id_in: if (internal.item as usize) < NUM_INGS { + // This is an ingredient inserter + other + } else { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .recipe_idx_with_this_item, + } + }, + storage_id_out: if (internal.item as usize) < NUM_INGS { + // This is an ingredient inserter + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_ing + [internal.item as usize] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing + [internal.item as usize] + .recipe_idx_with_this_item, + } + } else { + other + }, + }, + Rest::Belt { + belt_id, + belt_pos, + self_is_source, + } => Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage: if (internal.item as usize) < NUM_INGS { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_ing + [internal.item as usize] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing + [internal.item as usize] + .recipe_idx_with_this_item, + } + } else { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .recipe_idx_with_this_item, + } + }, + }, + }, + }), + ) } fn get_all_mut( &mut self, ) -> ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_INGS], + ), + ( + [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_OUTPUTS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ) { ( ( - self.ings_max_insert.each_mut().map(|b| &**b), + self.ings_max_insert.each_mut().map(|b| match b.get(0) { + Some(v) => MaxInsertionLimit::Global(*v), + None => PANIC_ON_INSERT, + }), + // self.ings_max_insert.each_mut().map(|b| &**b), self.ings.each_mut().map(|b| &mut **b), + self.waitlists_ings + .each_mut() + .into_iter() + .zip(self.waitlists_ings_needed.each_mut()) + .map(|(wait, needed)| (&mut **wait, &mut **needed)) + .collect_array() + .unwrap(), + ), + ( + self.outputs.each_mut().map(|b| &mut **b), + self.waitlists_outputs + .each_mut() + .into_iter() + .zip(self.waitlists_outputs_needed.each_mut()) + .map(|(wait, needed)| (&mut **wait, &mut **needed)) + .collect_array() + .unwrap(), ), - self.outputs.each_mut().map(|b| &mut **b), ) } @@ -1081,8 +1706,27 @@ impl usize { + self.len - self.holes.len() + } + + fn remove_wait_list_inserter( + &mut self, + self_index: u32, + item: Item, + info: crate::chest::WaitingInserterRemovalInfo, + data_store: &DataStore, + ) -> self::InserterReinsertionInfo { + let item_index = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + .iter() + .position(|recipe_item| recipe_item == &item) + .unwrap(); + + if item_index < NUM_INGS { + let our_waitlist = &mut self.waitlists_ings[item_index][self_index as usize]; + let v = our_waitlist + .inserters + .iter_mut() + .filter(|v| v.is_some()) + .find(|ins| match (&ins.as_ref().unwrap().rest, info) { + ( + InserterWithBeltsEnum::StorageStorage { index, .. }, + crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + ) => inserter_id == *index, + ( + InserterWithBeltsEnum::BeltStorage { + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + .. + }, + crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + ) => *ins_belt_id == belt_id && *ins_belt_pos == belt_pos, + + _ => false, + }) + .unwrap(); + + let ins = v.take().unwrap(); + + InserterReinsertionInfo { + movetime: ins.movetime.into(), + item: item, + current_hand: ins.current_hand, + max_hand: ins.max_hand.into(), + + conn: match ins.rest { + InserterWithBeltsEnum::StorageStorage { index, other, .. } => Conn::Storage { + index, + // This is an ingredient inserter + storage_id_in: other, + // This is an ingredient inserter + storage_id_out: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_ing[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + }, + InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id, + belt_pos, + } => Conn::Belt { + self_is_source, + belt_id, + belt_pos, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_ing[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + }, + }, + } + } else { + // This is an output inserter + let item_index = item_index - NUM_INGS; + let our_waitlist = &mut self.waitlists_outputs[item_index][self_index as usize]; + let v = our_waitlist + .inserters + .iter_mut() + .filter(|v| v.is_some()) + .find(|ins| match (&ins.as_ref().unwrap().rest, info) { + ( + InserterWithBeltsEnum::StorageStorage { index, .. }, + crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + ) => inserter_id == *index, + ( + InserterWithBeltsEnum::BeltStorage { + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + .. + }, + crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + ) => *ins_belt_id == belt_id && *ins_belt_pos == belt_pos, + + _ => false, + }) + .unwrap(); + + let ins = v.take().unwrap(); + + InserterReinsertionInfo { + movetime: ins.movetime.into(), + item: item, + current_hand: ins.current_hand, + max_hand: ins.max_hand.into(), + + conn: match ins.rest { + InserterWithBeltsEnum::StorageStorage { index, other, .. } => Conn::Storage { + index, + // This is an output inserter + storage_id_in: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_out[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out[item_index] + .recipe_idx_with_this_item, + }, + // This is an output inserter + storage_id_out: other, + }, + InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id, + belt_pos, + } => Conn::Belt { + self_is_source, + belt_id, + belt_pos, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_out[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out[item_index] + .recipe_idx_with_this_item, + }, + }, + }, + } + } + } } #[cfg(test)] @@ -1357,14 +2212,14 @@ mod test { #[bench] fn bench_multithreaded_assembler_update(bencher: &mut Bencher) { const NUM_RECIPES: usize = 12; - const NUM_ASSEMBLERS: usize = 30_000_000; + const NUM_ASSEMBLERS: usize = 1_000_000; let mut assemblers: Vec> = (0..NUM_RECIPES as u8) - .map(|_| MultiAssemblerStore::new(Recipe { id: 11 }, &DATA_STORE)) + .map(|_| MultiAssemblerStore::new(Recipe { id: 11 }, 0, &DATA_STORE)) .collect_vec(); let items: Vec> = vec![ - rayon::iter::repeat(rand::thread_rng().gen_range(250..=255)) + rayon::iter::repeat(rand::rng().random_range(250..=255)) .flat_map_iter(|v| std::iter::repeat_n(v, 10)) .take_any(NUM_ASSEMBLERS) .collect(); @@ -1432,9 +2287,5 @@ mod test { }); } }); - - dbg!(i); - dbg!(num_produced); - dbg!(assemblers[11].get_info(0, &DATA_STORE)); } } diff --git a/src/belt/mod.rs b/src/belt/mod.rs index c9f81fa..ebb06d3 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -1,5 +1,6 @@ #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; +use fixedbitset::FixedBitSet; #[cfg(feature = "client")] use get_size2::GetSize; @@ -13,11 +14,13 @@ pub mod splitter; mod sushi; use crate::belt::smart::EmptyBelt; -use crate::get_size::{Mutex, StableGraph}; +use crate::get_size::{self, Mutex, StableGraph}; use crate::item::ITEMCOUNTTYPE; use crate::par_generation::BeltKind; use petgraph::stable_graph::DefaultIx; +use std::num::NonZero; use std::ops::RangeInclusive; +use std::u32; use std::{ cell::UnsafeCell, collections::{HashMap, HashSet}, @@ -59,7 +62,7 @@ use smart::{BeltInserterInfo, InserterAdditionError, Side, SmartBelt, SpaceOccup use splitter::{ PureSplitter, SPLITTER_BELT_LEN, SplitterDistributionMode, SplitterSide, SushiSplitter, }; -use sushi::{SushiBelt, SushiInfo}; +use sushi::SushiBelt; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, PartialEq, Clone, Copy, serde::Deserialize, serde::Serialize)] @@ -117,6 +120,8 @@ pub struct BeltStore { pub belt_graph: StableGraph, BeltGraphConnection, Directed, DefaultIx>, + #[serde(skip)] + pub belt_graph_bfs: get_size::Bfs, pub belt_graph_lookup: HashMap, NodeIndex>, } @@ -269,15 +274,6 @@ struct SplitterStore { impl SplitterStore {} -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum AnyBeltBeltInserter { - PurePure(usize), - PureSushi(usize), - SushiPure(usize), - SushiSushi(usize), -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BeltBeltInserterStore { @@ -363,8 +359,8 @@ impl InnerBeltStore { fn get_pure_splitter_belt_ids<'a>( &'a self, - item: Item, - id: usize, + _item: Item, + _id: usize, ) -> [[AnyBelt; 2]; 2] { info!("Had to search for splitter belt ids!"); todo!() @@ -1523,6 +1519,7 @@ impl BeltStore { any_splitter_holes: vec![], belt_graph: StableGraph::default(), + belt_graph_bfs: get_size::Bfs::default(), belt_graph_lookup: HashMap::default(), } } @@ -1539,7 +1536,7 @@ impl BeltStore { .inserters .inserters .iter() - .filter_map(|(ins, item)| { + .filter_map(|(ins, item, _movetime, _hand_size)| { let (dir, _state) = ins.state.into(); (dir == Dir::StorageToBelt).then_some(*item) }) @@ -2047,6 +2044,9 @@ impl BeltStore { }, Err(None) => { // The belt is empty, nothing to do + + // Since we are an empty belt, nothing to propagate + return; }, Err(Some(_)) => { // This needs to be sushi @@ -2078,42 +2078,50 @@ impl BeltStore { ) { if dedup.contains(&tile_id) { todo!(); + } + + if let Some(_pre_calc) = done.get(&tile_id) { return; } - let (inserter_item_sources, items_on_belt): (Vec<_>, Vec<_>) = match tile_id { + let mut items_on_belt_or_from_inserters: Vec<_> = match tile_id { BeltTileId::AnyBelt(index, _) => match self.any_belts[index as usize] { AnyBelt::Smart(belt_id) => { let belt = self.inner.get_smart(belt_id); - let items_all_empty = belt.items().all(|loc| loc.is_none()); - match (belt.inserters.inserters.is_empty(), items_all_empty) { - (true, true) => (vec![], vec![]), - (true, false) => (vec![], vec![belt_id.item]), - (false, true) => (vec![belt_id.item], vec![]), - (false, false) => (vec![belt_id.item], vec![belt_id.item]), + // FIXME: This gives incorrect results when using extracted inserters + // if belt.inserters.inserters.is_empty() { + // FIXME: To "fix" this, I will assume each belt has an incoming inserter + if false { + let items_all_empty = belt.items().all(|loc| loc.is_none()); + + if items_all_empty { + vec![] + } else { + vec![belt_id.item] + } + } else { + vec![belt_id.item] } }, AnyBelt::Sushi(sushi_idx) => { let belt = self.inner.get_sushi(sushi_idx); - ( - belt.inserters - .inserters - .iter() - .filter_map(|(ins, item)| { - let (dir, _state) = ins.state.into(); - (dir == Dir::StorageToBelt).then_some(*item) - }) - .dedup() - .collect(), - belt.items().into_iter().flatten().dedup().collect(), - ) + belt.inserters + .inserters + .iter() + .filter_map(|(ins, item, _movetime, _hand_size)| { + let (dir, _state) = ins.state.into(); + (dir == Dir::StorageToBelt).then_some(*item) + }) + .chain(belt.items().into_iter().flatten()) + .dedup() + .collect() }, - AnyBelt::Empty(_empty_idx) => (vec![], vec![]), + AnyBelt::Empty(_empty_idx) => vec![], }, }; - let incoming_belts: Vec<_> = self + let incoming_belts = self .belt_graph .edges_directed( *self.belt_graph_lookup[&tile_id], @@ -2124,10 +2132,9 @@ impl BeltStore { self.belt_graph.node_weight(edge.source()).unwrap(), *edge.weight(), ) - }) - .collect(); + }); - let incoming_belt_items: Vec<_> = incoming_belts + let incoming_belt_items = incoming_belts .into_iter() .flat_map(|(belt, connection)| match connection { BeltGraphConnection::Sideload { dest_belt_pos: _ } @@ -2148,17 +2155,10 @@ impl BeltStore { filter, } => once(filter).collect(), }) - .dedup() - .collect(); + .dedup(); + items_on_belt_or_from_inserters.extend(incoming_belt_items); - done.insert( - tile_id, - items_on_belt - .into_iter() - .chain(inserter_item_sources) - .chain(incoming_belt_items) - .collect(), - ); + done.insert(tile_id, items_on_belt_or_from_inserters); } #[profiling::function] @@ -2256,7 +2256,7 @@ impl BeltStore { #[profiling::function] pub fn sushi_belt_update<'a, 'b, 'c, RecipeIdxType: IdxTrait>( &mut self, - storages_by_item: impl IndexedParallelIterator>, + _storages_by_item: impl IndexedParallelIterator>, _data_store: &DataStore, ) where 'b: 'a, @@ -2292,14 +2292,10 @@ impl BeltStore { { profiling::scope!("SushiBelt Inserter Update"); - for sushi_belt in &mut self.inner.sushi_belts { + for _sushi_belt in &mut self.inner.sushi_belts { // TODO: Update inserters! } } - - for current_timer in self.inner.belt_update_timers.iter_mut() { - *current_timer %= 120; - } } pub fn get_splitter_belt_ids( @@ -2349,7 +2345,7 @@ impl BeltStore { ) -> [[Option>; 2]; 2] { let items = match splitter_id { SplitterTileId::Any(index) => match self.any_splitters[index as usize] { - AnySplitter::Pure(item, id) => todo!(), + AnySplitter::Pure(_item, _id) => todo!(), AnySplitter::Sushi(id) => [ self.inner.get_sushi_splitter_inputs(id), self.inner.get_sushi_splitter_outputs(id), @@ -2598,8 +2594,9 @@ impl BeltStore { movetime: u16, hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { - let handle_sushi_belt = - |belt: &mut SushiBelt| belt.add_in_inserter(filter, pos, storage_id); + let handle_sushi_belt = |belt: &mut SushiBelt| { + belt.add_in_inserter(filter, pos, storage_id, movetime, hand_size) + }; match id { BeltTileId::AnyBelt(index, _) => { @@ -2626,7 +2623,7 @@ impl BeltStore { let now_sushi_belt = self.inner.get_sushi_mut(new_index); now_sushi_belt - .add_in_inserter(filter, pos, storage_id) + .add_in_inserter(filter, pos, storage_id, movetime, hand_size) .expect("We already became sushi, it should now work!"); }, } @@ -2667,8 +2664,9 @@ impl BeltStore { movetime: u16, hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { - let handle_sushi_belt = - |belt: &mut SushiBelt| belt.add_out_inserter(filter, pos, storage_id); + let handle_sushi_belt = |belt: &mut SushiBelt| { + belt.add_out_inserter(filter, pos, storage_id, movetime, hand_size) + }; match id { BeltTileId::AnyBelt(index, _) => { @@ -2695,7 +2693,7 @@ impl BeltStore { let now_sushi_belt = self.inner.get_sushi_mut(new_index); now_sushi_belt - .add_out_inserter(filter, pos, storage_id) + .add_out_inserter(filter, pos, storage_id, movetime, hand_size) .expect("We already became sushi, it should now work!"); }, } @@ -2828,112 +2826,10 @@ impl BeltStore { ret } - pub fn remove_sideloading_inserter(&mut self, index: usize) { + pub fn remove_sideloading_inserter(&mut self, _index: usize) { todo!() } - fn get_belt_belt_inserter_sushi_lists<'a>( - &'a self, - id: BeltTileId, - ) -> ( - impl IntoIterator> + Clone + use<'a, ItemIdxType>, - impl IntoIterator> + Clone + use<'a, ItemIdxType>, - ) { - // FIXME: Consider splitters!!!! - ( - self.inner - .belt_belt_inserters - .pure_to_pure_inserters - .iter() - .enumerate() - .map(|(i, v)| { - ( - v, - Item:: { - id: i.try_into().unwrap(), - }, - ) - }) - .map(move |(v, item)| { - v.iter().flatten().filter_map(move |ins| { - // (ins.1 .1 .0 == id).then_some(SushiInfo::Pure(Some(item))) - // FIXME: - None - }) - }) - .flatten() - .chain({ - let idx = self.belt_graph_lookup[&id]; - - let edges = self - .belt_graph - .edges_directed(*idx, petgraph::Direction::Incoming); - - edges.map(|edge| match edge.weight() { - BeltGraphConnection::BeltBeltInserter { - source_belt_pos: _, - dest_belt_pos: _, - filter, - } - | BeltGraphConnection::Connected { - filter: Some(filter), - } => SushiInfo::Pure(Some(*filter)), - BeltGraphConnection::Sideload { dest_belt_pos: _ } - | BeltGraphConnection::Connected { filter: None } => { - match self.belt_graph.node_weight(edge.source()).unwrap() { - BeltTileId::AnyBelt(index, _) => match self.any_belts - [*index as usize] - { - AnyBelt::Smart(belt_id) => SushiInfo::Pure(Some(belt_id.item)), - AnyBelt::Sushi(_) => SushiInfo::Sushi, - AnyBelt::Empty(_) => SushiInfo::Sushi, - }, - } - }, - }) - }), - self.inner - .belt_belt_inserters - .pure_to_pure_inserters - .iter() - .enumerate() - .map(|(i, v)| { - ( - v, - Item:: { - id: i.try_into().unwrap(), - }, - ) - }) - .map(move |(v, item)| { - v.iter().filter_map(move |ins| { - // (ins.1.source.0 == id).then_some(SushiInfo::Pure(Some(item))) - // FIXME: - None - }) - }) - .flatten(), // .chain( - // self.inner.belt_belt_inserters - // .sideload_inserters - // .iter() - // .filter_map(move |ins| { - // if ins.1.source.0 == id { - // match ins.1.dest.0 { - // BeltTileId::AnyBelt(index, _) => match &self.any_belts[index] { - // AnyBelt::Smart(_) => { - // unreachable!("Here a sushi belt is sideloading onto a smart belt. this MUST be impossible") - // }, - // AnyBelt::Sushi(_) => Some(SushiInfo::Sushi), - // }, - // } - // } else { - // None - // } - // }), - // ), - ) - } - pub fn get_inserter_info_at( &self, belt: BeltTileId, @@ -3010,7 +2906,8 @@ impl BeltStore { belt_id.item, new_src, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3018,7 +2915,8 @@ impl BeltStore { belt_pos, FakeUnionStorage::from_storage_with_statics_at_zero( src_item, new_src, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), @@ -3045,7 +2943,8 @@ impl BeltStore { belt_id.item, new_dest, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3053,7 +2952,8 @@ impl BeltStore { belt_pos, FakeUnionStorage::from_storage_with_statics_at_zero( dest_item, new_dest, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), @@ -3104,8 +3004,8 @@ impl BeltStore { .len() - 1) as u32 }, - (AnyBelt::Smart(source_belt_id), AnyBelt::Sushi(dest_index)) => todo!(), - (AnyBelt::Sushi(source_index), AnyBelt::Smart(dest_belt_id)) => todo!(), + (AnyBelt::Smart(_source_belt_id), AnyBelt::Sushi(_dest_index)) => todo!(), + (AnyBelt::Sushi(_source_index), AnyBelt::Smart(_dest_belt_id)) => todo!(), (AnyBelt::Sushi(source_index), AnyBelt::Sushi(dest_index)) => { self.inner .belt_belt_inserters @@ -3144,18 +3044,23 @@ impl BeltStore { } } - pub fn remove_belt_belt_inserter(&mut self, inserter_index: u32) { + pub fn remove_belt_belt_inserter(&mut self, _inserter_index: u32) { todo!() } - pub fn remove_inserter(&mut self, id: BeltTileId, pos: BeltLenType) { + pub fn remove_inserter( + &mut self, + id: BeltTileId, + pos: BeltLenType, + ) -> Result<(), (u32, BeltLenType)> { match id { BeltTileId::AnyBelt(index, _) => match &mut self.any_belts[index as usize] { AnyBelt::Smart(smart_belt_id) => { let smart_belt = self.inner.get_smart_mut(*smart_belt_id); - smart_belt - .remove_inserter(pos) - .expect("Failed to remove inserter from smart belt"); + match smart_belt.remove_inserter(pos) { + Ok(FakeUnionStorage { .. }) => Ok(()), + Err(()) => Err((smart_belt_id.index as u32, pos)), + } }, AnyBelt::Sushi(sushi_belt_id) => { let sushi_belt = &mut self.inner.sushi_belts[*sushi_belt_id]; @@ -3169,17 +3074,18 @@ impl BeltStore { info!("Unable to convert belt {id:?} to pure belt"); }, } + Ok(()) }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), }, - }; + } } pub fn change_inserter_movetime( &mut self, id: BeltTileId, pos: BeltLenType, - new_movetime: u16, + new_movetime: NonZero, ) { match id { BeltTileId::AnyBelt(index, _) => match &mut self.any_belts[index as usize] { @@ -3187,7 +3093,7 @@ impl BeltStore { let smart_belt = self.inner.get_smart_mut(*smart_belt_id); smart_belt.change_inserter_movetime(pos, new_movetime); }, - AnyBelt::Sushi(sushi_belt_id) => { + AnyBelt::Sushi(_sushi_belt_id) => { todo!() }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), @@ -3504,7 +3410,7 @@ impl BeltStore { self.merge_belts(front_tile_id, back_tile_id, data_store) }, - [AnyBelt::Empty(empty_idx), AnyBelt::Sushi(sushi_idx)] => { + [AnyBelt::Empty(empty_idx), AnyBelt::Sushi(_sushi_idx)] => { let new_id = self.inner.instantiate_empty_into_sushi(*empty_idx); self.any_belts[front as usize] = AnyBelt::Sushi(new_id); @@ -3797,8 +3703,168 @@ impl BeltStore { /// Remove the Splitter from the update list and its connections to belts. /// Does NOT remove any length of belt originally associated with a splitter world entity! - pub fn remove_splitter(&mut self, tile_id: SplitterTileId) { - todo!() + pub fn remove_splitter(&mut self, tile_id: SplitterTileId) -> Vec<(Item, u32)> { + match tile_id { + SplitterTileId::Any(idx) => { + self.any_splitter_holes.push(idx); + + let removed_items = match self.any_splitters[idx as usize] { + AnySplitter::Pure(item, idx) => { + let id = SplitterID { index: idx as u32 }; + + let removed_items = self.inner.smart_belts[item.into_usize()] + .belts_mut() + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }) + .collect_vec(); + + assert_eq!(removed_items.len(), 4); + + removed_items.into_iter().flatten().collect_vec() + }, + AnySplitter::Sushi(id) => { + let mut removed_items = self + .inner + .sushi_belts + .iter_mut() + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }) + .collect_vec(); + + removed_items.extend(self.inner.empty_belts.iter_mut().flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + })); + + if removed_items.len() < 4 { + removed_items.extend( + self.inner + .smart_belts + .iter_mut() + .flat_map(|store| store.belts_mut()) + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = belt + .remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = belt + .remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }), + ); + } + + assert_eq!(removed_items.len(), 4); + + removed_items.into_iter().flatten().collect_vec() + }, + }; + + self.any_splitters[idx as usize] = + AnySplitter::Sushi(SplitterID { index: u32::MAX }); + + removed_items + }, + } } pub fn get_len(&self, id: BeltTileId) -> u16 { diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 0e93852..f5e41c2 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1,17 +1,21 @@ use std::{ iter::repeat, + num::NonZero, ops::{Deref, DerefMut}, - sync::atomic::AtomicUsize, u8, }; +use crate::inserter::{ + belt_storage_inserter_non_const_gen::DynInserterState, + belt_storage_movement_list::{BeltStorageInserterInMovement, ReinsertionLists}, +}; use crate::{ inserter::{ InserterState, belt_storage_inserter::Dir, belt_storage_inserter_non_const_gen::BeltStorageInserterDyn, }, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, - storage_list::SingleItemStorages, + temp_vec::VecHolder, }; use bitvec::{ access::BitSafeUsize, @@ -23,8 +27,6 @@ use bitvec::{ use itertools::Either; use itertools::Itertools; use log::trace; -use static_assertions::const_assert; -use std::mem; use super::{ FreeIndex, SplitterID, @@ -39,21 +41,33 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_UPDATES: AtomicUsize = AtomicUsize::new(0); -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_FREE_CACHE_HITS: AtomicUsize = AtomicUsize::new(0); -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_LOCS_SEARCHED: AtomicUsize = AtomicUsize::new(0); -// HUGE FIXME: -pub const MOVETIME: u8 = 12; -pub const HAND_SIZE: u8 = 12; +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_BELT_INSERTER_UPDATES: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static TIMES_ALL_INCOMING_EARLY_RETURN: AtomicUsize = AtomicUsize::new(0); + +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_ITEMS: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_SPACE: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL: AtomicUsize = + AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static TIMES_INSERTERS_EXTRACTED: AtomicUsize = AtomicUsize::new(0); #[allow(clippy::module_name_repetitions)] #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[repr(align(64))] +// FIXME: Make sure a smart belt fits in a cacheline +// #[repr(align(64))] pub struct SmartBelt { pub(super) ty: u8, @@ -62,7 +76,7 @@ pub struct SmartBelt { /// Important, zero_index must ALWAYS be used using mod len pub(super) zero_index: BeltLenType, pub(super) locs: crate::get_size::BitBox, - pub(super) inserters: InserterStoreDyn, + pub inserters: InserterStoreDyn, pub(super) item: Item, @@ -70,9 +84,12 @@ pub struct SmartBelt { pub(super) input_splitter: Option<(SplitterID, SplitterSide)>, pub(super) output_splitter: Option<(SplitterID, SplitterSide)>, + + pub(crate) latest_inserter_pos_if_all_incoming: Option>, } -const_assert! {std::mem::size_of::>() <= 64} +// FIXME: +// const_assert! {std::mem::size_of::>() <= 64} #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)] @@ -87,10 +104,32 @@ pub struct EmptyBelt { pub len: u16, } +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct InserterExtractedWhenMoving { + pub(crate) storage: FakeUnionStorage, + pub(crate) belt_pos: BeltLenType, + pub(crate) movetime: NonZero, + pub(crate) outgoing: bool, + pub(crate) max_hand_size: ITEMCOUNTTYPE, + pub(crate) current_hand: ITEMCOUNTTYPE, +} + +// TODO: Idea: +// Have Belt inserters only be in belts as waitlist. +// when their hand is full, move them into a "store" of some kind +// where they are lazily waited on for their movetime. Once that is up, their outputs they either directly interact with the storage, +// or are put into the respective waitlist on the storage (i.e. assembler) +// Since the belts only interact with the "stores" 1.. lists and not the zeroth list (which interacts with the assemblers) +// We should be able to +// 1. Do the belt update in parallel with the belt_storage_store +// 2. Stop the belt updates from needing the storage lists, making them parallelizable with assembler updates +// This should ~double the amount of parallel processing we can do, +// and should prevent us from having to wait on the (always going to be) slow belt update for starting on assemblers (significantly improving the parallelism there also) #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct InserterStoreDyn { - pub(super) inserters: Box<[(BeltStorageInserterDyn, u8, ITEMCOUNTTYPE)]>, + pub inserters: Vec, } #[derive(Debug)] @@ -98,6 +137,8 @@ pub struct BeltInserterInfo { pub outgoing: bool, pub state: InserterState, pub connection: FakeUnionStorage, + pub hand_size: ITEMCOUNTTYPE, + pub movetime: u8, } #[derive(Debug)] @@ -119,7 +160,7 @@ impl SmartBelt { zero_index: 0, locs: bitbox![0; len.into()].into(), inserters: InserterStoreDyn { - inserters: vec![].into_boxed_slice(), + inserters: VecHolder::new(), }, item, @@ -128,6 +169,8 @@ impl SmartBelt { input_splitter: None, output_splitter: None, + + latest_inserter_pos_if_all_incoming: None, } } @@ -165,6 +208,8 @@ impl SmartBelt { input_splitter, output_splitter, + + latest_inserter_pos_if_all_incoming: _earliest_inserter_pos_if_all_incoming, } = self; SushiBelt { @@ -179,8 +224,24 @@ impl SmartBelt { zero_index, inserters: SushiInserterStoreDyn { inserters: inserters + .into_vec() .into_iter() - .map(|(inserter, _movetime, _hand_size)| (inserter, item)) + .map(|ins| { + ( + BeltStorageInserterDyn { + belt_pos: ins.belt_pos, + storage_id: ins.storage, + state: if ins.outgoing { + DynInserterState::BSWaitingForSourceItems(ins.current_hand) + } else { + DynInserterState::SBWaitingForSourceItems(ins.current_hand) + }, + }, + item, + ins.movetime, + ins.max_hand_size, + ) + }) .collect(), }, @@ -310,106 +371,100 @@ impl SmartBelt { } pub fn change_inserter_storage_id(&mut self, old: FakeUnionStorage, new: FakeUnionStorage) { - for inserter in &mut self.inserters.inserters { - if inserter.0.storage_id == old { - inserter.0.storage_id = new; + self.inserters.inserters.access_mut(|v| { + for inserter in v { + if inserter.storage == old { + inserter.storage = new; + } } - } + }); } pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { let mut pos = 0; - for inserter in self.inserters.inserters.iter_mut() { - pos += inserter.0.offset; - if pos == belt_pos { - inserter.0.storage_id = new; - return; - } else if pos > belt_pos { - unreachable!( - "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", - self.inserters - ); + let Ok(()) = self.inserters.inserters.access_mut(|v| { + for inserter in v { + pos = inserter.belt_pos; + if pos == belt_pos { + inserter.storage = new; + return Ok(()); + } } - pos += 1; - } + Err(()) + }) else { + unreachable!( + "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", + self.inserters + ); + }; } #[must_use] pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { let mut pos = 0; - for inserter in self.inserters.inserters.iter() { - pos += inserter.0.offset; - if pos == belt_pos { - let (dir, state) = inserter.0.state.into(); - return Some(BeltInserterInfo { - outgoing: dir == Dir::BeltToStorage, - state, - connection: inserter.0.storage_id, - }); - } else if pos > belt_pos { - return None; + self.inserters.inserters.access(|v| { + for inserter in v { + pos = inserter.belt_pos; + if pos == belt_pos { + return Some(BeltInserterInfo { + outgoing: inserter.outgoing, + state: if inserter.outgoing { + InserterState::WaitingForSourceItems(inserter.current_hand) + } else { + InserterState::WaitingForSpaceInDestination(inserter.current_hand) + }, + connection: inserter.storage, + movetime: inserter.movetime.into(), + hand_size: inserter.max_hand_size, + }); + } } - pos += 1; - } - - None + None + }) } - pub(super) fn change_inserter_movetime(&mut self, belt_pos: BeltLenType, new_movetime: u16) { - let mut pos = 0; - - for (inserter, movetime, _hand_size) in self.inserters.inserters.iter_mut() { - pos += inserter.offset; - if pos == belt_pos { - *movetime = new_movetime.try_into().unwrap_or(u8::MAX); - return; - } else if pos > belt_pos { - break; - } - pos += 1; - } - unreachable!("The belt did not have an inserter at position specified to change movetime") + pub(super) fn change_inserter_movetime( + &mut self, + belt_pos: BeltLenType, + new_movetime: NonZero, + ) { + let Some(inserter) = self + .inserters + .inserters + .iter_mut() + .find(|ins| ins.belt_pos == belt_pos) + else { + unreachable!( + "The belt did not have an inserter at position specified to change movetime" + ) + }; + inserter.movetime = u16::from(new_movetime) + .try_into() + .unwrap_or(u8::MAX) + .try_into() + .unwrap(); } pub fn remove_inserter(&mut self, pos: BeltLenType) -> Result { if usize::from(pos) >= self.locs.len() { - return Err(()); + unreachable!("Len out of range"); } - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&pos) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } + match self + .inserters + .inserters + .iter() + .position(|ins| ins.belt_pos == pos) + { + Some(idx) => { + let removed = self.inserters.inserters.access_mut(|v| v.remove(idx)); - let mut old_inserter = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - old_inserter = Some(ins.remove(i)); - ins.into_boxed_slice() - }); - let old_inserter = old_inserter.unwrap(); - // The offset after i (which has now shifted left to i) - if let Some(next_offs) = self.inserters.inserters.get_mut(i) { - next_offs.0.offset += old_inserter.0.offset + 1 + Ok(removed.storage) + }, + None => Err(()), } - - Ok(old_inserter.0.storage_id) } // FIXME: This is horrendously slow. it breaks my tests since they are compiled without optimizations!!! @@ -431,53 +486,23 @@ impl SmartBelt { "Bounds check {index} >= {}", self.locs.len() ); + self.latest_inserter_pos_if_all_incoming = None; if filter != self.item { return Err(InserterAdditionError::ItemMismatch); } - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&index) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(InserterAdditionError::SpaceOccupied), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = index - pos_after_last_inserter; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::BeltToStorage, - new_inserter_offset, - storage_id, - ), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); - ins.into_boxed_slice() + self.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: true, + max_hand_size: hand_size, + current_hand: 0, + }); }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some(next_ins) = next { - next_ins.0.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -498,55 +523,24 @@ impl SmartBelt { "Bounds check {index} >= {}", self.locs.len() ); - - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&index) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(InserterAdditionError::SpaceOccupied), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } + self.latest_inserter_pos_if_all_incoming = None; // We only only return an item mismatch if we know the space is free, so we do not transition to sushi, // And then fail anyway if filter != self.item { return Err(InserterAdditionError::ItemMismatch); } - - // Insert at i - let new_inserter_offset = index - pos_after_last_inserter; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::StorageToBelt, - new_inserter_offset, - storage_id, - ), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); - ins.into_boxed_slice() + self.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: false, + max_hand_size: hand_size, + current_hand: 0, + }); }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some(next_ins) = next { - next_ins.0.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -579,98 +573,172 @@ impl SmartBelt { pub fn update_inserters<'a, 'b>( &mut self, - storages: SingleItemStorages<'a, 'b>, - grid_size: usize, + self_index: u32, + reinsertion_outgoing: &mut ReinsertionLists< + '_, + { Dir::BeltToStorage }, + { Dir::BeltToStorage }, + >, + reinsertion_incoming: &mut ReinsertionLists< + '_, + { Dir::BeltToStorage }, + { Dir::StorageToBelt }, + >, ) { - if self.get_len() == 0 { + let Some(extracted) = self.update_inserters_lazy() else { return; + }; + + for ins in extracted { + let in_movement = BeltStorageInserterInMovement { + movetime: ins.movetime, + storage: ins.storage, + belt: self_index, + belt_pos: ins.belt_pos, + max_hand_size: ins.max_hand_size, + current_hand: ins.current_hand, + }; + if ins.outgoing { + reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); + } else { + reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); + } } - let mut i = 0; + } + + pub fn update_inserters_lazy( + &mut self, + ) -> Option + Send> { + if self.get_len() == 0 { + return None; + } + #[cfg(feature = "debug-stat-gathering")] + NUM_BELT_INSERTER_UPDATES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let old_first_free = match self.first_free_index { FreeIndex::FreeIndex(idx) => idx, FreeIndex::OldFreeIndex(idx) => idx, }; - // for ins in self.inserters.inserters.iter_mut() { - // i += usize::from(ins.offset); - // let idx = (i + usize::from(self.zero_index)) % self.locs.len(); - // let loc = self.locs.get_mut(idx); - - // match loc { - // Some(mut loc) => { - // let changed = - // ins.update(loc.as_mut(), storages, MOVETIME, HAND_SIZE, grid_size); - - // if changed { - // // the inserter changed something. - // if !*loc && i < usize::from(first_possible_free_pos) { - // // This is the new first free pos. - // first_possible_free_pos = BeltLenType::try_from(i).unwrap(); - // self.first_free_index = - // FreeIndex::FreeIndex(BeltLenType::try_from(i).unwrap()); - // } else if *loc && i == usize::from(first_possible_free_pos) { - // // This was the old first free pos - // self.first_free_index = - // FreeIndex::OldFreeIndex(BeltLenType::try_from(i).unwrap()); - // } - // } - // }, - // None => unreachable!( - // "Adding the offsets of the inserters is bigger than the length of the belt." - // ), - // } + if let Some(lastest_pos) = self.latest_inserter_pos_if_all_incoming { + // All incoming inserters are incoming and their positions are <= lastest_pos - // i += 1; - // } + if BeltLenType::from(lastest_pos) < old_first_free { + // All inserters are incoming AND all inserters are trying to put onto positions, which are fully filled + // Thus, nothing will change + #[cfg(feature = "debug-stat-gathering")] + if !self.inserters.inserters.is_empty() { + TIMES_ALL_INCOMING_EARLY_RETURN + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } - let mut first_free_changed = false; - for (ins, movetime, hand_size) in self.inserters.inserters.iter_mut() { - i += ins.offset; - // Taken from VecDeque::wrap_index - let logical_index = usize::from(self.zero_index) + usize::from(i); - let loc_idx = if logical_index >= self.locs.len() { - logical_index - self.locs.len() - } else { - logical_index - }; + return None; + } + } - if i < old_first_free { - // We KNOW this position is filled - debug_assert!(self.locs[loc_idx]); - let mut loc = true; - let _changed = ins.update(&mut loc, storages, *movetime, *hand_size, grid_size); - - if !loc { - self.locs.set(loc_idx, false); - if !first_free_changed { - self.first_free_index = FreeIndex::FreeIndex(i); - first_free_changed = true; - } + let mut new_first_free = old_first_free; + + let mut min_pos = Some(NonZero::new(1).unwrap()); + + let zero_index = &mut self.zero_index; + let first_free_index = &mut self.first_free_index; + let locs = &mut self.locs; + let latest_inserter_pos_if_all_incoming = &mut self.latest_inserter_pos_if_all_incoming; + + let extracted = self.inserters.inserters.access_mut(|v| { + v.extract_if(.., move |ins| { + // FIXME: This should not be needed, if we did not incorrectly insert inserters always in the belt + if ins.current_hand == 0 && !ins.outgoing { + return true; } - } else { - let mut loc = self.locs.get_mut(loc_idx).unwrap(); - let changed = ins.update(loc.as_mut(), storages, *movetime, *hand_size, grid_size); + // Taken from VecDeque::wrap_index + let logical_index = usize::from(*zero_index) + usize::from(ins.belt_pos); + let loc_idx = if logical_index >= locs.len() { + logical_index - locs.len() + } else { + logical_index + }; + + #[cfg(feature = "debug-stat-gathering")] + if ins.outgoing { + NUM_INSERTER_LOADS_WAITING_FOR_ITEMS + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } else { + NUM_INSERTER_LOADS_WAITING_FOR_SPACE + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } - if changed { - // the inserter changed something. - if !first_free_changed && i == old_first_free && *loc { - // This was the old first free pos - self.first_free_index = - FreeIndex::OldFreeIndex(BeltLenType::try_from(i).unwrap()); + if ins.outgoing { + min_pos = None; + } else { + if let Some(min_pos) = &mut min_pos { + *min_pos = std::cmp::max(*min_pos, NonZero::new(ins.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")); } - // if !first_free_changed && i == old_first_free && !*loc { - // // This is the new first_free_pos - // self.first_free_index = - // FreeIndex::FreeIndex(BeltLenType::try_from(i).unwrap()); - // first_free_changed = true; - // } } - } + *latest_inserter_pos_if_all_incoming = min_pos; + + let extract = if ins.belt_pos < old_first_free { + // We KNOW this position is filled + debug_assert!(locs[loc_idx]); + if ins.outgoing { + ins.current_hand += 1; + locs.set(loc_idx, false); + if ins.belt_pos <= new_first_free { + *first_free_index = FreeIndex::FreeIndex(ins.belt_pos); + new_first_free = ins.belt_pos; + } + + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } + } else { + #[cfg(feature = "debug-stat-gathering")] + NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + false + } + } else { + let mut loc = locs.get_mut(loc_idx).unwrap(); + + if ins.outgoing && *loc { + *loc = false; + ins.current_hand += 1; + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } + } else if !ins.outgoing && !*loc { + *loc = true; + ins.current_hand -= 1; + + if ins.belt_pos == new_first_free && *loc { + // This was the old first free pos + *first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); + } + + if ins.current_hand == 0 { true } else { false } + } else { + false + } + }; - i += 1; - } + #[cfg(feature = "debug-stat-gathering")] + if extract { + TIMES_INSERTERS_EXTRACTED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + extract + }) + // TODO: When using a SmallCapVec, the we cannot lazily return the extract_if iterator, since the vec no longer exists after the closure returns + // This could be fixed using a custom extract_if implementation, but that is very hairy unsafe code I am unwilling to commit to for now + .collect_vec() + }); + + Some(extracted) } fn remove_first_free_pos_maybe(&mut self, now_filled_pos: BeltLenType) { @@ -705,7 +773,7 @@ impl SmartBelt { pub fn get_update_size(&self) -> (usize, usize, usize, usize, usize) { let free_index_search_indices = match self.first_free_index { - FreeIndex::FreeIndex(idx) => vec![], + FreeIndex::FreeIndex(_idx) => vec![], FreeIndex::OldFreeIndex(idx) => self .items() .skip(usize::from(idx)) @@ -725,43 +793,34 @@ impl SmartBelt { * std::mem::size_of::()) .div_ceil(64); + let mut old_idx = 0; let cache_lines_from_inserter_belt_lookup = self .inserters .inserters .iter() - .map(|ins| match ins.0.state.into() { - (Dir::StorageToBelt, InserterState::WaitingForSpaceInDestination(_)) => { - (ins.0.offset, true) - }, - (Dir::BeltToStorage, InserterState::WaitingForSourceItems(_)) => { - (ins.0.offset, true) - }, + .map(|ins| match (ins.outgoing,) { + (false,) => (ins.belt_pos, true), + (true,) => (ins.belt_pos, true), - _ => (ins.0.offset, false), - }) - .fold((0u16, 0usize), |(old_idx, old_count), (offs, needs)| { - let our_idx = old_idx + offs; - ( - our_idx + 1, - old_count - + usize::from( - needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), - ), - ) + _ => (ins.belt_pos, false), }) - .1; + .fold(0usize, |old_count, (belt_pos, needs)| { + let our_idx = belt_pos; + let new_count = old_count + + usize::from( + needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), + ); + old_idx = our_idx; + new_count + }); let cache_lines_from_storage_lookup = self .inserters .inserters .iter() - .filter_map(|ins| match ins.0.state.into() { - (Dir::StorageToBelt, InserterState::WaitingForSpaceInDestination(_)) => { - Some(ins.0.storage_id) - }, - (Dir::BeltToStorage, InserterState::WaitingForSourceItems(_)) => { - Some(ins.0.storage_id) - }, + .filter_map(|ins| match (ins.outgoing,) { + (false,) => Some(ins.storage), + (true,) => Some(ins.storage), _ => None, }) @@ -955,6 +1014,8 @@ impl SmartBelt { input_splitter: None, output_splitter, + + latest_inserter_pos_if_all_incoming: _, } = front else { unreachable!() @@ -976,6 +1037,8 @@ impl SmartBelt { input_splitter, output_splitter: None, + + latest_inserter_pos_if_all_incoming: _, } = back else { unreachable!() @@ -990,28 +1053,17 @@ impl SmartBelt { // Important, first_free_index must ALWAYS be used using mod len let back_zero_index = usize::from(back_zero_index) % back_locs.len(); - let num_front_inserters = front_inserters.inserters.len(); - let _num_back_inserters = back_inserters.inserters.len(); - - let free_spots_before_last_inserter_front: u16 = - front_inserters.inserters.iter().map(|i| i.0.offset).sum(); - let length_after_last_inserter = TryInto::::try_into(front_len) - .expect("Belt should be max u16::MAX long") - - free_spots_before_last_inserter_front - - TryInto::::try_into(num_front_inserters) - .expect("Belt should be max u16::MAX long"); - - if let Some(ins) = back_inserters.inserters.get_mut(0) { - ins.0.offset += length_after_last_inserter; - } - let mut new_inserters = front_inserters; - take_mut::take(&mut new_inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - let mut other = vec![].into_boxed_slice(); - mem::swap(&mut other, &mut back_inserters.inserters); - ins.extend(other.into_vec().drain(..)); - ins.into_boxed_slice() + new_inserters.inserters.access_mut(|new| { + back_inserters.inserters.access_mut(|back| { + new.extend(back.drain(..).map(|mut back_ins| { + back_ins.belt_pos = back_ins + .belt_pos + .checked_add(front_len.try_into().unwrap()) + .unwrap(); + back_ins + })); + }); }); let mut front_locs_vec = BitBox::from(front_locs).into_bitvec(); @@ -1039,6 +1091,9 @@ impl SmartBelt { input_splitter, output_splitter, + + // Since this is just an optimization, and will be rechecked on next update, None is fine + latest_inserter_pos_if_all_incoming: None, } } @@ -1063,6 +1118,8 @@ impl SmartBelt { input_splitter, output_splitter, + + latest_inserter_pos_if_all_incoming: _earliest_inserter_pos_if_all_incoming, } = self; match side { @@ -1079,7 +1136,7 @@ impl SmartBelt { let old_len = locs.len(); - let (new_empty, new_zero, front_extension_amount) = match side { + let (new_empty, new_zero, _front_extension_amount) = match side { Side::FRONT => { locs.splice( usize::from(zero_index)..usize::from(zero_index), @@ -1105,13 +1162,14 @@ impl SmartBelt { }; if side == Side::FRONT { - if !inserters.inserters.is_empty() { - inserters.inserters[0].0.offset = inserters.inserters[0] - .0 - .offset - .checked_add(front_extension_amount) - .expect("Max length of belt (u16::MAX) reached"); - } + inserters.inserters.access_mut(|v| { + for ins in v { + ins.belt_pos = ins + .belt_pos + .checked_add(len) + .expect("Max length of belt (u16::MAX) reached"); + } + }); } let mut new = Self { @@ -1131,6 +1189,9 @@ impl SmartBelt { input_splitter, output_splitter, + + // Since this is just an optimization, and will be rechecked on next update, None is fine + latest_inserter_pos_if_all_incoming: None, }; new.find_and_update_real_first_free_index(); @@ -1138,103 +1199,104 @@ impl SmartBelt { new } - pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { - // TODO: Is this correct - if self.is_circular { - assert!(self.input_splitter.is_none()); - assert!(self.output_splitter.is_none()); - self.is_circular = false; - self.first_free_index = FreeIndex::OldFreeIndex(0); - self.zero_index = belt_pos_to_break_at; - // FIXME: This will teleport items - return None; - } + pub fn break_belt_at(&mut self, _belt_pos_to_break_at: u16) -> Option { + todo!() + // // TODO: Is this correct + // if self.is_circular { + // assert!(self.input_splitter.is_none()); + // assert!(self.output_splitter.is_none()); + // self.is_circular = false; + // self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = belt_pos_to_break_at; + // // FIXME: This will teleport items + // return None; + // } - if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { - return None; - } + // if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { + // return None; + // } - let mut new_locs = None; - take_mut::take(&mut self.locs, |locs| { - let mut locs_vec = BitBox::from(locs).into_bitvec(); + // let mut new_locs = None; + // take_mut::take(&mut self.locs, |locs| { + // let mut locs_vec = BitBox::from(locs).into_bitvec(); - let len = locs_vec.len(); + // let len = locs_vec.len(); - locs_vec.rotate_left(usize::from(self.zero_index) % len); + // locs_vec.rotate_left(usize::from(self.zero_index) % len); - new_locs = Some( - locs_vec - .split_off(belt_pos_to_break_at.into()) - .into_boxed_bitslice(), - ); + // new_locs = Some( + // locs_vec + // .split_off(belt_pos_to_break_at.into()) + // .into_boxed_bitslice(), + // ); - locs_vec.into_boxed_bitslice().into() - }); + // locs_vec.into_boxed_bitslice().into() + // }); - self.zero_index = 0; - self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = 0; + // self.first_free_index = FreeIndex::OldFreeIndex(0); - let new_locs = new_locs.unwrap(); + // let new_locs = new_locs.unwrap(); - let mut offsets = self - .inserters - .inserters - .iter() - .map(|i| i.0.offset) - .enumerate(); + // let mut offsets = self + // .inserters + // .inserters + // .iter() + // .map(|i| i.0.offset) + // .enumerate(); - let mut current_pos = 0; + // let mut current_pos = 0; - let (split_at_inserters, new_offs) = loop { - let Some((i, next_offset)) = offsets.next() else { - break (self.inserters.inserters.len(), 0); - }; + // let (split_at_inserters, new_offs) = loop { + // let Some((i, next_offset)) = offsets.next() else { + // break (self.inserters.inserters.len(), 0); + // }; - // Skip next_offset spots - current_pos += next_offset; + // // Skip next_offset spots + // current_pos += next_offset; - if current_pos >= belt_pos_to_break_at { - break (i, current_pos - belt_pos_to_break_at); - } + // if current_pos >= belt_pos_to_break_at { + // break (i, current_pos - belt_pos_to_break_at); + // } - // The spot, the inserter corresponding to this offset is placed - current_pos += 1; - }; + // // The spot, the inserter corresponding to this offset is placed + // current_pos += 1; + // }; - let mut new_inserters = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); - ins.into_boxed_slice() - }); - let mut new_inserters = new_inserters.unwrap(); + // let mut new_inserters = None; + // take_mut::take(&mut self.inserters.inserters, |ins| { + // let mut ins = ins.into_vec(); + // new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); + // ins.into_boxed_slice() + // }); + // let mut new_inserters = new_inserters.unwrap(); - if let Some(ins) = new_inserters.get_mut(0) { - ins.0.offset = new_offs; - } + // if let Some(ins) = new_inserters.get_mut(0) { + // ins.0.offset = new_offs; + // } - // Since we split off the back portion, it will own our input splitter if we have one - let input_splitter = self.input_splitter.take(); + // // Since we split off the back portion, it will own our input splitter if we have one + // let input_splitter = self.input_splitter.take(); - let new_belt = Self { - ty: self.ty, + // let new_belt = Self { + // ty: self.ty, - is_circular: false, - first_free_index: FreeIndex::OldFreeIndex(0), - zero_index: 0, - locs: new_locs.into(), - inserters: InserterStoreDyn { - inserters: new_inserters, - }, - item: self.item, + // is_circular: false, + // first_free_index: FreeIndex::OldFreeIndex(0), + // zero_index: 0, + // locs: new_locs.into(), + // inserters: InserterStoreDyn { + // inserters: new_inserters, + // }, + // item: self.item, - last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), + // last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), - input_splitter, - output_splitter: None, - }; + // input_splitter, + // output_splitter: None, + // }; - Some(new_belt) + // Some(new_belt) } } @@ -1286,12 +1348,14 @@ impl EmptyBelt { zero_index: 0, locs: bitbox![0; self.len as usize].into(), inserters: InserterStoreDyn { - inserters: vec![].into_boxed_slice(), + inserters: VecHolder::new(), }, item, last_moving_spot: 0, input_splitter: self.input_splitter, output_splitter: self.output_splitter, + + latest_inserter_pos_if_all_incoming: None, } } @@ -1415,7 +1479,7 @@ impl Belt for EmptyBelt { self.len } - fn add_length(&mut self, amount: BeltLenType, side: Side) -> BeltLenType { + fn add_length(&mut self, amount: BeltLenType, _side: Side) -> BeltLenType { self.len += amount; self.len } @@ -1464,16 +1528,16 @@ impl Belt for SmartBelt { return (vec![], self.get_len()); } - let before_inserter_positions = (0..self.inserters.inserters.len()) - .map(|i| { - let offsets: u16 = self.inserters.inserters[..=i] - .iter() - .map(|i| i.0.offset) - .sum(); - let occupied_spaces = i; - let pos = offsets as usize + occupied_spaces; - pos - }) + let kept_range = match side { + Side::FRONT => amount..self.get_len(), + Side::BACK => 0..(self.get_len().checked_sub(amount).unwrap()), + }; + + let before_inserter_positions = self + .inserters + .inserters + .iter() + .map(|ins| ins.belt_pos) .collect_vec(); assert!(!self.is_circular); @@ -1496,51 +1560,31 @@ impl Belt for SmartBelt { locs.into_boxed_bitslice().into() }); - let kept_range = match side { - Side::FRONT => amount..(self.get_len() + amount), - Side::BACK => 0..self.get_len(), - }; - - let mut pos_after_last_inserter = 0; - let mut pos_after_last_removed_inserter = 0; - - take_mut::take(&mut self.inserters.inserters, |inserters| { - let mut inserters = inserters.into_vec(); - - // FIXME: This is awful, but it should work - inserters.retain(|inserter| { - let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; - pos_after_last_inserter = next_inserter_pos + 1; - - if !kept_range.contains(&next_inserter_pos) { - pos_after_last_removed_inserter = pos_after_last_inserter; + self.inserters.inserters.access_mut(|v| { + v.retain(|inserter| { + if !kept_range.contains(&inserter.belt_pos) { false } else { true } }); - - inserters.into_boxed_slice() }); if side == Side::FRONT { - if let Some(ins) = self.inserters.inserters.first_mut() { - ins.0.offset -= amount - pos_after_last_removed_inserter; - } + self.inserters.inserters.access_mut(|v| { + for ins in v { + ins.belt_pos = ins.belt_pos.checked_sub(amount).unwrap(); + } + }); } self.first_free_index = FreeIndex::OldFreeIndex(0); - let after_inserter_positions = (0..self.inserters.inserters.len()) - .map(|i| { - let offsets: u16 = self.inserters.inserters[..=i] - .iter() - .map(|i| i.0.offset) - .sum(); - let occupied_spaces = i; - let pos = offsets as usize + occupied_spaces; - pos - }) + let after_inserter_positions = self + .inserters + .inserters + .iter() + .map(|ins| ins.belt_pos) .collect_vec(); match side { @@ -1551,7 +1595,7 @@ impl Belt for SmartBelt { .zip(after_inserter_positions.iter().rev()) { assert_eq!( - *before - amount as usize, + *before - amount, *after, "before: {:?}\n after: {:?}", before_inserter_positions, @@ -1670,7 +1714,7 @@ impl Belt for SmartBelt { }, } - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_UPDATES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); match self.first_free_index { @@ -1680,7 +1724,7 @@ impl Belt for SmartBelt { FreeIndex::OldFreeIndex(_) => {}, } } - let (old_free, need_to_check) = match self.first_free_index { + let (_old_free, need_to_check) = match self.first_free_index { FreeIndex::FreeIndex(idx) => (idx, false), FreeIndex::OldFreeIndex(idx) => (idx, true), }; @@ -1693,7 +1737,7 @@ impl Belt for SmartBelt { let Some(first_free_index_real) = first_free_index_real else { // All slots are full - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_LOCS_SEARCHED.fetch_add( (len - old_free) as usize, @@ -1703,7 +1747,7 @@ impl Belt for SmartBelt { return; }; - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_LOCS_SEARCHED.fetch_add( (first_free_index_real - old_free) as usize, diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index fc298d0..255dbb0 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -1,5 +1,5 @@ -use std::collections::HashMap; -use std::{iter::repeat, mem}; +use std::mem; +use std::num::NonZero; use itertools::Itertools; @@ -8,7 +8,9 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; +use crate::belt::smart::InserterExtractedWhenMoving; use crate::inserter::belt_storage_inserter::Dir; +use crate::item::ITEMCOUNTTYPE; use crate::{ belt::belt::NoSpaceError, item::{IdxTrait, Item, WeakIdxTrait}, @@ -24,8 +26,6 @@ use crate::inserter::FakeUnionStorage; use crate::inserter::belt_storage_inserter_non_const_gen::BeltStorageInserterDyn; use itertools::Either; -use crate::belt::smart::{HAND_SIZE, MOVETIME}; - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct SushiBelt { @@ -48,7 +48,14 @@ pub struct SushiBelt { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub(super) struct SushiInserterStoreDyn { - pub(super) inserters: Box<[(BeltStorageInserterDyn, Item)]>, + pub(super) inserters: Box< + [( + BeltStorageInserterDyn, + Item, + NonZero, + ITEMCOUNTTYPE, + )], + >, } #[derive(Debug, PartialEq, Eq)] @@ -82,6 +89,8 @@ impl SushiBelt { filter: Item, pos: BeltLenType, storage_id: FakeUnionStorage, + movetime: u16, + hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -89,47 +98,17 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&pos) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(SpaceOccupiedError), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = pos - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::StorageToBelt, - new_inserter_offset, - storage_id, - ), - filter, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::StorageToBelt, pos, storage_id), + filter, + movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some((next_ins, _item)) = next { - next_ins.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -138,6 +117,8 @@ impl SushiBelt { filter: Item, pos: BeltLenType, storage_id: FakeUnionStorage, + movetime: u16, + hand_size: ITEMCOUNTTYPE, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -145,70 +126,36 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&pos) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(SpaceOccupiedError), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = pos - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::BeltToStorage, - new_inserter_offset, - storage_id, - ), - filter, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::BeltToStorage, pos, storage_id), + filter, + movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some((next_ins, _item)) = next { - next_ins.offset -= new_inserter_offset + 1; - } - Ok(()) } #[must_use] pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { - let mut pos = 0; - - for (inserter, _item) in self.inserters.inserters.iter() { - pos += inserter.offset; - if pos == belt_pos { - let (dir, state) = inserter.state.into(); - return Some(BeltInserterInfo { + self.inserters + .inserters + .iter() + .find(|ins| ins.0.belt_pos == belt_pos) + .map(|ins| { + let (dir, state) = ins.0.state.into(); + BeltInserterInfo { outgoing: dir == Dir::BeltToStorage, state, - connection: inserter.storage_id, - }); - } else if pos > belt_pos { - return None; - } - pos += 1; - } - - None + connection: ins.0.storage_id, + hand_size: ins.3, + movetime: ins.2.into(), + } + }) } pub fn get_inserter_item(&self, belt_pos: u16) -> Item { @@ -218,41 +165,24 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&belt_pos) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - self.inserters.inserters[i].1 + self.inserters + .inserters + .iter() + .find(|ins| ins.0.belt_pos == belt_pos) + .map(|ins| ins.1) + .expect("No inserter at pos") } pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { - let mut pos = 0; - - for (inserter, _item) in self.inserters.inserters.iter_mut() { - pos += inserter.offset; - if pos == belt_pos { - inserter.storage_id = new; - return; - } else if pos >= belt_pos { - unreachable!() - } - pos += 1; - } + let Some(ins) = self + .inserters + .inserters + .iter_mut() + .find(|ins| ins.0.belt_pos == belt_pos) + else { + unreachable!() + }; + ins.0.storage_id = new; } pub fn remove_inserter(&mut self, pos: BeltLenType) { @@ -262,24 +192,12 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|(i, _item)| i.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&pos) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } + let i = self + .inserters + .inserters + .iter() + .position(|ins| ins.0.belt_pos == pos) + .unwrap(); let mut removed = None; take_mut::take(&mut self.inserters.inserters, |ins| { @@ -288,8 +206,7 @@ impl SushiBelt { ins.into_boxed_slice() }); let removed = removed.unwrap(); - // The offset after i (which has now shifted left to i) - self.inserters.inserters[i].0.offset += removed.0.offset + 1; + todo!("{:?}", removed); } pub(super) fn check_sushi( @@ -309,7 +226,7 @@ impl SushiBelt { .inserters .inserters .iter() - .map(|(_, item)| *item) + .map(|(_, item, _movetime, _hand_size)| *item) .chain(belt_belt_filter_in.into_iter().filter_map(|info| { let SushiInfo::Pure(item) = info else { unreachable!() @@ -395,20 +312,35 @@ impl SushiBelt { .collect::().into(), inserters: InserterStoreDyn { // FIXME: Some of these inserters might have a different item than what we are converting to. This will result in crashes and item transmutation - inserters: inserters.into_iter().map(|(ins, inserter_item)| { + inserters: inserters.into_iter().map(|(ins, inserter_item, movetime, hand_size)| { assert_eq!(item, inserter_item, "FIXME: We need to handle inserters which will never work again in smart belts"); // if item != inserter_item { // error!("We need to handle inserters which will never work again in smart belts!!!!!!!"); // } - (ins, MOVETIME, HAND_SIZE) - }).collect(), + let (dir, state) = ins.state.into(); + InserterExtractedWhenMoving { + storage: ins.storage_id, + belt_pos: ins.belt_pos, + movetime, + outgoing: dir == Dir::BeltToStorage, + max_hand_size: hand_size, + current_hand: match state { + crate::inserter::InserterState::WaitingForSourceItems(hand) => hand, + crate::inserter::InserterState::WaitingForSpaceInDestination(hand) => hand, + crate::inserter::InserterState::FullAndMovingOut(_) => hand_size, + crate::inserter::InserterState::EmptyAndMovingBack(_) => 0, + }, + } + }).collect::>().try_into().unwrap(), }, item, last_moving_spot, input_splitter, - output_splitter + output_splitter, + + latest_inserter_pos_if_all_incoming: None, } } @@ -446,99 +378,100 @@ impl SushiBelt { self.output_splitter.take() } - pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { - // TODO: Is this correct - if self.is_circular { - self.is_circular = false; - self.first_free_index = FreeIndex::OldFreeIndex(0); - self.zero_index = belt_pos_to_break_at; - return None; - } + pub fn break_belt_at(&mut self, _belt_pos_to_break_at: u16) -> Option { + todo!() + // // TODO: Is this correct + // if self.is_circular { + // self.is_circular = false; + // self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = belt_pos_to_break_at; + // return None; + // } - if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { - return None; - } + // if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { + // return None; + // } - let mut new_locs = None; - take_mut::take(&mut self.locs, |locs| { - let mut locs_vec = locs.into_vec(); + // let mut new_locs = None; + // take_mut::take(&mut self.locs, |locs| { + // let mut locs_vec = locs.into_vec(); - let len = locs_vec.len(); + // let len = locs_vec.len(); - locs_vec.rotate_left(usize::from(self.zero_index) % len); + // locs_vec.rotate_left(usize::from(self.zero_index) % len); - new_locs = Some( - locs_vec - .split_off(belt_pos_to_break_at.into()) - .into_boxed_slice(), - ); + // new_locs = Some( + // locs_vec + // .split_off(belt_pos_to_break_at.into()) + // .into_boxed_slice(), + // ); - locs_vec.into_boxed_slice() - }); + // locs_vec.into_boxed_slice() + // }); - self.zero_index = 0; - self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = 0; + // self.first_free_index = FreeIndex::OldFreeIndex(0); - let new_locs = new_locs.unwrap(); + // let new_locs = new_locs.unwrap(); - let mut offsets = self - .inserters - .inserters - .iter() - .map(|(i, _item)| i.offset) - .enumerate(); + // let mut offsets = self + // .inserters + // .inserters + // .iter() + // .map(|(i, _item, _movetime, _hand_size)| i.offset) + // .enumerate(); - let mut current_pos = 0; + // let mut current_pos = 0; - let (split_at_inserters, new_offs) = loop { - let Some((i, next_offset)) = offsets.next() else { - break (self.inserters.inserters.len(), 0); - }; + // let (split_at_inserters, new_offs) = loop { + // let Some((i, next_offset)) = offsets.next() else { + // break (self.inserters.inserters.len(), 0); + // }; - // Skip next_offset spots - current_pos += next_offset; + // // Skip next_offset spots + // current_pos += next_offset; - if current_pos >= belt_pos_to_break_at { - break (i, current_pos - belt_pos_to_break_at); - } + // if current_pos >= belt_pos_to_break_at { + // break (i, current_pos - belt_pos_to_break_at); + // } - // The spot, the inserter corresponding to this offset is placed - current_pos += 1; - }; + // // The spot, the inserter corresponding to this offset is placed + // current_pos += 1; + // }; - let mut new_inserters = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); - ins.into_boxed_slice() - }); - let mut new_inserters = new_inserters.unwrap(); + // let mut new_inserters = None; + // take_mut::take(&mut self.inserters.inserters, |ins| { + // let mut ins = ins.into_vec(); + // new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); + // ins.into_boxed_slice() + // }); + // let mut new_inserters = new_inserters.unwrap(); - if let Some(new_ins) = new_inserters.get_mut(0) { - new_ins.0.offset = new_offs; - } + // if let Some(new_ins) = new_inserters.get_mut(0) { + // new_ins.0.offset = new_offs; + // } - // Since self will end up as the front half, any inputting splitter will end up at the back belt - let input_splitter = self.input_splitter.take(); + // // Since self will end up as the front half, any inputting splitter will end up at the back belt + // let input_splitter = self.input_splitter.take(); - let new_belt = Self { - ty: self.ty, + // let new_belt = Self { + // ty: self.ty, - is_circular: false, - first_free_index: FreeIndex::OldFreeIndex(0), - zero_index: 0, - locs: new_locs, - inserters: SushiInserterStoreDyn { - inserters: new_inserters, - }, + // is_circular: false, + // first_free_index: FreeIndex::OldFreeIndex(0), + // zero_index: 0, + // locs: new_locs, + // inserters: SushiInserterStoreDyn { + // inserters: new_inserters, + // }, - last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), + // last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), - input_splitter, - output_splitter: None, - }; + // input_splitter, + // output_splitter: None, + // }; - Some(new_belt) + // Some(new_belt) } pub fn make_circular(&mut self) { @@ -600,30 +533,19 @@ impl SushiBelt { // Important, first_free_index must ALWAYS be used using mod len let back_zero_index = usize::from(back_zero_index) % back_locs.len(); - let num_front_inserters = front_inserters.inserters.len(); - let _num_back_inserters = back_inserters.inserters.len(); - - let free_spots_before_last_inserter_front: u16 = front_inserters - .inserters - .iter() - .map(|(i, _item)| i.offset) - .sum(); - let length_after_last_inserter = TryInto::::try_into(front_len) - .expect("Belt should be max u16::MAX long") - - free_spots_before_last_inserter_front - - TryInto::::try_into(num_front_inserters) - .expect("Belt should be max u16::MAX long"); - - if let Some((i, _item)) = back_inserters.inserters.get_mut(0) { - i.offset += length_after_last_inserter; - } - let mut new_inserters = front_inserters; take_mut::take(&mut new_inserters.inserters, |ins| { let mut ins = ins.into_vec(); let mut other = vec![].into_boxed_slice(); mem::swap(&mut other, &mut back_inserters.inserters); - ins.extend(other.into_vec().drain(..)); + ins.extend(other.into_vec().drain(..).map(|mut back_ins| { + back_ins.0.belt_pos = back_ins + .0 + .belt_pos + .checked_add(front_len.try_into().unwrap()) + .unwrap(); + back_ins + })); ins.into_boxed_slice() }); @@ -992,160 +914,162 @@ impl Belt for SushiBelt { fn remove_length( &mut self, - amount: BeltLenType, - side: Side, + _amount: BeltLenType, + _side: Side, ) -> (Vec<(Item, u32)>, BeltLenType) { - if amount == 0 { - return (vec![], self.get_len()); - } - - assert!(!self.is_circular); - assert!(amount <= self.get_len()); - - self.locs - .rotate_left(self.zero_index as usize % self.locs.len()); - self.zero_index = 0; - let mut item_counts = HashMap::default(); - take_mut::take(&mut self.locs, |locs| { - let mut locs = locs.into_vec(); - - let removed_items = match side { - Side::FRONT => locs.drain(..(amount as usize)), - Side::BACK => locs.drain((locs.len() - (amount as usize))..), - }; - - item_counts = removed_items.flatten().counts(); - - locs.into_boxed_slice() - }); - - let kept_range = match side { - Side::FRONT => amount..(self.get_len() + amount), - Side::BACK => 0..self.get_len(), - }; - - let mut pos_after_last_inserter = 0; - let mut pos_after_last_removed_inserter = 0; - - take_mut::take(&mut self.inserters.inserters, |inserters| { - let mut inserters = inserters.into_vec(); - - // FIXME: This is awful, but it should work - inserters.retain(|inserter| { - let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; - pos_after_last_inserter = next_inserter_pos + 1; - - if !kept_range.contains(&next_inserter_pos) { - pos_after_last_removed_inserter = pos_after_last_inserter; - false - } else { - true - } - }); - - inserters.into_boxed_slice() - }); - - if side == Side::FRONT { - if let Some((i, _ietm)) = self.inserters.inserters.first_mut() { - i.offset -= amount - pos_after_last_removed_inserter; - } - } - - self.first_free_index = FreeIndex::OldFreeIndex(0); - - ( - item_counts - .into_iter() - .sorted_by_key(|(k, _)| *k) - .map(|(k, v)| (k, v.try_into().unwrap())) - .collect_vec(), - self.get_len(), - ) + todo!() + // if amount == 0 { + // return (vec![], self.get_len()); + // } + + // assert!(!self.is_circular); + // assert!(amount <= self.get_len()); + + // self.locs + // .rotate_left(self.zero_index as usize % self.locs.len()); + // self.zero_index = 0; + // let mut item_counts = HashMap::default(); + // take_mut::take(&mut self.locs, |locs| { + // let mut locs = locs.into_vec(); + + // let removed_items = match side { + // Side::FRONT => locs.drain(..(amount as usize)), + // Side::BACK => locs.drain((locs.len() - (amount as usize))..), + // }; + + // item_counts = removed_items.flatten().counts(); + + // locs.into_boxed_slice() + // }); + + // let kept_range = match side { + // Side::FRONT => amount..(self.get_len() + amount), + // Side::BACK => 0..self.get_len(), + // }; + + // let mut pos_after_last_inserter = 0; + // let mut pos_after_last_removed_inserter = 0; + + // take_mut::take(&mut self.inserters.inserters, |inserters| { + // let mut inserters = inserters.into_vec(); + + // // FIXME: This is awful, but it should work + // inserters.retain(|inserter| { + // let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; + // pos_after_last_inserter = next_inserter_pos + 1; + + // if !kept_range.contains(&next_inserter_pos) { + // pos_after_last_removed_inserter = pos_after_last_inserter; + // false + // } else { + // true + // } + // }); + + // inserters.into_boxed_slice() + // }); + + // if side == Side::FRONT { + // if let Some((i, _item, _movetime, _hand_size)) = self.inserters.inserters.first_mut() { + // i.offset -= amount - pos_after_last_removed_inserter; + // } + // } + + // self.first_free_index = FreeIndex::OldFreeIndex(0); + + // ( + // item_counts + // .into_iter() + // .sorted_by_key(|(k, _)| *k) + // .map(|(k, v)| (k, v.try_into().unwrap())) + // .collect_vec(), + // self.get_len(), + // ) } - fn add_length(&mut self, amount: BeltLenType, side: super::smart::Side) -> BeltLenType { - let len = self.get_len(); - - take_mut::take(self, |slf| { - let Self { - ty, - - is_circular: _, - first_free_index, - zero_index, - locs, - mut inserters, - - last_moving_spot, - - input_splitter, - output_splitter, - } = slf; - - match side { - Side::FRONT => assert!(output_splitter.is_none()), - Side::BACK => assert!(input_splitter.is_none()), - } - - // Important, first_free_index must ALWAYS be used using mod len - let zero_index = zero_index % len; - - let mut locs = locs.into_vec(); - - let old_len = locs.len(); - - let (new_empty, new_zero, front_extension_amount) = match side { - Side::FRONT => { - locs.splice( - usize::from(zero_index)..usize::from(zero_index), - repeat(None).take(usize::from(amount)), - ); - (FreeIndex::FreeIndex(0), zero_index, amount) - }, - Side::BACK => { - locs.splice( - ((usize::from(zero_index) + (old_len - 1)) % old_len) - ..((usize::from(zero_index) + (old_len - 1)) % old_len), - repeat(None).take(usize::from(amount)), - ); - ( - match first_free_index { - FreeIndex::FreeIndex(idx) => FreeIndex::FreeIndex(idx), - FreeIndex::OldFreeIndex(idx) => FreeIndex::OldFreeIndex(idx), - }, - zero_index + amount, - 0, - ) - }, - }; - - if side == Side::FRONT { - if !inserters.inserters.is_empty() { - inserters.inserters[0].0.offset = inserters.inserters[0] - .0 - .offset - .checked_add(front_extension_amount) - .expect("Max length of belt (u16::MAX) reached"); - } - } - - Self { - ty, - - is_circular: false, - first_free_index: new_empty, - zero_index: new_zero, - locs: locs.into_boxed_slice(), - inserters, - - last_moving_spot, - - input_splitter, - output_splitter, - } - }); - - len + amount + fn add_length(&mut self, _amount: BeltLenType, _side: super::smart::Side) -> BeltLenType { + todo!() + // let len = self.get_len(); + + // take_mut::take(self, |slf| { + // let Self { + // ty, + + // is_circular: _, + // first_free_index, + // zero_index, + // locs, + // mut inserters, + + // last_moving_spot, + + // input_splitter, + // output_splitter, + // } = slf; + + // match side { + // Side::FRONT => assert!(output_splitter.is_none()), + // Side::BACK => assert!(input_splitter.is_none()), + // } + + // // Important, first_free_index must ALWAYS be used using mod len + // let zero_index = zero_index % len; + + // let mut locs = locs.into_vec(); + + // let old_len = locs.len(); + + // let (new_empty, new_zero, front_extension_amount) = match side { + // Side::FRONT => { + // locs.splice( + // usize::from(zero_index)..usize::from(zero_index), + // repeat(None).take(usize::from(amount)), + // ); + // (FreeIndex::FreeIndex(0), zero_index, amount) + // }, + // Side::BACK => { + // locs.splice( + // ((usize::from(zero_index) + (old_len - 1)) % old_len) + // ..((usize::from(zero_index) + (old_len - 1)) % old_len), + // repeat(None).take(usize::from(amount)), + // ); + // ( + // match first_free_index { + // FreeIndex::FreeIndex(idx) => FreeIndex::FreeIndex(idx), + // FreeIndex::OldFreeIndex(idx) => FreeIndex::OldFreeIndex(idx), + // }, + // zero_index + amount, + // 0, + // ) + // }, + // }; + + // if side == Side::FRONT { + // if !inserters.inserters.is_empty() { + // inserters.inserters[0].0.offset = inserters.inserters[0] + // .0 + // .offset + // .checked_add(front_extension_amount) + // .expect("Max length of belt (u16::MAX) reached"); + // } + // } + + // Self { + // ty, + + // is_circular: false, + // first_free_index: new_empty, + // zero_index: new_zero, + // locs: locs.into_boxed_slice(), + // inserters, + + // last_moving_spot, + + // input_splitter, + // output_splitter, + // } + // }); + + // len + amount } } diff --git a/src/blueprint/blueprint_string.rs b/src/blueprint/blueprint_string.rs index 76b9b50..06f7ebf 100644 --- a/src/blueprint/blueprint_string.rs +++ b/src/blueprint/blueprint_string.rs @@ -1,5 +1,6 @@ use base64::engine::general_purpose::STANDARD; use flate2::Compression; +use log::error; use super::Blueprint; use super::BlueprintAction; @@ -46,7 +47,7 @@ struct BlueprintStringInternal { Option, )>, - inserters: Vec<(BaseEntity, Option)>, + inserters: Vec<(BaseEntity, Option, Option>)>, set_recipe: Vec<(Position, usize)>, movetime: Vec<(Position, Option>)>, @@ -59,8 +60,13 @@ struct BlueprintStringInternal { modules: Vec<(Position, usize)>, } +#[derive(Debug)] +pub enum BlueprintImportError { + BlueprintStringInvalid(BlueprintString), +} + impl TryFrom for Blueprint { - type Error = (); + type Error = BlueprintImportError; fn try_from(value: BlueprintString) -> Result { let raw_str = value.0; @@ -69,7 +75,10 @@ impl TryFrom for Blueprint { let Ok(internal) = bincode::serde::decode_from_reader(dec, bincode::config::standard()) else { - return Err(()); + error!("Blueprint failed to deserialize!"); + return Err(BlueprintImportError::BlueprintStringInvalid( + BlueprintString(raw_str), + )); }; let BlueprintStringInternal { @@ -95,6 +104,8 @@ impl TryFrom for Blueprint { ores, } = internal; + // dbg!(&movetime); + let actions = assemblers .into_iter() .map(|BaseEntity { pos, ty, rotation }| { @@ -207,12 +218,14 @@ impl TryFrom for Blueprint { )); let actions = actions.chain(inserters.into_iter().map( - |(BaseEntity { pos, ty, rotation }, filter)| { + |(BaseEntity { pos, ty, rotation }, filter, movetime)| { BlueprintAction::PlaceEntity(BlueprintPlaceEntity::Inserter { pos, ty: data_strings[ty].clone(), dir: rotation, filter: filter.map(|idx| data_strings[idx].clone()), + + movetime, }) }, )); @@ -308,6 +321,7 @@ impl From for BlueprintString { dir, filter, ty, + movetime, } => { internal.inserters.push(( BaseEntity { @@ -318,13 +332,14 @@ impl From for BlueprintString { filter.map(|item| { internal.data_strings.get_index_or_insert(item.into()) }), + movetime, )); }, super::BlueprintPlaceEntity::Belt { pos, direction, ty, - copied_belt_info, + copied_belt_info: _, } => { internal.belts.push(BaseEntity { pos, @@ -337,7 +352,7 @@ impl From for BlueprintString { direction, ty, underground_dir, - copied_belt_info, + copied_belt_info: _, } => { internal.underground_belts.push(( BaseEntity { diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 59801dc..b6a9bfd 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -32,7 +32,6 @@ use crate::{ }, }, item::{IdxTrait, Item, Recipe}, - replays::Replay, }; pub mod blueprint_string; @@ -40,7 +39,7 @@ pub mod blueprint_string; // For now blueprint will just be a list of actions #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Blueprint { - pub actions: Vec, + pub(crate) actions: Vec, } #[derive(Debug, Clone)] @@ -49,7 +48,7 @@ pub struct ReusableBlueprint Arc { #[derive( Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize, )] -enum BeltId { +pub(crate) enum BeltId { Sushi(usize), Pure(usize), } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum BlueprintPlaceEntity { +pub(crate) enum BlueprintPlaceEntity { Assembler { pos: Position, ty: Arc, @@ -105,6 +104,8 @@ enum BlueprintPlaceEntity { /// The Item the inserter will move, must fit both the in and output side filter: Option>, + movetime: Option>, + #[serde(default = "default_inserter")] ty: Arc, }, @@ -173,6 +174,7 @@ impl BlueprintAction { data_store: &DataStore, ) -> Self { match action { + ActionType::SpawnPlayer { .. } => unreachable!(), ActionType::PlaceFloorTile(_) => unimplemented!(), ActionType::PlaceEntity(place_entity_info) => { match place_entity_info.entities.clone() { @@ -190,13 +192,15 @@ impl BlueprintAction { dir, filter, ty, - user_movetime: _, + user_movetime, } => BlueprintPlaceEntity::Inserter { pos, dir, filter: filter .map(|item| data_store.item_names[item.into_usize()].clone()), ty: data_store.inserter_infos[ty as usize].name.clone(), + + movetime: user_movetime, }, PlaceEntityType::Belt { pos, direction, ty } => { BlueprintPlaceEntity::Belt { @@ -310,7 +314,8 @@ impl BlueprintAction { amount: *amount, }, ActionType::Remove(_) => unimplemented!(), - ActionType::SetActiveResearch { .. } => unimplemented!(), + ActionType::AddResearchToQueue { .. } => unimplemented!(), + ActionType::RemoveResearchFromQueue { .. } => unimplemented!(), ActionType::CheatUnlockTechnology { .. } => unimplemented!(), ActionType::CheatRelockTechnology { .. } => unimplemented!(), ActionType::Ping(_) => unimplemented!(), @@ -342,6 +347,7 @@ impl BlueprintAction { dir, filter, ty, + movetime, } => PlaceEntityType::Inserter { pos: *pos, dir: *dir, @@ -365,7 +371,7 @@ impl BlueprintAction { .position(|info| info.name == *ty) .expect("No inserter for name") as u8, - user_movetime: None, + user_movetime: *movetime, }, BlueprintPlaceEntity::Belt { pos, @@ -929,9 +935,17 @@ impl Blueprint { self.actions.len() } + pub(crate) fn extract_if(&mut self, filter: impl Fn(&BlueprintAction) -> bool) -> Self { + let extracted = self.actions.extract_if(.., |action| (filter)(action)); + + Self { + actions: extracted.collect(), + } + } + pub fn optimize(&mut self) { info!("Optimizing Blueprint"); - self.actions.par_sort_unstable_by_key(|v| match v { + self.actions.par_sort_by_key(|v| match v { BlueprintAction::PlaceEntity(e) => match e { BlueprintPlaceEntity::Assembler { pos, .. } => { (1, 3, (BeltId::Pure(0), 0), *pos, 0) @@ -975,13 +989,14 @@ impl Blueprint { BlueprintPlaceEntity::FluidTank { pos, .. } => { (1, 2, (BeltId::Pure(0), 0), *pos, 0) }, + BlueprintPlaceEntity::MiningDrill { pos, .. } => { (1, 3, (BeltId::Pure(0), 0), *pos, 0) }, }, BlueprintAction::SetRecipe { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), BlueprintAction::OverrideInserterMovetime { pos, .. } => { - (1, 5, (BeltId::Pure(0), 0), *pos, 1) + (1, 6, (BeltId::Pure(0), 0), *pos, 1) }, BlueprintAction::AddModules { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), BlueprintAction::SetChestSlotLimit { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), @@ -1054,24 +1069,6 @@ impl Blueprint { ); } - pub fn from_replay< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - DS: Borrow>, - >( - replay: &Replay, - ) -> Self { - Self { - actions: replay - .actions - .iter() - .map(|ra| { - BlueprintAction::from_with_datastore(&ra.action, replay.data_store.borrow()) - }) - .collect(), - } - } - pub fn from_area( world: &World, sim_state: &SimulationState, @@ -1112,8 +1109,8 @@ impl Blueprint { ty, pos, rotation, - drill_id, - internal_inserter, + drill_id: _, + internal_inserter: _, } => { vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::MiningDrill { @@ -1276,7 +1273,7 @@ impl Blueprint { ty, .. } => { - let mut ret = vec![BlueprintAction::PlaceEntity( + let ret = vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::Inserter { pos: Position { x: pos.x - base_pos.x, @@ -1285,18 +1282,19 @@ impl Blueprint { dir: *direction, filter: None, ty: data_store.inserter_infos[*ty as usize].name.clone(), + movetime: *user_movetime, }, )]; - if let Some(user_movetime) = *user_movetime { - ret.push(BlueprintAction::OverrideInserterMovetime { - pos: Position { - x: pos.x - base_pos.x, - y: pos.y - base_pos.y, - }, - new_movetime: Some(user_movetime), - }); - } + // if let Some(user_movetime) = *user_movetime { + // ret.push(BlueprintAction::OverrideInserterMovetime { + // pos: Position { + // x: pos.x - base_pos.x, + // y: pos.y - base_pos.y, + // }, + // new_movetime: Some(user_movetime), + // }); + // } ret }, @@ -1309,13 +1307,7 @@ impl Blueprint { ty: data_store.chest_names[*ty as usize].clone(), })] }, - crate::frontend::world::tile::Entity::Roboport { - ty, - pos, - power_grid, - network, - id, - } => todo!(), + crate::frontend::world::tile::Entity::Roboport { .. } => todo!(), crate::frontend::world::tile::Entity::SolarPanel { pos, ty, .. } => { vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::SolarPanel { @@ -1798,3 +1790,23 @@ pub(crate) mod test { }) } } + +#[macro_export] +macro_rules! get_const_string { + ($path:literal) => {{ + #[cfg(target_arch = "wasm32")] + { + include_str!(concat!("../", $path)).to_string() + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut s = String::new(); + let mut file = File::open($path).expect(&format!("Failed to open bp file {}", $path)); + + file.read_to_string(&mut s) + .expect(&format!("Failed reading from bp file {}", $path)); + s + } + }}; +} diff --git a/src/bucket_store/const_reinsertion.rs b/src/bucket_store/const_reinsertion.rs new file mode 100644 index 0000000..d091ffe --- /dev/null +++ b/src/bucket_store/const_reinsertion.rs @@ -0,0 +1,93 @@ +pub(crate) struct ConstReinsertionBucketStore< + const STATE_COUNT: usize, + Reinsertion: super::Reinsertion, +> { + pub reinsertion_time: u32, + + states: [State; STATE_COUNT], +} + +struct State { + moving_time_counts: Box<[u32]>, + moving_values: Vec, + ticking_values: Vec, +} + +impl + ConstReinsertionBucketStore +{ + pub fn new(reinsertion_time: u32) -> Self { + assert_eq!(Reinsertion::STATE_COUNT, STATE_COUNT); + assert!(Reinsertion::STATE_COUNT > 0); + assert!(reinsertion_time > 0); + Self { + reinsertion_time, + states: std::array::from_fn(|_| State { + moving_time_counts: vec![0; reinsertion_time as usize].into_boxed_slice(), + moving_values: vec![], + ticking_values: vec![], + }), + } + } + + pub fn update<'a>(&mut self, world_state: &mut Reinsertion::WorldState<'a>) { + for state in &mut self.states { + // Done moving + let (ticking, (moving, moving_time_counts)) = { + ( + &mut state.ticking_values, + (&mut state.moving_values, &mut state.moving_time_counts), + ) + }; + + let amount_done_moving = moving_time_counts[0]; + let extract_range = 0..(amount_done_moving as usize); + + let removed_moving = moving.drain(extract_range); + let added_ticking = removed_moving.map(|moving| Reinsertion::moving_to_ticking(moving)); + + ticking.extend(added_ticking); + + moving_time_counts.rotate_left(1); + assert_eq!( + amount_done_moving, + moving_time_counts[self.reinsertion_time as usize - 1] + ); + moving_time_counts[self.reinsertion_time as usize - 1] = 0; + } + + for state in 0..STATE_COUNT { + let next_state = (state + 1) % STATE_COUNT; + + // Start Moving + let (ticking, (moving, moving_time_counts)) = if state == next_state { + assert!(STATE_COUNT == 1); + let state = &mut self.states[state]; + ( + &mut state.ticking_values, + (&mut state.moving_values, &mut state.moving_time_counts), + ) + } else { + assert!(STATE_COUNT > 1); + let [this, next] = self.states.get_disjoint_mut([state, next_state]).unwrap(); + + ( + &mut this.ticking_values, + (&mut next.moving_values, &mut next.moving_time_counts), + ) + }; + + let extracted_ticking = + ticking.extract_if(.., |ticking| Reinsertion::tick(state, ticking, world_state)); + + let reinserted_moving = + extracted_ticking.filter_map(|ticking| Reinsertion::ticking_to_moving(ticking)); + + let moving_old_len = moving.len(); + moving.extend(reinserted_moving); + let added = moving.len() - moving_old_len; + + moving_time_counts[self.reinsertion_time as usize - 1] += added as u32; + } + } +} diff --git a/src/bucket_store/mod.rs b/src/bucket_store/mod.rs new file mode 100644 index 0000000..158f8f5 --- /dev/null +++ b/src/bucket_store/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod const_reinsertion; + +mod storage_storage_inserter_store; + +pub(crate) trait Reinsertion { + const STATE_COUNT: usize; + + type MovingValue: Copy; + type TickingValue: Copy; + + type WorldState<'a>; + + fn tick<'a>( + state: usize, + value: &mut Self::TickingValue, + world_state: &mut Self::WorldState<'a>, + ) -> bool; + fn ticking_to_moving(value: Self::TickingValue) -> Option; + + fn moving_to_ticking(value: Self::MovingValue) -> Self::TickingValue; +} diff --git a/src/bucket_store/storage_storage_inserter_store/mod.rs b/src/bucket_store/storage_storage_inserter_store/mod.rs new file mode 100644 index 0000000..33acff3 --- /dev/null +++ b/src/bucket_store/storage_storage_inserter_store/mod.rs @@ -0,0 +1,115 @@ +use std::num::NonZero; + +use enum_map::Enum; + +use crate::{bucket_store::Reinsertion, item::ITEMCOUNTTYPE}; + +struct StorageStorageInserterStore {} + +struct Info { + last_ticked: u16, + state_at_last_tick: ImplicitState, +} + +enum ImplicitState { + WaitingForSourceItems(ITEMCOUNTTYPE), + WaitingForSpaceInDestination(ITEMCOUNTTYPE), +} + +struct StorageStorageInserterReinsertion; + +#[derive(Debug, Enum)] +enum State { + PickingUpItems, + DroppingOffItems, +} + +#[derive(Debug, Clone, Copy)] +struct MovingInserter { + id: u32, + todo: !, +} + +#[derive(Debug, Clone, Copy)] +struct TickingInserter { + id: u32, + hand: ITEMCOUNTTYPE, + todo: !, +} + +struct WorldState<'a> { + max_hand_size: ITEMCOUNTTYPE, + + current_tick: u32, + infos: &'a mut [Info], +} + +impl Reinsertion for StorageStorageInserterReinsertion { + const STATE_COUNT: usize = State::LENGTH; + + type MovingValue = MovingInserter; + + type TickingValue = TickingInserter; + + type WorldState<'a> = WorldState<'a>; + + #[inline] + fn tick<'a>( + state: usize, + value: &mut Self::TickingValue, + world_state: &mut Self::WorldState<'a>, + ) -> bool { + let WorldState { + current_tick, + infos, + max_hand_size, + } = world_state; + + infos[value.id as usize].last_ticked = *current_tick as u16; + + match State::from_usize(state) { + State::PickingUpItems => { + let amount_picked_up: ITEMCOUNTTYPE = todo!("Pick up actually"); + + if amount_picked_up > 0 { + value.hand += amount_picked_up; + infos[value.id as usize].state_at_last_tick = + ImplicitState::WaitingForSourceItems(value.hand); + if value.hand == *max_hand_size { + todo!("Try to put it in waitlist"); + true + } else { + false + } + } else { + false + } + }, + State::DroppingOffItems => { + let amount_dropped_off: ITEMCOUNTTYPE = todo!("Drop off actually"); + + if amount_dropped_off > 0 { + value.hand -= amount_dropped_off; + infos[value.id as usize].state_at_last_tick = + ImplicitState::WaitingForSpaceInDestination(value.hand); + if value.hand == 0 { + todo!("Try to put it in waitlist"); + true + } else { + false + } + } else { + false + } + }, + } + } + + fn ticking_to_moving(value: Self::TickingValue) -> Option { + todo!() + } + + fn moving_to_ticking(value: Self::MovingValue) -> Self::TickingValue { + todo!() + } +} diff --git a/src/chest.rs b/src/chest.rs index bf056c9..eb196a0 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -1,9 +1,14 @@ use std::cmp::max; use std::{cmp::min, u8}; -use rayon::iter::IndexedParallelIterator; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; +use crate::assembler::simd::{InserterReinsertionInfo, InserterWaitList, InserterWithBelts}; +use crate::belt::belt::BeltLenType; +use crate::inserter::storage_storage_with_buckets_indirect::InserterId; +use crate::inserter::{FakeUnionStorage, StaticID}; +use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; use crate::{ data::DataStore, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait, usize_from}, @@ -20,6 +25,37 @@ const CHEST_GOAL_AMOUNT: ITEMCOUNTTYPE = ITEMCOUNTTYPE::MAX / 2; pub type ChestSize = u32; pub type SignedChestSize = i32; +// #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +// #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +// #[repr(align(64))] +// pub struct InserterWaitList { +// pub inserters: [Option; 3], +// } + +// const_assert!(std::mem::size_of::() <= 64); + +// #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +// pub struct Inserter { +// // item: u8, +// self_is_source: bool, +// // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything +// // This does mean we can only fit 3 Inserters per cacheline :/ +// // This is fixed by the item arena optimization +// pub current_hand: ITEMCOUNTTYPE, +// pub max_hand: NonZero, +// pub movetime: u16, +// pub(crate) index: InserterId, +// pub other: FakeUnionStorage, +// } + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +struct InternalInserterReinsertionInfo { + inserter: InserterWithBelts, + self_index: u32, +} + #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct FullChestStore { @@ -31,17 +67,75 @@ impl FullChestStore { pub fn update( &mut self, data_store: &DataStore, - ) { + ) -> impl IndexedParallelIterator + + ParallelIterator< + Item = impl Iterator>, + > { self.stores .par_iter_mut() .enumerate() - .for_each(|(item_id, store)| { + .map(|(item_id, store)| { + let item = store.item; profiling::scope!( "Chest Update", format!("Item: {}", data_store.item_display_names[item_id]).as_str() ); - store.update_simd() - }); + store.update_simd().map( + move |InternalInserterReinsertionInfo { + inserter, + self_index, + }| InserterReinsertionInfo { + movetime: inserter.movetime.into(), + item, + current_hand: inserter.current_hand, + max_hand: inserter.max_hand.into(), + + conn: match inserter.rest { + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + index, + other, + } => crate::assembler::simd::Conn::Storage { + index, + storage_id_in: if self_is_source { + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } + } else { + other + }, + storage_id_out: if self_is_source { + other + } else { + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } + }, + }, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + }, + }, + }, + }, + ) + }) + .collect::>() + .into_par_iter() } } @@ -50,26 +144,46 @@ impl FullChestStore { pub struct MultiChestStore { item: Item, max_insert: Vec, + pub last_inout: Vec, pub inout: Vec, storage: Vec, // TODO: Any way to not have to store this a billion times? max_items: Vec, holes: Vec, + wait_list: Vec, + wait_list_min: Vec, + + #[serde(skip)] + inserter_reinsertion_vec: Vec, + num_large_chests: usize, } +#[derive(Debug, Clone, Copy)] +pub enum WaitingInserterRemovalInfo { + StorageStorage { inserter_id: InserterId }, + BeltStorage { belt_id: u32, belt_pos: BeltLenType }, +} + impl MultiChestStore { #[must_use] pub fn new(item: Item) -> Self { Self { item, inout: vec![], + last_inout: vec![], storage: vec![], max_insert: vec![], max_items: vec![], + + wait_list: vec![], + wait_list_min: vec![], + holes: vec![], + inserter_reinsertion_vec: vec![], + num_large_chests: 0, } } @@ -91,23 +205,73 @@ impl MultiChestStore { if let Some(hole) = self.holes.pop() { self.inout[hole] = 0; + self.last_inout[hole] = 0; self.storage[hole] = 0; self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + self.wait_list[hole] = InserterWaitList::default(); + self.wait_list_min[hole] = ITEMCOUNTTYPE::MAX; + hole.try_into().unwrap() } else { self.inout.push(0); + self.last_inout.push(0); self.storage.push(0); self.max_insert .push(max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX)); self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); + self.wait_list.push(InserterWaitList::default()); + self.wait_list_min.push(ITEMCOUNTTYPE::MAX); (self.inout.len() - 1).try_into().unwrap() } } + pub(crate) fn remove_inserter_from_waitlist( + &mut self, + id: u32, + inserter_info: WaitingInserterRemovalInfo, + ) { + for wait_slot in &mut self.wait_list[id as usize].inserters { + if let Some(inserter) = wait_slot { + match (&inserter_info, &inserter.rest) { + ( + WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + index, + other, + }, + ) => { + if *index == *inserter_id { + let removed = wait_slot.take().unwrap(); + // TODO: What do I want to return + return; + } + }, + ( + WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + }, + ) => { + if *belt_id == *ins_belt_id && *ins_belt_pos == *belt_pos { + let removed = wait_slot.take().unwrap(); + // TODO: What do I want to return + return; + } + }, + + _ => {}, + } + } + } + } + pub fn add_custom_chest(&mut self, max_items: ChestSize) -> u32 { if max_items > 255 { self.num_large_chests += 1; @@ -115,19 +279,27 @@ impl MultiChestStore { if let Some(hole) = self.holes.pop() { self.inout[hole] = 0; + self.last_inout[hole] = 0; self.storage[hole] = 0; self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + self.wait_list[hole] = InserterWaitList::default(); + self.wait_list_min[hole] = ITEMCOUNTTYPE::MAX; + hole.try_into().unwrap() } else { self.inout.push(0); + self.last_inout.push(0); self.storage.push(0); self.max_insert .push(max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX)); self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); + self.wait_list.push(InserterWaitList::default()); + self.wait_list_min.push(ITEMCOUNTTYPE::MAX); + (self.inout.len() - 1).try_into().unwrap() } } @@ -141,8 +313,11 @@ impl MultiChestStore { } let items = ChestSize::from(self.inout[index]) + self.storage[index]; self.inout[index] = 0; + self.last_inout[index] = 0; self.storage[index] = 0; self.max_items[index] = 0; + self.wait_list[index] = InserterWaitList::default(); + self.wait_list_min[index] = ITEMCOUNTTYPE::MAX; items } @@ -179,7 +354,9 @@ impl MultiChestStore { } } - pub fn update_simd(&mut self) { + fn update_simd(&mut self) -> impl Iterator { + self.inserter_reinsertion_vec + .reserve((self.inout.len() * 4).saturating_sub(self.inserter_reinsertion_vec.len())); #[cfg(debug_assertions)] { if self.num_large_chests == 0 { @@ -188,32 +365,94 @@ impl MultiChestStore { } // TODO: Splitting this into large chests and small chests would be better since now a single large chest will make all small chests in the world expensive - if self.num_large_chests > 0 { - for (inout, (storage, max_items)) in self - .inout - .iter_mut() - .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) - { - let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); - - let switch = ChestSize::from(*inout >= CHEST_GOAL_AMOUNT); - - let moved: SignedChestSize = (switch as SignedChestSize - + (1 - switch as SignedChestSize) * -1) - * (min( - ChestSize::from(to_move), - (max_items - *storage) * switch + (1 - switch) * *storage, - ) as SignedChestSize); - - *inout = (ChestSize::from(*inout)).wrapping_sub_signed(moved) as u8; - *storage = (*storage).wrapping_add_signed(moved) as ChestSize; + // With wait lists we cannot avoid updating all chests (even small chests) + // if self.num_large_chests > 0 { + for ( + index, + ((((inout, last_inout), (storage, max_items)), (wait_list, wait_list_min)), max_insert), + ) in self + .inout + .iter_mut() + .zip(self.last_inout.iter_mut()) + .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) + .zip(self.wait_list.iter_mut().zip(self.wait_list_min.iter_mut())) + .zip(self.max_insert.iter()) + .enumerate() + { + let is_full = *inout == *max_insert; + let is_empty = *inout == 0; + let was_full = *last_inout == *max_insert; + let was_empty = *last_inout == 0; + *last_inout = *inout; - debug_assert!(*storage <= max_items); + let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); + + let switch = ChestSize::from(*inout >= CHEST_GOAL_AMOUNT); + + if (was_full && !is_full) || (was_empty && !is_empty) { + for ins in wait_list.inserters.iter_mut() { + if let Some(inserter) = ins { + let self_is_source = match inserter.rest { + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + .. + } => self_is_source, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + self_is_source, + .. + } => self_is_source, + }; + + if self_is_source && !is_empty { + let taken_by_this_inserter = min( + ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, + *inout, + ); + + *inout -= taken_by_this_inserter; + inserter.current_hand += taken_by_this_inserter; + } else if !self_is_source && !is_full { + let taken_by_this_inserter = + min(inserter.current_hand, *max_insert - *inout); + + *inout += taken_by_this_inserter; + inserter.current_hand -= taken_by_this_inserter; + } + + if (inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) + && self_is_source) + || (inserter.current_hand == 0 && !self_is_source) + { + let removed = ins.take().unwrap(); + self.inserter_reinsertion_vec + .push_within_capacity(InternalInserterReinsertionInfo { + inserter: removed, + self_index: index as u32, + }) + .unwrap(); + } + } + } } - } else { - // Since all chests have a max_items of 0, we will never need/want to move items out of the inout - // So we can just return + + let moved: SignedChestSize = (switch as SignedChestSize + + (1 - switch as SignedChestSize) * -1) + * (min( + ChestSize::from(to_move), + (max_items - *storage) * switch + (1 - switch) * *storage, + ) as SignedChestSize); + + *inout = (ChestSize::from(*inout)).wrapping_sub_signed(moved) as u8; + *storage = (*storage).wrapping_add_signed(moved) as ChestSize; + + debug_assert!(*storage <= max_items); } + // } else { + // // Since all chests have a max_items of 0, we will never need/want to move items out of the inout + // // So we can just return + // } + + self.inserter_reinsertion_vec.drain(..) } pub fn add_items_to_chest( @@ -322,8 +561,22 @@ impl MultiChestStore { removed_items } - pub fn storage_list_slices(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { - (self.max_insert.as_slice(), self.inout.as_mut_slice()) + pub fn storage_list_slices<'a>( + &'a mut self, + ) -> ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ) { + ( + MaxInsertionLimit::PerMachine(self.max_insert.as_slice()), + self.inout.as_mut_slice(), + // InserterWaitLists::None, + InserterWaitLists::PerMachine( + self.wait_list.as_mut_slice(), + self.wait_list_min.as_mut_slice(), + ), + ) } } diff --git a/src/data/factorio_1_1.fgmod b/src/data/factorio_1_1.fgmod index 06fb826..b35ba53 100644 --- a/src/data/factorio_1_1.fgmod +++ b/src/data/factorio_1_1.fgmod @@ -12,10 +12,10 @@ output: [ ( item: "factory_game::iron_ore", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -30,10 +30,10 @@ output: [ ( item: "factory_game::copper_ore", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -48,10 +48,10 @@ output: [ ( item: "factory_game::coal", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -66,10 +66,10 @@ output: [ ( item: "factory_game::stone", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -628,7 +628,7 @@ ), ( name: "factory_game::light_oil_cracking", - display_name: "Heavy Oil Cracking", + display_name: "Light Oil Cracking", possible_machines: [ "factory_game::chemical_plant", ], @@ -1815,6 +1815,7 @@ display_name: "Assembling Machine", tile_size: (3, 3), working_power_draw: (150000), + power_drain: (2500), fluid_connection_offsets: [], fluid_connection_flowthrough: [], num_module_slots: 0, @@ -1826,6 +1827,7 @@ display_name: "Assembling Machine 2", tile_size: (3, 3), working_power_draw: (75000), + power_drain: (5000), fluid_connection_offsets: [ ( allowed_fluid_directions: Both( @@ -1846,6 +1848,7 @@ display_name: "Assembling Machine 3", tile_size: (3, 3), working_power_draw: (375000), + power_drain: (12500), fluid_connection_offsets: [ ( allowed_fluid_directions: Both( @@ -1866,6 +1869,7 @@ display_name: "Rocket Silo", tile_size: (9, 9), working_power_draw: (4_000_000), + power_drain: (0), fluid_connection_offsets: [], fluid_connection_flowthrough: [], num_module_slots: 4, @@ -1877,6 +1881,7 @@ display_name: "Electric Furnace", tile_size: (3, 3), working_power_draw: (180000), + power_drain: (6000), fluid_connection_offsets: [], fluid_connection_flowthrough: [], num_module_slots: 2, @@ -1888,6 +1893,7 @@ display_name: "Chemical Plant", tile_size: (3, 3), working_power_draw: (210000), + power_drain: (7000), fluid_connection_offsets: [ ( allowed_fluid_directions: Single( @@ -1932,6 +1938,7 @@ display_name: "Oil Refinery", tile_size: (5, 5), working_power_draw: (420000), + power_drain: (14000), fluid_connection_offsets: [ ( allowed_fluid_directions: Single( @@ -2261,9 +2268,6 @@ RecipeUnlock("factory_game::raw_oil_generation"), RecipeUnlock("factory_game::water_generation"), RecipeUnlock("factory_game::stone_generation"), - - // TODO: - RecipeUnlock("factory_game::solid_fuel_from_light_oil"), ], ), ( @@ -2942,6 +2946,7 @@ RecipeUnlock("factory_game::advanced_oil_processing"), RecipeUnlock("factory_game::heavy_oil_cracking"), RecipeUnlock("factory_game::light_oil_cracking"), + RecipeUnlock("factory_game::solid_fuel_from_light_oil"), ], ), ( @@ -3357,7 +3362,7 @@ solar_panels: [ ( name: "factory_game::infinity_battery", - display_name: "Infinity Battery", + display_name: "Infinite Power Source", tile_size: (2, 2), output: Constant((1000000000000)), ), diff --git a/src/data/factorio_1_1.rs b/src/data/factorio_1_1.rs index 021f19f..3eb3894 100644 --- a/src/data/factorio_1_1.rs +++ b/src/data/factorio_1_1.rs @@ -370,6 +370,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Assembling Machine".to_string(), tile_size: (3, 3), working_power_draw: Watt(150_000), + power_drain: Watt(2_500), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -381,6 +382,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Assembling Machine 2".to_string(), tile_size: (3, 3), working_power_draw: Watt(75_000), + power_drain: Watt(5_000), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -392,6 +394,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Assembling Machine 3".to_string(), tile_size: (3, 3), working_power_draw: Watt(375_000), + power_drain: Watt(12_500), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -403,6 +406,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Electric Furnace".to_string(), tile_size: (3, 3), working_power_draw: Watt(180000), + power_drain: Watt(6_000), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], num_module_slots: 2, diff --git a/src/data/mod.rs b/src/data/mod.rs index d016035..6295487 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use std::{ array, - cmp::{max, min}, + cmp::max, collections::{BTreeSet, HashMap}, iter, }; @@ -17,6 +17,7 @@ use sha2::{Digest, Sha256}; use strum::IntoEnumIterator; pub mod factorio_1_1; +pub mod pedia; use crate::{ assembler::TIMERTYPE, @@ -25,6 +26,7 @@ use crate::{ item::{ITEMCOUNTTYPE, IdxTrait, Item, Recipe, WeakIdxTrait}, power::{Joule, Watt}, }; +use std::num::NonZero; type ItemString = String; type RecipeString = String; @@ -65,6 +67,7 @@ struct RawAssemblingMachine { name: String, display_name: String, tile_size: (u8, u8), + power_drain: Watt, working_power_draw: Watt, fluid_connection_offsets: Vec, fluid_connection_flowthrough: Vec, @@ -161,7 +164,7 @@ struct RawFluidFlowthrough { struct RawInserter { name: String, display_name: String, - time_per_trip: TIMERTYPE, + time_per_trip: NonZero, handsize: ITEMCOUNTTYPE, tile_size: [u8; 2], @@ -329,6 +332,8 @@ pub struct AssemblerInfo { pub base_speed: u8, pub base_prod: u8, pub base_power_consumption: Watt, + + pub power_drain: Watt, } impl AssemblerInfo { @@ -437,6 +442,20 @@ impl SolarPanelOutputFunction { }, } } + + pub(crate) fn max(&self) -> Watt { + match self { + SolarPanelOutputFunction::Constant(watt) => *watt, + SolarPanelOutputFunction::Segmented(power_generation_segments) => { + power_generation_segments + .iter() + .map(|segment| segment.end_power) + .max() + .unwrap() + }, + SolarPanelOutputFunction::Lookup(watts) => *watts.iter().max().unwrap(), + } + } } #[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] @@ -473,7 +492,7 @@ pub struct InserterInfo { pub display_name: String, pub size: [u8; 2], - pub swing_time_ticks: u16, + pub swing_time_ticks: NonZero, /// pre any increases by technology pub base_hand_size: ITEMCOUNTTYPE, @@ -575,6 +594,7 @@ pub struct DataStore { pub technology_costs: Vec<(u64, Box<[ITEMCOUNTTYPE]>)>, pub belt_infos: Vec, pub mining_drill_info: Vec, + pub recipe_item_index_to_item: Box<[Box<[Item]>]>, } #[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] @@ -718,12 +738,12 @@ pub struct FluidConnection { } impl FluidConnection { - pub fn with_fluid_tank_rotation(self, rotation: Dir) -> Self { + pub fn with_fluid_tank_rotation(self, _rotation: Dir) -> Self { todo!() } } -#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PowerPoleData { pub name: Arc, pub display_name: String, @@ -732,7 +752,9 @@ pub struct PowerPoleData { pub connection_range: u8, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Deserialize, serde::Serialize, +)] pub enum ItemRecipeDir { Ing, Out, @@ -1401,11 +1423,8 @@ impl RawDataStore { unit_increase_per_level: units_per_level, } }, - InfiniteCostScaling::Quadradic { units_per_level } => todo!(), - InfiniteCostScaling::Exponential { - units_per_level, - exponential_base, - } => todo!(), + InfiniteCostScaling::Quadradic { .. } => todo!(), + InfiniteCostScaling::Exponential { .. } => todo!(), }, level_counter_offset: infinite_info.display_level_offset, } @@ -1511,33 +1530,34 @@ impl RawDataStore { instantly_finished_technologies, belt_infos: vec![ + // TODO: For now only have one kind of transport belt since connection is still borked BeltInfo { name: "factory_game::fast_transport_belt".to_string().into(), - display_name: "Fast Transport Belt".to_string(), + display_name: "Express Transport Belt".to_string(), has_underground: Some(BeltUndergroundInfo { max_distance: 9 }), has_splitter: None, timer_increase: 45 * 2, }, - BeltInfo { - name: "factory_game::transport_belt".to_string().into(), - display_name: "Transport Belt".to_string(), - has_underground: Some(BeltUndergroundInfo { max_distance: 6 }), - has_splitter: None, - timer_increase: 15 * 2, - }, + // BeltInfo { + // name: "factory_game::transport_belt".to_string().into(), + // display_name: "Transport Belt".to_string(), + // has_underground: Some(BeltUndergroundInfo { max_distance: 6 }), + // has_splitter: None, + // timer_increase: 15 * 2, + // }, ], // FIXME: mining_drill_info: vec![ - MiningDrillInfo { - name: "factory_game::mining_drill".to_string().into(), - display_name: "Electric Mining Drill".to_string().into(), - size: [3, 3], - mining_range: [5, 5], - base_speed: 20, - resource_drain: (1, 1), - output_offset: Some([1, -1]), - }, + // MiningDrillInfo { + // name: "factory_game::mining_drill".to_string().into(), + // display_name: "Electric Mining Drill".to_string().into(), + // size: [3, 3], + // mining_range: [5, 5], + // base_speed: 20, + // resource_drain: (1, 1), + // output_offset: Some([1, -1]), + // }, MiningDrillInfo { name: "factory_game::mining_drill_small_no_output" .to_string() @@ -1552,6 +1572,19 @@ impl RawDataStore { }, ], + recipe_item_index_to_item: recipe_to_items + .iter() + .map(|recipe| { + let items = recipe + .clone() + .into_iter() + .sorted_by_key(|p| p.0) + .map(|p| p.1); + + items.collect() + }) + .collect(), + recipe_allowed_assembling_machines: self .recipes .iter() @@ -1588,6 +1621,8 @@ impl RawDataStore { base_prod: m.base_bonus_prod, base_power_consumption: m.working_power_draw, + power_drain: m.power_drain, + fluid_connections: m .fluid_connection_offsets .iter() diff --git a/src/data/pedia/mod.rs b/src/data/pedia/mod.rs new file mode 100644 index 0000000..3b70fe1 --- /dev/null +++ b/src/data/pedia/mod.rs @@ -0,0 +1,228 @@ +use egui::{ScrollArea, Window}; + +use crate::{ + TICKS_PER_SECOND_LOGIC, + data::{DataStore, ItemRecipeDir}, + item::{IdxTrait, Indexable, Item, Recipe, WeakIdxTrait}, +}; + +#[derive(Debug)] +pub(crate) struct Pedia { + open_page: OpenPage, + + entry: Option>, +} + +#[derive(Debug)] +struct OpenPage { + catagory: String, +} + +#[derive(Debug)] +enum OpenEntry { + Item { item: Item }, + Recipe { recipe: Recipe }, +} + +impl Pedia { + pub(crate) fn new(_data_store: &DataStore) -> Self { + Self { + open_page: OpenPage { + catagory: "TODO".to_string(), + }, + entry: Some(OpenEntry::Item { + item: Item { id: 10.into() }, + }), + } + } + + pub(crate) fn show_window( + &mut self, + open: &mut bool, + ctx: &egui::Context, + data_store: &DataStore, + ) { + Window::new("Datapedia").open(open).show(ctx, |ui| { + ui.columns_const(|[_grid, entry_ui]| { + // Grid + // TODO: Grid category buttons + + match Self::show_grid(&self.open_page, data_store) { + Some(new_entry) => self.entry = Some(new_entry), + None => {}, + } + + // Entry + match &mut self.entry { + Some(entry) => Self::show_entry(entry, entry_ui, data_store), + None => {}, + } + }); + }); + } + + fn show_grid( + _open_page: &OpenPage, + _data_store: &DataStore, + ) -> Option> { + // TODO: + None + } + + fn show_entry( + entry: &mut OpenEntry, + ui: &mut egui::Ui, + data_store: &DataStore, + ) { + ScrollArea::vertical().show(ui, |ui| { + match entry { + OpenEntry::Item { item: item_item } => { + let item = item_item.into_usize(); + ui.heading(&data_store.item_display_names[item]); + if !data_store.item_is_fluid[item] { + ui.label(&format!( + "Stack Size: {}", + data_store.item_stack_sizes[item] + )); + } + // TODO: How do I want to find the main recipe? + let main_recipe = data_store + .recipe_names + .iter() + .position(|recipe_name| **recipe_name == *data_store.item_names[item]); + + if let Some(main_recipe) = main_recipe { + let ingredients = data_store.recipe_to_items_and_amounts[main_recipe] + .iter() + .filter(|(dir, _item, _count)| *dir == ItemRecipeDir::Ing); + let results = data_store.recipe_to_items_and_amounts[main_recipe] + .iter() + .filter(|(dir, _item, _count)| *dir == ItemRecipeDir::Out); + + let crafting_time = data_store.recipe_timers[main_recipe] as f32 + / (TICKS_PER_SECOND_LOGIC as f32); + + ui.label("Ingredients:"); + for (_, ing, count) in ingredients { + if ui + .label(&format!( + "{count}x: {}", + data_store.item_display_names[ing.into_usize()] + )) + .clicked() + { + *item_item = *ing; + } + } + ui.label(&format!("{:.2}s Crafting time", crafting_time)); + ui.label("Results:"); + for (_, result, count) in results { + if ui + .label(&format!( + "{count}x: {}", + data_store.item_display_names[result.into_usize()] + )) + .clicked() + { + *item_item = *result; + } + } + + let made_in = &data_store.recipe_allowed_assembling_machines[main_recipe]; + + ui.label("Made In:"); + for machine in made_in { + let name = &data_store.assembler_info[*machine as usize].display_name; + + if ui.label(name).clicked() { + todo!() + } + } + + ui.separator(); + } + + let mut alternative_recipes = data_store + .recipe_names + .iter() + .enumerate() + .filter(|(_id, recipe_name)| ***recipe_name != *data_store.item_names[item]) + .filter(|(id, _name)| { + data_store.recipe_to_items[*id] + .contains(&(ItemRecipeDir::Out, *item_item)) + }) + .peekable(); + + if alternative_recipes.peek().is_some() { + ui.label("Alternative Recipes:"); + for (recipe_id, name) in alternative_recipes { + if ui + .label(&data_store.recipe_display_names[recipe_id]) + .clicked() + { + if let Some(item) = data_store + .item_names + .iter() + .position(|item_name| item_name == name) + { + // Show the item + *item_item = Item { + id: ItemIdxType::try_from(item).unwrap(), + }; + return; + } else { + *entry = OpenEntry::Recipe { + recipe: Recipe { + id: RecipeIdxType::try_from(recipe_id).unwrap(), + }, + }; + return; + } + } + } + } + + let mut used_in = data_store + .recipe_names + .iter() + .enumerate() + .filter(|(id, _name)| { + data_store.recipe_to_items[*id] + .contains(&(ItemRecipeDir::Ing, *item_item)) + }) + .peekable(); + + if used_in.peek().is_some() { + ui.label("Used In:"); + for (recipe_id, name) in used_in { + if ui + .label(&data_store.recipe_display_names[recipe_id]) + .clicked() + { + if let Some(item) = data_store + .item_names + .iter() + .position(|item_name| item_name == name) + { + // Show the item + *item_item = Item { + id: ItemIdxType::try_from(item).unwrap(), + }; + return; + } else { + *entry = OpenEntry::Recipe { + recipe: Recipe { + id: RecipeIdxType::try_from(recipe_id).unwrap(), + }, + }; + return; + } + } + } + } + }, + OpenEntry::Recipe { recipe } => todo!("Show entry for recipe {recipe:?}"), + } + }); + } +} diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs new file mode 100644 index 0000000..7910fca --- /dev/null +++ b/src/example_worlds/mod.rs @@ -0,0 +1,379 @@ +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; + +use std::{iter, ops::RangeInclusive, sync::LazyLock}; + +#[cfg(feature = "client")] +use crate::progress_info::ProgressInfo; +use crate::{ + app_state::GameState, + data::DataStore, + frontend::{action::ActionType, world::Position}, + power::Watt, + replays::GenerationInformation, + research::Technology, +}; + +pub(crate) struct WorldValueStore { + name_field: String, + worlds: Vec>, +} + +impl Default for WorldValueStore { + fn default() -> Self { + let v = WORLDS + .iter() + .map(|world| { + world + .values + .iter() + .map(|value| value.default.clone()) + .collect() + }) + .collect(); + + WorldValueStore { + name_field: "New World".to_string(), + worlds: v, + } + } +} + +#[cfg(feature = "client")] +pub(crate) fn list_example_worlds( + values: &mut WorldValueStore, + ui: &mut egui::Ui, +) -> Option) -> GameState + 'static> { + ui.horizontal(|ui| { + ui.label("World Name:"); + ui.text_edit_singleline(&mut values.name_field); + }); + + for (idx, (world, world_values)) in WORLDS.iter().zip(values.worlds.iter_mut()).enumerate() { + let v = ui.horizontal(|ui| { + ui.label(world.name); + ui.label(world.description); + + for (desc, value) in world.values.iter().zip(world_values.iter_mut()) { + ui.vertical(|ui| { + ui.label(desc.name); + match &desc.kind { + ValueKind::Range { allowed, log } => { + let ValueValue::Range(value) = value else { + unreachable!(); + }; + + ui.add( + egui::Slider::new(value, allowed.clone()) + .logarithmic(*log) + .text(desc.name), + ); + }, + ValueKind::Toggle {} => { + let ValueValue::Toggle(value) = value else { + unreachable!(); + }; + + ui.checkbox(value, ()); + }, + } + }); + } + + let allowed = if cfg!(target_arch = "wasm32") { + (world.allowed_on_wasm)(world_values) == AllowedOnWasm::True + } else { + true + }; + + let disabled_str = if cfg!(target_arch = "wasm32") { + match (world.allowed_on_wasm)(world_values) { + AllowedOnWasm::True => "", + AllowedOnWasm::False(s) => s.unwrap_or("Not available on WASM"), + } + } else { + "" + }; + + if ui + .add_enabled(allowed, egui::Button::new("Create")) + .on_disabled_hover_text(disabled_str) + .clicked() + { + let world_values = world_values.clone(); + let name = values.name_field.clone(); + let fun = world.creation_fn; + Some(move |progress, data_store: &'_ DataStore| { + (fun)( + name, + progress, + GenerationInformation { + example_idx: idx, + example_settings: world_values, + }, + data_store, + ) + }) + } else { + None + } + }); + + if let Some(v) = v.inner { + return Some(v); + } + + ui.separator(); + } + + None +} + +pub(crate) fn get_builder( + name: String, + idx: usize, + values: Vec, +) -> impl FnOnce(ProgressInfo, &DataStore) -> GameState + 'static { + let fun = WORLDS + .get(idx) + .expect("Example World index out of bounds") + .creation_fn; + move |progress, data_store: &'_ DataStore| { + (fun)( + name, + progress, + GenerationInformation { + example_idx: idx, + example_settings: values, + }, + data_store, + ) + } +} + +struct ExampleWorld { + name: &'static str, + description: &'static str, + values: Vec, + + // TODO: I might want to change this to depend on the values + allowed_on_wasm: fn(&[ValueValue]) -> AllowedOnWasm, + creation_fn: + fn(String, ProgressInfo, GenerationInformation, &DataStore) -> GameState, +} + +#[derive(Debug, PartialEq)] +enum AllowedOnWasm { + True, + False(Option<&'static str>), +} + +struct WorldValue { + name: &'static str, + kind: ValueKind, + default: ValueValue, +} + +enum ValueKind { + Range { + allowed: RangeInclusive, + log: bool, + }, + Toggle {}, +} + +// FIXME: Naming??? +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum ValueValue { + Range(usize), + Toggle(bool), +} + +const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { + [ + ExampleWorld { + name: "Empty World", + description: "Completely Empty", + values: vec![], + + allowed_on_wasm: |_| AllowedOnWasm::True, + + creation_fn: |name, _, gen_info, data_store| GameState::new(name, gen_info, data_store), + }, + ExampleWorld { + name: "Megabase", + description: "A world consisting of a single 40k SPM Megabase designed by Smurphy", + values: vec![WorldValue { + name: "Generate Solar Field", + kind: ValueKind::Toggle {}, + default: ValueValue::Toggle(false), + }], + + allowed_on_wasm: |_| AllowedOnWasm::True, + + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Toggle(use_solar_field)] = &gen_info.example_settings[..] else { + unreachable!(); + }; + + let gs = GameState::new_with_megabase( + name, + gen_info, + use_solar_field, + progress, + data_store, + ); + + let techs = 0..data_store.technology_costs.len(); + + let cheat_unlocks = techs.map(|id| ActionType::CheatUnlockTechnology { + tech: crate::research::Technology { + id: id.try_into().unwrap(), + }, + }); + + let mining_prod_id = data_store + .technology_tree + .node_indices() + .find_map(|node| { + let tech = data_store.technology_tree.node_weight(node).unwrap(); + + (tech.name == "Mining Productivity").then_some(node.index()) + }) + .unwrap(); + + GameState::apply_actions( + &mut *gs.simulation_state.lock(), + &mut *gs.world.lock(), + cheat_unlocks.chain(iter::once(ActionType::AddResearchToQueue { + tech: Technology { + id: mining_prod_id.try_into().unwrap(), + }, + })), + data_store, + ); + + gs + }, + }, + ExampleWorld { + name: "Gigabase", + description: "A world consisting of multiple copies of a 40k SPM Megabase", + values: vec![WorldValue { + name: "Megabase Count", + kind: ValueKind::Range { + allowed: 0..=1000, + log: true, + }, + default: ValueValue::Range(40), + }], + + allowed_on_wasm: |_| { + AllowedOnWasm::False(Some( + "WASM does not support enough memory to run a gigabase, consider switching to native", + )) + }, + + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Range(count)] = &gen_info.example_settings[..] else { + unreachable!(); + }; + + let gs = GameState::new_with_gigabase( + name, + gen_info, + count.try_into().unwrap(), + progress, + data_store, + ); + + let techs = 0..data_store.technology_costs.len(); + + let cheat_unlocks = techs.map(|id| ActionType::CheatUnlockTechnology { + tech: crate::research::Technology { + id: id.try_into().unwrap(), + }, + }); + + let mining_prod_id = data_store + .technology_tree + .node_indices() + .find_map(|node| { + let tech = data_store.technology_tree.node_weight(node).unwrap(); + + (tech.name == "Mining Productivity").then_some(node.index()) + }) + .unwrap(); + + GameState::apply_actions( + &mut *gs.simulation_state.lock(), + &mut *gs.world.lock(), + cheat_unlocks.chain(iter::once(ActionType::AddResearchToQueue { + tech: Technology { + id: mining_prod_id.try_into().unwrap(), + }, + })), + data_store, + ); + + gs + }, + }, + ExampleWorld { + name: "Trip around the world", + description: "A small ring around the world", + values: vec![], + allowed_on_wasm: |_| AllowedOnWasm::True, + creation_fn: |name, progress, gen_info, data_store| { + GameState::new_with_world_train_ride(name, gen_info, progress, data_store) + }, + }, + ExampleWorld { + name: "Solar Field", + description: "A world containing a Solar Field", + values: vec![WorldValue { + name: "Panel Count", + kind: ValueKind::Range { + allowed: 1..=1_000_000_000, + log: true, + }, + default: ValueValue::Range(1_000_000), + }], + allowed_on_wasm: |values| { + let [ValueValue::Range(count)] = values else { + unreachable!(); + }; + + // Testing said ~100 bytes per solar panel + let expected_size = (*count) as u64 * 100 + 1_000_000_000; + + // Wasm only has 4GB of RAM so limit to ~3GB + if expected_size > 3_000_000_000 { + AllowedOnWasm::False(Some( + "Generated World Size would exceed maximum memory on WASM, consider reducing panel count", + )) + } else { + AllowedOnWasm::True + } + }, + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Range(count)] = &gen_info.example_settings[..] else { + unreachable!(); + }; + + GameState::new_with_tons_of_solar( + name, + gen_info, + Watt(42_000) * count as u64, + Position { x: 1_600, y: 1_600 }, + None, + progress, + data_store, + ) + }, + }, + ] +}); diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index b494cf5..c1daf33 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -7,17 +7,18 @@ use std::{ num::NonZero, }; -use egui::Layout; use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph}; +use enum_map::EnumMap; use itertools::Itertools; use log::{error, warn}; use petgraph::Directed; use crate::{ + NewWithDataStore, TICKS_PER_SECOND_LOGIC, app_state::SimulationState, belt::splitter::SplitterDistributionMode, blueprint::Blueprint, - data::{self, DataStore}, + data::{self, DataStore, pedia::Pedia}, frontend::{ action::{ place_entity::{EntityPlaceOptions, PlaceEntityInfo}, @@ -46,10 +47,63 @@ pub const WIDTH_PER_LEVEL: usize = 16; pub struct Hotbar { slots: [Option>; 10], } -impl Default for Hotbar { - fn default() -> Self { +impl NewWithDataStore for Hotbar { + fn new( + data_store: impl std::borrow::Borrow>, + ) -> Self { Self { - slots: array::from_fn(|_| None), + slots: array::from_fn(|idx| match idx + 1 { + 1 => Some(HeldObject::Entity(PlaceEntityType::Lab { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 2 => Some(HeldObject::Entity(PlaceEntityType::Assembler { + pos: Position { x: 0, y: 0 }, + ty: 0, + rotation: Dir::North, + })), + 3 => Some(HeldObject::Entity(PlaceEntityType::Belt { + pos: Position { x: 0, y: 0 }, + ty: 0, + direction: Dir::North, + })), + 4 => Some(HeldObject::Entity(PlaceEntityType::Inserter { + pos: Position { x: 0, y: 0 }, + ty: 0, + dir: Dir::North, + filter: None, + user_movetime: None, + })), + 5 => Some(HeldObject::Entity(PlaceEntityType::PowerPole { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 6 => Some(HeldObject::Entity(PlaceEntityType::Chest { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 7 => Some(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position { x: 0, y: 0 }, + ty: 0, + direction: Dir::North, + underground_dir: UndergroundDir::Entrance, + })), + 8 => Some(HeldObject::Entity(PlaceEntityType::SolarPanel { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + 9 => Some(HeldObject::Entity(PlaceEntityType::FluidTank { + pos: Position { x: 0, y: 0 }, + ty: 0, + rotation: Dir::North, + })), + 10 => Some(HeldObject::Entity(PlaceEntityType::Beacon { + pos: Position { x: 0, y: 0 }, + ty: 0, + })), + + _ => unreachable!(), + }), } } } @@ -59,8 +113,6 @@ pub struct ActionStateMachine, @@ -80,8 +132,6 @@ pub struct ActionStateMachine, pub state: ActionStateMachineState, - pub escape_menu_open: bool, - pub debug_view_options: DebugViewOptions, pub zoom_level: f32, @@ -98,12 +148,31 @@ pub struct ActionStateMachine, pub hotbar: Hotbar, - pub hotbar_window_open: bool, + + pub last_tick_seen_for_autosave: u32, + pub autosave_interval: u32, + + pub open_windows: EnumMap, + + pub datapedia: Pedia, +} + +#[derive(Debug, enum_map::Enum, PartialEq)] +pub(crate) enum Window { + Tip, + Technology, + Statistics, + Hotbar, + Escape, + Datapedia, } +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] pub struct ForkSaveInfo { pub recv: interprocess::unnamed_pipe::Recver, @@ -177,19 +246,19 @@ impl ActionStateMachine { #[must_use] - pub fn new( + fn new( my_player_id: PLAYERID, local_player_pos: (f32, f32), data_store: &DataStore, ) -> Self { + let open_windows = EnumMap::from_fn(|win| win == Window::Tip); + Self { my_player_id, local_player_pos, tech_tree_render: None, - statistics_panel_open: false, - technology_panel_open: true, statistics_panel: StatisticsPanel::default(), statistics_panel_locked_scale: false, production_filters: vec![true; data_store.item_display_names.len()], @@ -202,7 +271,6 @@ impl zoom_level: 1.0, map_view_info: None, - escape_menu_open: false, debug_view_options: DebugViewOptions { highlight_sushi_belts: false, sushi_belt_len_threshhold: 1, @@ -220,10 +288,74 @@ impl mouse_wheel_sensitivity: 1.0, + #[cfg(not(target_arch = "wasm32"))] current_fork_save_in_progress: None, - hotbar: Hotbar::default(), - hotbar_window_open: true, + hotbar: Hotbar::new(data_store), + + last_tick_seen_for_autosave: 0, + autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, + + open_windows, + datapedia: Pedia::new(data_store), + } + } + + #[must_use] + pub fn new_from_gamestate( + my_player_id: PLAYERID, + world: &World, + sim_state: &SimulationState, + data_store: &DataStore, + ) -> Self { + let player_pos = world.players[my_player_id as usize].pos; + let open_windows = EnumMap::from_fn(|win| win == Window::Tip); + + Self { + my_player_id, + local_player_pos: player_pos, + + tech_tree_render: None, + + statistics_panel: StatisticsPanel::default(), + statistics_panel_locked_scale: false, + production_filters: vec![true; data_store.item_display_names.len()], + consumption_filters: vec![true; data_store.item_display_names.len()], + + current_mouse_pos: (0.0, 0.0), + current_held_keys: HashSet::new(), + state: ActionStateMachineState::Idle, + + zoom_level: 1.0, + map_view_info: None, + + debug_view_options: DebugViewOptions { + highlight_sushi_belts: false, + sushi_belt_len_threshhold: 1, + + sushi_finder_view_lock: None, + }, + + copy_info: None, + + show_graph_dot_output: false, + + recipe: PhantomData, + + get_size_cache: HashMap::new(), + + mouse_wheel_sensitivity: 1.0, + + #[cfg(not(target_arch = "wasm32"))] + current_fork_save_in_progress: None, + + hotbar: Hotbar::new(data_store), + + last_tick_seen_for_autosave: 0, + autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, + + open_windows, + datapedia: Pedia::new(data_store), } } @@ -237,7 +369,7 @@ impl ) -> impl Iterator> + use<'a, 'b, 'c, 'd, 'e, ItemIdxType, RecipeIdxType, I> { input.into_iter().map(|input| { - if self.escape_menu_open && input != Input::KeyPress(Key::Esc) { + if self.open_windows[Window::Escape] && input != Input::KeyPress(Key::Esc) { match input { Input::KeyPress(key) => { self.current_held_keys.insert(key); @@ -344,7 +476,7 @@ impl match held_object { HeldObject::Blueprint(bp) => { - bp.get_reusable(force, data_store).actions_with_base_pos(Self::player_mouse_to_tile( + bp.get_reusable(force, data_store).optimize().actions_with_base_pos(Self::player_mouse_to_tile( self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, @@ -508,7 +640,10 @@ impl let x_range = min(start_pos.x, end_pos.x)..(max(start_pos.x, end_pos.x) + 1); let y_range = min(start_pos.y, end_pos.y)..(max(start_pos.y, end_pos.y) + 1); - self.state = ActionStateMachineState::Holding(HeldObject::Blueprint(Blueprint::from_area(world, sim_state, [x_range, y_range], data_store))); + let mut bp = Blueprint::from_area(world, sim_state, [x_range, y_range], data_store); + bp.optimize(); + + self.state = ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); vec![] }, ActionStateMachineState::DeleteDragInProgress { start_pos } => { @@ -537,6 +672,167 @@ impl Input::MouseMove(x, y) => { self.current_mouse_pos = (x, y); + match &mut self.state { + ActionStateMachineState::CtrlCPressed + | ActionStateMachineState::CopyDragInProgress { start_pos: _ } => {}, + ActionStateMachineState::DelPressed + | ActionStateMachineState::DeleteDragInProgress { start_pos: _ } => {}, + + ActionStateMachineState::Idle | ActionStateMachineState::Viewing(_) => {}, + ActionStateMachineState::Holding(held_object) => match held_object { + HeldObject::Blueprint(_) => {}, + + HeldObject::Tile(_floor_tile) => {}, + HeldObject::Entity(place_entity_type) => match place_entity_type { + PlaceEntityType::Assembler { + pos: position, + ty: _, + rotation: _, + .. + } => { + *position = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Inserter { + pos, + dir: _, + filter: _, + ty: _, + .. + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Belt { + pos, + ty: _, + direction: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Underground { + pos, + ty: _, + direction: _, + underground_dir: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::PowerPole { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Splitter { + pos, + direction: _, + ty: _, + in_mode: _, + out_mode: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Chest { pos, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::SolarPanel { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Accumulator { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Lab { pos, ty: _, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Beacon { ty: _, pos, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::FluidTank { + ty: _, + pos, + rotation: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::MiningDrill { + ty: _, + pos, + rotation: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + }, + HeldObject::OrePlacement { .. } => {}, + }, + ActionStateMachineState::Deconstructing(position, _timer) => { + if let Some(e) = world.get_entity_at(*position, data_store) { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + let mouse_pos = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); + + if mouse_pos.contained_in(e_pos, e_size) { + // We are still deconstructing. Continue + } else { + // The mouse is no longer over the entity + self.state = ActionStateMachineState::Idle; + } + + } else { + // The entity is gone + self.state = ActionStateMachineState::Idle; + } + + }, + } + vec![] }, Input::KeyPress(code) => { @@ -564,7 +860,7 @@ impl let Position {x: mouse_x, y: mouse_y} = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); match delta { (_, y) => { - self.zoom_level -= y as f32 / 10.0 * self.mouse_wheel_sensitivity; + self.zoom_level -= y as f32 * 4.0 * self.mouse_wheel_sensitivity; }, } if let Some(view_center) = &mut self.map_view_info { @@ -733,7 +1029,7 @@ impl }, )); }, - Some(Entity::Roboport { ty, .. }) => todo!(), + Some(Entity::Roboport { .. }) => todo!(), Some(Entity::SolarPanel { pos: _, ty, @@ -809,7 +1105,7 @@ impl }, (_, Key::E) => { - self.hotbar_window_open = !self.hotbar_window_open; + self.open_windows[Window::Hotbar] = !self.open_windows[Window::Hotbar]; vec![] }, @@ -996,12 +1292,12 @@ impl }, (_, Key::P) => { - self.statistics_panel_open = !self.statistics_panel_open; + self.open_windows[Window::Statistics] = !self.open_windows[Window::Statistics]; vec![] }, (_, Key::T) => { - self.technology_panel_open = !self.technology_panel_open; + self.open_windows[Window::Technology] = !self.open_windows[Window::Technology]; vec![] }, @@ -1028,7 +1324,7 @@ impl }, (_, Key::Esc) => { - self.escape_menu_open = !self.escape_menu_open; + self.open_windows[Window::Escape] = !self.open_windows[Window::Escape]; vec![] }, @@ -1036,7 +1332,7 @@ impl }; // Do not send any actions if we are in the escape menu - if self.escape_menu_open { + if self.open_windows[Window::Escape] { vec![].into_iter() } else { ret.into_iter() @@ -1066,8 +1362,8 @@ impl ); Position { - x: mouse_pos.0 as i32, - y: mouse_pos.1 as i32, + x: mouse_pos.0.floor() as i32, + y: mouse_pos.1.floor() as i32, } } @@ -1218,8 +1514,26 @@ impl }, HeldObject::OrePlacement { .. } => {}, }, - ActionStateMachineState::Deconstructing(position, timer) => { - //todo!("Check if we are still over the same thing") + ActionStateMachineState::Deconstructing(position, _timer) => { + if let Some(e) = world.get_entity_at(*position, data_store) { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + let mouse_pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + + if mouse_pos.contained_in(e_pos, e_size) { + // We are still deconstructing. Continue + } else { + // The mouse is no longer over the entity + self.state = ActionStateMachineState::Idle; + } + } else { + // The entity is gone + self.state = ActionStateMachineState::Idle; + } }, } @@ -1290,7 +1604,7 @@ impl data_store: &DataStore, ) { // Possible Actions - ui.columns_const(|uis: &mut [egui::Ui; 12]| { + ui.columns_const(|uis: &mut [egui::Ui; 13]| { for (i, ui) in uis.iter_mut().enumerate() { let ty_count = match i { 0 => data_store.assembler_info.len(), @@ -1305,6 +1619,7 @@ impl 9 => data_store.solar_panel_info.len(), 10 => data_store.lab_info.len(), 11 => data_store.inserter_infos.len(), + 12 => data_store.mining_drill_info.len(), _ => unreachable!(), } as u8; @@ -1413,6 +1728,14 @@ impl }), &data_store.inserter_infos[ty as usize].display_name, ), + 12 => ( + HeldObject::Entity(PlaceEntityType::MiningDrill { + pos: Position { x: 0, y: 0 }, + ty, + rotation: Dir::North, + }), + &data_store.mining_drill_info[ty as usize].display_name, + ), _ => unreachable!(), }; @@ -1493,14 +1816,11 @@ impl PlaceEntityType::MiningDrill { ty, .. } => Cow::Borrowed( &*data_store.mining_drill_info[ty as usize].display_name, ), - - _ => unreachable!(), }, _ => unreachable!(), }) .unwrap_or(Cow::Borrowed("None")); - let slot_id = egui::Id::new(("hotbar_slot", slot_idx)); let response = ui.label(&format!("{}", text)); if let Some(new) = response.dnd_release_payload::>() { diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index 118ba80..287b57a 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -7,7 +7,7 @@ use strum::IntoEnumIterator; use crate::{ app_state::SimulationState, belt::{ - BeltStore, BeltTileId, SplitterInfo, + BeltTileId, SplitterInfo, belt::BeltLenType, smart::Side, splitter::{SPLITTER_BELT_LEN, SplitterDistributionMode, SplitterSide}, @@ -395,11 +395,11 @@ pub fn handle_underground_removal impl Strategy>> { - Just(vec![ - place(PlaceEntityType::Assembler { - pos: Position { x: 0, y: 3 }, - ty: 0, - rotation: Dir::North, - }), - ActionType::SetRecipe(SetRecipeInfo { - pos: Position { x: 0, y: 3 }, - recipe: Recipe { id: 0 }, - }), - place(PlaceEntityType::Inserter { - pos: Position { x: 2, y: 2 }, - dir: crate::frontend::world::tile::Dir::North, - filter: None, - ty: 0, - user_movetime: None, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 2, y: 1 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }), - place(PlaceEntityType::PowerPole { - pos: Position { x: 0, y: 2 }, - ty: 0, - }), - place(PlaceEntityType::PowerPole { - pos: Position { x: 5, y: 0 }, - ty: 0, - }), - place(PlaceEntityType::SolarPanel { - pos: Position { x: 6, y: 0 }, - ty: 0, - }), - ]) - } - - fn belts_into_sideload() -> impl Strategy>> { - Just(vec![ - place(PlaceEntityType::Belt { - pos: Position { x: 3, y: 1 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 0 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 1 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 2 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - ]) - } + use crate::{ + frontend::{ + action::action_state_machine::HeldObject, + world::{ + Position, + tile::{Dir, PlaceEntityType, UndergroundDir}, + }, + }, + test_world_harness::*, + }; - fn sideload_items() -> impl Strategy>> { - (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { - a.extend(b.into_iter()); - a - }) + #[test] + fn underground_blocking_crash() { + let mut test = Test::default(); + + test.hold_bad(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position::default(), + direction: Dir::default(), + ty: 0, + underground_dir: UndergroundDir::Entrance, + })); + test.rotate_holding(Dir::East); + // Place outer pair of undergrounds + test.place((0, 0)); + test.place((5, 0)); + + // // Place inner pair of undergrounds + test.place((2, 0)); + test.place((3, 0)); + + test.clear_hand(); + test.assert_tile_occupied((2, 0)); + + // Remove left of inner pair of undergrounds + test.mouse_to((2, 0)); + test.right_mouse_down(); + test.tick_many(35); + + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); + test.assert_tile_empty((2, 0)); + test.assert_tile_occupied((3, 0)); + + // Remove right of inner pair of undergrounds + test.mouse_to((3, 0)); + test.right_mouse_down(); + test.tick_many(35); + + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); + test.assert_tile_empty((2, 0)); + test.assert_tile_empty((3, 0)); } - fn place(ty: PlaceEntityType) -> ActionType { - ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), - force: false, - }) - } - - proptest! { - - #[test] - fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { - let mut state = GameState::new( &DATA_STORE); - - let bp = Blueprint { actions }; - - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - prop_assert!(assembler_powered); - - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - prop_assume!(assembler_working, "{:?}", ent); - - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - prop_assert!(inserter_attached, "{:?}", ent); - } - - #[test] - fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); - - let bp = Blueprint { actions }; - - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - prop_assert!(assembler_powered); - - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - prop_assume!(assembler_working, "{:?}", ent); - - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - prop_assert!(inserter_attached, "{:?}", ent); - } - - #[test] - fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); - - let bp = Blueprint { actions }; - - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - } - - #[test] - fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); + #[test] + fn place_underground() { + let mut test = Test::default(); - let bp = Blueprint { actions }; + test.hold_bad(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position::default(), + direction: Dir::default(), + ty: 0, + underground_dir: UndergroundDir::Entrance, + })); + test.rotate_holding(Dir::East); + test.place((0, 0)); + test.place((5, 0)); - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - } - - #[test] - fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { - let mut state = GameState::new( &DATA_STORE); - - let bp = Blueprint { actions }; - - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - prop_assume!(assembler_working, "{:?}", assembler); - - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - prop_assume!(inserter_attached, "{:?}", ent); - - for _ in 0..200 { - state.update(&DATA_STORE); - } - - let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - unreachable!() - }; - - let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); - - prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); - - prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); - } - - #[test] - fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); - - let bp = Blueprint { actions }; - - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - prop_assume!(assembler_powered); - - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - prop_assume!(assembler_working, "{:?}", ent); - - let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - prop_assume!(inserter_attached); - - for _ in 0..2000 { - state.update(&DATA_STORE); - } - - let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - unreachable!() - }; - - let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { - unreachable!() - }; - - let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); - - prop_assume!(produced > 0, "{:?}", produced); - - prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); - } + test.clear_hand(); + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); } } diff --git a/src/frontend/action/mod.rs b/src/frontend/action/mod.rs index 4c6a000..767a9f5 100644 --- a/src/frontend/action/mod.rs +++ b/src/frontend/action/mod.rs @@ -53,8 +53,12 @@ pub enum ActionType { Remove(Position), - SetActiveResearch { - tech: Option, + AddResearchToQueue { + tech: Technology, + }, + + RemoveResearchFromQueue { + tech: Technology, }, CheatUnlockTechnology { @@ -72,6 +76,9 @@ pub enum ActionType { }, Ping(Position), + + // TODO: Does this need args? + SpawnPlayer {}, } impl ActionType { @@ -104,11 +111,13 @@ impl ActionType Some(*pos), ActionType::SetChestSlotLimit { pos, .. } => Some(*pos), ActionType::Remove(position) => Some(*position), - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { pos, .. } => Some(*pos), ActionType::Ping(position) => Some(*position), + ActionType::SpawnPlayer { .. } => None, } } @@ -171,11 +180,14 @@ impl ActionType None, ActionType::SetChestSlotLimit { .. } => None, ActionType::Remove(_) => None, - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { .. } => None, ActionType::Ping(_) => None, + + ActionType::SpawnPlayer { .. } => None, } } @@ -195,11 +207,14 @@ impl ActionType Some([1, 1]), ActionType::SetChestSlotLimit { .. } => Some([1, 1]), ActionType::Remove(_) => Some([1, 1]), - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { pos, .. } => Some([1, 1]), ActionType::Ping(_) => None, + + ActionType::SpawnPlayer { .. } => None, }) } } diff --git a/src/frontend/input.rs b/src/frontend/input.rs index b763d2c..743d6a9 100644 --- a/src/frontend/input.rs +++ b/src/frontend/input.rs @@ -75,7 +75,20 @@ impl TryFrom for Input { Ok(Input::MouseScoll((delta.x as f64, delta.y as f64))) }, eframe::egui::MouseWheelUnit::Line => { - Ok(Input::MouseScoll((delta.x as f64, delta.y as f64))) + // TODO: This is hardcoded to the default of egui + // See InputOptions::default and https://github.com/emilk/egui/issues/461 + let units_per_line = if cfg!(all(target_arch = "wasm32", target_os = "unknown")) + { + // FIXME: Why does it seem like I need to use 40 here, if egui uses 8 on wasm???? + 40.0 + } else { + 40.0 + }; + + Ok(Input::MouseScoll(( + delta.x as f64 / units_per_line, + delta.y as f64 / units_per_line, + ))) }, eframe::egui::MouseWheelUnit::Page => Err(()), }, diff --git a/src/frontend/world/mod.rs b/src/frontend/world/mod.rs index 00e9f8e..4760ce3 100644 --- a/src/frontend/world/mod.rs +++ b/src/frontend/world/mod.rs @@ -9,7 +9,17 @@ pub mod tile; // TODO: Do not use usize for anything that might go to another machine, where it could be different size! #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive( - Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, + Debug, + Clone, + Copy, + Default, + serde::Serialize, + serde::Deserialize, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, )] pub struct Position { pub x: i32, diff --git a/src/frontend/world/sparse_grid/bounding_box_grid.rs b/src/frontend/world/sparse_grid/bounding_box_grid.rs index be0edfa..67dcbc6 100644 --- a/src/frontend/world/sparse_grid/bounding_box_grid.rs +++ b/src/frontend/world/sparse_grid/bounding_box_grid.rs @@ -1,3 +1,4 @@ +use crate::frontend::world::sparse_grid::SparseGrid; use crate::saving::{save_at, save_at_fork}; use super::GetGridIndex; @@ -7,6 +8,7 @@ use rayon::slice::ParallelSlice; use std::cmp::{max, min}; use std::fmt::Debug; use std::fs::create_dir_all; +use std::mem; use std::ops::RangeInclusive; use std::path::PathBuf; @@ -27,59 +29,85 @@ pub struct BoundingBoxGrid { // FIXME: currently only for i32 to allow getting the number of slots type I = i32; -impl> BoundingBoxGrid { - pub fn new() -> Self { +impl + 'static> SparseGrid for BoundingBoxGrid { + fn new() -> Self { Self { extent: None, values: vec![], } } - pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + fn get_extent(&self) -> Option<[RangeInclusive; 2]> { + self.extent.map(|v| v.map(|[start, end]| start..=end)) + } + + fn get_default(&mut self, x: I, y: I) -> &T where - F: Fn([I; 2]) -> T + Sync, - T: Send, + T: Default, { - let extent = Some([ - [top_left[0], bottom_right[0]], - [top_left[1], bottom_right[1]], - ]); + if self.get(x, y).is_none() { + let old = self.insert(x, y, T::default()); + assert!(old.is_none()); + } + self.get(x, y).unwrap() + } - let width = usize::try_from(bottom_right[0] - top_left[0]) - .expect("Check bounding box argument order") - + 1; + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.include_in_extent(x, y); - let height = usize::try_from(bottom_right[1] - top_left[1]) - .expect("Check bounding box argument order") - + 1; + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + debug_assert!(self.values[index].is_none()); + let mut old = Some(value); + mem::swap(&mut old, &mut self.values[index]); + old + } - let mut values = Vec::with_capacity(width * height); + fn get(&self, x: I, y: I) -> Option<&T> { + if let Some(extent) = &self.extent { + if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + return None; + } + } - (0..(width * height)) - .into_par_iter() - .map(|v| (v % width, v / width)) - .map(|(x_offs, y_offs)| { - ( - top_left[0] - .checked_add_unsigned(x_offs.try_into().unwrap()) - .unwrap(), - top_left[1] - .checked_add_unsigned(y_offs.try_into().unwrap()) - .unwrap(), - ) - }) - .map(|(x, y)| Some(generation_fn([x, y]))) - .collect_into_vec(&mut values); + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - Self { extent, values } + self.values[index].as_ref() + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + if let Some(extent) = &self.extent { + if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + return None; + } + } + + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + + self.values[index].as_mut() + } + + fn occupied_entries(&self) -> impl Iterator { + self.values.iter().enumerate().filter_map(|(idx, v)| { + let [x, y] = Self::calculate_pos(&self.extent.unwrap(), idx); + v.as_ref().map(|v| ((x, y), v)) + }) + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + self.values.iter_mut().enumerate().filter_map(|(idx, v)| { + let [x, y] = Self::calculate_pos(&self.extent.unwrap(), idx); + v.as_mut().map(|v| ((x, y), v)) + }) } // TODO: Do I want to save None values? - pub fn save_fork(&self, base_path: PathBuf) + fn save_single_thread(&self, base_path: PathBuf) where - T: Sync + serde::Serialize, + T: serde::Serialize, { - create_dir_all(&base_path).expect("Failed to create world dir"); + create_dir_all(&base_path).expect("Failed to create chunk dir"); + + save_at_fork(&self.extent, base_path.join("extent")); // TODO: Choose a chunk size self.values @@ -94,11 +122,13 @@ impl> BoundingBoxGrid { } // TODO: Do I want to save None values? - pub fn par_save(&self, base_path: PathBuf) + fn par_save(&self, base_path: PathBuf) where T: Sync + serde::Serialize, { - create_dir_all(&base_path).expect("Failed to create world dir"); + create_dir_all(&base_path).expect("Failed to create chunk dir"); + + save_at(&self.extent, base_path.join("extent")); // TODO: Choose a chunk size self.values @@ -112,10 +142,12 @@ impl> BoundingBoxGrid { // todo!("Serialize the rest") } - pub fn par_load(base_path: PathBuf) -> Self + fn par_load(base_path: PathBuf) -> Self where for<'a> T: Send + serde::Deserialize<'a>, { + let extent = load_at(base_path.join("extent")); + let values: Vec<_> = (0..) .map(|chunk_id| base_path.join(format!("chunk-{chunk_id}"))) .take_while(|path| { @@ -128,35 +160,99 @@ impl> BoundingBoxGrid { .flatten() .collect(); - let top_left = values - .iter() - .flatten() - .map(|value| value.get_grid_index()) - .reduce(|a, b| (min(a.0, b.0), min(a.1, b.1))); + // TODO: This assertion requires storing the pos + // for (index, chunk) in values + // .iter() + // .enumerate() + // .filter_map(|(index, chunk)| chunk.as_ref().map(|chunk| (index, chunk))) + // { + // assert_eq!( + // Self::calculate_index(&extent.unwrap(), chunk.get_grid_index().into()), + // index + // ); + // } - let bottom_right = values - .iter() - .flatten() - .map(|value| value.get_grid_index()) - .reduce(|a, b| (max(a.0, b.0), max(a.1, b.1))); + Self { extent, values } + } + + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + let x_min = positions + .clone() + .into_iter() + .map(|(x, _y)| x) + .min() + .unwrap(); + let x_max = positions + .clone() + .into_iter() + .map(|(x, _y)| x) + .max() + .unwrap(); - let extent = match (top_left, bottom_right) { - (None, None) => None, - (Some((min_x, min_y)), Some((max_x, max_y))) => Some([[min_x, max_x], [min_y, max_y]]), + let y_min = positions + .clone() + .into_iter() + .map(|(_x, y)| y) + .min() + .unwrap(); + let y_max = positions + .clone() + .into_iter() + .map(|(_x, y)| y) + .max() + .unwrap(); - _ => unreachable!(), - }; + self.include_in_extent(x_min, y_min); + self.include_in_extent(x_max, y_max); - for (index, chunk) in values - .iter() - .enumerate() - .filter_map(|(index, chunk)| chunk.as_ref().map(|chunk| (index, chunk))) - { - assert_eq!( - Self::calculate_index(&extent.unwrap(), chunk.get_grid_index().into()), - index - ); + for (value, (x, y)) in values.into_iter().zip(positions) { + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + debug_assert!(self.values[index].is_none()); + self.values[index] = Some(value); } + } +} + +impl> BoundingBoxGrid { + pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + where + F: Fn([I; 2]) -> T + Sync, + T: Send, + { + let extent = Some([ + [top_left[0], bottom_right[0]], + [top_left[1], bottom_right[1]], + ]); + + let width = usize::try_from(bottom_right[0] - top_left[0]) + .expect("Check bounding box argument order") + + 1; + + let height = usize::try_from(bottom_right[1] - top_left[1]) + .expect("Check bounding box argument order") + + 1; + + let mut values = Vec::with_capacity(width * height); + + (0..(width * height)) + .into_par_iter() + .map(|v| (v % width, v / width)) + .map(|(x_offs, y_offs)| { + ( + top_left[0] + .checked_add_unsigned(x_offs.try_into().unwrap()) + .unwrap(), + top_left[1] + .checked_add_unsigned(y_offs.try_into().unwrap()) + .unwrap(), + ) + }) + .map(|(x, y)| Some(generation_fn([x, y]))) + .collect_into_vec(&mut values); Self { extent, values } } @@ -189,9 +285,22 @@ impl> BoundingBoxGrid { height_offs as usize * width as usize + width_offs as usize } + fn calculate_pos(extent: &[[I; 2]; 2], idx: usize) -> [I; 2] { + let width = extent[0][1] - extent[0][0]; + let x_offs = idx as i32 % width; + let y_offs = idx as i32 / width; + + let x = extent[0][0] + x_offs; + let y = extent[1][0] + y_offs; + + [x, y] + } + fn reorder_for_new_extent(&mut self, new_point: [I; 2]) { let [x, y] = new_point; + let old_extent = self.extent.unwrap_or([[x, x], [y, y]]); + let extent = self.extent.get_or_insert([[x, x], [y, y]]); let [x_range, y_range] = extent; @@ -208,112 +317,49 @@ impl> BoundingBoxGrid { self.values.resize_with(new_size, || None); - for old_val in values { - let pos = old_val.get_grid_index(); + for (idx, old_val) in values.into_iter().enumerate() { + let pos = Self::calculate_pos(&old_extent, idx); let index = Self::calculate_index(extent, pos.into()); self.values[index] = Some(old_val); } } - pub fn get_extent(&self) -> Option<[RangeInclusive; 2]> { - self.extent.map(|v| v.map(|[start, end]| start..=end)) - } - - pub fn insert(&mut self, x: I, y: I, value: T) { - self.include_in_extent(x, y); - - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - debug_assert!(self.values[index].is_none()); - self.values[index] = Some(value); - } - - pub fn insert_many( - &mut self, - positions: impl IntoIterator + Clone, - values: impl IntoIterator + Clone, - ) where - T: PartialEq + Debug, - { - let x_min = positions - .clone() - .into_iter() - .map(|(x, _y)| x) - .min() - .unwrap(); - let x_max = positions - .clone() - .into_iter() - .map(|(x, _y)| x) - .max() - .unwrap(); - - let y_min = positions - .clone() - .into_iter() - .map(|(_x, y)| y) - .min() - .unwrap(); - let y_max = positions - .clone() - .into_iter() - .map(|(_x, y)| y) - .max() - .unwrap(); - - self.include_in_extent(x_min, y_min); - self.include_in_extent(x_max, y_max); - - #[cfg(debug_assertions)] - { - assert!( - positions - .clone() - .into_iter() - .zip(values.clone()) - .all(|(pos, v)| pos == v.get_grid_index()) - ); - } - - for (value, (x, y)) in values.into_iter().zip(positions) { - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - debug_assert!(self.values[index].is_none()); - self.values[index] = Some(value); - } - } - - pub fn get(&self, x: I, y: I) -> Option<&T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } - } - - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - - self.values[index].as_ref() - } - - pub fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } + pub(super) fn get_extent_after_insertion( + &self, + positions: impl IntoIterator, + ) -> [RangeInclusive; 2] { + let mut positions = positions.into_iter(); + let [x, y] = positions.next().unwrap(); + let extent = self.extent.unwrap_or([[x, x], [y, y]]); + let [mut x_range, mut y_range] = extent; + + for [x, y] in positions { + x_range[0] = min(x_range[0], x); + x_range[1] = max(x_range[1], x); + + y_range[0] = min(y_range[0], y); + y_range[1] = max(y_range[1], y); } - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - - self.values[index].as_mut() - } - - pub fn occupied_entries(&self) -> impl Iterator { - self.values - .iter() - .filter_map(|v| v.as_ref().map(|v| (v.get_grid_index(), v))) + [x_range[0]..=x_range[1], y_range[0]..=y_range[1]] } - pub fn occupied_entries_mut(&mut self) -> impl Iterator { + pub(super) fn into_iter(self) -> impl Iterator { self.values - .iter_mut() - .filter_map(|v| v.as_mut().map(|v| (v.get_grid_index(), v))) + .into_iter() + .enumerate() + .filter_map(|(idx, v)| v.map(|v| (idx, v))) + .map(move |(idx, chunk)| { + ( + Self::calculate_pos( + &self + .extent + .expect("Since we have any chunks, we must have an extent"), + idx, + ) + .into(), + chunk, + ) + }) } } diff --git a/src/frontend/world/sparse_grid/dynamic.rs b/src/frontend/world/sparse_grid/dynamic.rs new file mode 100644 index 0000000..a2d7ef8 --- /dev/null +++ b/src/frontend/world/sparse_grid/dynamic.rs @@ -0,0 +1,236 @@ +use std::{fs::create_dir_all, iter}; + +use itertools::Either; + +use crate::{ + frontend::world::sparse_grid::{ + GetGridIndex, SparseGrid, bounding_box_grid::BoundingBoxGrid, map_grid::BtreeMapGrid, + }, + saving::{load_at, save_at, save_at_fork}, +}; + +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; + +// If more than every 20th chunk is inhabited, switch to map +// TODO: Find a good value for this +const SWITCH_RATIO: usize = 20; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub(crate) struct DynamicGrid { + pub(crate) num_chunks: usize, + store: Backing, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum Backing { + BoundingBoxGrid(BoundingBoxGrid), + Map(BtreeMapGrid), +} + +type I = i32; +impl + 'static> SparseGrid for DynamicGrid { + fn new() -> Self { + Self { + num_chunks: 0, + store: Backing::BoundingBoxGrid(BoundingBoxGrid::new()), + } + } + + fn get_extent(&self) -> Option<[std::ops::RangeInclusive; 2]> { + match &self.store { + Backing::BoundingBoxGrid(grid) => grid.get_extent(), + Backing::Map(grid) => grid.get_extent(), + } + } + + fn get_default(&mut self, _x: I, _y: I) -> &T + where + T: Default, + { + unimplemented!() + } + + fn get(&self, x: I, y: I) -> Option<&T> { + match &self.store { + Backing::BoundingBoxGrid(grid) => grid.get(x, y), + Backing::Map(grid) => grid.get(x, y), + } + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + match &mut self.store { + Backing::BoundingBoxGrid(grid) => grid.get_mut(x, y), + Backing::Map(grid) => grid.get_mut(x, y), + } + } + + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.num_chunks += 1; + match &mut self.store { + Backing::BoundingBoxGrid(grid) => { + let [x_ext, y_ext] = grid.get_extent_after_insertion(iter::once([x, y])); + + let new_size = x_ext.count() * y_ext.count(); + + if self.num_chunks * SWITCH_RATIO < new_size { + log::error!("SWITCHING TO SLOW WORLD REPRESENTATION TO SAVE RAM!"); + let mut ret = None; + take_mut::take(&mut self.store, |store| { + let Backing::BoundingBoxGrid(grid) = store else { + unreachable!() + }; + let mut new_grid = BtreeMapGrid::new_with_values( + grid.get_extent().unwrap(), + grid.into_iter(), + ); + ret = Some(new_grid.insert(x, y, value)); + Backing::Map(new_grid) + }); + + ret.unwrap() + } else { + grid.insert(x, y, value) + } + }, + Backing::Map(grid) => grid.insert(x, y, value), + } + } + + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + self.num_chunks += positions.clone().into_iter().count(); + match &mut self.store { + Backing::BoundingBoxGrid(grid) => { + let [x_ext, y_ext] = grid + .get_extent_after_insertion(positions.clone().into_iter().map(|v| v.into())); + + let new_size = x_ext.count() * y_ext.count(); + + if self.num_chunks * SWITCH_RATIO < new_size { + log::error!("SWITCHING TO SLOW WORLD REPRESENTATION TO SAVE RAM!"); + take_mut::take(&mut self.store, |store| { + let Backing::BoundingBoxGrid(grid) = store else { + unreachable!() + }; + let mut new_grid = BtreeMapGrid::new_with_values( + grid.get_extent().unwrap(), + grid.into_iter(), + ); + new_grid.insert_many(positions, values); + Backing::Map(new_grid) + }); + } else { + grid.insert_many(positions, values) + } + }, + Backing::Map(grid) => grid.insert_many(positions, values), + } + } + + fn occupied_entries(&self) -> impl Iterator { + match &self.store { + Backing::BoundingBoxGrid(grid) => Either::Left(grid.occupied_entries()), + Backing::Map(grid) => Either::Right(grid.occupied_entries()), + } + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + match &mut self.store { + Backing::BoundingBoxGrid(grid) => Either::Left(grid.occupied_entries_mut()), + Backing::Map(grid) => Either::Right(grid.occupied_entries_mut()), + } + } + + fn save_single_thread(&self, base_path: std::path::PathBuf) + where + T: serde::Serialize, + I: serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + let is_map = match &self.store { + Backing::BoundingBoxGrid(_) => false, + Backing::Map(_) => true, + }; + + save_at_fork(&is_map, base_path.join("is_map")); + + match &self.store { + Backing::BoundingBoxGrid(grid) => { + grid.save_single_thread(base_path); + }, + Backing::Map(grid) => { + grid.save_single_thread(base_path); + }, + } + } + + fn par_save(&self, base_path: std::path::PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + let is_map = match &self.store { + Backing::BoundingBoxGrid(_) => false, + Backing::Map(_) => true, + }; + + save_at(&is_map, base_path.join("is_map")); + + match &self.store { + Backing::BoundingBoxGrid(grid) => { + grid.par_save(base_path); + }, + Backing::Map(grid) => { + grid.par_save(base_path); + }, + } + } + + fn par_load(base_path: std::path::PathBuf) -> Self + where + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>, + { + let is_map = load_at::(base_path.join("is_map")); + + let store = match is_map { + true => Backing::Map(BtreeMapGrid::par_load(base_path)), + false => Backing::BoundingBoxGrid(BoundingBoxGrid::par_load(base_path)), + }; + + let count = match &store { + Backing::BoundingBoxGrid(grid) => grid.occupied_entries().count(), + Backing::Map(grid) => grid.occupied_entries().count(), + }; + + Self { + num_chunks: count, + store, + } + } +} + +impl + 'static> DynamicGrid { + pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + where + F: Fn([I; 2]) -> T + Sync, + T: Send, + { + let store = BoundingBoxGrid::new_with_filled_grid(top_left, bottom_right, generation_fn); + Self { + num_chunks: (store.occupied_entries().count()), + store: Backing::BoundingBoxGrid(store), + } + } +} diff --git a/src/frontend/world/sparse_grid/map_grid.rs b/src/frontend/world/sparse_grid/map_grid.rs new file mode 100644 index 0000000..6d8a3e4 --- /dev/null +++ b/src/frontend/world/sparse_grid/map_grid.rs @@ -0,0 +1,175 @@ +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; +use itertools::Itertools; +use rayon::iter::IntoParallelIterator; +use rayon::iter::ParallelIterator; +use std::cmp::{max, min}; +use std::collections::BTreeMap; +use std::fs::File; +use std::fs::create_dir_all; +use std::hash::Hash; +use std::ops::RangeInclusive; + +use crate::frontend::world::sparse_grid::{FILE_CHUNK_SIZE, SparseGrid}; +use crate::saving::load_at; +use crate::saving::save_at_fork; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BtreeMapGrid { + extent: Option<[[I; 2]; 2]>, + values: BTreeMap<(I, I), T>, +} + +impl SparseGrid for BtreeMapGrid { + fn new() -> Self { + Self { + extent: None, + values: Default::default(), + } + } + + fn get_extent(&self) -> Option<[RangeInclusive; 2]> { + self.extent.map(|v| v.map(|[start, end]| start..=end)) + } + + fn get_default(&mut self, x: I, y: I) -> &T + where + T: Default, + { + self.include_in_extent(x, y); + self.values.entry((x, y)).or_default() + } + + fn get(&self, x: I, y: I) -> Option<&T> { + // TODO: Why is this commented out? + // if let Some(extent) = &self.extent { + // if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + // return None; + // } + // } + + self.values.get(&(x, y)) + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + self.values.get_mut(&(x, y)) + } + + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.include_in_extent(x, y); + self.values.insert((x, y), value) + } + + fn occupied_entries(&self) -> impl Iterator { + self.values + .iter() + // .sorted_by_key(|(k, _)| **k) + .map(|(a, b)| (*a, b)) + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + self.values + .iter_mut() + // .sorted_by_key(|(k, _)| **k) + .map(|(a, b)| (*a, b)) + } + + fn save_single_thread(&self, base_path: std::path::PathBuf) + where + T: serde::Serialize, + I: serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + self.values + .iter() + .chunks(FILE_CHUNK_SIZE) + .into_iter() + .enumerate() + .for_each(|(i, chunks)| { + save_at_fork(&chunks.collect_vec(), base_path.join(format!("chunk-{i}"))); + }); + } + + fn par_save(&self, base_path: std::path::PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize, + { + // FIXME: This is single threaded + self.values + .iter() + .chunks(FILE_CHUNK_SIZE) + .into_iter() + .enumerate() + .for_each(|(i, chunks)| { + save_at_fork(&chunks.collect_vec(), base_path.join(format!("chunk-{i}"))); + }); + } + + fn par_load(base_path: std::path::PathBuf) -> Self + where + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>, + { + let values: BTreeMap<_, _> = (0..) + .map(|chunk_id| base_path.join(format!("chunk-{chunk_id}"))) + .take_while(|path| { + // FIXME: Use another function + File::open(path).is_ok() + }) + .collect_vec() + .into_par_iter() + .map(|file_path| load_at::>(file_path)) + .flatten() + .collect(); + + let top_left = values + .iter() + .map(|value| *value.0) + .reduce(|a, b| (min(a.0, b.0), min(a.1, b.1))); + + let bottom_right = values + .iter() + .map(|value| *value.0) + .reduce(|a, b| (max(a.0, b.0), max(a.1, b.1))); + + let extent = match (top_left, bottom_right) { + (None, None) => None, + (Some((min_x, min_y)), Some((max_x, max_y))) => Some([[min_x, max_x], [min_y, max_y]]), + + _ => unreachable!(), + }; + + Self { extent, values } + } +} + +impl BtreeMapGrid { + pub(super) fn new_with_values( + extent: [RangeInclusive; 2], + values: impl IntoIterator, + ) -> Self { + Self { + extent: Some(extent.map(|extent| [*extent.start(), *extent.end()])), + values: BTreeMap::from_iter(values), + } + } + + fn include_in_extent(&mut self, x: I, y: I) { + if let Some(extent) = &mut self.extent { + let [x_range, y_range] = extent; + + x_range[0] = min(x_range[0], x); + x_range[1] = max(x_range[1], x); + + y_range[0] = min(y_range[0], y); + y_range[1] = max(y_range[1], y); + } else { + self.extent = Some([[x, x], [y, y]]); + } + } +} diff --git a/src/frontend/world/sparse_grid/mod.rs b/src/frontend/world/sparse_grid/mod.rs index 0b13cae..93f4bef 100644 --- a/src/frontend/world/sparse_grid/mod.rs +++ b/src/frontend/world/sparse_grid/mod.rs @@ -1,103 +1,51 @@ -#[cfg(feature = "client")] -use egui_show_info_derive::ShowInfo; -#[cfg(feature = "client")] -use get_size2::GetSize; -use std::cmp::{max, min}; -use std::collections::BTreeMap; -use std::hash::Hash; -use std::ops::RangeInclusive; +use std::{ops::RangeInclusive, path::PathBuf}; pub mod bounding_box_grid; // pub mod perfect_grid; +pub mod dynamic; +pub mod map_grid; -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct SparseGrid { - extent: Option<[[I; 2]; 2]>, - values: BTreeMap<(I, I), T>, -} +const FILE_CHUNK_SIZE: usize = 100_000; -impl SparseGrid { - pub fn new() -> Self +pub trait SparseGrid { + fn new() -> Self; + fn get_extent(&self) -> Option<[RangeInclusive; 2]>; + fn get_default(&mut self, x: I, y: I) -> &T where - I: Default, - { - Self { - extent: None, - values: Default::default(), - } - } + T: Default; - fn include_in_extent(&mut self, x: I, y: I) { - if let Some(extent) = &mut self.extent { - let [x_range, y_range] = extent; + fn get(&self, x: I, y: I) -> Option<&T>; + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T>; + fn insert(&mut self, x: I, y: I, value: T) -> Option; - x_range[0] = min(x_range[0], x); - x_range[1] = max(x_range[1], x); - - y_range[0] = min(y_range[0], y); - y_range[1] = max(y_range[1], y); - } else { - self.extent = Some([[x, x], [y, y]]); - } - } + fn occupied_entries(&self) -> impl Iterator; + fn occupied_entries_mut(&mut self) -> impl Iterator; - pub fn get_extent(&self) -> Option<[RangeInclusive; 2]> { - self.extent.map(|v| v.map(|[start, end]| start..=end)) - } - - pub fn get_default(&mut self, x: I, y: I) -> &T + fn save_single_thread(&self, base_path: PathBuf) where - T: Default, - { - self.include_in_extent(x, y); - self.values.entry((x, y)).or_default() - } - - pub fn get(&self, x: I, y: I) -> Option<&T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } - } + T: serde::Serialize, + I: serde::Serialize; - self.values.get(&(x, y)) - } - - pub fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { - self.values.get_mut(&(x, y)) - } - - pub fn insert(&mut self, x: I, y: I, value: T) -> Option { - self.include_in_extent(x, y); - self.values.insert((x, y), value) - } + // TODO: Do I want to save None values? + fn par_save(&self, base_path: PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize; - pub fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option + fn par_load(base_path: PathBuf) -> Self where - T: PartialEq + Default, - { - self.include_in_extent(x, y); - if value == T::default() { - self.values.remove(&(x, y)) - } else { - self.values.insert((x, y), value) + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>; + + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + for (v, pos) in values.into_iter().zip(positions) { + self.insert(pos.0, pos.1, v); } } - - pub fn occupied_entries(&self) -> impl Iterator { - self.values - .iter() - // .sorted_by_key(|(k, _)| **k) - .map(|(a, b)| (*a, b)) - } - - pub fn occupied_entries_mut(&mut self) -> impl Iterator { - self.values - .iter_mut() - // .sorted_by_key(|(k, _)| **k) - .map(|(a, b)| (*a, b)) - } } pub trait GetGridIndex { diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index d19f7c1..94527b2 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1,5 +1,6 @@ +use crate::frontend::world::sparse_grid::SparseGrid; +use crate::frontend::world::sparse_grid::dynamic::DynamicGrid; use crate::frontend::world::tile::belt_placement::expected_belt_state; -use crate::mining_drill; use crate::mining_drill::AddMinerError; use crate::mining_drill::FullOreStore; use crate::mining_drill::MiningDrillIdentifier; @@ -8,6 +9,8 @@ use egui::Color32; #[cfg(feature = "client")] use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; use log::error; +use petgraph::visit::NodeIndexable; +use petgraph::visit::VisitMap; use rayon::iter::IndexedParallelIterator; use rayon::iter::ParallelIterator; use rayon::prelude::ParallelSliceMut; @@ -23,6 +26,7 @@ use std::{ num::NonZero, ops::{Add, ControlFlow, Range}, }; +use strum::IntoEnumIterator; use crate::frontend::world::sparse_grid::GetGridIndex; @@ -42,7 +46,7 @@ use strum::EnumIter; use itertools::Itertools; -use noise::{NoiseFn, Simplex}; +use noise::Simplex; use crate::mining_drill::OreLookup; use crate::{ @@ -74,10 +78,8 @@ use serde::Serializer; use std::iter; use noise::Seedable; -use petgraph::prelude::Bfs; use super::Position; -use super::sparse_grid::bounding_box_grid::BoundingBoxGrid; use crate::liquid::FluidSystemId; pub const BELT_LEN_PER_TILE: u16 = 4; @@ -106,8 +108,8 @@ const_assert!(CHUNK_SIZE * CHUNK_SIZE - 1 <= u8::MAX as u16); #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct Chunk { - base_pos: (i32, i32), +pub struct Chunk { + // base_pos: (i32, i32), pub floor_tiles: Option>, chunk_tile_to_entity_into: Option>, entities: Vec>, @@ -126,10 +128,6 @@ enum FloorOre { }, } -fn is_default(val: &T) -> bool { - *val == T::default() -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PlayerInfo { @@ -173,11 +171,10 @@ pub struct World { // TODO: I don´t think I want FP pub players: Vec, - chunks: BoundingBoxGrid>, + pub(crate) chunks: DynamicGrid>, belt_lookup: BeltIdLookup, belt_recieving_input_directions: HashMap>, - power_grid_lookup: PowerGridConnectedDevicesLookup, pub ore_lookup: OreLookup, @@ -254,12 +251,6 @@ enum WorldUpdate { NewEntity { pos: Position }, } -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct PowerGridConnectedDevicesLookup { - grid_to_chunks: BTreeMap>, -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct BeltIdLookup { @@ -267,11 +258,9 @@ struct BeltIdLookup { } #[derive(Debug, Clone, Copy)] -pub struct ChunkMissingError; - -#[derive(Debug, Clone, Copy)] -enum AddEntityError { - ChunkMissingError(ChunkMissingError), +pub(crate) enum AddEntityError { + ChunkMissingError {}, + CannotFitError {}, } struct CascadingUpdate { @@ -289,7 +278,7 @@ fn try_attaching_fluids( new_assembler_pos: Position, ) -> CascadingUpdate { CascadingUpdate { - update: Box::new(move |world, sim_state, updates, data_store| { + update: Box::new(move |world, sim_state, _updates, data_store| { profiling::scope!("try_attaching_fluids"); let Some( e @ Entity::Assembler { @@ -398,8 +387,7 @@ fn try_attaching_fluids( Storage::Assembler { grid: id.grid, index: id.assembler_index, - recipe_idx_with_this_item: data_store - .recipe_to_translated_index[&(id.recipe, item)], + recipe_idx_with_this_item: id.recipe.id, }, dest_conn, Box::new(|_weak_index: WeakIndex| {}) @@ -419,7 +407,8 @@ fn try_attaching_fluids( conn_fluid, conn_storage, data_store, - ), + ) + .unwrap(), conn_pos, ) }, @@ -431,7 +420,8 @@ fn try_attaching_fluids( conn_fluid, conn_storage, data_store, - ), + ) + .unwrap(), conn_pos, ) }, @@ -457,19 +447,30 @@ fn try_instantiating_inserters_for_belt_cascade, ) -> CascadingUpdate { CascadingUpdate { - update: Box::new(move |_world, sim_state, updates, data_store| { + update: Box::new(move |_world, sim_state, updates, _data_store| { profiling::scope!("try_instantiating_inserters_for_belt_cascade"); - let mut reachable = Bfs::new( - &*sim_state.factory.belts.belt_graph, - *sim_state.factory.belts.belt_graph_lookup[&belt_id], - ); + let reachable = &mut *sim_state.factory.belts.belt_graph_bfs; + + // Reset the BFS. We reuse the bfs here since otherwise 10% of time is used to just allocated the bitmap for the discovered set. + + reachable + .discovered + .grow(sim_state.factory.belts.belt_graph.node_bound()); + // FIXME: This memset is sloooow, if the factory is large + reachable.discovered.clear(); + reachable + .discovered + .visit(*sim_state.factory.belts.belt_graph_lookup[&belt_id]); + reachable.stack.clear(); + reachable + .stack + .push_front(*sim_state.factory.belts.belt_graph_lookup[&belt_id]); + while let Some(idx) = reachable.next(&*sim_state.factory.belts.belt_graph) { let belt = sim_state.factory.belts.belt_graph.node_weight(idx).unwrap(); // FIXME: What if the graph contains cycles??? updates.push(try_instantiating_inserters_for_belt(*belt)); } - // // In order to avoid problems wit - // updates.push(try_instantiating_all_inserters_cascade()); }), } } @@ -701,7 +702,7 @@ fn instantiate_inserter_cascade( } }, Err(e) => { - info!( + log::trace!( "try_instantiate_inserter failed at {:?}, with {e:?}", new_instantiate_pos ); @@ -812,7 +813,7 @@ fn instantiate_mining_drill_internal_inserter { unreachable!("We are bypassing this check"); }, - Err(InstantiateInserterError::PleaseSpecifyFilter { - belts_which_could_help, - }) => { + Err(InstantiateInserterError::PleaseSpecifyFilter { .. }) => { unreachable!("We are specifying a filter"); }, Err(e) => { @@ -920,7 +919,7 @@ fn new_lab_cascade( 2 * data_store.max_beacon_range.1 as u16 + size.1, ); - for entity in world.get_entities_colliding_with( + for entity in world.get_entities_in_chunks_colliding_with( beacon_search_start_pos, beacon_search_size, data_store, @@ -1034,13 +1033,7 @@ fn new_power_pole( } => { *info = AssemblerInfo::PoweredNoRecipe(pole_pos); }, - Entity::Roboport { - ty, - pos, - power_grid, - network, - id, - } => todo!(), + Entity::Roboport { .. } => todo!(), Entity::SolarPanel { pos, ty, @@ -1052,6 +1045,17 @@ fn new_power_pole( *pole_position = Some((pole_pos, weak_index)); }, + Entity::Accumulator { + pos, + ty, + pole_position: pole_position @ None, + } => { + let weak_index = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_accumulator(*pos, *ty, pole_pos, data_store); + + *pole_position = Some((pole_pos, weak_index)); + }, Entity::Lab { pos, ty, @@ -1233,7 +1237,7 @@ fn newly_working_assembler( 2 * data_store.max_beacon_range.1 as u16 + size.1, ); - for entity in world.get_entities_colliding_with( + for entity in world.get_entities_in_chunks_colliding_with( beacon_search_start_pos, beacon_search_size, data_store, @@ -1324,7 +1328,10 @@ fn removal_of_possible_inserter_connection { - sim_state.factory.belts.remove_inserter(*id, *belt_pos); + match sim_state.factory.belts.remove_inserter(*id, *belt_pos) { + Ok(()) => {}, + Err((_belt_id, _belt_pos)) => todo!(), + } *info = InserterInfo::NotAttached {}; }, @@ -1339,10 +1346,29 @@ fn removal_of_possible_inserter_connection {}, + Err(side) => match side { + crate::inserter::WaitlistSearchSide::Source => { + if start_pos.contained_in(pos, size) { + // No need to remove it. The storage which this inserter was waiting on was removed anywy + } else { + todo!() + } + }, + crate::inserter::WaitlistSearchSide::Dest => { + if end_pos.contained_in(pos, size) { + // No need to remove it. The storage which this inserter was waiting on was removed anywy + } else { + todo!() + } + }, + }, + }; *info = InserterInfo::NotAttached {}; }, @@ -1354,7 +1380,7 @@ fn removal_of_possible_inserter_connection { match internal_inserter { @@ -1376,7 +1402,7 @@ fn removal_of_possible_inserter_connection {}, + Err(side) => match side { + crate::inserter::WaitlistSearchSide::Source => { + // No need to remove it. The mining_drill which this inserter was waiting on was removed anywy + }, + crate::inserter::WaitlistSearchSide::Dest => { + todo!() + }, + }, + }; }, } *internal_inserter = InternalInserterInfo::NotAttached {}; @@ -1405,9 +1441,6 @@ fn removal_of_possible_inserter_connection GetGridIndex for Chunk { fn get_grid_index(&self) -> (i32, i32) { - ( - self.base_pos.0 / i32::from(CHUNK_SIZE), - self.base_pos.1 / i32::from(CHUNK_SIZE), - ) + // ( + // self.base_pos.0.div_floor(i32::from(CHUNK_SIZE)), + // self.base_pos.1.div_floor(i32::from(CHUNK_SIZE)), + // ) + unimplemented!() } } @@ -1490,7 +1524,6 @@ impl World World World World World World World World Self { - let grid = BoundingBoxGrid::new_with_filled_grid( - [ - top_left.x / i32::from(CHUNK_SIZE), - top_left.y / i32::from(CHUNK_SIZE), - ], - [ - bottom_right.x / i32::from(CHUNK_SIZE), - bottom_right.y / i32::from(CHUNK_SIZE), - ], - |[x, y]| Chunk { - base_pos: (x * i32::from(CHUNK_SIZE), y * i32::from(CHUNK_SIZE)), + let grid = DynamicGrid::new_with_filled_grid( + Self::get_chunk_pos_for_tile(top_left).into(), + Self::get_chunk_pos_for_tile(bottom_right).into(), + |[_x, _y]| Chunk { + // base_pos: (x * i32::from(CHUNK_SIZE), y * i32::from(CHUNK_SIZE)), floor_tiles: None, chunk_tile_to_entity_into: None, entities: vec![], @@ -1645,9 +1666,6 @@ impl World World + Clone) { + self.chunks.insert_many( + chunks.clone(), + chunks.into_iter().map(|_pos| Chunk { + // base_pos: (pos.0 * i32::from(CHUNK_SIZE), pos.1 * i32::from(CHUNK_SIZE)), + floor_tiles: None, + chunk_tile_to_entity_into: None, + entities: vec![], + }), + ) + } + pub fn get_ore_type_at_pos(&self, pos: Position) -> Option> { self.ore_lookup .ore_lookup @@ -1684,8 +1714,8 @@ impl World Option<(Item, u32)> { None // let v = noise.get([ @@ -1813,7 +1843,7 @@ impl World World World World unreachable!("Called change recipe on non assembler: {e:?}"), @@ -1973,8 +2003,7 @@ impl World World World, sim_state: &mut SimulationState, data_store: &DataStore, - ) -> Result<(), ()> { + ) -> Result<(), AddEntityError> { if !self.can_fit( entity.get_pos(), entity.get_entity_size(data_store), data_store, ) { warn!("Tried to place entity where it does not fit"); - return Err(()); + return Err(AddEntityError::CannotFitError {}); } profiling::scope!("add_entity {}", entity.get_type_name()); let pos = entity.get_pos(); - let chunk_pos = self.get_chunk_pos_for_tile(pos); + let chunk_pos = Self::get_chunk_pos_for_tile(pos); if self.get_chunk_for_tile_mut(pos).is_none() { error!("Tried to place entity outside generated chunks"); - return Err(()); + return Err(AddEntityError::ChunkMissingError {}); } let mut cascading_updates = self.cascading_updates.take().unwrap_or_default().recycle(); match entity { - Entity::Lab { - pos, pole_position, .. - } => { - if let Some((pole_pos, _, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - + Entity::Lab { pos, .. } => { cascading_updates.push(new_lab_cascade(pos, data_store)); }, - Entity::SolarPanel { pole_position, .. } => { - if let Some((pole_pos, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - }, - Entity::Accumulator { pole_position, .. } => { - if let Some((pole_pos, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - }, + Entity::SolarPanel { .. } => {}, + Entity::Accumulator { .. } => {}, Entity::Assembler { info, .. } => match info { AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) => {}, - AssemblerInfo::PoweredNoRecipe(pole_position) => { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_position]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - }, - AssemblerInfo::Powered { - id: AssemblerID { grid, .. }, - pole_position, - .. - } => { - let lookup_grid = - sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_position]; - assert_eq!(grid, lookup_grid); - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - + AssemblerInfo::PoweredNoRecipe(_) => {}, + AssemblerInfo::Powered { .. } => { cascading_updates.push(newly_working_assembler(pos, data_store)); }, }, Entity::PowerPole { pos: pole_pos, .. } => { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - // Handle Entities that are newly powered cascading_updates.push(new_power_pole(pole_pos, data_store)); }, @@ -2434,8 +2406,7 @@ impl World World World { + let chunk_pos = Self::get_chunk_pos_for_tile(pos); self.belt_lookup .belt_id_to_chunks .entry(id) .or_default() - .insert((pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE))); + .insert(chunk_pos); }, AttachedInserter::BeltBelt { item, inserter } => { todo!("Set the correct belt_ids in the belt_lookup") @@ -2625,7 +2596,7 @@ impl World, - movetime: u16, + movetime: NonZero, hand_size: crate::item::ITEMCOUNTTYPE, known_filter: Option>, @@ -3004,11 +2975,11 @@ impl World InserterInstantiationNewOptions::Belts(vec![start_belt_id, dest_belt_id]), ( - InserterConnection::Belt(start_belt_id, start_belt_pos), + InserterConnection::Belt(start_belt_id, _start_belt_pos), InserterConnection::Storage(dest_storage_untranslated), ) => { let dest_storage_untranslated = match dest_storage_untranslated { @@ -3184,7 +3155,7 @@ impl World, filter: Item, - movetime: u16, + movetime: NonZero, hand_size: crate::item::ITEMCOUNTTYPE, start: InserterConnection, @@ -3214,17 +3185,13 @@ impl World { - let dest_storage_untranslated = match dest_storage_untranslated { + let dest_storage = match dest_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let dest_storage = dest_storage_untranslated - .translate(filter, data_store) - .unwrap(); - match simulation_state.factory.belts.add_belt_storage_inserter( filter, start_belt_id, @@ -3233,7 +3200,8 @@ impl World World { - let start_storage_untranslated = match start_storage_untranslated { + let start_storage = match start_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let start_storage = start_storage_untranslated - .translate(filter, data_store) - .unwrap(); - match simulation_state.factory.belts.add_storage_belt_inserter( filter, dest_belt_id, @@ -3271,7 +3235,8 @@ impl World World { - let start_storage_untranslated = match start_storage_untranslated { + let start_storage = match start_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let dest_storage_untranslated = match dest_storage_untranslated { + let dest_storage = match dest_storage_untranslated { Static::Done(storage) => storage, Static::ToInstantiate => { unreachable!("Storages must be instantiated before calling this function") }, }; - let start_storage = start_storage_untranslated - .translate(filter, data_store) - .unwrap(); - let dest_storage = dest_storage_untranslated - .translate(filter, data_store) - .unwrap(); - let index = simulation_state.factory.storage_storage_inserters.add_ins( filter, - movetime.into(), + movetime, start_storage, dest_storage, hand_size, @@ -3328,110 +3286,6 @@ impl World, - old_id: PowerGridIdentifier, - new_id: PowerGridIdentifier, - ) { - let old_chunks = self.power_grid_lookup.grid_to_chunks.remove(&old_id); - - for chunk_pos in old_chunks.iter().flatten() { - let chunk = self - .chunks - .get_mut(chunk_pos.0, chunk_pos.1) - .expect("Ungenerated chunk in belt map!"); - - for entity in &mut chunk.entities { - match entity { - Entity::SolarPanel { - pole_position: None, - .. - } => {}, - Entity::SolarPanel { - pole_position: Some(_), - .. - } => todo!(), - Entity::Accumulator { - pole_position: None, - .. - } => {}, - Entity::Accumulator { - pole_position: Some(_), - .. - } => todo!(), - Entity::Lab { - pole_position: None, - .. - } => {}, - Entity::Lab { - pole_position: Some(_), - .. - } => todo!(), - Entity::Assembler { info, .. } => match info { - AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) => {}, - AssemblerInfo::PoweredNoRecipe(_) => {}, - AssemblerInfo::Powered { - id: - AssemblerID { - grid: grid_in_id, .. - }, - pole_position, - .. - } => { - let grid = - sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position]; - assert_eq!(grid, new_id); - if *grid_in_id == old_id { - *grid_in_id = new_id; - } - }, - }, - Entity::PowerPole { .. } => {}, - Entity::Inserter { info, .. } => match info { - InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached { - info: attached_inserter, - .. - } => match attached_inserter { - AttachedInserter::BeltStorage { id, belt_pos } => { - // TODO - }, - AttachedInserter::BeltBelt { item, inserter } => { - // TODO - }, - AttachedInserter::StorageStorage { .. } => todo!(), - }, - }, - Entity::Roboport { power_grid, .. } => { - if *power_grid == Some(old_id) { - *power_grid = Some(new_id); - } - }, - Entity::MiningDrill { - ty, - pos, - rotation, - drill_id, - internal_inserter, - } => todo!(), - Entity::Belt { .. } - | Entity::Underground { .. } - | Entity::Splitter { .. } - | Entity::Chest { .. } - | Entity::Beacon { .. } - | Entity::FluidTank { .. } => {}, - } - } - } - - self.power_grid_lookup - .grid_to_chunks - .entry(new_id) - .or_default() - .extend(old_chunks.into_iter().flatten()); - } - pub fn update_belt_id_after( &mut self, sim_state: &mut SimulationState, @@ -3443,9 +3297,11 @@ impl World= {} {:?} to {:?}", - belt_pos_earliest, old_id, new_id + belt_pos_earliest, + old_id, + new_id ); if belt_pos_earliest != 0 { if let Some(waiting) = self.to_instantiate_by_belt.get(&old_id) { @@ -3473,7 +3329,7 @@ impl World true, InserterInfo::Attached { info, .. } => match info { AttachedInserter::BeltStorage { id, .. } => *id != old_id, - AttachedInserter::BeltBelt { item, inserter } => { + AttachedInserter::BeltBelt { .. } => { // TODO: true }, @@ -3496,7 +3352,7 @@ impl World 150 { warn!("Having to check a lot of chunks: {}", old_chunks.len()); } @@ -3557,7 +3413,7 @@ impl World todo!(), + AttachedInserter::BeltBelt { .. } => todo!(), AttachedInserter::StorageStorage { .. } => {}, }, }, @@ -3589,7 +3445,7 @@ impl World, data_store: &DataStore, ) { - info!("Change belt_id {:?} to {:?}", old_id, new_id); + log::debug!("Change belt_id {:?} to {:?}", old_id, new_id); if let Some(waiting) = self.to_instantiate_by_belt.remove(&old_id) { self.to_instantiate_by_belt .entry(new_id) @@ -3605,7 +3461,7 @@ impl World 150 { warn!("Having to check a lot of chunks: {}", num_chunks); } @@ -3685,7 +3541,7 @@ impl World todo!(), + AttachedInserter::BeltBelt { .. } => todo!(), AttachedInserter::StorageStorage { .. } => {}, }, }, @@ -3720,10 +3576,10 @@ impl World { + AttachedInserter::BeltBelt { .. } => { // TODO: }, - AttachedInserter::StorageStorage { item, inserter } => { + AttachedInserter::StorageStorage { .. } => { // TODO: }, } @@ -3734,20 +3590,23 @@ impl World Option<&Chunk> { - self.chunks - .get(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + let (chunk_x, chunk_y) = Self::get_chunk_pos_for_tile(pos); + self.chunks.get(chunk_x, chunk_y) } fn get_chunk_for_tile_mut( &mut self, pos: Position, ) -> Option<&mut Chunk> { - self.chunks - .get_mut(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + let (chunk_x, chunk_y) = Self::get_chunk_pos_for_tile(pos); + self.chunks.get_mut(chunk_x, chunk_y) } - fn get_chunk_pos_for_tile(&self, pos: Position) -> (i32, i32) { - (pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + fn get_chunk_pos_for_tile(pos: Position) -> (i32, i32) { + ( + pos.x.div_floor(i32::from(CHUNK_SIZE)), + pos.y.div_floor(i32::from(CHUNK_SIZE)), + ) } fn get_chunk_mut( @@ -3763,7 +3622,8 @@ impl World, ) -> Option { - self.get_entities_colliding_with( + // We can use *_in_chunks_* here, since we do a overlaps test later anyway. This way we do not have to check twice + self.get_entities_in_chunks_colliding_with( Position { x: entity_pos.x - i32::from(data_store.max_power_search_range), y: entity_pos.y - i32::from(data_store.max_power_search_range), @@ -3839,11 +3699,11 @@ impl World World World, ) -> bool { let bb_top_left = (pos.x, pos.y); let bb_bottom_right = (pos.x + i32::from(size.0), pos.y + i32::from(size.1)); - let chunk_range_x = - (bb_top_left.0 / i32::from(CHUNK_SIZE))..=(bb_bottom_right.0 / i32::from(CHUNK_SIZE)); - let chunk_range_y = - (bb_top_left.1 / i32::from(CHUNK_SIZE))..=(bb_bottom_right.1 / i32::from(CHUNK_SIZE)); + let chunk_range_x = (bb_top_left.0.div_floor(i32::from(CHUNK_SIZE))) + ..=(bb_bottom_right.0.div_floor(i32::from(CHUNK_SIZE))); + let chunk_range_y = (bb_top_left.1.div_floor(i32::from(CHUNK_SIZE))) + ..=(bb_bottom_right.1.div_floor(i32::from(CHUNK_SIZE))); assert!(chunk_range_x.clone().count() >= 1); assert!(chunk_range_y.clone().count() >= 1); @@ -4114,11 +3975,11 @@ impl World World, ) -> impl IntoIterator, IntoIter: Clone> + + use<'a, 'b, ItemIdxType, RecipeIdxType> { + self.get_entities_in_chunks_colliding_with(pos, size, data_store) + .into_iter() + .filter(move |e| { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + + pos.overlap(size, e_pos, (e_size.0.into(), e_size.1.into())) + }) + } + + pub fn get_entities_in_chunks_colliding_with<'a, 'b>( + &'a self, + pos: Position, + size: (u16, u16), + data_store: &'b DataStore, + ) -> impl IntoIterator, IntoIter: Clone> + use<'a, 'b, ItemIdxType, RecipeIdxType> { let max_size = data_store.max_entity_size; @@ -4160,24 +4038,25 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); + // dbg!( + // chunk_range_x + // .clone() + // .cartesian_product(chunk_range_y.clone()) + // .count() + // ); + chunk_range_x .cartesian_product(chunk_range_y) .filter_map(|(chunk_x, chunk_y)| self.chunks.get(chunk_x, chunk_y)) .flat_map(move |chunk| chunk.entities.iter()) - .filter(move |e| { - let e_pos = e.get_pos(); - let e_size = e.get_entity_size(data_store); - - pos.overlap(size, e_pos, (e_size.0.into(), e_size.1.into())) - }) } pub fn get_entity_at_mut( @@ -4249,7 +4128,7 @@ impl World { @@ -4338,7 +4217,7 @@ impl World { if pos.contained_in(e.get_pos(), e.get_entity_size(data_store)) { @@ -4433,10 +4312,10 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); @@ -4834,7 +4713,49 @@ impl World todo!(), + Entity::Splitter { + pos, direction, id, .. + } => { + let removed_items = sim_state.factory.belts.remove_splitter(*id); + // TODO: Handle these removed items + + // Handle the shortening of the belt due to the removal of the splitter len + // Front + for side in SplitterSide::iter() { + let side_offs = match side { + SplitterSide::Left => (0, 0), + SplitterSide::Right => direction.turn_right().into_offset(), + }; + let pos = *pos + *direction; + + let pos = Position { + x: pos.x + i32::from(side_offs.0), + y: pos.y + i32::from(side_offs.1), + }; + + // FIXME: SIDELOADING MUST BE HANDLED, AND TURNING + if let Some(Entity::Belt { id, .. }) = self.get_entity_at(pos, data_store) { + } + } + // BACK + for side in SplitterSide::iter() { + let side_offs = match side { + SplitterSide::Left => (0, 0), + SplitterSide::Right => direction.turn_right().into_offset(), + }; + let pos = *pos + (direction.reverse()); + + let pos = Position { + x: pos.x + i32::from(side_offs.0), + y: pos.y + i32::from(side_offs.1), + }; + + // FIXME: SIDELOADING MUST BE HANDLED, AND TURNING + if let Some(Entity::Belt { id, .. }) = self.get_entity_at(pos, data_store) { + self.modify_belt_pos(*id, true, SPLITTER_BELT_LEN); + } + } + }, Entity::Chest { ty, pos, item, .. } => { if let Some((item, index)) = item { @@ -4849,13 +4770,7 @@ impl World todo!(), + Entity::Roboport { .. } => todo!(), Entity::Inserter { info: InserterInfo::NotAttached { .. }, @@ -4863,6 +4778,8 @@ impl World {}, Entity::Inserter { ty, + pos, + direction, user_movetime, info: InserterInfo::Attached { @@ -4871,30 +4788,169 @@ impl World match attached_inserter { - AttachedInserter::BeltStorage { id, belt_pos } => { - sim_state.factory.belts.remove_inserter(*id, *belt_pos); + AttachedInserter::BeltStorage { id, belt_pos } => 'remove: { + match sim_state.factory.belts.remove_inserter(*id, *belt_pos) { + Ok(()) => { + log::trace!( + "Successfully removed belt storage inserter from sim_state.factory.belts" + ); + }, + Err((belt_id, belt_pos)) => { + let item = sim_state + .factory + .belts + .get_pure_item(*id) + .expect("Sushi belts do not currently use waitlists"); + let start_pos = + data_store.inserter_start_pos(*ty, *pos, *direction); + + let (belt_to_storage, storage_to_belt) = &mut sim_state + .factory + .belt_storage_inserters[item.into_usize()]; + + let start_at_belt = match self.get_entity_at(start_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Belt { .. } => {true}, + Entity::Underground { ..} => {true}, + Entity::Splitter { .. } => {true}, + _ => false + }; + + if start_at_belt { + if belt_to_storage.0.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + if belt_to_storage.1.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + } else { + if storage_to_belt.0.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + if storage_to_belt.1.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + } + + let removed_stuff = match self.get_entity_at(start_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, data_store); + true + }, + Entity::Belt { .. } => {false}, + Entity::Underground { ..} => {false}, + Entity::Splitter { .. } => {false}, + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, item); + sim_state.factory.chests.stores[item.into_usize()] + .remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }); + true + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Labs currently do not have waitlists"), + Entity::MiningDrill { .. } => unreachable!("Drills currently do not have waitlists"), + + _ => unreachable!() + }; + + if !removed_stuff { + let end_pos = + data_store.inserter_end_pos(*ty, *pos, *direction); + match self.get_entity_at(end_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, data_store); + }, + Entity::Belt { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Underground { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Splitter { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, item); + sim_state.factory.chests.stores[item.into_usize()] + .remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }); + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Labs currently do not have waitlists"), + Entity::MiningDrill { .. } => unreachable!("Drills currently do not have waitlists"), + + _ => unreachable!() + } + } + }, + } }, AttachedInserter::BeltBelt { inserter, .. } => { sim_state.factory.belts.remove_belt_belt_inserter(*inserter); }, AttachedInserter::StorageStorage { item, inserter } => { - sim_state.factory.storage_storage_inserters.remove_ins( + match sim_state.factory.storage_storage_inserters.remove_ins( *item, user_movetime .map(|v| v.into()) .unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks) .into(), *inserter, - ); + ) { + Ok(()) => { + log::trace!( + "Successfully removed storage storage inserter from sim_state.factory.storage_storage_inserters" + ); + }, + Err(side) => { + log::trace!( + "Failed removing storage storage inserter from sim_state.factory.storage_storage_inserters, need to look at {:?}", + side + ); + + let search_pos = match side { + crate::inserter::WaitlistSearchSide::Source => { + data_store.inserter_start_pos(*ty, *pos, *direction) + }, + crate::inserter::WaitlistSearchSide::Dest => { + data_store.inserter_end_pos(*ty, *pos, *direction) + }, + }; + + match self.get_entity_at(search_pos, data_store).expect("The inserter is supposed to be in the waitlist of this entity so it should exist") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, *item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); + }, + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, *item); + sim_state.factory.chests.stores[item.into_usize()].remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }); + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Currently Labs do not have a waitlist implementation"), + Entity::MiningDrill { .. } => unreachable!("Currently Drills do not have a waitlist implementation"), + + e => unreachable!("A storage_storage inserter cannot attach to a {e:?}") + } + + log::trace!( + "Successfully removed storage storage inserter from waitlist" + ); + }, + } }, }, - Entity::MiningDrill { - ty, - pos, - rotation, - drill_id, - internal_inserter, - } => todo!(), + Entity::MiningDrill { .. } => todo!(), } let chunk = self.get_chunk_for_tile_mut(e_pos).unwrap(); @@ -4963,24 +5019,32 @@ impl World chunk.can_fit(pos, size, data_store), - // None => false, - // }, - // ) + let chunk_range_x = (pos.x.div_floor(i32::from(CHUNK_SIZE))) + ..=((pos.x + i32::from(size.0) - 1).div_floor(i32::from(CHUNK_SIZE))); + let chunk_range_y = (pos.y.div_floor(i32::from(CHUNK_SIZE))) + ..=((pos.y + i32::from(size.1) - 1).div_floor(i32::from(CHUNK_SIZE))); + + chunk_range_x + .cartesian_product(chunk_range_y) + .all( + |(chunk_x, chunk_y)| match self.get_chunk(chunk_x, chunk_y) { + Some(chunk) => chunk.can_fit( + pos, + size, + Position { + x: chunk_x * i32::from(CHUNK_SIZE), + y: chunk_y * i32::from(CHUNK_SIZE), + }, + data_store, + ), + None => false, + }, + ) // !self.any_entity_colliding_with(pos, size, data_store) - self.get_entities_colliding_with(pos, size, data_store) - .into_iter() - .next() - .is_none() + // self.get_entities_colliding_with(pos, size, data_store) + // .into_iter() + // .next() + // .is_none() } pub fn get_power_poles_which_could_connect_to_pole_at<'a, 'b>( @@ -4992,19 +5056,37 @@ impl World impl IntoIterator, IntoIter: Clone> + Clone + use<'a, 'b, ItemIdxType, RecipeIdxType> { - self.get_entities_colliding_with( + self.get_entities_in_chunks_colliding_with( Position { x: pole_pos.x - i32::from(connection_range), y: pole_pos.y - i32::from(connection_range), }, ( - 2 * connection_range as u16 + pole_size.0 as u16, - 2 * connection_range as u16 + pole_size.1 as u16, + 2 * connection_range as u16 + pole_size.0, + 2 * connection_range as u16 + pole_size.1, ), data_store, ) .into_iter() - .filter(|e| matches!(e, Entity::PowerPole { .. })) + .filter(move |e| match e { + Entity::PowerPole { ty, pos } => { + let other_range = data_store.power_pole_data[*ty as usize].connection_range; + let other_size = data_store.power_pole_data[*ty as usize].size; + + pole_pos.overlap( + pole_size, + Position { + x: pos.x - other_range as i32, + y: pos.y - other_range as i32, + }, + ( + 2 * other_range as u16 + other_size.0, + 2 * other_range as u16 + other_size.1, + ), + ) + }, + _ => false, + }) } fn get_power_pole_range( @@ -5047,27 +5129,55 @@ impl Chunk, + _data_store: &DataStore, ) -> Option<&Entity> { - self.entities.iter().find(|e| { - let e_pos = e.get_pos(); - let e_size = e.get_entity_size(data_store); + let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); + let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); - pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - }) + if let Some(arr) = &self.chunk_tile_to_entity_into { + match arr_val_to_state(self.entities.len(), arr[x][y]) { + ChunkTileState::Index(idx) => return Some(&self.entities[idx]), + ChunkTileState::Empty => return None, + ChunkTileState::OtherChunk => return None, + ChunkTileState::EmptyOrOtherChunk => return None, + } + } else { + None + } + + // self.entities.iter().find(|e| { + // let e_pos = e.get_pos(); + // let e_size = e.get_entity_size(data_store); + + // pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) + // }) } pub fn get_entity_at_mut( &mut self, pos: Position, - data_store: &DataStore, + _data_store: &DataStore, ) -> Option<&mut Entity> { - self.entities.iter_mut().find(|e| { - let e_pos = e.get_pos(); - let e_size = e.get_entity_size(data_store); + let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); + let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); - pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - }) + if let Some(arr) = &self.chunk_tile_to_entity_into { + match arr_val_to_state(self.entities.len(), arr[x][y]) { + ChunkTileState::Index(idx) => return Some(&mut self.entities[idx]), + ChunkTileState::Empty => return None, + ChunkTileState::OtherChunk => return None, + ChunkTileState::EmptyOrOtherChunk => return None, + } + } else { + None + } + + // self.entities.iter_mut().find(|e| { + // let e_pos = e.get_pos(); + // let e_size = e.get_entity_size(data_store); + + // pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) + // }) } #[must_use] @@ -5075,23 +5185,24 @@ impl Chunk, + chunk_top_left: Position, + _data_store: &DataStore, ) -> bool { if let Some(arr) = &self.chunk_tile_to_entity_into { - let x_in_chunk = pos.x.rem_euclid(i32::from(CHUNK_SIZE)) as usize; - let y_in_chunk = pos.y.rem_euclid(i32::from(CHUNK_SIZE)) as usize; - for x in x_in_chunk - ..min( - x_in_chunk + usize::from(size.0), - usize::from(CHUNK_SIZE) - 1, - ) - { - for y in y_in_chunk - ..min( - y_in_chunk + usize::from(size.1), - usize::from(CHUNK_SIZE) - 1, - ) - { + let x_start = max(pos.x - chunk_top_left.x, 0) as usize; + let y_start = max(pos.y - chunk_top_left.y, 0) as usize; + + let x_end = min( + (pos.x + size.0 as i32) - chunk_top_left.x, + CHUNK_SIZE as i32, + ) as usize; + let y_end = min( + (pos.y + size.1 as i32) - chunk_top_left.y, + CHUNK_SIZE as i32, + ) as usize; + + for x in x_start..x_end { + for y in y_start..y_end { match arr_val_to_state(self.entities.len(), arr[x][y]) { ChunkTileState::Index(_) => return false, ChunkTileState::Empty => {}, @@ -5191,18 +5302,11 @@ pub enum UndergroundDir { Exit, } -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -struct PipeConnection { - pipe_pos: Position, - connection_weak_index: WeakIndex, -} - pub type ModuleTy = u8; #[derive(Debug, Clone, PartialEq)] pub struct ModuleSlots(pub thin_dst::ThinBox<(), Option>); -pub type ModuleSlotDedupIndex = u32; +pub type ModuleSlotDedupIndex = u16; impl serde::Serialize for ModuleSlots { fn serialize(&self, serializer: S) -> Result @@ -5235,10 +5339,10 @@ impl GetSize for ModuleSlots { impl, Info: EguiDisplayable> ShowInfo for ModuleSlots { fn show_fields>( &self, - extractor: &mut E, - ui: &mut egui::Ui, - path: String, - cache: &mut C, + _extractor: &mut E, + _ui: &mut egui::Ui, + _path: String, + _cache: &mut C, ) { } } @@ -5449,7 +5553,7 @@ impl Entity) -> Color32 { + pub fn get_map_color(&self, _data_store: &DataStore) -> Color32 { match self { Self::Assembler { .. } => hex_color!("#0086c9"), Self::PowerPole { .. } => hex_color!("#eeee29"), @@ -5589,12 +5693,6 @@ pub enum PlaceEntityType { }, } -impl PlaceEntityType { - pub fn order_for_optimization(&self, other: &Self) -> std::cmp::Ordering { - todo!() - } -} - impl PlaceEntityType { pub fn cares_about_power(&self) -> bool { match self { @@ -5727,55 +5825,46 @@ impl Add for Position { #[cfg(test)] mod test { - use proptest::{prop_assert, prop_assert_eq, proptest}; - - // use crate::{ - // DATA_STORE, - // app_state::GameState, - // blueprint::{ - // Blueprint, - // test::{random_entity_to_place, random_position}, - // }, - // frontend::{ - // action::{ActionType, place_entity::PlaceEntityInfo}, - // world::Position, - // }, - // replays::Replay, - // }; + use proptest::{prop_assert, proptest}; - proptest! { - - // #[test] - // fn test_get_entity(position in random_position(), ent in random_entity_to_place(&DATA_STORE)) { - // let mut state = GameState::new(&DATA_STORE); - - // let mut rep = Replay::new(&state, None, &*DATA_STORE); + use crate::{ + DATA_STORE, + app_state::GameState, + blueprint::test::{random_entity_to_place, random_position}, + frontend::{ + action::{ActionType, place_entity::PlaceEntityInfo}, + world::Position, + }, + replays::GenerationInformation, + }; - // rep.append_actions([ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ent) })]); + proptest! { - // let bp = Blueprint::from_replay(&rep); + #[test] + fn test_get_entity(position in random_position(), ent in random_entity_to_place(&DATA_STORE)) { + let state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - // bp.apply(false, position, &mut state, &DATA_STORE); + GameState::apply_actions(&mut *state.simulation_state.lock(), &mut *state.world.lock(), [ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ent) })], &DATA_STORE); - // let mut e_pos = None; - // let mut e_size = None; - // state.world.get_entities_colliding_with(position, (100, 100), &DATA_STORE).into_iter().for_each(|v| { - // e_pos = Some(v.get_pos()); - // e_size = Some(v.get_entity_size(&DATA_STORE)); - // }); + let mut e_pos = None; + let mut e_size = None; + state.world.lock().get_entities_colliding_with(position, (100, 100), &DATA_STORE).into_iter().for_each(|v| { + e_pos = Some(v.get_pos()); + e_size = Some(v.get_entity_size(&DATA_STORE)); + }); - // prop_assert!(e_pos.is_some()); - // prop_assert!(e_size.is_some()); + prop_assert!(e_pos.is_some()); + prop_assert!(e_size.is_some()); - // let e_pos = e_pos.unwrap(); - // let e_size = e_size.unwrap(); + let e_pos = e_pos.unwrap(); + let e_size = e_size.unwrap(); - // for x_pos in e_pos.x..(e_pos.x + (e_size.0 as i32)) { - // for y_pos in e_pos.y..(e_pos.y + (e_size.1 as i32)) { - // prop_assert_eq!(state.world.get_entities_colliding_with(Position { x: x_pos, y: y_pos }, (1, 1), &DATA_STORE).into_iter().count(), 1, "test_pos = {:?}, world + {:?}", Position {x: x_pos, y: y_pos}, state.world.get_chunk_for_tile(position)); - // } - // } - // } + for x_pos in e_pos.x..(e_pos.x + (e_size.0 as i32)) { + for y_pos in e_pos.y..(e_pos.y + (e_size.1 as i32)) { + prop_assert!(state.world.lock().get_entity_at(Position { x: x_pos, y: y_pos }, &DATA_STORE).is_some(), "test_pos = {:?}, world + {:?}", Position {x: x_pos, y: y_pos}, state.world.lock().get_chunk_for_tile(position)); + } + } + } } } diff --git a/src/get_size.rs b/src/get_size.rs index 14e21ac..6da7134 100644 --- a/src/get_size.rs +++ b/src/get_size.rs @@ -331,6 +331,13 @@ impl Mutex { } } +// NOTE (Tim): This impl is not ideal since if it is derived, it might not respect the locking order of things +impl Clone for Mutex { + fn clone(&self) -> Self { + Self::new(self.lock().clone()) + } +} + impl Deref for Mutex { type Target = parking_lot::Mutex; fn deref(&self) -> &Self::Target { @@ -470,6 +477,61 @@ impl DerefMut for BitBox { } } +#[derive(Clone)] +#[repr(transparent)] +pub struct Bfs { + pub(crate) bfs: petgraph::visit::Bfs, +} + +impl Default for Bfs { + fn default() -> Self { + Self { + bfs: petgraph::visit::Bfs::default(), + } + } +} + +impl std::fmt::Debug for Bfs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Bfs").field("bfs", &"TODO").finish() + } +} + +#[cfg(feature = "client")] +impl GetSize for Bfs { + fn get_heap_size(&self) -> usize { + 0 + } +} + +#[cfg(feature = "client")] +impl> ShowInfo + for Bfs +{ + fn show_fields>( + &self, + _extractor: &mut Extractor, + _ui: &mut egui::Ui, + _path: String, + _cache: &mut C, + ) { + } +} + +impl Deref for Bfs { + type Target = petgraph::visit::Bfs; + + fn deref(&self) -> &Self::Target { + &self.bfs + } +} + +impl DerefMut for Bfs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.bfs + } +} + impl From for BitBox { fn from(value: bitvec::prelude::BitBox) -> Self { Self { bitbox: value } diff --git a/src/inserter/belt_storage_inserter.rs b/src/inserter/belt_storage_inserter.rs index c41cc76..e6a5458 100644 --- a/src/inserter/belt_storage_inserter.rs +++ b/src/inserter/belt_storage_inserter.rs @@ -82,7 +82,8 @@ impl BeltStorageInserter<{ Dir::BeltToStorage }> { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (max_insert, old, _) = + index_fake_union(None, storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -134,7 +135,8 @@ impl BeltStorageInserter<{ Dir::StorageToBelt }> { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (_max_insert, old, _) = + index_fake_union(None, storages, self.storage_id, grid_size); let to_extract = min(max_hand_size - count, *old); diff --git a/src/inserter/belt_storage_inserter_non_const_gen.rs b/src/inserter/belt_storage_inserter_non_const_gen.rs index be40213..a744789 100644 --- a/src/inserter/belt_storage_inserter_non_const_gen.rs +++ b/src/inserter/belt_storage_inserter_non_const_gen.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + belt::belt::BeltLenType, item::ITEMCOUNTTYPE, storage_list::{SingleItemStorages, index_fake_union}, }; @@ -65,16 +66,16 @@ impl From for (Dir, InserterState) { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BeltStorageInserterDyn { - pub offset: u16, + pub belt_pos: u16, pub storage_id: FakeUnionStorage, pub state: DynInserterState, } impl BeltStorageInserterDyn { #[must_use] - pub const fn new(dir: Dir, offset: u16, id: FakeUnionStorage) -> Self { + pub const fn new(dir: Dir, belt_pos: BeltLenType, id: FakeUnionStorage) -> Self { Self { - offset, + belt_pos, storage_id: id, state: match dir { Dir::BeltToStorage => DynInserterState::BSWaitingForSourceItems(0), @@ -88,6 +89,7 @@ impl BeltStorageInserterDyn { #[inline(always)] pub fn update( &mut self, + item_id: usize, mut loc: impl DerefMut + Deref, storages: SingleItemStorages, movetime: u8, @@ -109,7 +111,8 @@ impl BeltStorageInserterDyn { } }, DynInserterState::BSWaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (max_insert, old, _) = + index_fake_union(Some(item_id), storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -144,7 +147,8 @@ impl BeltStorageInserterDyn { false }, DynInserterState::SBWaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let (_max_insert, old, _) = + index_fake_union(Some(item_id), storages, self.storage_id, grid_size); let to_extract = min(max_hand_size - count, *old); diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs new file mode 100644 index 0000000..8e72e73 --- /dev/null +++ b/src/inserter/belt_storage_movement_list.rs @@ -0,0 +1,308 @@ +use std::{cmp::min, num::NonZero}; + +use crate::{ + assembler::simd::{InserterWithBelts, InserterWithBeltsEnum}, + belt::{ + belt::{Belt, BeltLenType}, + smart::{InserterExtractedWhenMoving, SmartBelt}, + }, + inserter::{FakeUnionStorage, belt_storage_inserter::Dir}, + item::{ITEMCOUNTTYPE, IdxTrait}, + storage_list::{SingleItemStorages, index_fake_union}, + temp_vec::VecHolder, +}; + +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +pub struct BeltStorageInserterInMovement { + pub(crate) movetime: NonZero, + pub(crate) storage: FakeUnionStorage, + pub(crate) belt: u32, + pub(crate) belt_pos: BeltLenType, + + pub(crate) current_hand: ITEMCOUNTTYPE, + pub(crate) max_hand_size: ITEMCOUNTTYPE, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct List { + zero_index: usize, + lists: Box<[Vec]>, +} + +impl Default for List { + fn default() -> Self { + Self { + zero_index: 0, + lists: vec![vec![]; u8::MAX as usize + 2].into_boxed_slice(), + } + } +} + +pub struct FinishedMovingLists<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> { + list: &'a mut Vec, +} + +pub struct ReinsertionLists<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> { + first: &'a mut [Vec], + second: &'a mut [Vec], +} + +impl<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> + ReinsertionLists<'a, SWING_DIR, ITEM_FLOW_DIR> +{ + pub fn reinsert(&mut self, movetime: NonZero, ins: BeltStorageInserterInMovement) { + // TODO: Maybe it is worth it to rotate the lists to avoid this branch? + if (u16::from(movetime) as usize) < self.first.len() { + self.first + .get_mut(u16::from(movetime) as usize) + .expect(&format!("movetime: {:?}", movetime)) + .push(ins); + } else { + self.second + .get_mut((u16::from(movetime) as usize) - self.first.len()) + .expect(&format!("movetime: {:?}", movetime)) + .push(ins); + } + } +} + +impl List { + pub fn tick(&mut self) { + self.zero_index += 1; + self.zero_index %= self.lists.len(); + } + + pub fn get( + &mut self, + ) -> ( + FinishedMovingLists<'_, SWING_DIR, ITEM_FLOW_DIR>, + ReinsertionLists<'_, SWING_DIR, ITEM_FLOW_DIR>, + ) { + let (second, rest) = self.lists.split_at_mut(self.zero_index); + + let ([finished], first) = rest.split_at_mut(1) else { + unreachable!() + }; + + ( + FinishedMovingLists { list: finished }, + ReinsertionLists { first, second }, + ) + } + + pub fn reinsert(&mut self, movetime: NonZero, ins: BeltStorageInserterInMovement) { + let index = (usize::from(u16::from(movetime)) + self.zero_index) % self.lists.len(); + self.lists[index].push(ins); + } + + pub fn remove_inserter(&mut self, belt_id: u32, belt_pos: BeltLenType) -> Result<(), ()> { + let index = self + .lists + .iter() + .enumerate() + .find_map(|(list_index, list)| { + list.iter() + .position(|moving_inserter| { + moving_inserter.belt == belt_id && moving_inserter.belt_pos == belt_pos + }) + .map(|v| (list_index, v)) + }); + + if let Some((list_index, index)) = index { + // TODO: Swap remove might do weird things to the priority but it should be fine + let v = self.lists[list_index].swap_remove(index); + Ok(()) + } else { + Err(()) + } + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> { + pub fn update( + self, + item_id: usize, + grid_size: usize, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::BeltToStorage }>, + storages: SingleItemStorages, + ) { + self.list.retain_mut(|inserter| { + let (max_insert, data, wait_list) = + index_fake_union(Some(item_id), storages, inserter.storage, grid_size); + + let items_moved = min(inserter.current_hand, *max_insert - *data); + + inserter.current_hand -= items_moved; + *data += items_moved; + + if inserter.current_hand == 0 { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { + if pos == 0 { + *wait_list_needed = inserter.current_hand; + } + *empty = Some(InserterWithBelts { + current_hand: inserter.current_hand, + max_hand: inserter.max_hand_size.into(), + rest: InserterWithBeltsEnum::BeltStorage { + self_is_source: false, + belt_id: inserter.belt, + belt_pos: inserter.belt_pos, + }, + movetime: inserter.movetime.into(), + }); + false + } else { + true + } + } else { + true + } + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> { + pub fn update( + self, + item_id: usize, + grid_size: usize, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::StorageToBelt }>, + storages: SingleItemStorages, + ) { + self.list.retain_mut(|inserter| { + let (_max_insert, data, wait_list) = + index_fake_union(Some(item_id), storages, inserter.storage, grid_size); + + let items_moved = min(inserter.max_hand_size - inserter.current_hand, *data); + + inserter.current_hand += items_moved; + *data -= items_moved; + + if inserter.current_hand == inserter.max_hand_size { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { + if pos == 0 { + *wait_list_needed = inserter.max_hand_size - inserter.current_hand; + } + *empty = Some(InserterWithBelts { + current_hand: inserter.current_hand, + max_hand: inserter.max_hand_size.into(), + rest: InserterWithBeltsEnum::BeltStorage { + self_is_source: true, + belt_id: inserter.belt, + belt_pos: inserter.belt_pos, + }, + movetime: inserter.movetime.into(), + }); + false + } else { + true + } + } else { + true + } + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::StorageToBelt }> { + pub fn update( + self, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::BeltToStorage }, { Dir::StorageToBelt }>, + belts: &mut [SmartBelt], + ) { + self.list.retain(|inserter| { + let belt = &mut belts[inserter.belt as usize]; + + let mut current_hand = inserter.max_hand_size; + + if belt.try_insert_correct_item(inserter.belt_pos).is_ok() { + current_hand -= 1; + } + + if current_hand == 0 { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + belt.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: false, + max_hand_size: inserter.max_hand_size, + current_hand, + }); + }); + // This is an incoming inserter + if let Some(latest) = &mut belt.latest_inserter_pos_if_all_incoming { + *latest = std::cmp::max(*latest, NonZero::new(inserter.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")) + } + false + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::BeltToStorage }> { + pub fn update( + self, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::BeltToStorage }, { Dir::BeltToStorage }>, + belts: &mut [SmartBelt], + ) { + self.list.retain(|inserter| { + let belt = &mut belts[inserter.belt as usize]; + + let mut current_hand = 0; + + if belt.remove_item(inserter.belt_pos).is_some() { + current_hand += 1; + } + + if current_hand == inserter.max_hand_size { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + belt.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: true, + max_hand_size: inserter.max_hand_size, + current_hand, + }); + }); + // This is an outgoing inserter + belt.latest_inserter_pos_if_all_incoming = None; + false + } + }); + } +} diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index a70a156..f839ed3 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -246,6 +246,7 @@ impl BucketedStorageStorageInserterStoreFrontend { }); } + #[allow(non_snake_case)] for (i, _) in sizes.into_iter().enumerate().sorted_by_key(|v| v.0) { let MOVING_OUT_END: usize = store.list_len(); let WATING_FOR_SPACE: usize = MOVING_OUT_END; @@ -613,6 +614,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_item_ins( + item_id: usize, inserter: &mut UpdatingInserter, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, @@ -633,7 +635,8 @@ impl BucketedStorageStorageInserterStore { } }, Dir::StorageToBelt => { - let (_max_insert, old) = index_fake_union(storages, inserter.storage_id, grid_size); + let (_max_insert, old, _) = + index_fake_union(Some(item_id), storages, inserter.storage_id, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -663,6 +666,7 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_space_ins( + item_id: usize, inserter: &mut UpdatingInserter, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, @@ -673,7 +677,8 @@ impl BucketedStorageStorageInserterStore { ) -> bool { match DIR { Dir::BeltToStorage => { - let (max_insert, old) = index_fake_union(storages, inserter.storage_id, grid_size); + let (max_insert, old, _) = + index_fake_union(Some(item_id), storages, inserter.storage_id, grid_size); let to_insert = min(inserter.current_hand, *max_insert - *old); @@ -751,6 +756,7 @@ impl BucketedStorageStorageInserterStore { #[profiling::function] pub fn update( &mut self, + item_id: usize, frontend: &mut BucketedStorageStorageInserterStoreFrontend, storages: SingleItemStorages, belts: &mut [SmartBelt], @@ -808,6 +814,7 @@ impl BucketedStorageStorageInserterStore { Dir::BeltToStorage => { let now_moving = self.waiting_for_item.extract_if(.., |inserter| { Self::handle_waiting_for_item_ins::( + item_id, inserter, frontend, storages, @@ -835,6 +842,7 @@ impl BucketedStorageStorageInserterStore { Dir::StorageToBelt => { let now_moving = self.waiting_for_item.extract_if(.., |inserter| { Self::handle_waiting_for_item_ins::( + item_id, inserter, frontend, storages, @@ -912,6 +920,7 @@ impl BucketedStorageStorageInserterStore { ItemIdxType, { Dir::BeltToStorage }, >( + item_id, inserter, frontend, storages, @@ -944,6 +953,7 @@ impl BucketedStorageStorageInserterStore { ItemIdxType, { Dir::StorageToBelt }, >( + item_id, inserter, frontend, storages, @@ -1269,6 +1279,7 @@ mod test { use crate::{ belt::smart::SmartBelt, inserter::{FakeUnionStorage, belt_storage_inserter::Dir}, + storage_list::{InserterWaitLists, MaxInsertionLimit}, }; use super::*; @@ -1307,25 +1318,43 @@ mod test { let mut belt_ids = (0..(NUM_INSERTERS as u32)) .map(|v| v % NUM_BELTS as u32) .collect_vec(); - values.shuffle(&mut rand::thread_rng()); - belt_ids.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); + belt_ids.shuffle(&mut rand::rng()); for (storage, belt) in values.into_iter().zip(belt_ids) { if random::() < 1 { store[item].0.update( + item, &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + InserterWaitLists::None, + ), ], &mut belts[item], 10, current_tick, ); store[item].1.update( + item, &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + InserterWaitLists::None, + ), ], &mut belts[item], 10, @@ -1394,20 +1423,38 @@ mod test { } } store.0.update( + 0, frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + InserterWaitLists::None, + ), ], belts, 10, current_tick, ); store.1.update( + 0, frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + InserterWaitLists::None, + ), ], belts, 10, diff --git a/src/inserter/mod.rs b/src/inserter/mod.rs index 4da4953..88ff284 100644 --- a/src/inserter/mod.rs +++ b/src/inserter/mod.rs @@ -1,6 +1,6 @@ use std::{marker::PhantomData, u16}; -use crate::item::{ITEMCOUNTTYPE, Indexable}; +use crate::item::ITEMCOUNTTYPE; use crate::{ data::DataStore, item::{IdxTrait, Item, Recipe, WeakIdxTrait}, @@ -29,6 +29,7 @@ pub mod belt_storage_pure_buckets; pub mod storage_storage_inserter; pub mod storage_storage_with_buckets; // pub mod storage_storage_with_buckets_compressed; +pub mod belt_storage_movement_list; pub mod storage_storage_with_buckets_indirect; // mod bucket_bit_compressed; @@ -55,6 +56,19 @@ enum SushiInserterState { EmptyAndMovingBack(u8), } +#[derive(Debug)] +pub struct WaitlistSearchInfo { + side_to_search: WaitlistSearchSide, + source_id: FakeUnionStorage, + dest_id: FakeUnionStorage, +} + +#[derive(Debug)] +pub enum WaitlistSearchSide { + Source, + Dest, +} + // TODO: We still need to store the timer and hand fullness. For the timer, 5 bits should suffice // I don't think we need to store the recipe_id in 8 bits, since 32 recipes should be max for any resource (so 5 bits) // Current Plan: htttttrrrrrsssssssssssssssssssss @@ -179,11 +193,14 @@ impl FakeUnionStorage { } } - pub fn from_storage_with_statics_at_zero( + pub fn from_storage_with_statics_at_zero< + ItemIdxType: WeakIdxTrait, + RecipeIdxType: WeakIdxTrait, + >( item: Item, storage: Storage, data_store: &DataStore, - ) -> Self { + ) -> Result { let grid_size: usize = grid_size(item, data_store); let static_size: usize = static_size(item, data_store); @@ -195,11 +212,16 @@ impl FakeUnionStorage { recipe_idx_with_this_item, index, } => { - assert!( - recipe_idx_with_this_item.into_usize() - < data_store.num_recipes_with_item[item.into_usize()] - ); - Self { + let recipe_idx_with_this_item = *data_store + .recipe_to_translated_index + .get(&( + Recipe { + id: recipe_idx_with_this_item, + }, + item, + )) + .ok_or(())?; + Ok(Self { index: u32::from(index), grid_or_static_flag: u16::from(grid) .checked_add(u16::try_from(grid_offset).unwrap()) @@ -208,21 +230,21 @@ impl FakeUnionStorage { recipe_idx_with_this_item, )) .unwrap(), - } + }) }, - Storage::Lab { grid, index } => Self { + Storage::Lab { grid, index } => Ok(Self { index: u32::from(index), grid_or_static_flag: u16::from(grid) .checked_add(u16::try_from(grid_offset).unwrap()) .expect("Grid ID too high (would overflow the grid_or_static)"), recipe_idx_with_this_item: data_store.num_recipes_with_item[usize_from(item.id)] as u16, - }, - Storage::Static { index, static_id } => Self { + }), + Storage::Static { index, static_id } => Ok(Self { index: u32::try_from(index).unwrap(), grid_or_static_flag: 0, recipe_idx_with_this_item: static_id, - }, + }), } } } @@ -248,30 +270,6 @@ pub enum Storage { } impl Storage { - pub fn translate( - self, - item: Item, - data_store: &DataStore, - ) -> Option { - match self { - Storage::Assembler { - grid, - recipe_idx_with_this_item, - index, - } => Some(Storage::Assembler { - grid, - recipe_idx_with_this_item: *data_store.recipe_to_translated_index.get(&( - Recipe { - id: recipe_idx_with_this_item, - }, - item, - ))?, - index, - }), - storage => Some(storage), - } - } - pub fn change_grid(self, new_id: PowerGridIdentifier) -> Self { match self { Storage::Assembler { @@ -291,43 +289,13 @@ impl Storage { } } - fn into_inner_and_outer_indices( - self, - num_grids_total: usize, - num_recipes: usize, - grid_size: usize, - ) -> (usize, usize) { - match self { - Storage::Assembler { - grid, - recipe_idx_with_this_item, - index, - } => { - debug_assert!( - usize_from(recipe_idx_with_this_item) < num_recipes, - "The recipe stored in an inserter needs to be translated!" - ); - let outer = Into::::into(grid) * grid_size - + Into::::into(recipe_idx_with_this_item); - (outer, index.try_into().unwrap()) - }, - Storage::Lab { grid, index } => { - let outer = Into::::into(grid) * grid_size + num_recipes; - (outer, index.try_into().unwrap()) - }, - Storage::Static { static_id, index } => { - // debug_assert!(usize::from(static_id) < data_store.num_different_static_containers); - let outer = num_grids_total * grid_size + Into::::into(static_id as u8); - (outer, index.try_into().unwrap()) - }, - } - } - - fn into_inner_and_outer_indices_with_statics_at_zero( + fn into_inner_and_outer_indices_with_statics_at_zero( self, + item: Item, num_recipes: usize, grid_size: usize, static_size: usize, + data_store: &DataStore, ) -> (usize, usize) { let grid_offset = static_size.div_ceil(grid_size); @@ -337,6 +305,15 @@ impl Storage { recipe_idx_with_this_item, index, } => { + let recipe_idx_with_this_item = *data_store + .recipe_to_translated_index + .get(&( + Recipe { + id: recipe_idx_with_this_item, + }, + item, + )) + .unwrap(); debug_assert!( usize_from(recipe_idx_with_this_item) < num_recipes, "The recipe stored in an inserter needs to be translated!" @@ -378,6 +355,17 @@ pub enum StaticID { PureSoloOwnedMiningDrill = 1, } +impl TryFrom for StaticID { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Chest), + 1 => Ok(Self::PureSoloOwnedMiningDrill), + _ => Err(()), + } + } +} + #[cfg(test)] mod test { use crate::DATA_STORE; @@ -443,28 +431,28 @@ mod test { proptest! { - #[test] - fn storage_and_fake_union_result_in_same_indices((item, num_grids, storage) in union_test_input()) { - let grid_size = grid_size(item, &DATA_STORE); + // #[test] + // fn storage_and_fake_union_result_in_same_indices((item, num_grids, storage) in union_test_input()) { + // let grid_size = grid_size(item, &DATA_STORE); - let storage_union = FakeUnionStorage::from_storage(item, storage, &DATA_STORE); + // let storage_union = FakeUnionStorage::from_storage(item, storage, &DATA_STORE); - let union_indices = storage_union.into_inner_and_outer_indices(num_grids.into(), grid_size); + // let union_indices = storage_union.into_inner_and_outer_indices(num_grids.into(), grid_size); - let storage_indices = storage.into_inner_and_outer_indices(num_grids.into(), DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size); + // let storage_indices = storage.into_inner_and_outer_indices(Item::try_from(0).unwrap(), num_grids.into(), DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, &DATA_STORE); - prop_assert_eq!(union_indices, storage_indices); - } + // prop_assert_eq!(union_indices, storage_indices); + // } #[test] fn storage_and_fake_union_result_in_same_indices_with_statics_at_zero((item, _num_grids, storage) in union_test_input()) { let grid_size = grid_size(item, &DATA_STORE); - let storage_union = FakeUnionStorage::from_storage_with_statics_at_zero(item, storage, &DATA_STORE); + let storage_union = FakeUnionStorage::from_storage_with_statics_at_zero(item, storage, &DATA_STORE).unwrap(); let union_indices = storage_union.into_inner_and_outer_indices_with_statics_at_zero(grid_size); - let storage_indices = storage.into_inner_and_outer_indices_with_statics_at_zero(DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, static_size(item, &DATA_STORE)); + let storage_indices = storage.into_inner_and_outer_indices_with_statics_at_zero(item, DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size, static_size(item, &DATA_STORE), &DATA_STORE); prop_assert_eq!(union_indices, storage_indices); } diff --git a/src/inserter/storage_storage_inserter.rs b/src/inserter/storage_storage_inserter.rs index 9b1f087..8f48ef0 100644 --- a/src/inserter/storage_storage_inserter.rs +++ b/src/inserter/storage_storage_inserter.rs @@ -43,6 +43,7 @@ impl StorageStorageInserter { pub fn update( &mut self, + item_id: usize, storages: SingleItemStorages, movetime: u8, max_hand_size: ITEMCOUNTTYPE, @@ -53,7 +54,8 @@ impl StorageStorageInserter { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = index_fake_union(storages, self.storage_id_in, grid_size); + let (_max_insert, old, _) = + index_fake_union(Some(item_id), storages, self.storage_id_in, grid_size); let to_extract = min(max_hand_size - count, *old); @@ -69,7 +71,8 @@ impl StorageStorageInserter { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = index_fake_union(storages, self.storage_id_out, grid_size); + let (max_insert, old, _) = + index_fake_union(Some(item_id), storages, self.storage_id_out, grid_size); let to_insert = min(count, *max_insert - *old); diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs index 53a2ac4..dc7e0e0 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -249,6 +249,7 @@ impl BucketedStorageStorageInserterStoreFrontend { ); } + #[allow(non_snake_case)] for (i, _) in sizes.into_iter().enumerate().sorted_by_key(|v| v.0) { let MOVING_OUT_END: usize = store.list_len(); let WATING_FOR_SPACE: usize = MOVING_OUT_END + 1; @@ -653,7 +654,8 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (_max_insert, old) = index_fake_union(storages, inserter.storage_id_in, grid_size); + let (_max_insert, old, _) = + index_fake_union(None, storages, inserter.storage_id_in, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -686,7 +688,8 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (max_insert, old) = index_fake_union(storages, inserter.storage_id_out, grid_size); + let (max_insert, old, _) = + index_fake_union(None, storages, inserter.storage_id_out, grid_size); let to_insert = min(inserter.current_hand, *max_insert - *old); @@ -1275,11 +1278,14 @@ mod test { use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use test::Bencher; - use crate::inserter::{ - FakeUnionStorage, - storage_storage_with_buckets::{ - BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, + use crate::{ + inserter::{ + FakeUnionStorage, + storage_storage_with_buckets::{ + BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, + }, }, + storage_list::{InserterWaitLists, MaxInsertionLimit}, }; use super::BucketedStorageStorageInserterStore; @@ -1299,14 +1305,22 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_tick, @@ -1364,8 +1378,16 @@ mod test { store.update( frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_tick, @@ -1393,7 +1415,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; @@ -1402,8 +1424,16 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_time, @@ -1468,7 +1498,7 @@ mod test { for v in &mut storages_out { *v = 0u8; } - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); to_find = (0..(NUM_VISIBLE as u32)) .into_iter() .map(|i| InserterIdentifier { @@ -1489,8 +1519,16 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_time, @@ -1517,7 +1555,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; @@ -1526,8 +1564,16 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_time, @@ -1601,8 +1647,16 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_time, diff --git a/src/inserter/storage_storage_with_buckets_compressed.rs b/src/inserter/storage_storage_with_buckets_compressed.rs index a58d6d6..d32df7f 100644 --- a/src/inserter/storage_storage_with_buckets_compressed.rs +++ b/src/inserter/storage_storage_with_buckets_compressed.rs @@ -1229,7 +1229,7 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( @@ -1324,7 +1324,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; @@ -1399,7 +1399,7 @@ mod test { for v in &mut storages_out { *v = 0u8; } - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); to_find = (0..(NUM_VISIBLE as u32)) .into_iter() .map(|i| InserterIdentifier { @@ -1448,7 +1448,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 305ffc5..b9653ff 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -5,12 +5,16 @@ use super::{ FakeUnionStorage, InserterStateInfo, storage_storage_with_buckets::LargeInserterState, }; use crate::{ + assembler::simd::InserterWithBelts as WaitListInserter, + inserter::WaitlistSearchSide, item::ITEMCOUNTTYPE, join_many::join, storage_list::{SingleItemStorages, index_fake_union}, }; use std::cmp::min; +use std::num::NonZero; + #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] @@ -56,29 +60,29 @@ pub enum ImplicitState { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct InserterIdentifier { - id: InserterId, + pub id: InserterId, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] -struct InserterId { +pub struct InserterId { index: u32, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] -struct InserterBucketData { +pub struct InserterBucketData { pub storage_id_in: FakeUnionStorage, pub storage_id_out: FakeUnionStorage, - index: InserterId, - current_hand: ITEMCOUNTTYPE, - max_hand_size: ITEMCOUNTTYPE, + pub index: InserterId, + pub current_hand: ITEMCOUNTTYPE, + pub max_hand_size: ITEMCOUNTTYPE, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BucketedStorageStorageInserterStore { - pub movetime: u16, + pub movetime: NonZero, holes: Vec, inserters: Vec, @@ -95,17 +99,23 @@ const PLACEHOLDER: InserterState = InserterState { state: ImplicitState::WaitingForSourceItems(0), }; +struct UpdateResult { + extract: bool, + reinsert: bool, +} + impl BucketedStorageStorageInserterStore { - pub fn new(movetime: u16) -> Self { + pub fn new(movetime: NonZero) -> Self { Self { movetime, holes: vec![], inserters: vec![], waiting_for_item: vec![], - full_and_moving_out: vec![vec![]; movetime as usize + 1].into_boxed_slice(), + full_and_moving_out: vec![vec![]; u16::from(movetime) as usize + 1].into_boxed_slice(), waiting_for_space_in_destination: vec![], - empty_and_moving_back: vec![vec![]; movetime as usize + 1].into_boxed_slice(), + empty_and_moving_back: vec![vec![]; u16::from(movetime) as usize + 1] + .into_boxed_slice(), current_tick: 0, } } @@ -146,74 +156,96 @@ impl BucketedStorageStorageInserterStore { } fn list_len(&self) -> usize { - self.movetime as usize + 1 + u16::from(self.movetime) as usize + 1 } - pub fn remove_inserter(&mut self, id: InserterIdentifier) -> Inserter { + pub fn remove_inserter( + &mut self, + id: InserterIdentifier, + ) -> Result { let state = self.inserters[id.id.index as usize]; - let ret = |storage_id_in, storage_id_out, max_hand_size| Inserter { - storage_id_in, - storage_id_out, - last_update_time: state.last_update_time, - max_hand_size, - state: state.state, + let ret = |storage_id_in, storage_id_out, max_hand_size| { + Ok(Inserter { + storage_id_in, + storage_id_out, + last_update_time: state.last_update_time, + max_hand_size, + state: state.state, + }) }; - self.inserters[id.id.index as usize] = PLACEHOLDER; - - if let Some(pos) = self - .waiting_for_item - .iter() - .position(|ins| id.id == ins.index) - { - let removed = self.waiting_for_item.remove(pos); - - return ret( - removed.storage_id_in, - removed.storage_id_out, - removed.max_hand_size, - ); - } - - if let Some(pos) = self - .waiting_for_space_in_destination - .iter() - .position(|ins| id.id == ins.index) - { - let removed = self.waiting_for_space_in_destination.remove(pos); + let hand = self.inserters[id.id.index as usize].state; - return ret( - removed.storage_id_in, - removed.storage_id_out, - removed.max_hand_size, - ); - } + self.inserters[id.id.index as usize] = PLACEHOLDER; - for full in &mut self.full_and_moving_out { - if let Some(pos) = full.iter().position(|ins| id.id == ins.index) { - let removed = full.remove(pos); + if let ImplicitState::WaitingForSourceItems(_) = hand { + if let Some(pos) = self + .waiting_for_item + .iter() + .position(|ins| id.id == ins.index) + { + let removed = self.waiting_for_item.remove(pos); return ret( removed.storage_id_in, removed.storage_id_out, removed.max_hand_size, ); + } else { + return Err(WaitlistSearchSide::Source); } } - for empty in &mut self.empty_and_moving_back { - if let Some(pos) = empty.iter().position(|ins| id.id == ins.index) { - let removed = empty.remove(pos); + if let ImplicitState::WaitingForSpaceInDestination(_) = hand { + if let Some(pos) = self + .waiting_for_space_in_destination + .iter() + .position(|ins| id.id == ins.index) + { + let removed = self.waiting_for_space_in_destination.remove(pos); return ret( removed.storage_id_in, removed.storage_id_out, removed.max_hand_size, ); + } else { + return Err(WaitlistSearchSide::Dest); + } + } + + if let ImplicitState::FullAndMovingOut = hand { + for full in &mut self.full_and_moving_out { + if let Some(pos) = full.iter().position(|ins| id.id == ins.index) { + let removed = full.remove(pos); + + return ret( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand_size, + ); + } } + unreachable!() } + if let ImplicitState::EmptyAndMovingBack = hand { + for empty in &mut self.empty_and_moving_back { + if let Some(pos) = empty.iter().position(|ins| id.id == ins.index) { + let removed = empty.remove(pos); + + return ret( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand_size, + ); + } + } + unreachable!() + } + + // TODO: Use match statemen unreachable!() } @@ -272,17 +304,19 @@ impl BucketedStorageStorageInserterStore { } fn handle_waiting_for_item_ins( + item_id: usize, inserter: &mut InserterState, bucket_data: &mut InserterBucketData, storages: SingleItemStorages, grid_size: usize, current_tick: u32, - _movetime: u16, - ) -> bool { + movetime: std::num::NonZero, + ) -> UpdateResult { let storage_id = bucket_data.storage_id_in; - let (_max_insert, old) = index_fake_union(storages, storage_id, grid_size); + let (_max_insert, old, wait_list) = + index_fake_union(Some(item_id), storages, storage_id, grid_size); let old_val = *old; let max_hand_size = bucket_data.max_hand_size; @@ -297,29 +331,71 @@ impl BucketedStorageStorageInserterStore { inserter.state = ImplicitState::WaitingForSourceItems(bucket_data.current_hand); } - let extract = bucket_data.current_hand == max_hand_size; - - if extract { + if bucket_data.current_hand == max_hand_size { inserter.state = ImplicitState::FullAndMovingOut; // Only use the lower 2 bytes inserter.last_update_time = current_tick as u16; - } + UpdateResult { + extract: true, + reinsert: true, + } + } else { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { + if pos == 0 { + *wait_list_needed = bucket_data.max_hand_size - bucket_data.current_hand; + } + + *empty = Some(WaitListInserter { + current_hand: bucket_data.current_hand, + max_hand: bucket_data.max_hand_size.try_into().unwrap(), + movetime: movetime, - extract + rest: crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source: true, + index: bucket_data.index, + other: bucket_data.storage_id_out, + }, + }); + + UpdateResult { + extract: true, + reinsert: false, + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } } fn handle_waiting_for_space_ins( + item_id: usize, inserter: &mut InserterState, bucket_data: &mut InserterBucketData, storages: SingleItemStorages, grid_size: usize, current_tick: u32, - _movetime: u16, - ) -> bool { + movetime: std::num::NonZero, + ) -> UpdateResult { let storage_id = bucket_data.storage_id_out; - let (max_insert, old) = index_fake_union(storages, storage_id, grid_size); + let (max_insert, old, wait_list) = + index_fake_union(Some(item_id), storages, storage_id, grid_size); let old_val = *old; let max_insert = *max_insert; @@ -333,15 +409,55 @@ impl BucketedStorageStorageInserterStore { inserter.state = ImplicitState::WaitingForSpaceInDestination(bucket_data.current_hand); } - let extract = bucket_data.current_hand == 0; - - if extract { + if bucket_data.current_hand == 0 { inserter.state = ImplicitState::EmptyAndMovingBack; // Only use the lower 2 bytes inserter.last_update_time = current_tick as u16; - } + UpdateResult { + extract: true, + reinsert: true, + } + } else { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { + if pos == 0 { + *wait_list_needed = bucket_data.current_hand; + } + + *empty = Some(WaitListInserter { + current_hand: bucket_data.current_hand, + max_hand: bucket_data.max_hand_size.try_into().unwrap(), + movetime: movetime, + + rest: crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source: false, + index: bucket_data.index, + other: bucket_data.storage_id_in, + }, + }); - extract + UpdateResult { + extract: true, + reinsert: false, + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } } pub fn get_load_info(&self) -> (usize, usize, usize, usize) { @@ -392,7 +508,8 @@ impl BucketedStorageStorageInserterStore { grid_size: usize, current_tick: u32, ) { - let old_len: usize = self.get_list_sizes().iter().sum(); + // #[cfg(debug_assertions)] + // let old_len: usize = self.get_list_sizes().iter().sum(); assert!(self.current_tick < self.list_len()); @@ -456,6 +573,7 @@ impl BucketedStorageStorageInserterStore { ); let now_moving = self.waiting_for_item.extract_if(start..end, |inserter| { Self::handle_waiting_for_item_ins( + item_id, &mut self.inserters[inserter.index.index as usize], inserter, storages, @@ -463,10 +581,12 @@ impl BucketedStorageStorageInserterStore { current_tick, self.movetime, ) + .extract }); - self.full_and_moving_out[(self.current_tick + usize::from(self.movetime)) % len] - .extend(now_moving); + self.full_and_moving_out + [(self.current_tick + usize::from(u16::from(self.movetime))) % len] + .extend(now_moving.filter(|ins| ins.current_hand == ins.max_hand_size)); } // { @@ -529,6 +649,7 @@ impl BucketedStorageStorageInserterStore { self.waiting_for_space_in_destination .extract_if(start..end, |inserter| { Self::handle_waiting_for_space_ins( + item_id, &mut self.inserters[inserter.index.index as usize], inserter, storages, @@ -536,10 +657,12 @@ impl BucketedStorageStorageInserterStore { current_tick, self.movetime, ) + .extract }); - self.empty_and_moving_back[(self.current_tick + usize::from(self.movetime)) % len] - .extend(now_moving_back); + self.empty_and_moving_back + [(self.current_tick + usize::from(u16::from(self.movetime))) % len] + .extend(now_moving_back.filter(|ins| ins.current_hand == 0)); } { @@ -605,7 +728,15 @@ impl BucketedStorageStorageInserterStore { self.current_tick = (self.current_tick + 1) % self.list_len(); - assert_eq!(old_len, self.get_list_sizes().iter().sum::()); + // TODO: This does not hold with waitlists. As such this should only be active if waitlists are disabled + // #[cfg(debug_assertions)] + // { + // assert_eq!( + // old_len, + // self.get_list_sizes().iter().sum::(), + // "Updating inserters lost an inserter from the update lists" + // ); + // } } fn get_list_sizes(&self) -> Vec { @@ -658,25 +789,27 @@ impl BucketedStorageStorageInserterStore { first_tick_value_with_this_lower_part(ins.last_update_time, current_tick), ); - u16::try_from(u32::from(self.movetime).strict_sub(time_passed)).expect( - &format!( + u16::try_from(u32::from(u16::from(self.movetime)).strict_sub(time_passed)) + .expect(&format!( "Inserter has been moving for more than u16::MAX ticks: {}", - u32::from(self.movetime).strict_sub(current_tick.strict_sub( - first_tick_value_with_this_lower_part( + u32::from(u16::from(self.movetime)).strict_sub( + current_tick.strict_sub(first_tick_value_with_this_lower_part( ins.last_update_time, current_tick - ) - )) - ), - ) + )) + ) + )) }), ImplicitState::EmptyAndMovingBack => LargeInserterState::EmptyAndMovingBack( - u16::try_from(u32::from(self.movetime).strict_sub(current_tick.strict_sub( - first_tick_value_with_this_lower_part(ins.last_update_time, current_tick), - ))) + u16::try_from(u32::from(u16::from(self.movetime)).strict_sub( + current_tick.strict_sub(first_tick_value_with_this_lower_part( + ins.last_update_time, + current_tick, + )), + )) .expect(&format!( "Inserter has been moving for more than u16::MAX ticks: {}", - u32::from(self.movetime).strict_sub(current_tick.strict_sub( + u32::from(u16::from(self.movetime)).strict_sub(current_tick.strict_sub( first_tick_value_with_this_lower_part( ins.last_update_time, current_tick @@ -909,11 +1042,23 @@ impl BucketedStorageStorageInserterStore { unreachable!() } + + pub fn reinsert_empty(&mut self, inserter: InserterBucketData) { + self.empty_and_moving_back[(self.current_tick + usize::from(u16::from(self.movetime))) + % self.empty_and_moving_back.len()] + .push(inserter); + } + + pub fn reinsert_full(&mut self, inserter: InserterBucketData) { + self.full_and_moving_out[(self.current_tick + usize::from(u16::from(self.movetime))) + % self.full_and_moving_out.len()] + .push(inserter); + } } #[cfg(test)] mod test { - const MOVETIME: u16 = 120; + const MOVETIME: NonZero = NonZero::new(120).unwrap(); const NUM_INSERTERS: usize = 20_000_000; const NUM_ITEMS: usize = 5; @@ -924,6 +1069,8 @@ mod test { use rand::{random, seq::SliceRandom}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; + use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; + use super::*; #[bench] @@ -939,14 +1086,22 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( 0, &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_tick, @@ -1000,8 +1155,16 @@ mod test { store.update( 0, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + InserterWaitLists::None, + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + InserterWaitLists::None, + ), ], 10, current_tick, diff --git a/src/item.rs b/src/item.rs index de37a11..db18e7b 100644 --- a/src/item.rs +++ b/src/item.rs @@ -47,10 +47,11 @@ pub trait WeakIdxTrait: + Hash + Ord + 'static + + Debug { } -pub fn usize_from(t: T) -> usize { +pub fn usize_from(t: T) -> usize { Into::::into(t) } diff --git a/src/lab.rs b/src/lab.rs index ca53c87..969f3a6 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -26,7 +26,7 @@ pub struct MultiLabStore { pub sciences: Box<[Vec]>, timer: Vec, prod_timer: Vec, - holes: Vec, + pub holes: Vec, /// Base Crafting Speed in 5% increments /// i.e. 28 => 140% Crafting speed diff --git a/src/lib.rs b/src/lib.rs index fa48c38..d235a93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,11 @@ #![feature(adt_const_params)] #![feature(array_try_map)] #![feature(never_type)] -#![feature(mixed_integer_ops_unsigned_sub)] #![feature(int_roundings)] -#![feature(strict_overflow_ops)] -#![feature(thin_box)] -#![feature(ptr_metadata)] +#![feature(vec_push_within_capacity)] +#![feature(iterator_try_collect)] +// the vec recycle crate will collide with Vec::recycle at some point. Once that happens I want to switch over to std anyway +#![allow(unstable_name_collisions)] extern crate test; @@ -38,9 +38,9 @@ use parking_lot::Mutex; use app_state::GameState; use data::{DataStore, factorio_1_1::get_raw_data_test}; #[cfg(feature = "client")] -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] use eframe::NativeOptions; -use frontend::world::{Position, tile::CHUNK_SIZE_FLOAT}; +use frontend::world::Position; #[cfg(feature = "client")] use frontend::{action::action_state_machine::ActionStateMachine, input::Input}; use item::{IdxTrait, WeakIdxTrait}; @@ -55,13 +55,12 @@ use rendering::{ window::{LoadedGame, LoadedGameSized}, }; -#[cfg(not(feature = "client"))] -use directories::ProjectDirs; - -use saving::{load, load_readable}; +use saving::load; use std::path::PathBuf; use crate::item::Indexable; +#[cfg(feature = "client")] +use crate::{progress_info::ProgressInfo, replays::GenerationInformation}; const TICKS_PER_SECOND_LOGIC: u64 = 60; @@ -76,9 +75,20 @@ pub mod item; pub mod lab; pub mod mining_drill; pub mod power; +pub mod progress_info; pub mod research; +// For future modding capabilities +// pub mod scenario; + +#[cfg(test)] +pub mod test_world_harness; + +#[cfg(feature = "client")] +mod example_worlds; + mod shopping_list_arena; +mod temp_vec; // This is an experiment. Before I can use it, I need to run it through a miri gauntlet // mod small_box_slice; @@ -119,6 +129,10 @@ pub mod liquid; mod par_generation; +// In progress +// mod bucket_store; +mod lockfile; + impl WeakIdxTrait for u8 {} impl WeakIdxTrait for u16 {} impl IdxTrait for u8 {} @@ -152,8 +166,20 @@ impl NewWithDataStore for T { } } -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] -pub fn main() -> Result<(), ()> { +fn get_version() -> &'static str { + if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { + crate::built_info::PKG_VERSION + } else { + let version = crate::built_info::GIT_VERSION.unwrap_or( + crate::built_info::GIT_COMMIT_HASH_SHORT.unwrap_or("Could not get git version"), + ); + version + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[allow(unused)] +pub fn main(input: &Vec) -> Result<(), args::ArgsError> { // use ron::ser::PrettyConfig; // let raw = crate::data::factorio_1_1::get_raw_data_fn(); @@ -170,6 +196,7 @@ pub fn main() -> Result<(), ()> { .env() .init() .unwrap(); + log::info!("Welcome to main on native"); #[cfg(feature = "client")] { @@ -192,25 +219,58 @@ pub fn main() -> Result<(), ()> { #[cfg(not(feature = "client"))] { - info!("Running Dedicated server!"); - // let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); - // let save_file_dir = dir.data_dir().join("save.save"); - // run_dedicated_server(StartGameInfo::Load(save_file_dir)); - run_dedicated_server(StartGameInfo::Load( - "/home/tim/.local/share/factory_game/save.save" - .try_into() - .unwrap(), - )); + use crate::saving::save_folder; + + log::info!("This is the dedicated server"); + let mut args = args::Args::new("factory", "FactoryGame dedicated server"); + + args.flag("h", "help", "Print the usage menu"); + args.flag("c", "create", "Create a new world"); + args.flag( + "o", + "override", + "Allows overriding an existing world when creating world", + ); + + args.parse(input)?; + log::trace!("Parsed input"); + + let help = args.value_of("help")?; + if help { + println!("{}", args.full_usage()); + return Ok(()); + } + + let create = args.value_of("create")?; + let overwrite = args.value_of("override")?; + + log::info!("Running Dedicated server"); + let save_path = save_folder().join("dedicated_server_save.save"); + log::info!("Loading Save game from {:?}", &save_path); + + let start_game = if create { + StartGameInfo::Create { + name: "dedicated_server_save.save".to_string(), + info: GameCreationInfo::Empty, + allow_overwrite: overwrite, + } + } else { + StartGameInfo::Load(save_path) + }; + run_dedicated_server(start_game); } } #[cfg(target_arch = "wasm32")] -pub fn main() { +pub fn main(_input: &Vec) -> Result<(), args::ArgsError> { + console_error_panic_hook::set_once(); + puffin::set_scopes_on(true); use eframe::wasm_bindgen::JsCast as _; // Redirect `log` message to `console.log` and friends: - eframe::WebLogger::init(log::LevelFilter::Error).ok(); + eframe::WebLogger::init(log::LevelFilter::Warn).ok(); + log::info!("Welcome to main on wasm"); let web_options = eframe::WebOptions::default(); @@ -249,12 +309,19 @@ pub fn main() { } } }); + + Ok(()) } enum StartGameInfo { Load(PathBuf), LoadReadable(PathBuf), - Create(GameCreationInfo), + Create { + name: String, + gen_info: GenerationInformation, + info: GameCreationInfo, + allow_overwrite: bool, + }, } enum GameCreationInfo { @@ -272,13 +339,18 @@ enum GameCreationInfo { SolarField(Watt, Position), + TrainRide, + FromBP(PathBuf), } #[cfg(feature = "client")] fn run_integrated_server( - progress: Arc, - start_game_info: StartGameInfo, + progress: ProgressInfo, + game_creation_fn: impl FnOnce(ProgressInfo, &DataStore) -> GameState, + + // FIXME: This type is wrong + listen_addr: Option<&'static str>, ) -> (LoadedGame, Arc, Sender) { // TODO: Do mod loading here let raw_data = get_raw_data_test(); @@ -286,84 +358,37 @@ fn run_integrated_server( let tick_counter: Arc = Arc::new(AtomicU64::new(0)); - let connections: Arc>> = Arc::default(); - - let local_addr = "127.0.0.1:57267"; + let (new_conn_send, new_conn_recv) = channel(); let cancel: Arc = Default::default(); - - // accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); + if let Some(listen_addr) = listen_addr { + accept_continously(listen_addr, new_conn_send, cancel.clone()).unwrap(); + } match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let (send, recv) = channel(); + + let game_state = game_creation_fn(progress, &data_store); + let game_state = Arc::new(game_state); + let state_machine: Arc>> = - Arc::new(Mutex::new(ActionStateMachine::new( + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( 0, - (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), &data_store, ))); - let game_state = Arc::new(match start_game_info { - StartGameInfo::Load(path) => load(path) - .map(|sg| { - if sg.checksum != data_store.checksum { - // Try reconciliation - // todo!("Checksum mismatch, try to merge old and new mod state") - } else { - } - sg.game_state - }) - .unwrap(), - StartGameInfo::LoadReadable(path) => load_readable(path) - .map(|sg| { - assert_eq!( - sg.checksum, data_store.checksum, - "A savegame can only be loaded with the EXACT same mods!" - ); - sg.game_state - }) - .unwrap(), - StartGameInfo::Create(info) => match info { - GameCreationInfo::Empty => GameState::new(&data_store), - GameCreationInfo::RedGreen => { - GameState::new_with_beacon_production(progress, &data_store) - }, - GameCreationInfo::RedGreenBelts => { - GameState::new_with_beacon_belt_production(progress, &data_store) - }, - GameCreationInfo::RedWithLabs => { - GameState::new_with_production(progress, &data_store) - }, - GameCreationInfo::Megabase(use_solar_field) => { - GameState::new_with_megabase(use_solar_field, progress, &data_store) - }, - GameCreationInfo::Gigabase(count) => { - GameState::new_with_gigabase(count, progress, &data_store) - }, - GameCreationInfo::SolarField(wattage, base_pos) => { - GameState::new_with_tons_of_solar( - wattage, - base_pos, - None, - progress, - &data_store, - ) - }, - GameCreationInfo::LotsOfBelts => { - GameState::new_with_lots_of_belts(progress, &data_store) - }, - - GameCreationInfo::FromBP(path) => GameState::new_with_bp(&data_store, path), - }, - }); - let (ui_sender, ui_recv) = channel(); let mut game = Game::new( GameInitData::IntegratedServer { game_state: game_state.clone(), tick_counter: tick_counter.clone(), - info: ServerInfo { connections }, + info: ServerInfo { + connections: vec![], + new_connection_recv: new_conn_recv, + }, action_state_machine: state_machine.clone(), inputs: recv, ui_actions: ui_recv, @@ -372,7 +397,9 @@ fn run_integrated_server( // This is a little hack. Our connection accept thread is stuck waiting for connections and will only exit if anything connects. // So we just connect to ourselves :) // See https://stackoverflow.com/questions/56692961/how-do-i-gracefully-exit-tcplistener-incoming - let _ = TcpStream::connect(local_addr); + if let Some(local_addr) = listen_addr { + let _ = TcpStream::connect(local_addr); + } }), }, &data_store, @@ -413,28 +440,46 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let raw_data = get_raw_data_test(); let data_store = raw_data.process(); - let progress = Default::default(); - - let connections: Arc>> = Arc::default(); + // let progress = ProgressInfo::new(); - let local_addr = "127.0.0.1:8080"; + let local_addr = "0.0.0.0:42069"; let cancel: Arc = Default::default(); - accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); + log::warn!("Hosting on {}", &local_addr); + + let (new_conn_send, new_conn_recv) = channel(); + + accept_continously(local_addr, new_conn_send, cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { - let game_state = load(todo!("Add a console argument for the save file path")) - .map(|save| save.game_state) - .unwrap_or_else(|| { - // GameState::new(&data_store) - GameState::new_with_beacon_production(progress, &data_store) - }); + let game_state = match start_game_info { + StartGameInfo::Load(path_buf) => load(path_buf) + .map(|save| save.game_state) + .expect("Could not load game"), + StartGameInfo::LoadReadable(_path_buf) => unimplemented!(), + StartGameInfo::Create { + name, + gen_info, + info: _info, + allow_overwrite, + } => { + if !allow_overwrite { + log::error!( + "Currently allow_overwrite is ignored and overwriting is always allowed!!!!" + ); + } + GameState::new(name, gen_info, &data_store) + }, + }; let mut game = Game::new( GameInitData::DedicatedServer( game_state, - ServerInfo { connections }, + ServerInfo { + connections: vec![], + new_connection_recv: new_conn_recv, + }, Box::new(move || { cancel.store(true, Ordering::Relaxed); // This is a little hack. Our connection accept thread is stuck waiting for connections and will only exit if anything connects. @@ -451,8 +496,6 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let data_store = Arc::new(data_store); match game.run(stop, &data_store) { - multiplayer::ExitReason::UserQuit => exit(0), - multiplayer::ExitReason::ConnectionDropped => exit(1), multiplayer::ExitReason::LoopStopped => exit(0), } }, @@ -461,7 +504,10 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { } #[cfg(feature = "client")] -fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender) { +fn run_client( + remote_addr: SocketAddr, + game_state_sender: Sender<(LoadedGame, Arc, Sender)>, +) { // TODO: Do mod loading here let raw_data = get_raw_data_test(); let data_store = raw_data.process(); @@ -471,56 +517,45 @@ fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender { let (send, recv) = channel(); - let state_machine: Arc>> = - Arc::new(Mutex::new(ActionStateMachine::new( - 1, - (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), - &data_store, - ))); - - let game_state = Arc::new( - // FIXME: When running in client mode, we should download the gamestate from the server instead of loading it from disk - load(PathBuf::new()) - .map(|save| save.game_state) - .unwrap_or_else(|| GameState::new(&data_store)), - ); let (ui_sender, ui_recv) = channel(); + let stop = Arc::new(AtomicBool::new(false)); + let m_stop = stop.clone(); + let m_data_store = data_store.clone(); + let data_store = Arc::new(Mutex::new(data_store)); + let m_tick_counter = tick_counter.clone(); let mut game = Game::new( GameInitData::Client { - game_state: game_state.clone(), - action_state_machine: state_machine.clone(), + game_state_start_fun: Box::new(move |game_state, state_machine| { + log::info!("GameState Recieved Successfully"); + + game_state_sender + .send(( + LoadedGame::ItemU8RecipeU8(LoadedGameSized { + state: game_state, + state_machine, + data_store, + ui_action_sender: ui_sender, + stop_update_thread: stop, + }), + tick_counter, + send, + )) + .unwrap(); + }), inputs: recv, - tick_counter: tick_counter.clone(), + tick_counter: m_tick_counter, info: ClientConnectionInfo { addr: remote_addr }, ui_actions: ui_recv, }, - &data_store, + &m_data_store, ) .expect("Could not start Game"); - let stop = Arc::new(AtomicBool::new(false)); - - let m_data_store = data_store.clone(); - let m_stop = stop.clone(); thread::spawn(move || { game.run(m_stop, &m_data_store); }); - - let data_store = Arc::new(Mutex::new(data_store)); - return ( - LoadedGame::ItemU8RecipeU8(LoadedGameSized { - state: game_state, - state_machine, - data_store, - ui_action_sender: ui_sender, - - stop_update_thread: stop, - }), - tick_counter, - send, - ); }, _ => todo!(), } @@ -561,125 +596,65 @@ pub fn simple( #[cfg(test)] mod tests { - use std::{iter, rc::Rc}; + use std::sync::Arc; - use test::{Bencher, black_box}; + use test::Bencher; use crate::{ - TICKS_PER_SECOND_LOGIC, - app_state::GameState, - data::factorio_1_1::get_raw_data_test, - frontend::{action::ActionType, world::Position}, - replays::{Replay, run_till_finished}, + app_state::GameState, data::factorio_1_1::get_raw_data_test, progress_info::ProgressInfo, + replays::GenerationInformation, }; #[bench] fn clone_empty_simulation(b: &mut Bencher) { let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new(&data_store); - - let replay = Replay::new(&game_state, None, Rc::new(data_store)); + let game_state = GameState::new( + "Test World".to_string(), + GenerationInformation::default(), + &data_store, + ); - b.iter(|| replay.clone()); + b.iter(|| game_state.clone()); } #[bench] fn empty_simulation(b: &mut Bencher) { - // 1 hour - const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new(&data_store); - - let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - - for _ in 0..NUM_TICKS { - replay.tick(); - } - - replay.finish(); + let game_state = Arc::new(GameState::new( + "Test World".to_string(), + GenerationInformation::default(), + &data_store, + )); - b.iter(|| black_box(replay.clone().run().with(run_till_finished))); + b.iter(|| { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &data_store, + ) + }); } #[bench] - fn noop_actions_simulation(b: &mut Bencher) { - // 1 hour - const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - + fn bench_megabase(b: &mut Bencher) { let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new(&data_store); - - let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - - for _ in 0..NUM_TICKS { - replay.append_actions( - iter::repeat(ActionType::Ping(Position { x: 100, y: 100 })).take(5), + let game_state = GameState::new_with_megabase( + "Test World".to_string(), + GenerationInformation::default(), + false, + ProgressInfo::new(), + &data_store, + ); + + b.iter(|| { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &data_store, ); - replay.tick(); - } - - replay.finish(); - - b.iter(|| replay.clone().run().with(run_till_finished)); + }) } - - // #[rstest] - // fn crashing_replays(#[files("crash_replays/*.rep")] path: PathBuf) { - // use std::io::Read; - - // // Keep running for 30 seconds - // const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; - - // let mut file = File::open(&path).unwrap(); - - // let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); - - // file.read_to_end(&mut v).unwrap(); - - // // TODO: For non u8 IdxTypes this will fail - // let mut replay: Replay> = bitcode::deserialize(v.as_slice()) - // .expect( - // format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), - // ); - - // replay.finish(); - - // let running_replay = replay.run(); - - // let (mut game_state_before_crash, data_store) = running_replay.with(run_till_finished); - - // for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { - // game_state_before_crash.update(&data_store); - // } - // } - - // #[bench] - // fn bench_huge_red_green_sci(b: &mut Bencher) { - // let game_state = GameState::new_with_beacon_red_green_production_many_grids( - // Default::default(), - // &DATA_STORE, - // ); - - // let mut game_state = game_state.clone(); - - // b.iter(|| { - // game_state.update(&DATA_STORE); - // }) - // } - - // #[bench] - // fn bench_12_beacon_red(b: &mut Bencher) { - // let game_state = - // GameState::new_with_beacon_belt_production(Default::default(), &DATA_STORE); - - // let mut game_state = game_state.clone(); - - // b.iter(|| { - // game_state.update(&DATA_STORE); - // }) - // } } diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs index c199187..564869b 100644 --- a/src/liquid/mod.rs +++ b/src/liquid/mod.rs @@ -23,8 +23,6 @@ use get_size2::GetSize; pub mod connection_logic; -const FLUID_INSERTER_MOVETIME: u16 = 1; - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct FluidSystemId { @@ -682,6 +680,7 @@ impl FluidSystemStore { { if *inc == old_storage { *inc = new_storage; + log::trace!("Found connection to update"); } } for outgoing in self.fluid_systems_with_fluid[fluid.into_usize()][id.index] @@ -693,10 +692,13 @@ impl FluidSystemStore { { if *outgoing == old_storage { *outgoing = new_storage; + log::trace!("Found connection to update"); } } }, - None => {}, + None => { + log::trace!("No need to update fluid connections for empty fluid network"); + }, } } @@ -1070,7 +1072,7 @@ impl FluidSystem { fn add_output( &mut self, - fluid: Item, + _fluid: Item, source_pipe_position: Position, dest: FakeUnionStorage, _dest_pos: Position, @@ -1095,7 +1097,7 @@ impl FluidSystem { fn add_input( &mut self, - fluid: Item, + _fluid: Item, dest_pipe_position: Position, source: FakeUnionStorage, _source_pos: Position, @@ -1311,10 +1313,10 @@ impl FluidSystem { for conn in other.graph.weak_components_mut() { match conn { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, FluidSystemEntity::Input { inserter_id, .. } => { @@ -1378,18 +1380,18 @@ impl FluidSystem { let mut fluid_distribution = old_fluid_level; for (weak_index, connection_to_remove) in connections_to_remove.into_iter().collect_vec() { - let fluid = old_fluid.expect("If we have any connections we MUST have a fluid set"); + let _fluid = old_fluid.expect("If we have any connections we MUST have a fluid set"); match connection_to_remove { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { self.remove_input(fluid_box_position, weak_index); }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { self.remove_output(fluid_box_position, weak_index); }, } @@ -1407,19 +1409,19 @@ impl FluidSystem { match new_system.state { FluidSystemState::NoFluid => {}, - FluidSystemState::HasFluid { fluid } => { + FluidSystemState::HasFluid { fluid: _ } => { for connection in new_system.graph.weak_components_mut() { match connection { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { todo!() }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { todo!() }, } @@ -1495,16 +1497,16 @@ impl FluidSystem { FluidSystemState::HasFluid { fluid } => { for connection in new_system.graph.weak_components_mut() { match connection { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { todo!() }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { todo!() }, } @@ -1531,7 +1533,7 @@ impl FluidSystem { match self.state { FluidSystemState::NoFluid => {}, - FluidSystemState::HasFluid { fluid } => { + FluidSystemState::HasFluid { fluid: _ } => { assert!(fluid_left_for_us <= self.hot_data.storage_capacity.try_into().unwrap()); self.hot_data.current_fluid_level = fluid_left_for_us; }, @@ -1553,6 +1555,7 @@ impl FluidSystem { } pub fn update_fluid_system( + item_id: usize, hot_data: &mut FluidSystemHotData, storages: SingleItemStorages, grid_size: usize, @@ -1566,7 +1569,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == 0 { break; } - let (max, data) = index_fake_union(storages, outgoing_conn, grid_size); + let (max, data, _) = index_fake_union(Some(item_id), storages, outgoing_conn, grid_size); let amount_wanted = *max - *data; let amount_extracted = min( @@ -1592,7 +1595,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == hot_data.storage_capacity { break; } - let (_max, data) = index_fake_union(storages, incoming_conn, grid_size); + let (_max, data, _) = index_fake_union(Some(item_id), storages, incoming_conn, grid_size); let amount_wanted = *data; let amount_extracted = min( diff --git a/src/lockfile/mod.rs b/src/lockfile/mod.rs new file mode 100644 index 0000000..c27b9f0 --- /dev/null +++ b/src/lockfile/mod.rs @@ -0,0 +1,57 @@ +use std::{fs::File, path::Path}; + +#[derive(Debug)] +pub(crate) struct LockfileUnique { + file: std::fs::File, +} + +impl LockfileUnique { + /* + Blocks until the lockfile was locked exclusively + */ + pub fn create_blocking(path: impl AsRef) -> std::io::Result { + let file = File::create(path)?; + file.lock()?; + + Ok(Self { file }) + } + + pub fn release(self) -> std::io::Result<()> { + self.file.unlock()?; + + Ok(()) + } +} + +impl Drop for LockfileUnique { + fn drop(&mut self) { + if let Err(e) = self.file.unlock() { + log::error!("Failed unlocking unique lockfile: {e:?}"); + } + } +} + +#[derive(Debug)] +pub(crate) struct LockfileShared { + file: std::fs::File, +} + +impl LockfileShared { + /* + Blocks until the lockfile was locked shared + */ + pub fn create_blocking(path: impl AsRef) -> std::io::Result { + let file = File::create(path)?; + file.lock_shared()?; + + Ok(Self { file }) + } +} + +impl Drop for LockfileShared { + fn drop(&mut self) { + if let Err(e) = self.file.unlock() { + log::error!("Failed unlocking shared lockfile: {e:?}"); + } + } +} diff --git a/src/main.rs b/src/main.rs index 7bca002..8ef4b03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ +use std::env; + #[cfg(feature = "dhat-heap")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; #[cfg(not(feature = "dhat-heap"))] +#[cfg(not(debug_assertions))] +#[cfg(not(target_arch = "wasm32"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -14,5 +18,16 @@ fn main() -> Result<(), ()> { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); - factory::main() + let args: Vec = env::args().collect(); + + match factory::main(&args) { + Ok(()) => { + log::info!("Exiting"); + Ok(()) + }, + Err(e) => { + eprintln!("{e}"); + Err(()) + }, + } } diff --git a/src/mining_drill/mod.rs b/src/mining_drill/mod.rs index fe4eefb..9d281b5 100644 --- a/src/mining_drill/mod.rs +++ b/src/mining_drill/mod.rs @@ -2,6 +2,7 @@ use crate::inserter::StaticID; use crate::item::ITEMCOUNTTYPE; use crate::mining_drill::only_solo_owned::PureDrillStorageOnlySoloOwned; use crate::power::power_grid::MAX_POWER_MULT; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, frontend::world::Position, @@ -360,7 +361,7 @@ impl MiningDrillStore { pub fn storages_by_item( &mut self, - ) -> impl Iterator { + ) -> impl Iterator, &mut [ITEMCOUNTTYPE])> { self.pure_solo_owned .iter_mut() .map(|store| store.get_inventories()) diff --git a/src/mining_drill/only_solo_owned.rs b/src/mining_drill/only_solo_owned.rs index 21e1a25..b19c655 100644 --- a/src/mining_drill/only_solo_owned.rs +++ b/src/mining_drill/only_solo_owned.rs @@ -8,10 +8,10 @@ use crate::WeakIdxTrait; use crate::data::DataStore; use crate::item::ITEMCOUNTTYPE; use crate::item::IdxTrait; -use crate::item::Indexable; use crate::item::Item; use crate::mining_drill::MiningDrillInfo; use crate::power::Joule; +use crate::storage_list::MaxInsertionLimit; use std::cmp::min; use std::mem; @@ -132,7 +132,7 @@ impl PureDrillStorageOnlySoloOwned { owned_resource_tile_list } - pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + pub fn get_inventories(&mut self) -> (MaxInsertionLimit<'_>, &mut [ITEMCOUNTTYPE]) { (ALWAYS_FULL, self.inventory.as_mut_slice()) } diff --git a/src/mining_drill/with_shared_ore.rs b/src/mining_drill/with_shared_ore.rs index 18dc320..1e5a05a 100644 --- a/src/mining_drill/with_shared_ore.rs +++ b/src/mining_drill/with_shared_ore.rs @@ -1,5 +1,6 @@ use std::{cmp::min, iter, u8}; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, @@ -237,7 +238,7 @@ impl PureDrillStorageWithSharedOreTiles { Joule(0) } - pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + pub fn get_inventories(&mut self) -> (MaxInsertionLimit<'_>, &mut [ITEMCOUNTTYPE]) { (ALWAYS_FULL, self.inventory.as_mut_slice()) } } diff --git a/src/multiplayer/bad_tcp.rs b/src/multiplayer/bad_tcp.rs index 8b13789..19d1cda 100644 --- a/src/multiplayer/bad_tcp.rs +++ b/src/multiplayer/bad_tcp.rs @@ -1 +1,9 @@ +use std::net::TcpStream; +enum ClientState { + PreConnection(TcpStream), +} + +enum ServerState { + PreConnection(TcpStream), +} diff --git a/src/multiplayer/connection_reciever_tcp.rs b/src/multiplayer/connection_reciever_tcp.rs index 1496483..398920c 100644 --- a/src/multiplayer/connection_reciever_tcp.rs +++ b/src/multiplayer/connection_reciever_tcp.rs @@ -1,6 +1,6 @@ use std::{ net::{TcpListener, TcpStream, ToSocketAddrs}, - sync::{Arc, atomic::AtomicBool}, + sync::{Arc, atomic::AtomicBool, mpsc::Sender}, thread, }; @@ -10,7 +10,7 @@ pub type ConnectionList = Arc>>; pub fn accept_continously( local_addr: impl ToSocketAddrs, - connections: ConnectionList, + new_connection: Sender, cancel: Arc, ) -> Result<(), std::io::Error> { let listener = TcpListener::bind(local_addr)?; @@ -18,12 +18,15 @@ pub fn accept_continously( for conn in listener.incoming() { match conn { Ok(conn) => { + log::info!("Got new connection"); if cancel.load(std::sync::atomic::Ordering::SeqCst) { return; } else { conn.set_nonblocking(true) .expect("Setting connectiong to nonblocking failed"); - connections.lock().push(conn); + new_connection + .send(conn) + .expect("Sending via channel failed"); } }, Err(err) => match err.kind() { diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 223eaaa..e345dd6 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -21,7 +21,7 @@ use server::{ActionSource, GameStateUpdateHandler, HandledActionConsumer}; use crate::frontend::action::action_state_machine::ActionStateMachine; use crate::{ TICKS_PER_SECOND_RUNSPEED, - app_state::{GameState, SimulationState}, + app_state::{AuxillaryData, GameState, SimulationState}, data::DataStore, frontend::{action::ActionType, input::Input, world::tile::World}, item::{IdxTrait, WeakIdxTrait}, @@ -30,6 +30,7 @@ use crate::{ mod plumbing; // mod protocol; +mod bad_tcp; mod server; pub mod connection_reciever_tcp; @@ -43,7 +44,7 @@ pub(crate) enum Game { ), DedicatedServer( GameState, - Replay>, + Replay, GameStateUpdateHandler>, Box, ), @@ -51,7 +52,7 @@ pub(crate) enum Game { #[cfg(feature = "client")] IntegratedServer( Arc>, - Replay>, + Replay, GameStateUpdateHandler< ItemIdxType, RecipeIdxType, @@ -79,14 +80,19 @@ pub struct ClientConnectionInfo { } pub struct ServerInfo { - pub connections: Arc>>, + pub connections: Vec, + pub new_connection_recv: Receiver, } pub enum GameInitData { #[cfg(feature = "client")] Client { - game_state: Arc>, - action_state_machine: Arc>>, + game_state_start_fun: Box< + dyn FnOnce( + Arc>, + Arc>>, + ), + >, inputs: Receiver, ui_actions: Receiver>, tick_counter: Arc, @@ -111,8 +117,11 @@ pub enum GameInitData { pub enum ExitReason { LoopStopped, - UserQuit, - ConnectionDropped, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct PlayerIDInformation { + player_id: u16, } impl Game { @@ -123,14 +132,54 @@ impl Game { let stream = std::net::TcpStream::connect(info.addr)?; + + let mut buffer = vec![0; 1_000_000]; + let decoder = flate2::read::ZlibDecoder::new(stream); + + log::info!("Get player_id"); + let (PlayerIDInformation { player_id }, (decoder, _)) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve PlayerIDInformation from server!"); + log::info!("Get simulation_state"); + let (simulation_state, (decoder, _)): ( + crate::get_size::Mutex>, + _, + ) = postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + log::info!("Get world"); + let (world, (decoder, _)): (crate::get_size::Mutex>, _) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + log::info!("Get aux_data"); + let (aux_data, (decoder, _)): (crate::get_size::Mutex, _) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + + let game_state = Arc::new(GameState { + world, + simulation_state, + aux_data, + }); + + let action_state_machine = + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + player_id, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + data_store, + ))); + + let stream = decoder.into_inner(); + + game_state_start_fun(game_state.clone(), action_state_machine.clone()); + Ok(Self::Client( game_state, GameStateUpdateHandler::new(Client { @@ -143,10 +192,7 @@ impl Game { - #[cfg(debug_assertions)] - let replay = Replay::new(&game_state, None, data_store.clone()); - #[cfg(not(debug_assertions))] - let replay = Replay::new_dummy(data_store.clone()); + let replay = Replay::new(game_state.aux_data.lock().gen_info.1.clone(), data_store); Ok(Self::DedicatedServer( game_state, replay, @@ -164,10 +210,7 @@ impl Game { - #[cfg(debug_assertions)] - let replay = Replay::new(&game_state, None, data_store.clone()); - #[cfg(not(debug_assertions))] - let replay = Replay::new_dummy(data_store.clone()); + let replay = Replay::new(game_state.aux_data.lock().gen_info.1.clone(), data_store); Ok(Self::IntegratedServer( game_state, replay, @@ -201,7 +244,17 @@ impl Game return e, } - if !env::var("ZOOM").is_ok() { + let is_client = { + #[cfg(feature = "client")] + { + matches!(self, Game::Client(_, _, _)) + } + #[cfg(not(feature = "client"))] + { + false + } + }; + if !env::var("ZOOM").is_ok() || is_client { profiling::scope!("Wait"); update_interval.tick(); } @@ -217,11 +270,7 @@ impl Game { - game_state_update_handler.update::<&DataStore>( - &game_state, - None, - data_store, - ); + game_state_update_handler.update(&game_state, None, data_store); tick_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }, Game::DedicatedServer( @@ -229,7 +278,37 @@ impl Game game_state_update_handler.update(game_state, Some(replay), data_store), + ) => { + { + profiling::scope!("GameState dedicated server autosave"); + let simulation_state = game_state.simulation_state.lock(); + let world = game_state.world.lock(); + let aux_data = &mut *game_state.aux_data.lock(); + + // TODO: Autosave interval + if aux_data.current_tick % (60 * 60 * 1) == 0 { + // Autosave + #[cfg(not(target_arch = "wasm32"))] + { + use crate::saving::save_with_fork; + + profiling::scope!("Autosave"); + // TODO: Handle overlapping saves + let _ = save_with_fork( + "dedicated_server_save", + Some("dedicated_server_save.save"), + &world, + &simulation_state, + &aux_data, + data_store, + ); + } + } + } + log::trace!("Post Autosave"); + + game_state_update_handler.update(game_state, Some(replay), data_store); + }, #[cfg(feature = "client")] Game::IntegratedServer( game_state, @@ -255,14 +334,15 @@ impl Game ActionSource for Receiver> { - fn get<'a>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, - _current_tick: u64, - _: &World, - _: &SimulationState, - _: &DataStore, - ) -> impl Iterator> + use<'a, ItemIdxType, RecipeIdxType> - { + current_tick: u64, + world: &'b World, + sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, + data_store: &'c DataStore, + ) -> impl Iterator> + + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { self.try_iter() } } diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 414c220..cb84aef 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -1,5 +1,5 @@ use std::{ - io::Read, + io::{Read, Write}, marker::PhantomData, mem, net::TcpStream, @@ -7,7 +7,11 @@ use std::{ time::{Duration, Instant}, }; -use crate::app_state::SimulationState; +use crate::{ + app_state::{AuxillaryData, SimulationState}, + multiplayer::PlayerIDInformation, +}; +use flate2::Compression; use log::error; use parking_lot::Mutex; @@ -21,7 +25,6 @@ use crate::{ use super::{ ServerInfo, - connection_reciever_tcp::ConnectionList, server::{ActionSource, HandledActionConsumer}, }; @@ -34,7 +37,9 @@ pub(crate) struct Client } pub(crate) struct Server { - client_connections: ConnectionList, + client_connections: Mutex>, + new_connections: Receiver, + accepted_connections: Mutex>, item: PhantomData, recipe: PhantomData, @@ -43,7 +48,10 @@ pub(crate) struct Server impl Server { pub fn new(info: ServerInfo) -> Self { Self { - client_connections: info.connections, + client_connections: Mutex::new(info.connections), + new_connections: info.new_connection_recv, + accepted_connections: Mutex::new(vec![]), + item: PhantomData, recipe: PhantomData, } @@ -54,11 +62,12 @@ impl Server ActionSource for Client { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, _current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + _aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { @@ -71,23 +80,37 @@ impl ActionSource = state_machine - .handle_inputs(&self.local_input, world, sim_state, data_store) + .handle_inputs(self.local_input.try_iter(), world, sim_state, data_store) .into_iter() .collect(); + local_actions.extend(state_machine.once_per_update_actions(world, data_store)); + local_actions.extend(self.ui_actions.try_iter()); + mem::drop(state_machine); for action in &local_actions { postcard::to_io(&action, &self.server_connection).expect("tcp send failed"); } - let mut buffer = vec![0; 1_000_000]; - let (recieved_actions, _v): (Vec<_>, _) = - postcard::from_io((&self.server_connection, &mut buffer)).unwrap(); + // FIXME: This sucks + let mut buffer: Vec = vec![0; 1_000_000]; - recieved_actions - .into_iter() - .chain(self.ui_actions.try_iter()) + let flavor = + postcard::de_flavors::io::io::IOReader::new(&self.server_connection, &mut buffer); + + let mut deserializer = postcard::Deserializer::from_flavor(flavor); + + let recieved_actions: Vec<_> = match serde_path_to_error::deserialize(&mut deserializer) { + Ok(actions) => actions, + Err(err) => { + let path = err.path().to_string(); + log::warn!("Failed to recieve actions. Path \"{}\" failed!", path); + vec![] + }, + }; + + recieved_actions.into_iter() } } @@ -107,14 +130,16 @@ impl impl ActionSource for Server { - fn get( - &self, + fn get<'a, 'b, 'c, 'd, 'e>( + &'a self, _current_tick: u64, - _world: &World, - _sim_state: &SimulationState, - _data_store: &DataStore, - ) -> impl Iterator> + use - { + world: &'b World, + sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, + _data_store: &'c DataStore, + ) -> impl Iterator> + + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { + log::trace!("Server::Get"); const RECV_BUFFER_LEN: usize = 10_000; let start = Instant::now(); // This is the Server, it will just keep on chugging along and never block @@ -124,55 +149,133 @@ impl ActionSource Duration::from_millis(10) { error!("buffer {:?}", start.elapsed()); } - let recieved_actions: Vec> = self - .client_connections - .lock() - .iter() - .flat_map(|mut conn| { - let start = Instant::now(); - let mut ret = vec![]; - - match conn.peek(&mut buffer) { - Ok(len) => { - let mut written_buffer = &buffer[0..len]; - - loop { - if let Ok((v, rest)) = postcard::take_from_bytes(written_buffer) { - let consumed_len = written_buffer.len() - rest.len(); - written_buffer = rest; - std::io::copy( - &mut std::io::Read::by_ref(&mut conn).take(consumed_len as u64), - &mut std::io::sink(), - ) - .expect("Discarding used bytes failed"); - ret.push(v); - } else { - if written_buffer.len() == RECV_BUFFER_LEN { - error!("RECV_BUFFER_LEN exhausted!"); - } - break; + let mut recieved_actions: Vec> = vec![]; + + self.client_connections.lock().retain(|mut conn| { + let mut ret = vec![]; + + let keep = match conn.peek(&mut buffer) { + Ok(len) => { + let mut written_buffer = &buffer[0..len]; + + loop { + if let Ok((v, rest)) = postcard::take_from_bytes(written_buffer) { + let consumed_len = written_buffer.len() - rest.len(); + written_buffer = rest; + std::io::copy( + &mut std::io::Read::by_ref(&mut conn).take(consumed_len as u64), + &mut std::io::sink(), + ) + .expect("Discarding used bytes failed"); + ret.push(v); + } else { + if written_buffer.len() == RECV_BUFFER_LEN { + error!("RECV_BUFFER_LEN exhausted!"); } + break; } + } + + true + }, + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + // No data to read + true }, - Err(e) => match e.kind() { - std::io::ErrorKind::WouldBlock => { - // No data to read - }, - e => todo!("{:?}", e), + err => { + log::warn!("Dropping Connection: {:?}", err); + false }, - } + }, + }; - dbg!(start.elapsed()); - dbg!(ret.len()); - ret - }) - .collect(); + recieved_actions.extend(ret); + + keep + }); if start.elapsed() > Duration::from_millis(10) { error!("recieved_actions {:?}", start.elapsed()); } - recieved_actions.into_iter() + let spawn_player_actions = { + profiling::scope!("Accept new connections"); + + for (id_info, mut new_conn) in self.accepted_connections.lock().drain(..) { + assert_eq!( + self.client_connections + .lock() + .iter() + .find_map(|running_conn| (running_conn.peer_addr().unwrap() + == new_conn.peer_addr().unwrap()) + .then_some(new_conn.peer_addr().unwrap())), + None + ); + + log::info!("Sending GameState to new connection"); + if let Err(err) = new_conn.set_nonblocking(false) { + log::warn!("Failed setting connction to blocking: {:?}", err); + continue; + } + + let mut compressed = + flate2::write::ZlibEncoder::new(&mut new_conn, Compression::best()); + + if let Err(err) = postcard::to_io(&id_info, &mut compressed) { + // Sending failed, do not add this conn + log::warn!("Failed sending id_info: {:?}", err); + continue; + } + if let Err(err) = postcard::to_io(sim_state, &mut compressed) { + // Sending failed, do not add this conn + log::warn!("Failed sending sim_state: {:?}", err); + continue; + } + if let Err(err) = postcard::to_io(world, &mut compressed) { + // Sending failed, do not add this conn + log::warn!("Failed sending world: {:?}", err); + continue; + } + if let Err(err) = postcard::to_io(aux_data, &mut compressed) { + // Sending failed, do not add this conn + log::warn!("Failed sending aux_data: {:?}", err); + continue; + } + + std::mem::drop(compressed); + + if let Err(err) = new_conn.set_nonblocking(true) { + log::warn!("Failed setting connction to blocking: {:?}", err); + continue; + } + + if let Err(err) = new_conn.flush() { + log::warn!("Flushing failed: {:?}", err); + continue; + } + + self.client_connections.lock().push(new_conn); + } + + let mut actions = vec![]; + + for new_conn in self.new_connections.try_iter() { + let next_id = (world.players.len() + actions.len()) + .try_into() + .expect("Max of u16::MAX Player allowed"); + + actions.push(ActionType::SpawnPlayer {}); + + self.accepted_connections + .lock() + .push((PlayerIDInformation { player_id: next_id }, new_conn)); + } + + actions + }; + + recieved_actions.into_iter().chain(spawn_player_actions) } } @@ -186,11 +289,19 @@ impl ) { let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients - for conn in self.client_connections.lock().iter() { - for action in &actions { - postcard::to_io(&action, conn).expect("tcp send failed"); - } - } + self.client_connections.lock().retain(|mut conn| { + let mut keep = if let Err(err) = postcard::to_io(&actions, conn) { + log::warn!("Dropping Connection: {:?}", err); + false + } else { + true + }; + keep &= conn.flush().is_ok(); + + // let keep = serde_json::to_writer(conn, &actions).is_ok(); + + keep + }); } } @@ -206,11 +317,12 @@ pub(crate) struct IntegratedServer ActionSource for IntegratedServer { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { @@ -237,7 +349,7 @@ impl ActionSource Duration::from_millis(10) { diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 848e29e..f8a21ce 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -1,7 +1,7 @@ -use std::{borrow::Borrow, fs::File, io::Write, marker::PhantomData}; +use std::{fs::File, io::Write, marker::PhantomData}; use crate::{ - app_state::{GameState, SimulationState}, + app_state::{AuxillaryData, GameState, SimulationState}, data::DataStore, frontend::{action::ActionType, world::tile::World}, item::{IdxTrait, WeakIdxTrait}, @@ -21,11 +21,12 @@ where } pub(super) trait ActionSource { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, Self, ItemIdxType, RecipeIdxType>; @@ -64,47 +65,56 @@ impl< } } - pub fn update> + serde::Serialize>( + pub fn update( &mut self, game_state: &GameState, - replay: Option<&mut Replay>, + replay: Option<&mut Replay>, data_store: &DataStore, ) { + log::trace!("Start Update"); + let mut simulation_state = game_state.simulation_state.lock(); + let mut world = game_state.world.lock(); + let aux_data = &mut *game_state.aux_data.lock(); { profiling::scope!("GameState Update"); - GameState::update( - &mut *simulation_state, - &mut *game_state.aux_data.lock(), - data_store, - ); + + GameState::update(&mut *simulation_state, aux_data, data_store); } + log::trace!("Post Update"); - let mut world = game_state.world.lock(); - let current_tick = game_state.aux_data.lock().current_tick; + let current_tick = aux_data.current_tick; let actions_iter = { profiling::scope!("Get Actions"); - self.action_interface - .get(current_tick, &world, &simulation_state, data_store) + self.action_interface.get( + current_tick, + &world, + &simulation_state, + &aux_data, + data_store, + ) }; let actions: Vec<_> = actions_iter.into_iter().collect(); + #[cfg(feature = "replay")] { profiling::scope!("Update Replay"); if let Some(replay) = replay { - replay.append_actions(actions.iter().cloned()); + replay.append_actions(actions.iter().cloned(), data_store); replay.tick(); #[cfg(debug_assertions)] { + use ron::ser::PrettyConfig; + profiling::scope!("Serialize Replay to disk"); // If we are in debug mode, save the replay to a file let mut file = File::create("./last_replay.rep").expect("Could not open file"); - let ser = bitcode::serialize(replay).unwrap(); - file.write_all(ser.as_slice()) + let ser = ron::ser::to_string_pretty(replay, PrettyConfig::default()).unwrap(); + file.write_all(ser.as_bytes()) .expect("Could not write to file"); } } diff --git a/src/par_generation.rs b/src/par_generation.rs index 62cd876..0387d72 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; use itertools::{Itertools, assert_equal}; -use log::warn; +use log::info; -use crate::frontend::world::tile::BeltState; +use crate::frontend::world::tile::{BeltState, ModuleSlotDedupIndex}; use crate::inserter::FakeUnionStorage; +use crate::progress_info::ProgressInfo; +use crate::replays::GenerationInformation; use crate::{ DataStore, GameState, Position, WeakIdxTrait, app_state::{AuxillaryData, Factory, SimulationState, StorageStorageInserterStore}, @@ -716,28 +718,48 @@ impl ParGenerateInfo( + name: String, + gen_info: GenerationInformation, world_size: BoundingBox, generation_info: ParGenerateInfo, positions: Vec, + + progress: ProgressInfo, + data_store: &DataStore, ) -> GameState { let _timer = Timer::new("par_generate"); - warn!("par_generate"); + info!("par_generate"); + progress.push_stage(1.0 / NUM_STAGES as f64, Some("Generate Chunks".to_string())); let mut world = World::new_with_area(world_size.top_left, world_size.bottom_right); + progress.pop_stage(); world.set_module_combinations_trusted(generation_info.module_combinations.clone()); let num_grids = generation_info.power_pole_actions.num_grids; + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Power Poles", + generation_info.power_pole_actions.poles.len() * positions.len() + )), + ); let mut grid_store = power_pole_stage( generation_info.power_pole_actions, &mut world, positions.iter().copied(), data_store, ); + progress.pop_stage(); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Generating Assembler and belt layouts".to_string()), + ); let (assembler_entities, (belt_store, belt_entities)) = join!( || assembler_stage( &generation_info.module_combinations, @@ -749,11 +771,21 @@ pub fn par_generate( ), || belt_stage(generation_info.belt_actions, &positions, data_store,) ); - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Assembler Entities", + assembler_entities.len() + )), + ); for ent in assembler_entities { world.add_entity_trusted(ent, data_store); } + progress.pop_stage(); + progress.push_stage(1.0 / NUM_STAGES as f64, Some("Placing Labs".to_string())); lab_stage( &mut world, &generation_info.module_combinations, @@ -763,30 +795,54 @@ pub fn par_generate( &positions, data_store, ); - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Beacons", + generation_info.beacon_actions.len() * positions.len() + )), + ); beacon_stage( &mut world, &mut grid_store, generation_info.beacon_actions, num_grids, &positions, + progress.clone(), data_store, ); + progress.pop_stage(); - let mut chest_store = chest_stage( + let chest_store = chest_stage( &mut world, generation_info.chest_actions, positions.iter().copied(), data_store, ); - let mut storage_storage_store = StorageStorageInserterStore::new(data_store); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Pipes", + generation_info + .pipe_actions + .fluid_networks + .iter() + .map(|network| network.tanks.len()) + .sum::() + * positions.len() + )), + ); + let storage_storage_store = StorageStorageInserterStore::new(data_store); let fluid_store = pipe_stage( &mut world, generation_info.pipe_actions.fluid_networks, positions.iter().copied(), data_store, ); + progress.pop_stage(); let mut sim_state = SimulationState { factory: Factory { @@ -800,19 +856,29 @@ pub fn par_generate( ..SimulationState::new(data_store) }; + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Placing Belt Entities".to_string()), + ); { let _timer = Timer::new("belt_placement_stage"); - warn!("belt_placement_stage"); + info!("belt_placement_stage"); for ent in belt_entities { world.add_belt_entity_trusted(ent, data_store); } } + progress.pop_stage(); // splitter_stage(); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Placing Splitters".to_string()), + ); { let _timer = Timer::new("misc_stage"); - warn!("misc_stage"); + info!("misc_stage"); + let num_positions = positions.len(); for base_pos in positions.iter().copied() { GameState::apply_actions( &mut sim_state, @@ -823,21 +889,32 @@ pub fn par_generate( .map(|action| ReusableBlueprint::set_base_pos(action, base_pos)), data_store, ); + progress.add_progress(1.0 / num_positions as f64); } } - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Inserters", + generation_info.inserter_actions.len() * positions.len() + )), + ); inserter_stage( &mut world, &mut sim_state, generation_info.inserter_actions, &positions, + progress.clone(), data_store, ); + progress.pop_stage(); GameState { world: Mutex::new(world), simulation_state: Mutex::new(sim_state), - aux_data: Mutex::new(AuxillaryData::new(data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, gen_info, data_store)), } } @@ -856,7 +933,7 @@ fn power_pole_stage( data_store: &DataStore, ) -> PowerGridStorage { let _timer = Timer::new("power_pole_stage"); - warn!("power_pole_stage"); + info!("power_pole_stage"); let TrustedPowerPoleStageInfo { num_grids, poles } = pole_pole_stage_info; let mut store = PowerGridStorage::new(); @@ -920,7 +997,7 @@ fn belt_stage( * base_positions.len(); let _timer = Timer::new(format!("Placed {} belts", belt_count)); - warn!("belt_stage"); + info!("belt_stage"); let mut store = BeltStore::new(data_store); let mut ret = vec![]; @@ -992,7 +1069,7 @@ fn assembler_stage( let _timer = Timer::new(format!("Placed {} assemblers", count)); - warn!("assembler_stage"); + info!("assembler_stage"); let mut ret = vec![]; for (i, &base_pos) in base_positions.into_iter().enumerate() { @@ -1046,7 +1123,7 @@ fn assembler_stage( let ent = Entity::Assembler { ty, pos, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, info: match info { Some((pole_pos, id, weak_idx)) => { crate::frontend::world::tile::AssemblerInfo::Powered { @@ -1087,7 +1164,7 @@ fn lab_stage( let count: usize = lab_actions.len() * base_positions.len(); let _timer = Timer::new(format!("Placed {} labs", count)); - warn!("lab_stage"); + info!("lab_stage"); for (i, &base_pos) in base_positions.into_iter().enumerate() { for action in lab_actions.iter().copied() { let TrustedLabPlacement { @@ -1130,7 +1207,7 @@ fn lab_stage( let ent = Entity::Lab { pos, ty, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, pole_position: pole_pos, }; @@ -1145,14 +1222,17 @@ fn beacon_stage( beacon_actions: Vec, num_grids: usize, base_positions: &[Position], + progress: ProgressInfo, data_store: &DataStore, ) { let count: usize = beacon_actions.len() * base_positions.len(); let _timer = Timer::new(format!("Placed {} beacons", count)); let _timer = Timer::new("beacon_stage"); - warn!("beacon_stage"); + info!("beacon_stage"); + let num_positions = base_positions.len(); for (i, &base_pos) in base_positions.into_iter().enumerate() { + progress.add_progress(1.0 / num_positions as f64); for action in beacon_actions.iter().copied() { let TrustedBeaconPlacement { ty, @@ -1239,7 +1319,7 @@ fn beacon_stage( let ent = Entity::Beacon { pos, ty, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, pole_position: pole_pos, }; @@ -1253,15 +1333,22 @@ fn inserter_stage( sim_state: &mut SimulationState, mut inserter_actions: Vec>, base_positions: &[Position], + + progress: ProgressInfo, + data_store: &DataStore, ) { let count: usize = inserter_actions.len() * base_positions.len(); let _timer = Timer::new(format!("Placed {} inserters", count)); let _timer = Timer::new("inserter_stage"); - warn!("inserter_stage"); + info!("inserter_stage"); inserter_actions.sort_by_key(|a| a.get_pos()); - for action in inserter_actions { + let action_count = inserter_actions.len(); + for (i, action) in inserter_actions.into_iter().enumerate() { + if i % 1000 == 999 { + progress.add_progress(1.0 / (action_count / 1000) as f64); + } for &base_pos in base_positions { match action { ActionType::PlaceEntity(PlaceEntityInfo { @@ -1309,7 +1396,7 @@ fn chest_stage( data_store: &DataStore, ) -> FullChestStore { let _timer = Timer::new("chest_stage"); - warn!("chest_stage"); + info!("chest_stage"); let mut store = FullChestStore { stores: (0..data_store.item_display_names.len()) .map(|id| Item { @@ -1370,7 +1457,7 @@ fn pipe_stage( data_store: &DataStore, ) -> FluidSystemStore { let _timer = Timer::new("pipe_stage"); - warn!("pipe_stage"); + info!("pipe_stage"); let mut store: FluidSystemStore = FluidSystemStore::new(data_store); for base_pos in base_positions { @@ -1418,14 +1505,13 @@ fn pipe_stage( } => Storage::Assembler { grid: id.grid, index: id.assembler_index, - recipe_idx_with_this_item: data_store - .recipe_to_translated_index - [&(id.recipe, fluid.unwrap())], + recipe_idx_with_this_item: id.recipe.id, }, _ => unreachable!(), }, data_store, - ), + ) + .unwrap(), pos, ) }), @@ -1457,7 +1543,7 @@ fn pipe_stage( // data_store: &DataStore, // ) { // let _timer = Timer::new("pipe_stage"); -// warn!("pipe_stage"); +// info!("pipe_stage"); // pipe_actions.sort_by_key(|a| a.get_pos()); // for base_pos in base_positions { // for mut action in pipe_actions.iter().cloned() { @@ -1496,6 +1582,6 @@ impl> Timer { impl> Drop for Timer { fn drop(&mut self) { let dur = self.start.elapsed(); - warn!("[{}]: {:?}", self.text.borrow(), dur); + info!("[{}]: {:?}", self.text.borrow(), dur); } } diff --git a/src/power/mod.rs b/src/power/mod.rs index 0d84e66..1e3af8d 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1,4 +1,14 @@ -use crate::frontend::world::tile::ModuleSlots; +use crate::inserter::belt_storage_inserter::Dir; +use crate::inserter::belt_storage_movement_list::FinishedMovingLists; +use crate::inserter::belt_storage_movement_list::{ + BeltStorageInserterInMovement, ReinsertionLists, +}; +use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; +use crate::item::Indexable; +use crate::power::power_grid::MAX_POWER_MULT; +use crate::{ + app_state::StorageStorageInserterStore, frontend::world::tile::ModuleSlots, join_many::join, +}; use itertools::Itertools; use log::{error, warn}; use power_grid::{ @@ -213,7 +223,10 @@ impl PowerGridStorage PowerGridStorage PowerGridStorage, - _data_store: &DataStore, + data_store: &DataStore, ) -> PowerGridIdentifier { // TODO: This is O(N). Is that a problem? let hole_idx = self.power_grids.iter().position(|grid| grid.is_placeholder); @@ -306,6 +323,8 @@ impl PowerGridStorage PowerGridStorage, ) -> Option< impl IntoIterator> + // Asserting this is static means it does not capture the input lifetime of the connected_pole iter + + 'static + use, > { #[cfg(debug_assertions)] @@ -341,12 +362,51 @@ impl PowerGridStorage true, + _ => false, + }) + .count(); + let num_labs_in_sim = + grid.lab_stores.sciences[0].len() - grid.lab_stores.holes.len(); + + assert_eq!(num_labs_in_list, num_labs_in_graph); + assert_eq!(num_labs_in_list, num_labs_in_sim); + + let num_assemblers_in_list: usize = + grid.num_assemblers_of_type.iter().copied().sum(); + let num_assemblers_in_graph = grid + .grid_graph + .weak_components() + .filter(|grid_entity| match grid_entity { + (_, PowerGridEntity::Assembler { .. }) => true, + _ => false, + }) + .count(); + let num_assemblers_in_sim: usize = grid.stores.num_assemblers(); + + assert_eq!(num_assemblers_in_list, num_assemblers_in_graph); + assert_eq!(num_assemblers_in_list, num_assemblers_in_sim); + } + } let mut connected_poles: Vec<_> = connected_poles.into_iter().collect(); let ret = if !connected_poles.is_empty() { // Find the largest grid, and choose it as the base // If the size is a tossup pick the one with the smaller grid_id to reduce holes in the power_grid list - let grid = connected_poles + let kept_grid_id = connected_poles .iter() .map(|pos| self.pole_pos_to_grid_id[pos]) .max_by_key(|grid_id| { @@ -364,7 +424,7 @@ impl PowerGridStorage PowerGridStorage>() { // No need to merge a grid with itself. - if other_grid == grid { + if other_grid == kept_grid_id { continue; } ran_once = true; let storage_updates = self .merge_power_grids( - grid, + kept_grid_id, other_grid, data_store, pole_position, connected_poles.iter().copied(), ) .into_iter() - .flatten(); + .flatten() + .map(|update| { + // TODO: Make debug assert + assert_eq!(update.new_grid, kept_grid_id); + assert_eq!(update.old_grid, other_grid); + + update + }); + let old_len = storage_update_vec.len(); storage_update_vec.extend(storage_updates); + let new_updates = storage_update_vec.len() - old_len; } assert!(ran_once); #[cfg(debug_assertions)] { - for key in self.power_grids[grid as usize].grid_graph.keys() { + for key in self.power_grids[kept_grid_id as usize].grid_graph.keys() { if let Some(index) = connected_poles.iter().position(|v| v == key) { connected_poles.remove(index); } @@ -419,7 +488,7 @@ impl PowerGridStorage PowerGridStorage todo!(), + power_grid::BeaconAffectedEntity::Lab { .. } => todo!(), } } } @@ -576,7 +645,7 @@ impl PowerGridStorage { self.power_grids[id.grid as usize].is_assembler_id_a_hole(*id, data_store) }, - BeaconAffectedEntity::Lab { grid, index } => { + BeaconAffectedEntity::Lab { .. } => { // TODO: Check that this is not a lab hole false }, @@ -601,7 +670,7 @@ impl PowerGridStorage PowerGridStorage { self.power_grids[id.grid as usize].is_assembler_id_a_hole(*id, data_store) }, - BeaconAffectedEntity::Lab { grid, index } => { + BeaconAffectedEntity::Lab { .. } => { // TODO: Check that this is not a lab hole false }, @@ -683,27 +752,98 @@ impl PowerGridStorage= MIN_BEACON_POWER_MULT, - self.power_grids[usize::from(removed_id)].last_power_mult >= MIN_BEACON_POWER_MULT, + new_power_mult + == self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + new_power_mult + == self.power_grids[usize::from(kept_id)].power_mult_at_last_beacon_update, ) { (true, true) => vec![], - (true, false) => { - // Enable the beacons - self.power_grids[usize::from(removed_id)] - .beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (v.0, v.1, 0))) - .collect() - }, - (false, true) => { - // Disable the beacons - self.power_grids[usize::from(removed_id)] - .beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (-v.0, -v.1, -0))) - .collect() - }, - (false, false) => vec![], + (true, false) => self.power_grids[usize::from(removed_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .collect(), + (false, true) => self.power_grids[usize::from(kept_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(kept_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .collect(), + (false, false) => self.power_grids[usize::from(removed_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .chain( + self.power_grids[usize::from(kept_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(kept_id)] + .power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }), + ) + .collect(), }; { @@ -711,10 +851,13 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(id, update.1, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + id, + update.1.into(), + data_store, + ); }, - power_grid::BeaconAffectedEntity::Lab { grid, index } => { + power_grid::BeaconAffectedEntity::Lab { .. } => { // TODO: error!("Ignoring Beacon affect on lab"); }, @@ -738,7 +881,7 @@ impl PowerGridStorage PowerGridStorage, + ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::BeltToStorage }>, + ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::StorageToBelt }>, + FinishedMovingLists<'_, { Dir::BeltToStorage }, { Dir::StorageToBelt }>, + )], + data_store: &DataStore, ) -> (ResearchProgress, RecipeTickInfo, Option) { { @@ -1018,23 +1170,120 @@ impl PowerGridStorage>(); - let (research_progress, production_info, times_labs_used_science, beacon_updates) = self - .power_grids - .par_iter_mut() - .map(|grid| grid.update(&solar_production, tech_state, data_store)) - .reduce( - || (0, RecipeTickInfo::new(data_store), 0, vec![]), - |(acc_progress, infos, times_labs_used_science, mut old_updates), - (rhs_progress, info, new_times_labs_used_science, new_updates)| { - old_updates.extend(new_updates); - ( - acc_progress + rhs_progress, - infos + &info, - times_labs_used_science + new_times_labs_used_science, - old_updates, - ) - }, - ); + let (research_progress, production_info, times_labs_used_science, beacon_updates) = { + let lists = { + profiling::scope!("Update Grids"); + + self.power_grids + .par_iter_mut() + .map(|grid| { + grid.update(&solar_production, tech_state, current_tick, data_store) + }) + .collect_vec_list() + }; + + { + profiling::scope!("Fold lists"); + lists.into_iter().flatten().fold( + (0, RecipeTickInfo::new(data_store), 0, vec![]), + |(acc_progress, infos, times_labs_used_science, mut old_updates), + ( + rhs_progress, + info, + new_times_labs_used_science, + new_updates, + reinsertions, + )| { + join!( + || { + old_updates.extend(new_updates); + }, + || { + for inserter in reinsertions { + match inserter.conn { + crate::assembler::simd::Conn::Storage { + index, + storage_id_in, + storage_id_out, + } => { + let store = inserter_store.inserters + [inserter.item.into_usize()] + .get_mut(&inserter.movetime) + .unwrap(); + let ins = InserterBucketData { + storage_id_in: storage_id_in, + storage_id_out: storage_id_out, + index: index, + current_hand: inserter.current_hand, + max_hand_size: inserter.max_hand, + }; + if inserter.current_hand == 0 { + store.0.reinsert_empty(ins); + } else { + store.0.reinsert_empty(ins); + } + }, + crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage, + } => { + let ( + _belt_storage_exit_outgoing, + belt_storage_reinsertion_incoming, + storage_belt_reinsertion_outgoing, + _storage_belt_exit_incoming, + ) = &mut belt_storage_reinsertion_list + [inserter.item.into_usize()]; + + if self_is_source { + storage_belt_reinsertion_outgoing.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: inserter.max_hand, + movetime: inserter + .movetime + .try_into() + .unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } else { + belt_storage_reinsertion_incoming.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: 0, + movetime: inserter + .movetime + .try_into() + .unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } + }, + } + } + } + ); + + ( + acc_progress + rhs_progress, + infos + &info, + times_labs_used_science + new_times_labs_used_science, + old_updates, + ) + }, + ) + } + }; { profiling::scope!("Propagate beacon modifier changes"); @@ -1055,10 +1304,14 @@ impl PowerGridStorage PowerGridStorage= MIN_BEACON_POWER_MULT - { - // Add the full beacon effect since we are powered - ( + let effect = calculate_beacon_effect( + self.power_grids[usize::from(grid)].power_mult_at_last_beacon_update, + [ effect.0 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, effect.1 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, effect.2 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, - ) - } else { - // Not enough power, only add the power_consumption modifier - ( - 0, - 0, - effect.2 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 - / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, - ) - }; + ], + ); let affected_entities: Vec> = affected_entities.into_iter().collect(); @@ -1111,13 +1355,16 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(*id, effect, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + *id, + effect.into(), + data_store, + ); }, BeaconAffectedEntity::Lab { grid, index } => { self.power_grids[usize::from(*grid)].change_lab_module_modifiers( (*index).try_into().unwrap(), - effect, + effect.into(), data_store, ); }, @@ -1267,16 +1514,11 @@ impl PowerGridStorage= MIN_BEACON_POWER_MULT - { - // Add the full beacon effect since we are powered - raw_effect - } else { - // Not enough power, only add the power_consumption modifier - (0, 0, raw_effect.2) - }; + let effect = calculate_beacon_effect( + self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .power_mult_at_last_beacon_update, + raw_effect.into(), + ); let effect_sum = self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] .beacon_affected_entities @@ -1289,13 +1531,16 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(id, effect, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + id, + effect.into(), + data_store, + ); }, BeaconAffectedEntity::Lab { grid, index } => { self.power_grids[usize::from(grid)].change_lab_module_modifiers( index.try_into().unwrap(), - effect, + effect.into(), data_store, ); }, @@ -1306,3 +1551,23 @@ impl PowerGridStorage [i16; 3] { + linear_scaling_effect(power_mult, raw_effect) +} + +#[allow(unused)] +fn switching_effect(power_mult: u8, raw_effect: [i16; 3]) -> [i16; 3] { + if power_mult >= MIN_BEACON_POWER_MULT { + raw_effect + } else { + [0, 0, raw_effect[2]] + } +} + +fn linear_scaling_effect(power_mult: u8, raw_effect: [i16; 3]) -> [i16; 3] { + assert!(power_mult <= MAX_POWER_MULT); + raw_effect + .map(|e| i32::from(e) * i32::from(power_mult) / i32::from(MAX_POWER_MULT)) + .map(|v| v.try_into().unwrap()) +} diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index a632e39..8940548 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -1,5 +1,7 @@ +use crate::assembler::simd::InserterReinsertionInfo; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; +use crate::power::calculate_beacon_effect; use crate::{ assembler::{MultiAssemblerStore, simd::MultiAssemblerStore as MultiAssemblerStoreStruct}, join_many::join, @@ -113,7 +115,6 @@ pub struct PowerGrid { Network)>, steam_power_producers: SteamPowerProducerStore, - // TODO: Currently there can only be a single type of solar panel and accumulator pub num_solar_panels_of_type: Box<[u64]>, pub main_accumulator_count: Box<[u64]>, pub main_accumulator_charge: Box<[Joule]>, @@ -128,6 +129,7 @@ pub struct PowerGrid { max_lazy_power: Watt, pub last_power_mult: u8, + pub power_mult_at_last_beacon_update: u8, pub power_mult_history: Timeline, // FIXME: Not actually storing where the power consumption/production originates is not very useful :/ // pub power_consumption_history: Timeline, @@ -182,11 +184,14 @@ pub struct PowerPoleUpdateInfo { impl PowerGrid { #[must_use] - pub fn new_trusted(data_store: &DataStore) -> Self { + pub fn new_trusted( + future_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let network = Network::trusted_new_empty(); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -208,7 +213,8 @@ impl PowerGrid PowerGrid, first_pole_pos: Position, ) -> Self { let network = Network::new((), first_pole_pos); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -254,7 +261,8 @@ impl PowerGrid PowerGrid) -> Self { + pub fn new_placeholder( + future_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let network = Network::new((), Position { x: 0, y: 0 }); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -298,6 +309,7 @@ impl PowerGrid PowerGrid)>, data_store: &DataStore, ) -> Self { Self { grid_graph: graph, // TODO: If adding a power pole has addition side effect besides changing the graph, this is not correct - ..Self::new(data_store, Position { x: 0, y: 0 }) + ..Self::new(future_grid_id, data_store, Position { x: 0, y: 0 }) } } + pub fn set_grid_id( + &mut self, + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + self.stores.set_grid(grid_id, data_store); + } + #[must_use] #[profiling::function] pub fn join( @@ -476,6 +497,14 @@ impl PowerGrid= other.power_mult_at_last_beacon_update { + self.power_mult_at_last_beacon_update + } else { + other.power_mult_at_last_beacon_update + } + }, power_mult_history: { if self.last_power_consumption >= other.last_power_consumption { self.power_mult_history @@ -513,6 +542,18 @@ impl PowerGrid, + inserter_item: Item, + info: crate::chest::WaitingInserterRemovalInfo, + data_store: &DataStore, + ) { + // FIXME: Do I want to return something here? + self.stores + .remove_wait_list_inserter(assembler_id, inserter_item, info, data_store); + } + pub fn add_solar_panel( &mut self, panel_position: Position, @@ -709,7 +750,8 @@ impl PowerGrid = - Self::new_from_graph(network, data_store); + // FIXME: Is an Id 0 fine here? + Self::new_from_graph(0, network, data_store); let storage_updates: Vec<_> = self .move_connected_entities(&mut new_network, data_store) @@ -817,7 +859,7 @@ impl PowerGrid { let residual_items = self.lab_stores.remove_lab(*index); }, - PowerGridEntity::LazyPowerProducer { item, index } => { + PowerGridEntity::LazyPowerProducer { .. } => { todo!("Remove LazyPowerProducer (Steam Engine)") }, PowerGridEntity::SolarPanel { ty } => { @@ -838,13 +880,12 @@ impl PowerGrid= MIN_BEACON_POWER_MULT { - raw_effect - } else { - (0, 0, raw_effect.2) - }; + let effect = calculate_beacon_effect( + self.power_mult_at_last_beacon_update, + raw_effect.into(), + ); - if effect.0 > 0 || effect.1 > 0 || effect.2 > 0 { + if effect[0] > 0 || effect[1] > 0 || effect[2] > 0 { let removed_beacon_affected_entities = self .beacon_affected_entity_map .remove(&(pole_pos, weak_idx)) @@ -852,9 +893,9 @@ impl PowerGrid PowerGrid { + PowerGridEntity::LazyPowerProducer { .. } => { todo!("Move LazyPowerProducer (Steam Engine)") }, PowerGridEntity::SolarPanel { ty } => { @@ -2143,7 +2184,7 @@ impl PowerGrid PowerGrid PowerGrid, - ) -> (Watt, Vec) { - iter.map(|(power_used, times_ings_used, crafts_finished)| { - ( - power_used, - SingleRecipeTickInfo { - full_crafts: times_ings_used as u64, - prod_crafts: crafts_finished.checked_sub(times_ings_used).expect( - "More ingredients used than crafts finished?!? Negative productivity?", - ) as u64, + iter: impl ParallelIterator< + Item = ( + Watt, + u32, + u32, + impl Iterator>, + ), + >, + ) -> ( + Watt, + Vec, + Vec>, + ) { + iter.collect_vec_list() + .into_iter() + .flatten() + .map( + |(power_used, times_ings_used, crafts_finished, reinsertion)| { + ( + power_used, + SingleRecipeTickInfo { + full_crafts: times_ings_used as u64, + prod_crafts: crafts_finished.checked_sub(times_ings_used).expect( + "More ingredients used than crafts finished?!? Negative productivity?", + ) as u64, + }, + reinsertion, + ) + }, + ) + // .fold_with( + // (Watt(0), vec![]), + // |(acc_power, mut infos), (rhs_power, info)| { + // infos.push(info); + // (acc_power + rhs_power, infos) + // }, + // ) + .fold( + (Watt(0), vec![], vec![]), + |(acc_power, mut infos, mut reinsertions), (rhs_power, info, reinsertion)| { + infos.push(info); + reinsertions.extend(reinsertion); + + (acc_power + rhs_power, infos, reinsertions) }, ) - }) - .fold_with( - (Watt(0), vec![]), - |(acc_power, mut infos), (rhs_power, info)| { - infos.push(info); - - (acc_power + rhs_power, infos) - }, - ) - .reduce( - || (Watt(0), vec![]), - |(acc_power, mut infos), (rhs_power, info)| { - infos.extend_from_slice(&info); - - (acc_power + rhs_power, infos) - }, - ) } #[profiling::function] @@ -2312,29 +2370,40 @@ impl PowerGrid, ) -> ( ResearchProgress, RecipeTickInfo, u64, Vec<(BeaconAffectedEntity, (i16, i16, i16))>, + itertools::Either< + impl Iterator>, + std::iter::Empty>, + >, ) { if self.is_placeholder { - return (0, RecipeTickInfo::new(data_store), 0, vec![]); + return ( + 0, + RecipeTickInfo::new(data_store), + 0, + vec![], + itertools::Either::Right(std::iter::empty()), + ); } let active_recipes = tech_state.get_active_recipes(); let ( - (power_used_0_1, infos_0_1), - (power_used_1_1, infos_1_1), - (power_used_2_1, infos_2_1), - (power_used_2_2, infos_2_2), - (power_used_2_3, infos_2_3), - (power_used_3_1, infos_3_1), - (power_used_4_1, infos_4_1), - (power_used_5_1, infos_5_1), - (power_used_6_1, infos_6_1), + (power_used_0_1, infos_0_1, reinsertions_0_1), + (power_used_1_1, infos_1_1, reinsertions_1_1), + (power_used_2_1, infos_2_1, reinsertions_2_1), + (power_used_2_2, infos_2_2, reinsertions_2_2), + (power_used_2_3, infos_2_3, reinsertions_2_3), + (power_used_3_1, infos_3_1, reinsertions_3_1), + (power_used_4_1, infos_4_1, reinsertions_4_1), + (power_used_5_1, infos_5_1, reinsertions_5_1), + (power_used_6_1, infos_6_1, reinsertions_6_1), (lab_power_used, times_labs_used_science, tech_progress), ) = join!( || { @@ -2348,21 +2417,21 @@ impl PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid, (_, _, _))> = if next_power_mult - < MIN_BEACON_POWER_MULT - && self.last_power_mult >= MIN_BEACON_POWER_MULT - { - // Disable beacons (But keep power consumption modifier unchanged, to prevent flickering) - self.beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (-v.0, -v.1, -0))) - .collect() - } else if next_power_mult >= MIN_BEACON_POWER_MULT - && self.last_power_mult < MIN_BEACON_POWER_MULT - { - // Enable beacons (But keep power consumption modifier unchanged, to prevent flickering) - self.beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (v.0, v.1, 0))) - .collect() + // TODO: AFAIK Factorio does not update the effects of beacons every tick but more sparsely (to save UPS) + // For now I will do the same, and only update the beacon effectiveness every 60 ticks (1/seconds) + // We are still much more effective than Factorio here since AFAIK, they need to update beacosn even if the power satisfaction (and as such the beacon effect) did not change + // In that case I can just not do any updates + let beacon_updates: Vec<(BeaconAffectedEntity<_>, _)> = if current_tick % 60 == 0 { + let updates = if next_power_mult == self.power_mult_at_last_beacon_update { + vec![] + } else { + self.beacon_affected_entities + .iter() + .filter_map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(next_power_mult, (*v).into()); + + if old_effect == new_effect { + None + } else { + Some(( + *k, + ( + new_effect[0] - old_effect[0], + new_effect[1] - old_effect[1], + new_effect[2] - old_effect[2], + ), + )) + } + }) + .collect() + }; + + self.power_mult_at_last_beacon_update = next_power_mult; + updates } else { vec![] }; @@ -2684,6 +2782,18 @@ impl PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid= MIN_BEACON_POWER_MULT { - (-effect.0, -effect.1, -effect.2) - } else { - (-0, -0, -effect.2) - }; + let old_effect = + calculate_beacon_effect(self.power_mult_at_last_beacon_update, raw_effect.into()); + + let now_removed_effect = (-old_effect[0], -old_effect[1], -old_effect[2]); #[cfg(debug_assertions)] { @@ -2957,7 +3067,7 @@ struct MultiLazyPowerProducer { } impl MultiLazyPowerProducer { - fn new(info: &LazyPowerMachineInfo) -> Self { + fn new(_info: &LazyPowerMachineInfo) -> Self { Self { ingredient: vec![], stored_power: vec![], diff --git a/src/progress_info.rs b/src/progress_info.rs new file mode 100644 index 0000000..82f4027 --- /dev/null +++ b/src/progress_info.rs @@ -0,0 +1,122 @@ +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, +}; + +use parking_lot::Mutex; + +#[derive(Debug)] +pub struct ProgressInfo { + inner: Arc, +} + +#[derive(Debug)] +struct Inner { + // NOTE(Tim): This u64 is interpreted as a f64 + progress: AtomicU64, + stages: Mutex>, + message_changed: AtomicBool, +} + +#[derive(Debug)] +struct ProgressStage { + message: Option, + begin: f64, + multiplier: f64, +} + +impl Clone for ProgressInfo { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl ProgressInfo { + pub fn new() -> Self { + let zero: f64 = 0.0; + let zero: AtomicU64 = AtomicU64::new(zero.to_bits()); + + Self { + inner: Arc::new(Inner { + progress: zero, + stages: Mutex::new(vec![ProgressStage { + message: None, + begin: 0.0, + multiplier: 1.0, + }]), + message_changed: AtomicBool::new(false), + }), + } + } + + pub fn get_progress(&self) -> f64 { + f64::from_bits(self.inner.progress.load(Ordering::Relaxed)) + } + + pub fn get_message(&self) -> Option { + if self.inner.message_changed.load(Ordering::SeqCst) { + self.inner + .stages + .lock() + .iter() + .rev() + .find_map(|stage| stage.message.as_ref()) + .cloned() + } else { + None + } + } + + pub fn set_progress(&self, progress: f64) { + let binding = self.inner.stages.lock(); + let stage = binding.last().unwrap(); + + self.inner.progress.store( + (stage.begin + progress * stage.multiplier).to_bits(), + Ordering::Relaxed, + ); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn add_progress(&self, amount: f64) { + let binding = self.inner.stages.lock(); + let stage = binding.last().unwrap(); + + let current = self.get_progress(); + + self.inner.progress.store( + (current + amount * stage.multiplier).to_bits(), + Ordering::Relaxed, + ); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn push_stage(&self, perc_of_current_stage: f64, message: Option) { + let mut stages = self.inner.stages.lock(); + let multiplier = + stages.iter().map(|stage| stage.multiplier).product::() * perc_of_current_stage; + + let begin = self.get_progress(); + if message.is_some() { + self.inner.message_changed.store(true, Ordering::SeqCst); + } + stages.push(ProgressStage { + message, + begin, + multiplier, + }); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn pop_stage(&self) { + // When a stage is popped it must be done + self.set_progress(1.0); + let mut stages = self.inner.stages.lock(); + let stage = stages.pop().expect("Popped more stages than were pushed"); + if stage.message.is_some() { + self.inner.message_changed.store(true, Ordering::SeqCst); + } + } +} diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 85b3864..f4c75f2 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -1,28 +1,35 @@ use std::{ + fs::remove_dir_all, net::ToSocketAddrs, sync::{ Arc, - atomic::{AtomicU64, Ordering}, mpsc::{Sender, channel}, }, thread, time::Duration, }; +use chrono::Local; +use egui_extras::{Column, TableBuilder}; +use egui_graphs::LayoutStateTree; +use url::Url; use wasm_timer::Instant; -use directories::ProjectDirs; use parking_lot::Mutex; -use crate::{GameCreationInfo, run_client}; -use crate::{StartGameInfo, frontend::world::Position}; +use crate::{ + example_worlds, get_version, + progress_info::ProgressInfo, + run_client, + saving::{load, loading::SaveFileList, save_folder}, +}; use crate::{rendering::render_world::EscapeMenuOptions, run_integrated_server}; use eframe::{ egui::{CentralPanel, Event, PaintCallbackInfo, Shape}, egui_wgpu::{self, CallbackTrait}, }; use egui::{ - Align2, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, Slider, TextEdit, Window, + Align2, Button, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, TextEdit, Window, }; use log::{error, warn}; use tilelib::types::RawRenderer; @@ -50,6 +57,8 @@ pub struct App { last_rendered_update: u64, + world_creation_state: example_worlds::WorldValueStore, + pub input_sender: Option>, texture_atlas: Arc, @@ -70,10 +79,8 @@ impl App { ) }), input_sender: None, - state: AppState::MainMenu { - in_ip_box: None, - gigabase_size: 40, - }, + state: AppState::MainMenu { in_ip_box: None }, + world_creation_state: Default::default(), texture_atlas: atlas, currently_loaded_game: None, last_rendered_update: 0, @@ -98,6 +105,8 @@ impl eframe::App for App { match &self.state { AppState::MainMenu { .. } => {}, + AppState::LoadSaveMenu { .. } => {}, + AppState::NewGameMenu { .. } => {}, AppState::Loading { .. } => { ctx.request_repaint_after(Duration::from_secs_f32(1.0 / 60.0)); }, @@ -126,12 +135,183 @@ impl eframe::App for App { self.update_ingame(ctx, frame); }, + AppState::LoadSaveMenu { save_files } => { + let mut new_state = None; + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Back to Main Menu").clicked() { + new_state = Some(AppState::MainMenu { in_ip_box: None }); + } + + if ui + .add_enabled( + cfg!(not(target_arch = "wasm32")), + Button::new("Open Save File Folder"), + ) + .clicked() + { + #[cfg(not(target_arch = "wasm32"))] + { + let uri = + Url::from_file_path(save_folder()).expect("Could not generate URI"); + open::that(uri.as_str()).expect("Failed to Open Folder"); + } + } + + let mut dirty = false; + + TableBuilder::new(ui) + .columns(Column::remainder(), 5) + .header(1.0, |mut header| { + header.col(|ui| { + ui.label("Save File Name"); + }); + header.col(|ui| { + ui.label("Saved At"); + }); + header.col(|ui| { + ui.label("Playtime"); + }); + header.col(|ui| { + ui.label(""); + }); + }) + .body(|ui| { + ui.rows(1.0, save_files.save_files.len(), |mut row| { + let idx = row.index(); + let file = &save_files.save_files[idx]; + + match file { + Ok(file) => { + row.col(|ui| { + ui.label(&file.stored.name); + }); + row.col(|ui| { + ui.label(format!( + "{}", + file.stored + .saved_at + .with_timezone(&Local) + .format("%v %R") + )); + }); + row.col(|ui| { + let dur = chrono::Duration::from_std( + file.stored.playtime, + ) + .expect( + "Could not transform playtime to chrono duration", + ); + ui.label(format!( + "{:02}:{:02}:{:02}", + dur.num_hours(), + dur.num_minutes() % 60, + dur.num_seconds() % 60, + )); + }); + row.col(|ui| { + if ui + .add_enabled( + true, + Button::new( + RichText::new("Delete").color(Color32::RED), + ), + ) + .clicked() + { + remove_dir_all(&file.path) + .expect("Unable to delete save game"); + dirty = true; + } + }); + row.col(|ui| { + if ui.add_enabled(true, Button::new("Load")).on_disabled_hover_text("Currently WASM does not support saving or loading").clicked() { + let path = file.path.clone(); + let progress = + ProgressInfo::new(); + let (send, recv) = channel(); + + let progress_send = progress.clone(); + thread::spawn(move || { + send.send(run_integrated_server( + progress_send, + |progress, data_store| { + load(path).map(|sg| { + assert_eq!( + sg.checksum, data_store.checksum, + "A savegame can only be loaded with the EXACT same mods!" + ); + sg.game_state + }) + .unwrap() + }, + None, + )) + .expect("Channel send failed"); + }); + new_state = Some(AppState::Loading { + start_time: Instant::now(), + progress, + game_state_receiver: recv, + current_message: "Loading savegame".to_string(), + }); + } + }); + }, + Err((p, e)) => { + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.add_enabled(false, Button::new("Load")); + }); + row.col(|ui| { + if ui.add_enabled(true, Button::new("Delete")).clicked() + { + remove_dir_all(p) + .expect("Unable to delete save game"); + dirty = true; + } + }); + }, + } + }); + }); + + if dirty { + *save_files = SaveFileList::generate_from_save_folder(&save_folder()) + } + }); + // Borrow checker issue + if let Some(new_state) = new_state { + self.state = new_state; + } + }, + AppState::Loading { start_time, progress, game_state_receiver, + current_message, } => { - let progress = f64::from_bits(progress.load(Ordering::Relaxed)); + if let Some(new_message) = progress.get_message() { + *current_message = new_message; + } + let progress = progress.get_progress(); CentralPanel::default().show(ctx, |ui| { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] @@ -152,7 +332,7 @@ impl eframe::App for App { .default_pos((0.5, 0.5)) .show(ctx, |ui| { let mul: f64 = 1.0 / progress; - let text = if mul.is_infinite() { + let time_text = if mul.is_infinite() { format!("Calculating Remaining Time...") } else { if mul >= 1.0 { @@ -165,15 +345,21 @@ impl eframe::App for App { .div_ceil(60) ) } else { - error!("mul out of range 1.0..: {}", mul); - format!("Calculating Remaining Time...") + if mul < 0.99 { + error!("mul out of range 1.0..: {}", mul); + format!("Calculating Remaining Time...") + } else { + // Lets assume this is just floating point rounding crap + format!("Est Remaining 1 min") + } } }; ui.add( ProgressBar::new(progress as f32) .corner_radius(0) - .text(text), + .text(current_message.as_str()), ); + ui.label(time_text); if mul.is_finite() { ui.label(format!( "Est Full Time: {:?} min", @@ -194,30 +380,31 @@ impl eframe::App for App { state: new_state, tick: current_tick, }); + // FIXME: This is needed to prevent the tech tree from collapsing? + // TODO: Make an issue to investigae why this is needed + Window::new("FIXME").show(ctx, |ui| { + egui_graphs::reset_layout::( + ui, + Some("Tech Tree".to_string()), + ); + }); self.state = AppState::Ingame; } }, - AppState::MainMenu { - in_ip_box, - gigabase_size, - } => { + AppState::MainMenu { in_ip_box } => { Window::new("Version") .default_pos(Align2::RIGHT_TOP.pos_in_rect(&ctx.screen_rect())) .show(ctx, |ui| { Grid::new("version_grid").num_columns(2).show(ui, |ui| { ui.label("Version:"); - if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { - ui.label(crate::built_info::PKG_VERSION); - } else { - ui.label(crate::built_info::GIT_VERSION.unwrap()); - } + ui.label(get_version()); ui.end_row(); // TODO: This does not work because of nixos :/ - // ui.label("Built at:"); - // ui.label(crate::built_info::BUILT_TIME_UTC); - // ui.end_row(); + ui.label("Built at:"); + ui.label(crate::built_info::BUILT_TIME_UTC); + ui.end_row(); }) }); @@ -267,72 +454,49 @@ impl eframe::App for App { }) .inner { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); + let progress = ProgressInfo::new(); let (send, recv) = channel(); - send.send(run_client(ip)).expect("Channel send failed"); + run_client(ip, send); self.state = AppState::Loading { start_time: Instant::now(), progress, game_state_receiver: recv, + current_message: "Fetching gamestate from server".to_string(), }; return; } } - let gigabase_size = *gigabase_size; - CentralPanel::default().show(ctx, |ui| { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - ui.vertical_centered(|ui|{ - ui.label( - egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") - .heading() - .color(egui::Color32::RED), - ); - ui.label( - egui::RichText::new("For the best experience run on native.") - .heading() - .color(egui::Color32::RED), - ); - }); - - if ui.button("Load").clicked() { - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - if let Some(path) = rfd::FileDialog::new() - .set_directory( - ProjectDirs::from("de", "aschhoff", "factory_game") - .expect("No Home path found") - .data_dir(), - ) - .pick_folder() - { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Load(path), - )).expect("Channel send failed"); - }); + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") + .heading() + .color(egui::Color32::RED), + ); + ui.label( + egui::RichText::new("For the best experience run on native.") + .heading() + .color(egui::Color32::RED), + ); + }); - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; + if ui.add_enabled(cfg!(not(target_arch = "wasm32")), Button::new("Load")).on_disabled_hover_text("Saving/Loading not yet supported when running the browser").clicked() { + self.state = AppState::LoadSaveMenu { + save_files: SaveFileList::generate_from_save_folder( + &save_folder() + ), } - } + } // else if ui.button("Load Debug Save").clicked() { - // #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + // #[cfg(not(target_arch = "wasm32"))] // if let Some(path) = rfd::FileDialog::new() // .set_directory( - // ProjectDirs::from("de", "aschhoff", "factory_game") - // .expect("No Home path found") - // .data_dir(), + // save_folder(), // ) // .pick_file() // { @@ -352,180 +516,91 @@ impl eframe::App for App { // game_state_receiver: recv, // }; // } - // } - else if ui.add_enabled(cfg!(not(target_arch = "wasm32")), egui::Button::new("Connect over network")).on_disabled_hover_text("Disabled on WASM").clicked() { + // } + else if ui + .button( + "Create New Game" + ) + .clicked() + { + self.state = AppState::NewGameMenu { gigabase_size: 40, new_game_name: "My World".to_string()}; + } + else if ui + .add_enabled( + cfg!(not(target_arch = "wasm32")), + egui::Button::new("Connect over network"), + ) + .on_disabled_hover_text("Disabled on WASM") + .clicked() + { let AppState::MainMenu { in_ip_box, .. } = &mut self.state else { unreachable!() }; assert!(in_ip_box.is_none()); *in_ip_box = Some((String::new(), false)); - } else if ui.button("Empty World").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Empty), - )).expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Empty), - )); - - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } else if ui.button("Lots of Belts").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::LotsOfBelts), - )).expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::LotsOfBelts), - )); + } + }); + }, - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } else if ui.button("Megabase").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); + AppState::NewGameMenu { + gigabase_size, + new_game_name, + } => { + CentralPanel::default().show(ctx, |ui| { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") + .heading() + .color(egui::Color32::RED), + ); + ui.label( + egui::RichText::new("For the best experience run on native.") + .heading() + .color(egui::Color32::RED), + ); + }); - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(true)), - )).expect("Channel send failed"); - }); + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Creating a game on WASM might freeze the browser tab for some time. Do not worry if after clicking \'Create\' the tab freezes and your browser warns that \"This page is slowing down your browser\"") + .heading() + .color(egui::Color32::YELLOW), + ); + }); - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(true)), - )); + if ui.button("Back to Main Menu").clicked() { + self.state = AppState::MainMenu { in_ip_box: None }; + return; + } + let ret = + example_worlds::list_example_worlds(&mut self.world_creation_state, ui); - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } else if ui.button("Megabase with Infinity Battery").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); + if let Some(creation_fn) = ret { + let progress = ProgressInfo::new(); let (send, recv) = channel(); let progress_send = progress.clone(); #[cfg(not(target_arch = "wasm32"))] thread::spawn(move || { send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(false)), - )).expect("Channel send failed"); + progress_send.clone(), + creation_fn, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(false)), - )); - - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } else if { - let v = ui.horizontal( |ui| { - let ret = ui.button("Gigabase").clicked(); - - let AppState::MainMenu { gigabase_size, .. } = &mut self.state else { - unreachable!() - }; - - ui.add(Slider::new(gigabase_size, 1..=1_000).logarithmic(true).update_while_editing(true).text("Number of base copies to build")); - - let single_base_size = 15.4 / 40.0; - let single_base_usage = 40.0 / 40.0; - - ui.label(&format!("Est. Memory Usage: ~{:.1}GB", single_base_size * f64::from(*gigabase_size))); - ui.label(&format!("Est. Memory Bandwidth for 60 UPS: ~{:.1}GB/s", single_base_usage * f64::from(*gigabase_size))); - - ret - }); - - - v.inner - } { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::Gigabase(gigabase_size)), - )).expect("Channel send failed"); - }); + send.send(run_integrated_server(progress_send, creation_fn, None)); self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } else if ui.button("Solar Field").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::SolarField( - crate::power::Watt(1_000), - Position { x: 1600, y: 1600 }, - )), - )).expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, + current_message: "Creating world".to_string() }; - } else if ui.button("With bp file").clicked() { - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - if let Some(path) = rfd::FileDialog::new().pick_file() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create(GameCreationInfo::FromBP(path)), - )).expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; - } } }); }, @@ -535,10 +610,38 @@ impl eframe::App for App { fn on_exit(&mut self) { if let Some(state) = &self.currently_loaded_game { match &state.state { - LoadedGame::ItemU8RecipeU8(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU8RecipeU16(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU16RecipeU8(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU16RecipeU16(state) => save(&state.state, &state.data_store.lock()), + LoadedGame::ItemU8RecipeU8(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU8RecipeU16(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU16RecipeU8(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU16RecipeU16(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, } } } @@ -548,6 +651,9 @@ impl App { fn update_ingame(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { let size = ctx.available_rect(); + #[cfg(target_arch = "wasm32")] + let mut render_action_vec = vec![]; + CentralPanel::default().show(ctx, |ui| { if ui.ui_contains_pointer() { ctx.set_cursor_icon(CursorIcon::Default); @@ -559,137 +665,155 @@ impl App { let wants_pointer = ctx.wants_pointer_input(); let wants_keyboard = ctx.wants_keyboard_input(); - #[cfg(not(target_arch = "wasm32"))] - ui.input(|input_state| { - for event in &input_state.events { - match event { - Event::Copy - | Event::Cut - | Event::Paste(_) - | Event::Text(_) - | Event::Key { .. } => { - if wants_keyboard { - continue; - } - }, - Event::PointerMoved(_) - | Event::MouseMoved(_) - | Event::PointerButton { .. } - | Event::PointerGone - | Event::Zoom(_) - | Event::Touch { .. } - | Event::MouseWheel { .. } => { - if wants_pointer { - continue; - } - }, - _ => {}, - } - let input = if let Event::PointerMoved(dest) = event { - let pos_normalized = [dest.x / size.width(), dest.y / size.height()]; - - let ar = size.width() / size.height(); - - if pos_normalized[0] < 0.0 - || pos_normalized[0] > 1.0 - || pos_normalized[1] < 0.0 - || pos_normalized[1] > 1.0 - { - continue; + { + profiling::scope!("Inputs"); + #[cfg(not(target_arch = "wasm32"))] + ui.input(|input_state| { + for event in &input_state.events { + match event { + Event::Copy + | Event::Cut + | Event::Paste(_) + | Event::Text(_) + | Event::Key { .. } => { + if wants_keyboard { + continue; + } + }, + Event::PointerMoved(_) + | Event::MouseMoved(_) + | Event::PointerButton { .. } + | Event::PointerGone + | Event::Zoom(_) + | Event::Touch { .. } + | Event::MouseWheel { .. } => { + if wants_pointer { + continue; + } + }, + _ => {}, } + let input = if let Event::PointerMoved(dest) = event { + let pos_normalized = + [dest.x / size.width(), dest.y / size.height()]; - Ok(Input::MouseMove( - pos_normalized[0] - 0.5, - (pos_normalized[1] - 0.5) / ar, - )) - } else { - event.clone().try_into() - }; + let ar = size.width() / size.height(); - if let Ok(input) = input { - if self.input_sender.as_mut().unwrap().send(input).is_err() { - #[cfg(not(test))] - panic!("Could not send input"); - #[allow(unreachable_code)] + if pos_normalized[0] < 0.0 + || pos_normalized[0] > 1.0 + || pos_normalized[1] < 0.0 + || pos_normalized[1] > 1.0 { - error!("Could not send input"); + continue; } - } - } - } - }); - - match &game.state { - LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { - let cb = Callback { - raw_renderer: self - .raw_renderer - .clone() - .expect("Tried to Load a game without a renderer ready"), - texture_atlas: self.texture_atlas.clone(), - state_machine: loaded_game_sized.state_machine.clone(), - game_state: loaded_game_sized.state.clone(), - data_store: loaded_game_sized.data_store.clone(), - }; - painter.add(Shape::Callback(egui_wgpu::Callback::new_paint_callback( - size, cb, - ))); - let simulation_state = loaded_game_sized.state.simulation_state.lock(); - let world = loaded_game_sized.state.world.lock(); - let aux_data = loaded_game_sized.state.aux_data.lock(); - let state_machine = loaded_game_sized.state_machine.lock(); - let data_store = loaded_game_sized.data_store.lock(); - - let tick = game.tick.load(std::sync::atomic::Ordering::Relaxed); - - self.last_rendered_update = tick; + Ok(Input::MouseMove( + pos_normalized[0] - 0.5, + (pos_normalized[1] - 0.5) / ar, + )) + } else { + event.clone().try_into() + }; - match render_ui( - ctx, - ui, - state_machine, - simulation_state, - world, - aux_data, - data_store, - ) { - Ok(render_actions) => { - for action in render_actions { - #[cfg(not(target_arch = "wasm32"))] - loaded_game_sized - .ui_action_sender - .send(action) - .expect("Ui action channel died"); - - #[cfg(target_arch = "wasm32")] - render_action_vec.push(action); + if let Ok(input) = input { + if self.input_sender.as_mut().unwrap().send(input).is_err() { + #[cfg(not(test))] + panic!("Could not send input"); + #[allow(unreachable_code)] + { + error!("Could not send input"); + } } - }, - Err(escape) => match escape { - EscapeMenuOptions::BackToMainMenu => { - self.state = AppState::MainMenu { - in_ip_box: None, - gigabase_size: 40, - }; - self.last_rendered_update = 0; - self.input_sender = None; - - self.currently_loaded_game = None; - }, - }, + } } - }, - LoadedGame::ItemU8RecipeU16(loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - LoadedGame::ItemU16RecipeU8(loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - LoadedGame::ItemU16RecipeU16(loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - }; + }); + } + + { + profiling::scope!("Render UI"); + match &game.state { + LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { + let cb = Callback { + raw_renderer: self + .raw_renderer + .clone() + .expect("Tried to Load a game without a renderer ready"), + texture_atlas: self.texture_atlas.clone(), + state_machine: loaded_game_sized.state_machine.clone(), + game_state: loaded_game_sized.state.clone(), + data_store: loaded_game_sized.data_store.clone(), + }; + painter.add(Shape::Callback(egui_wgpu::Callback::new_paint_callback( + size, cb, + ))); + + // dbg!("pre simulation_state"); + let simulation_state = loaded_game_sized.state.simulation_state.lock(); + // dbg!("pre world"); + let world = loaded_game_sized.state.world.lock(); + // dbg!("pre aux_data"); + let aux_data = loaded_game_sized.state.aux_data.lock(); + // dbg!("pre state_machine"); + let state_machine = loaded_game_sized.state_machine.lock(); + // dbg!("pre data_store"); + let data_store = loaded_game_sized.data_store.lock(); + // dbg!("post data_store"); + + let tick = game.tick.load(std::sync::atomic::Ordering::Relaxed); + + self.last_rendered_update = tick; + + match render_ui( + ctx, + ui, + state_machine, + simulation_state, + world, + aux_data, + data_store, + ) { + Ok(render_actions) => { + for action in render_actions { + #[cfg(not(target_arch = "wasm32"))] + loaded_game_sized + .ui_action_sender + .send(action) + .expect("Ui action channel died"); + + #[cfg(target_arch = "wasm32")] + render_action_vec.push(action); + } + }, + Err(escape) => match escape { + EscapeMenuOptions::BackToMainMenu => { + #[cfg(not(target_arch = "wasm32"))] + save( + "Last Exit", + Some("last_exit.save"), + &loaded_game_sized.state, + &loaded_game_sized.data_store.lock(), + ); + + self.state = AppState::MainMenu { in_ip_box: None }; + self.last_rendered_update = 0; + self.input_sender = None; + + self.currently_loaded_game = None; + }, + }, + } + }, + LoadedGame::ItemU8RecipeU16(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + LoadedGame::ItemU16RecipeU8(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + LoadedGame::ItemU16RecipeU16(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + }; + } } else { warn!("No Game loaded!"); } @@ -773,10 +897,10 @@ impl App { let mut sim_state_lock = state.state.simulation_state.lock(); let sim_state = &mut *sim_state_lock; - let mut actions: Vec> = state + let mut actions: Vec> = state .state_machine .lock() - .handle_inputs(inputs, world, data_store) + .handle_inputs(inputs, world, sim_state, data_store) .collect(); actions.extend( diff --git a/src/rendering/map_view.rs b/src/rendering/map_view.rs index 46724f9..7fc546d 100644 --- a/src/rendering/map_view.rs +++ b/src/rendering/map_view.rs @@ -23,9 +23,11 @@ pub struct MapViewUpdate { pub color: Color32, } -const NUM_MAP_TILE_SIZES: usize = 4; +// FIXME: It seems we are rendering one map tile to much in the positive directions + +const NUM_MAP_TILE_SIZES: usize = 5; // TODO: Figure out a good tilesize. 1024 seems to work fine, but is larger or smaller better? -const TILE_SIZE_PIXELS: [u32; NUM_MAP_TILE_SIZES] = [1024, 1024, 1024, 4096]; +const TILE_SIZE_PIXELS: [u32; NUM_MAP_TILE_SIZES] = [1024, 1024, 1024, 1024, 4000]; // TODO: Since array::map is not const, we hack it like this const NUM_TILES_PER_AXIS: [u32; NUM_MAP_TILE_SIZES] = { let mut b = [0; NUM_MAP_TILE_SIZES]; @@ -36,8 +38,8 @@ const NUM_TILES_PER_AXIS: [u32; NUM_MAP_TILE_SIZES] = { } b }; -const TILE_PIXEL_TO_WORLD_TILE: [u32; NUM_MAP_TILE_SIZES] = [1, 4, 16, 64]; -pub const MIN_WIDTH: [u32; NUM_MAP_TILE_SIZES] = [0, 5_000, 10_000, 50_000]; +const TILE_PIXEL_TO_WORLD_TILE: [u32; NUM_MAP_TILE_SIZES] = [1, 4, 16, 64, 256]; +pub const MIN_WIDTH: [u32; NUM_MAP_TILE_SIZES] = [0, 10_000, 40_000, 100_000, 300_000]; #[profiling::function] pub fn create_map_textures_if_needed( @@ -121,19 +123,37 @@ pub fn create_map_textures_if_needed= real_tile_x_end); + assert!(tile_y_end >= real_tile_y_end); + let size = [ + (((map_tile_size) * pixel_to_tile - (tile_x_end - real_tile_x_end) as u32) + / pixel_to_tile) as usize, + (((map_tile_size) * pixel_to_tile - (tile_y_end - real_tile_y_end) as u32) + / pixel_to_tile) as usize, + ]; + + assert!(size[0] > 0); + assert!(size[1] > 0); + + renderer.create_runtime_texture_if_missing(tile_texture_id, size, || { + let ret = collect_colors( + world, + [tile_x as u32, tile_y as u32], + size.map(|v| v as u32), + map_tile_size, + pixel_to_tile, + data_store, + ); + + assert_eq!(Borrow::<[u8]>::borrow(&ret).len(), size[0] * size[1] * 4); + + ret + }); if let Some(allowed_time) = allowed_time { if start_time.elapsed() > allowed_time { @@ -174,6 +194,7 @@ static DEDUP_MAP: LazyLock>> = LazyLock::new(|| { fn collect_colors( world: &World, [tile_x, tile_y]: [u32; 2], + [size_x, size_y]: [u32; 2], map_tile_size: u32, pixel_to_tile: u32, data_store: &crate::data::DataStore, @@ -187,20 +208,20 @@ fn collect_colors( as i32, }, Position { - x: (i32::try_from((tile_x + 1) * map_tile_size * pixel_to_tile).unwrap() + x: (i32::try_from((tile_x * map_tile_size + size_x) * pixel_to_tile).unwrap() - 1_000_000) as i32, - y: (i32::try_from((tile_y + 1) * map_tile_size * pixel_to_tile).unwrap() + y: (i32::try_from((tile_y * map_tile_size + size_y) * pixel_to_tile).unwrap() - 1_000_000) as i32, }, ) .unwrap_or(true) { ColorResult::Generated(bytemuck::cast_vec( - ((tile_y * map_tile_size)..((tile_y + 1) * map_tile_size)) + ((tile_y * map_tile_size)..(tile_y * map_tile_size + size_y)) .into_par_iter() .flat_map_iter(|y_pos| { std::iter::repeat(y_pos) - .zip((tile_x * map_tile_size)..((tile_x + 1) * map_tile_size)) + .zip((tile_x * map_tile_size)..(tile_x * map_tile_size + size_x)) }) .map(|(y_pos, x_pos)| { let x_pos_world = @@ -208,6 +229,9 @@ fn collect_colors( let y_pos_world = (i32::try_from(y_pos * pixel_to_tile).unwrap() - 1_000_000) as i32; + assert!(x_pos_world <= 1_000_000); + assert!(y_pos_world <= 1_000_000); + let color = world.get_entity_color( Position { x: x_pos_world, @@ -221,12 +245,12 @@ fn collect_colors( .collect(), )) } else { - match DEDUP_MAP.get(&(map_tile_size * map_tile_size)) { + match DEDUP_MAP.get(&(size_x * size_y)) { Some(cached_alloc) => ColorResult::Const(&cached_alloc), None => ColorResult::Generated(bytemuck::cast_vec(vec![ Color32::BLACK; - map_tile_size as usize - * map_tile_size as usize + size_x as usize + * size_y as usize ])), } }; @@ -384,14 +408,30 @@ pub fn render_map_view( ); if renderer.has_runtime_texture(texture_id) { - map_layer.draw_runtime_texture( - texture_id, - DrawInstance { - position: [tile_draw_offs.0, tile_draw_offs.1], - size: [(map_tile_size * pixel_to_tile) as f32; 2], - animation_frame: 0, - }, - ); + let tile_x_end = + ((tile_x + 1) * map_tile_size as i32 * pixel_to_tile as i32) - 1_000_000; + let real_tile_x_end = min(tile_x_end, 1_000_000); + let tile_y_end = + ((tile_y + 1) * map_tile_size as i32 * pixel_to_tile as i32) - 1_000_000; + let real_tile_y_end = min(tile_y_end, 1_000_000); + assert!(tile_x_end >= real_tile_x_end); + assert!(tile_y_end >= real_tile_y_end); + let size = [ + ((map_tile_size) * pixel_to_tile - (tile_x_end - real_tile_x_end) as u32) + as f32, + ((map_tile_size) * pixel_to_tile - (tile_y_end - real_tile_y_end) as u32) + as f32, + ]; + if size[0] > 0.0 && size[1] > 0.0 { + map_layer.draw_runtime_texture( + texture_id, + DrawInstance { + position: [tile_draw_offs.0, tile_draw_offs.1], + size, + animation_frame: 0, + }, + ); + } } // map_layer.draw_sprite( // &Sprite::new(Texture::default()), diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index d3782ca..62a9ac8 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -169,6 +169,8 @@ pub struct TextureAtlas { underground: enum_map::EnumMap>, + accumulator: EntitySprite, + mining_drill: EntitySprite, solar_panel: EntitySprite, @@ -273,6 +275,7 @@ fn texture_atlas() -> TextureAtlas { no_power: entity_sprite_from_path_scaled!("temp_assets/no_power.png", 1, 1.0), assembler: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), + accumulator: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), chest: entity_sprite_from_path_tiling!("temp_assets/outside_world.png", 1), items: vec![sprite_from_path!("temp_assets/plate.png", 1); 200].into_boxed_slice(), @@ -495,6 +498,8 @@ fn texture_atlas() -> TextureAtlas { 1.0 / 2.0 ), + accumulator: entity_sprite_from_path_tiling!("temp_assets/krastorio/energy-storage.png", 1), + lab: entity_sprite_from_path_tiling!("temp_assets/krastorio/advanced-lab.png", 1), dark_square: sprite_from_path!("temp_assets/dark_square.png", 1), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 30ee5a2..0d1f881 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,8 +1,17 @@ use crate::belt::belt::Belt; -use crate::belt::smart::{HAND_SIZE, NUM_BELT_LOCS_SEARCHED, SmartBelt}; -use crate::belt::smart::{NUM_BELT_FREE_CACHE_HITS, NUM_BELT_UPDATES}; +#[cfg(feature = "debug-stat-gathering")] +use crate::belt::smart::{ + NUM_BELT_FREE_CACHE_HITS, NUM_BELT_INSERTER_UPDATES, NUM_BELT_LOCS_SEARCHED, NUM_BELT_UPDATES, + NUM_INSERTER_LOADS_WAITING_FOR_ITEMS, NUM_INSERTER_LOADS_WAITING_FOR_SPACE, + NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL, TIMES_ALL_INCOMING_EARLY_RETURN, + TIMES_INSERTERS_EXTRACTED, +}; + +use crate::belt::smart::SmartBelt; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; +use crate::frontend::action::action_state_machine; +#[cfg(not(target_arch = "wasm32"))] use crate::frontend::action::action_state_machine::ForkSaveInfo; use crate::frontend::action::place_entity::EntityPlaceOptions; use crate::frontend::action::place_entity::PlaceEntityInfo; @@ -14,9 +23,11 @@ use crate::get_size::RamUsage; use crate::item::{ITEMCOUNTTYPE, Indexable}; use crate::lab::{LabViewInfo, TICKS_PER_SCIENCE}; use crate::liquid::FluidSystemState; -use crate::par_generation::ParGenerateInfo; +use crate::par_generation::{ParGenerateInfo, Timer}; use crate::rendering::{BeltSide, Corner}; -use crate::saving::{save_components, save_with_fork}; +use crate::saving::save_components; +#[cfg(not(target_arch = "wasm32"))] +use crate::saving::save_with_fork; use crate::statistics::{NUM_DIFFERENT_TIMESCALES, TIMESCALE_NAMES}; use crate::{ TICKS_PER_SECOND_LOGIC, @@ -49,23 +60,25 @@ use eframe::egui::{ self, Align2, Color32, ComboBox, Context, CornerRadius, Label, Layout, ProgressBar, Stroke, Ui, Window, }; -use egui::{Button, CollapsingHeader, Modal, Rect, RichText, ScrollArea, Sense, Slider}; +use egui::{Button, CollapsingHeader, Modal, RichText, ScrollArea, Sense, Slider}; use egui_extras::{Column, TableBuilder}; -use egui_graphs::Graph; use egui_plot::{AxisHints, GridMark, Line, Plot, PlotPoints}; use egui_show_info::ShowInfo; use flate2::Compression; use flate2::write::ZlibEncoder; + +#[cfg(not(target_arch = "wasm32"))] use interprocess::os::unix::unnamed_pipe::UnnamedPipeExt; use itertools::Itertools; use log::error; -use log::{info, trace, warn}; +use log::{trace, warn}; use parking_lot::MutexGuard; use petgraph::dot::Dot; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::cmp::max; use std::fs::File; use std::io::{Read, Write}; +use std::num::NonZero; use std::sync::LazyLock; use std::{ cmp::{Ordering, min}, @@ -181,8 +194,8 @@ pub fn render_world( }; let player_chunk = ( - (camera_pos.0 / CHUNK_SIZE_FLOAT) as i32, - (camera_pos.1 / CHUNK_SIZE_FLOAT) as i32, + (camera_pos.0 / CHUNK_SIZE_FLOAT).floor() as i32, + (camera_pos.1 / CHUNK_SIZE_FLOAT).floor() as i32, ); if num_tiles_across_screen_horizontal > SWITCH_TO_MAPVIEW_TILES { @@ -197,16 +210,16 @@ pub fn render_world( state_machine.current_mouse_pos, ); - // bp.draw( - // ( - // x as f32 + num_tiles_across_screen_horizontal / 2.0, - // y as f32 + num_tiles_across_screen_vertical / 2.0, - // ), - // camera_pos, - // &mut entity_overlay_layer, - // texture_atlas, - // data_store, - // ); + bp.draw( + ( + x as f32 + num_tiles_across_screen_horizontal / 2.0, + y as f32 + num_tiles_across_screen_vertical / 2.0, + ), + camera_pos, + &mut entity_overlay_layer, + texture_atlas, + data_store, + ); } mem::drop(state_machine); @@ -242,13 +255,20 @@ pub fn render_world( }, num_tiles_across_screen_horizontal as u32, num_tiles_across_screen_vertical as u32, - // Only allow incremental map_view building for the last view level - map_view::MIN_WIDTH - .iter() - .all(|&v| v < num_tiles_across_screen_horizontal as u32) - .then_some(Duration::from_millis(15)), - // Some(Duration::from_millis(15)), - // None, + None, + data_store, + ); + } + + { + profiling::scope!("Create Map Textures Preemptively"); + create_map_textures_if_needed( + &world, + renderer, + Position { x: 0, y: 0 }, + 2_000_000, + 2_000_000, + Some(Duration::from_millis(1)), data_store, ); } @@ -300,9 +320,9 @@ pub fn render_world( pos_iter .map(|(x_offs, y_offs)| { let chunk_draw_offs = ( - x_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.0 % CHUNK_SIZE_FLOAT + x_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.0.rem_euclid(CHUNK_SIZE_FLOAT) + (0.5 * num_tiles_across_screen_horizontal), - y_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.1 % CHUNK_SIZE_FLOAT + y_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.1.rem_euclid(CHUNK_SIZE_FLOAT) + (0.5 * num_tiles_across_screen_vertical), ); @@ -567,6 +587,8 @@ pub fn render_world( prod_mod: _, power_consumption_mod: _, base_power_consumption: _, + + .. } = game_state .simulation_state .factory @@ -638,7 +660,7 @@ pub fn render_world( state, } => { let mut source_dir = None; - let (sprite, corner) = { + let (sprite, _corner) = { match state { BeltState::Straight => { (&texture_atlas.belt[*direction], None::) @@ -906,14 +928,19 @@ pub fn render_world( let start_pos = data_store.inserter_start_pos(*ty, *pos, *direction); let end_pos = data_store.inserter_end_pos(*ty, *pos, *direction); - let movetime: u16 = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks).into(); + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks).into(); match info { crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { - let hand_size = HAND_SIZE; let Some(state) = game_state.simulation_state.factory.belts.get_inserter_info_at(*id, *belt_pos) else { error!("Could not get rendering info for inserter!"); continue; }; + let hand_size = state.hand_size; + + // TODO: Due to clamping this does not currently hold: + // assert_eq!(movetime, u16::from(state.movetime)); + let movetime = u16::from(state.movetime); + let item = game_state.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); let (mut position, items): (f32, ITEMCOUNTTYPE) = match state.state { @@ -956,19 +983,19 @@ pub fn render_world( item_pos[1] += stack_offset; } }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { item, inserter } => { + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => { // TODO: }, crate::frontend::world::tile::AttachedInserter::StorageStorage { item, inserter } => { - let hand_size = HAND_SIZE; + let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size; let item = *item; let state = game_state.simulation_state.factory.storage_storage_inserters.get_inserter(item, movetime, *inserter, current_tick); let (position, items): (f32, ITEMCOUNTTYPE) = match state.state { crate::inserter::storage_storage_with_buckets::LargeInserterState::WaitingForSourceItems(count) => (0.0, count), crate::inserter::storage_storage_with_buckets::LargeInserterState::WaitingForSpaceInDestination(count) => (1.0, count), - crate::inserter::storage_storage_with_buckets::LargeInserterState::FullAndMovingOut(timer) => (1.0 - (timer as f32 / movetime as f32), hand_size), - crate::inserter::storage_storage_with_buckets::LargeInserterState::EmptyAndMovingBack(timer) => (timer as f32 / movetime as f32, 0), + crate::inserter::storage_storage_with_buckets::LargeInserterState::FullAndMovingOut(timer) => (1.0 - (timer as f32 / u16::from(movetime) as f32), hand_size), + crate::inserter::storage_storage_with_buckets::LargeInserterState::EmptyAndMovingBack(timer) => (timer as f32 / u16::from(movetime) as f32, 0), }; assert!(position >= 0.0); @@ -1205,7 +1232,7 @@ pub fn render_world( [usize::from(*ty)].size, 0, entity_layer); }, Entity::Accumulator { ty, pos, .. } => { - texture_atlas.chest.draw([ + texture_atlas.accumulator.draw([ draw_offset.0 + pos.x as f32, draw_offset.1 + pos.y as f32, ], data_store.solar_panel_info @@ -1452,7 +1479,7 @@ pub fn render_world( animation_frame: 0, }, ); - info!( + log::trace!( "Rendering other player {} at {:?}", player_id, [player.pos.0 - camera_pos.0, player.pos.1 - camera_pos.1,] @@ -1537,7 +1564,7 @@ pub fn render_world( ); }, - crate::frontend::action::action_state_machine::HeldObject::Tile(floor_tile) => { + crate::frontend::action::action_state_machine::HeldObject::Tile(_floor_tile) => { // TODO }, crate::frontend::action::action_state_machine::HeldObject::Entity( @@ -1564,11 +1591,7 @@ pub fn render_world( ); }, crate::frontend::world::tile::PlaceEntityType::Inserter { - pos, - dir, - filter, - ty, - user_movetime, + pos, dir, .. } => { // FIXME: Respect ty while rendering let size: [u16; 2] = [1, 1]; @@ -1668,14 +1691,7 @@ pub fn render_world( }, ); }, - crate::frontend::world::tile::PlaceEntityType::Splitter { - pos, - direction, - in_mode, - out_mode, - - ty, - } => { + crate::frontend::world::tile::PlaceEntityType::Splitter { .. } => { // TODO: }, crate::frontend::world::tile::PlaceEntityType::Chest { pos, ty } => { @@ -1702,7 +1718,7 @@ pub fn render_world( ); }, crate::frontend::world::tile::PlaceEntityType::Accumulator { pos, ty } => { - texture_atlas.chest.draw( + texture_atlas.accumulator.draw( [draw_offset.0 + pos.x as f32, draw_offset.1 + pos.y as f32], data_store.accumulator_info[usize::from(*ty)].size, 0, @@ -1819,8 +1835,8 @@ pub fn render_world( }, }, crate::frontend::action::action_state_machine::HeldObject::OrePlacement { - ore, - amount, + ore: _, + amount: _, } => { // TODO: }, @@ -1875,6 +1891,7 @@ pub enum EscapeMenuOptions { BackToMainMenu, } +#[profiling::function] pub fn render_ui< ItemIdxType: IdxTrait + ShowInfo, RecipeIdxType: IdxTrait + ShowInfo, @@ -1894,6 +1911,68 @@ pub fn render_ui< let data_store_ref = &*data_store; let mut actions = vec![]; + let current_tick = aux_data.current_tick; + + let tick = (current_tick % u64::from(state_machine_ref.autosave_interval)) as u32; + + #[cfg(not(target_arch = "wasm32"))] + if cfg!(target_os = "linux") { + if tick < state_machine_ref.last_tick_seen_for_autosave { + if state_machine_ref.current_fork_save_in_progress.is_none() { + let recv = save_with_fork( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + if let Some(recv) = recv { + recv.set_nonblocking(true) + .expect("Could not set pipe to nonblocking!"); + state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { + recv, + current_state: 0, + }); + } else { + error!("Nonblocking save failed to start! Saving in blocking mode"); + save_components( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + } + } else { + warn!( + "Save already in progress while trying to start autosave interval. If this was due to autosaves taking too long, consider increasing your autosave interval." + ); + } + } + } else { + // Ensure that the saving Window is on screen when the window freezes + if tick >= state_machine_ref.autosave_interval - 10 || tick <= 5 { + let progress = if tick > 1 && tick <= 5 { 1.0 } else { 0.0 }; + if tick < state_machine_ref.last_tick_seen_for_autosave { + let _timer = Timer::new("Saving"); + save_components( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + } + Window::new("Saving...").default_open(true).show(ctx, |ui| { + ui.add(ProgressBar::new(progress).corner_radius(0.0)); + }); + } + } + state_machine_ref.last_tick_seen_for_autosave = tick; + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] ui.vertical_centered(|ui|{ ui.label( @@ -1908,9 +1987,8 @@ pub fn render_ui< ); }); + #[cfg(not(target_arch = "wasm32"))] if let Some(recv) = &mut state_machine_ref.current_fork_save_in_progress { - const NUM_STATES: u8 = 12; - let mut v = [0]; if let Err(e) = recv.recv.read_exact(&mut v) { if e.kind() != std::io::ErrorKind::WouldBlock { @@ -1920,7 +1998,7 @@ pub fn render_ui< } else { if let Some(current_state) = v.last() { recv.current_state = *current_state; - if recv.current_state == NUM_STATES { + if recv.current_state == crate::saving::FORK_SAVE_STAGES as u8 { state_machine_ref.current_fork_save_in_progress = None; } } @@ -1928,27 +2006,52 @@ pub fn render_ui< if let Some(recv) = &state_machine_ref.current_fork_save_in_progress { Window::new("Saving...").default_open(true).show(ctx, |ui| { ui.add( - ProgressBar::new(recv.current_state as f32 / NUM_STATES as f32) - .corner_radius(0.0), + ProgressBar::new( + recv.current_state as f32 / crate::saving::FORK_SAVE_STAGES as f32, + ) + .corner_radius(0.0), ); }); } } - if state_machine_ref.escape_menu_open { + if state_machine_ref.open_windows[action_state_machine::Window::Escape] { if let Some(escape_action) = Modal::new("Pause Window".into()) .show(ctx, |ui| { ui.heading("Paused"); - if ui.button("Save").clicked() { - save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); - } + + let in_wasm = cfg!(target_arch = "wasm32"); if ui - .add_enabled( + .add_enabled(!in_wasm, Button::new("Save")) + .on_disabled_hover_text("I currently do not support saving in the Browser yet") + .clicked() + { + save_components( + &aux_data.game_name, + Some(&format!("{}.save", &aux_data.game_name)), + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + } + + let enabled = { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + false + } + + #[cfg(not(target_arch = "wasm32"))] + { cfg!(target_os = "linux") - && state_machine_ref.current_fork_save_in_progress.is_none(), - Button::new("Save with fork"), - ) + && state_machine_ref.current_fork_save_in_progress.is_none() + } + }; + + if ui + .add_enabled(enabled, Button::new("Save with fork")) .on_disabled_hover_text(if !cfg!(target_os = "linux") { "Only available on Linux" } else { @@ -1956,17 +2059,29 @@ pub fn render_ui< }) .clicked() { - let recv = - save_with_fork(&*world, &*simulation_state, &*aux_data, data_store_ref); - if let Some(recv) = recv { - recv.set_nonblocking(true) - .expect("Could not set pipe to nonblocking!"); - state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { - recv, - current_state: 0, - }); + #[cfg(not(target_arch = "wasm32"))] + { + let recv = save_with_fork( + &aux_data.game_name, + Some(&format!("{}.save", &aux_data.game_name)), + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + if let Some(recv) = recv { + recv.set_nonblocking(true) + .expect("Could not set pipe to nonblocking!"); + state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { + recv, + current_state: 0, + }); + } } } + if ui.button("Reopen Tip Window").clicked() { + state_machine_ref.open_windows[action_state_machine::Window::Tip] = true; + } if ui.button("Main Menu").clicked() { return Some(EscapeMenuOptions::BackToMainMenu); } @@ -1977,6 +2092,21 @@ pub fn render_ui< .logarithmic(true), ); + let mut autosave_interval_minutes = + state_machine_ref.autosave_interval / 60 / (TICKS_PER_SECOND_LOGIC as u32); + ui.add( + egui::Slider::new(&mut autosave_interval_minutes, 1..=100) + .integer() + .custom_formatter(|v, _range| { + let value: u32 = v as u32; + + format!("{value} min") + }) + .text("Autosave interval"), + ); + state_machine_ref.autosave_interval = + autosave_interval_minutes * 60 * (TICKS_PER_SECOND_LOGIC as u32); + None }) .inner @@ -1993,30 +2123,57 @@ pub fn render_ui< }; let game_state_ref = &mut fake_game_state; - Window::new("Mouse Pos").default_open(true).show(ctx, |ui| { - ui.label( - format!( - "{:?}", - ActionStateMachine::::player_mouse_to_tile( - state_machine_ref.zoom_level, - state_machine_ref - .map_view_info - .unwrap_or(state_machine_ref.local_player_pos), - state_machine_ref.current_mouse_pos - ) - ) - .as_str(), - ) - }); + // Window::new("Mouse Pos").default_open(true).show(ctx, |ui| { + // ui.label( + // format!( + // "{:?}", + // ActionStateMachine::::player_mouse_to_tile( + // state_machine_ref.zoom_level, + // state_machine_ref + // .map_view_info + // .unwrap_or(state_machine_ref.local_player_pos), + // state_machine_ref.current_mouse_pos + // ) + // ) + // .as_str(), + // ) + // }); // TODO: Make this conditional - let mut open = state_machine_ref.hotbar_window_open; + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Hotbar]; Window::new("Customize Hotbar") .open(&mut open) .show(ctx, |ui| { state_machine_ref.hotbar_window(ui, data_store_ref); }); - state_machine_ref.hotbar_window_open = open; + state_machine_ref.open_windows[action_state_machine::Window::Hotbar] = open; + + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Tip]; + Window::new("Tip").open(&mut open).show(ctx, |ui| { + ui.label("Use Mining Drills to mine Resources, Smelt them in Furnaces and Assemble them into Science Packs. These can then be used in Laboratories to research new technologies."); + ui.label("Inserters can pull items out of machines, drop them onto Conveyor Belts, or load them back into different machines."); + ui.label("Machines require power to run. Power Poles supply nearby machines and extract power from nearby power sources."); + + ui.separator(); + + ui.label("Use [WASD] to move around."); + ui.label("Press [E] to open the hotbar customization menu."); + ui.label("Press [P] to open your production statistics."); + ui.label("Press [T] to open the technology tree."); + ui.label("Press [M] to switch to map view."); + ui.label("Use the [Scroll Wheel] to zoom in or out."); + ui.label("Use [Q] to pipette the entity under your cursor."); + ui.label("Use [Left Click] to place a held entity or Blueprint or inspect an entity."); + ui.label("Hold [Right Click] to deconstruct an entity."); + ui.label("Press [Ctrl + C] and start dragging to copy an area into a Blueprint."); + }); + state_machine_ref.open_windows[action_state_machine::Window::Tip] = open; + + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Datapedia]; + state_machine_ref + .datapedia + .show_window(&mut open, ctx, data_store_ref); + state_machine_ref.open_windows[action_state_machine::Window::Datapedia] = open; Window::new("Size") .fixed_size(egui::vec2(1920f32, 1080f32)) @@ -2047,26 +2204,6 @@ pub fn render_ui< }); }); - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - Window::new("Import BP") - .default_open(false) - .show(ctx, |ui| { - if ui.button("Import").clicked() { - if let Some(path) = rfd::FileDialog::new().pick_file() { - if let Ok(mut file) = File::open(path) { - let mut bp_string = BlueprintString(String::new()); - file.read_to_string(&mut bp_string.0) - .expect("Failed to read from file"); - - if let Ok(bp) = bp_string.try_into() { - state_machine_ref.state = - ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); - } - } - } - } - }); - let tech_response = Window::new("Tech") .anchor(Align2::RIGHT_TOP, [0.0, 0.0]) .title_bar(false) @@ -2076,7 +2213,8 @@ pub fn render_ui< if let Some(tech) = game_state_ref .simulation_state .tech_state - .current_technology + .research_queue + .first() { let (tech_cost_units, _tech_cost_items) = &data_store.technology_costs[tech.id as usize]; @@ -2100,10 +2238,7 @@ pub fn render_ui< data::RepeatableCostScaling::Linear { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), - data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, - } => todo!(), + data::RepeatableCostScaling::Exponential { .. } => todo!(), }; tech_cost_units += tech_cost_increase; @@ -2176,7 +2311,6 @@ pub fn render_ui< .include_y(0.0) .set_margin_fraction([0.0, 0.05].into()) .x_grid_spacer(|grid_input| { - let min_step = grid_input.base_step_size; (0..NUM_X_AXIS_TICKS[SCALE]) .map(|v| GridMark { value: v as f64 / (NUM_X_AXIS_TICKS[SCALE] as f64) @@ -2201,7 +2335,6 @@ pub fn render_ui< lower_dec = 1.0; } - dbg!(lower_dec); lower_dec = lower_dec * ticks_per_value / 60.0 / 60.0; (0..40) @@ -2257,20 +2390,20 @@ pub fn render_ui< } if tech_response.clicked() { - state_machine_ref.technology_panel_open = true; + state_machine_ref.open_windows[action_state_machine::Window::Technology] = true; } Window::new("DEBUG USE WITH CARE") .default_open(false) .show(ctx, |ui| { - if ui.button("⚠️DEFRAGMENT GAMESTATE").clicked() { - // TODO: + // TODO: + // if ui.button("⚠️DEFRAGMENT GAMESTATE").clicked() { // let mut new_state = game_state_ref.clone(); // mem::swap(&mut new_state, &mut *game_state_ref); // mem::drop(new_state); - } + // } if ui.button("Switch from generation assemblers to miners (inserter_transfer)").clicked() { for entity in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()) { @@ -2299,7 +2432,7 @@ pub fn render_ui< actions.push(ActionType::Remove(assembler_pos)); - let area = data_store.mining_drill_info[1].size(*rotation); + let area = data_store.mining_drill_info[0].size(*rotation); for x in assembler_pos.x..(assembler_pos.x + i32::from(area[0])) { for y in assembler_pos.y..(assembler_pos.y + i32::from(area[1])) { @@ -2310,7 +2443,7 @@ pub fn render_ui< } } - let inserter_rotation = game_state_ref.world.get_entities_colliding_with(Position { + let inserter_rotation = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { x: assembler_pos.x - i32::from(data_store.max_inserter_search_range), y: assembler_pos.y - i32::from(data_store.max_inserter_search_range), }, (assembler_size.0 + 2 * u16::from(data_store.max_inserter_search_range), assembler_size.1 + 2 * u16::from(data_store.max_inserter_search_range)), data_store_ref).into_iter().find_map(|inserter_entity| { @@ -2332,7 +2465,7 @@ pub fn render_ui< entities: EntityPlaceOptions::Single(PlaceEntityType::MiningDrill { pos: assembler_pos, rotation: inserter_rotation, - ty: 1, + ty: 0, }), force: false, })); @@ -2374,7 +2507,7 @@ pub fn render_ui< let assembler_pos = entity.get_pos(); let assembler_size = entity.get_entity_size(data_store_ref); - for inserter_entity in game_state_ref.world.get_entities_colliding_with(Position { + for inserter_entity in game_state_ref.world.get_entities_in_chunks_colliding_with(Position { x: assembler_pos.x - i32::from(data_store.max_inserter_search_range), y: assembler_pos.y - i32::from(data_store.max_inserter_search_range), }, (assembler_size.0 + 2 * u16::from(data_store.max_inserter_search_range), assembler_size.1 + 2 * u16::from(data_store.max_inserter_search_range)), data_store_ref) { @@ -2458,30 +2591,26 @@ pub fn render_ui< if ui.button("⚠️Auto Clock Inserters").clicked() { let inserters_without_values_set = game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { Entity::Inserter { ty, user_movetime, direction, pos, info, .. } => { - if user_movetime.is_none() { - match info { - crate::frontend::world::tile::InserterInfo::NotAttached { .. } => None, - crate::frontend::world::tile::InserterInfo::Attached { info } => match info { - crate::frontend::world::tile::AttachedInserter::BeltStorage {id, belt_pos, .. } => { - let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); - let start_pos = - data_store.inserter_start_pos(*ty, *pos, *direction); - let end_pos = - data_store.inserter_end_pos(*ty, *pos, *direction); - Some((ty, pos, start_pos, end_pos, item, true)) - }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => None, - crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => { - let start_pos = + match info { + crate::frontend::world::tile::InserterInfo::NotAttached { .. } => None, + crate::frontend::world::tile::InserterInfo::Attached { info } => match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage {id, belt_pos, .. } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + let start_pos = data_store.inserter_start_pos(*ty, *pos, *direction); let end_pos = data_store.inserter_end_pos(*ty, *pos, *direction); - Some((ty, pos, start_pos, end_pos, *item, false)) - }, + Some((ty, pos, start_pos, end_pos, item, true)) }, - } - } else { - None + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => None, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => { + let start_pos = + data_store.inserter_start_pos(*ty, *pos, *direction); + let end_pos = + data_store.inserter_end_pos(*ty, *pos, *direction); + Some((ty, pos, start_pos, end_pos, *item, false)) + }, + }, } }, _ => None, @@ -2492,7 +2621,7 @@ pub fn render_ui< if let Some(e) = game_state_ref.world.get_entity_at(end_pos, data_store_ref) { match e { - Entity::Assembler {info, .. } => { + Entity::Assembler {pos: assembler_pos, ty: assembler_ty, rotation: assembler_rotation, info, .. } => { match info { AssemblerInfo::UnpoweredNoRecipe => {}, AssemblerInfo::Unpowered(_) => {}, @@ -2501,11 +2630,73 @@ pub fn render_ui< let (_, _, count_in_recipe) = data_store_ref.recipe_to_items_and_amounts[id.recipe.into_usize()].iter().find(|(dir, recipe_item, _)| *dir == ItemRecipeDir::Ing && item == *recipe_item).unwrap(); let time_per_recipe = data_store_ref.recipe_timers[usize_from(id.recipe.id)] as f32; - let AssemblerOnclickInfo { base_speed, speed_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); + let AssemblerOnclickInfo { base_speed, speed_mod, prod_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); let crafting_speed = base_speed * (1.0 + speed_mod); - let time_per_craft = time_per_recipe / crafting_speed; + let mut crafts_per_tick = crafting_speed / time_per_recipe; + + let mut outputs = data_store.recipe_to_items_and_amounts[id.recipe.into_usize()] + .iter() + .filter(|(dir, _, _)| *dir == data::ItemRecipeDir::Out).map(|(_, item ,amount_in_recipe)| (*item, *amount_in_recipe, 0.0)) + .collect_vec(); + + let inserters = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { + x: assembler_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: assembler_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).0, + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).1 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); + + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime)) + 2.0); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + for (item, amount) in inserters { + outputs.iter_mut().find(|(list_item, _, _)| *list_item == item).unwrap().2 += amount; + } - let items_needed_per_tick = *count_in_recipe as f32 / time_per_craft; + for (item, amount_in_recipe, amount_removed_by_inserters_per_tick) in outputs { + let recipes_per_tick = amount_removed_by_inserters_per_tick / (f32::from(amount_in_recipe) * (1.0 + prod_mod)); + + + if !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { + if recipes_per_tick < crafts_per_tick { + crafts_per_tick = recipes_per_tick; + } + + } + } + + + let items_needed_per_tick = *count_in_recipe as f32 * crafts_per_tick; // FIXME: Take tech level into consideration let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; @@ -2522,18 +2713,71 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); }, } }, + Entity::Chest { ty: chest_ty, pos: chest_pos, item, slot_limit } => { + let inserters = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { + x: chest_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: chest_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 3, u16::from(data_store_ref.max_inserter_search_range) * 3 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*chest_pos, data_store_ref.chest_tile_sizes[*chest_ty as usize]) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); + + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime))); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + let outgoing_amount: f32 = inserters.map(|(_item, amount_per_tick)| amount_per_tick).sum(); + + let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; + + let full_rotations_needed_per_tick = outgoing_amount / hand_size; + + let full_rotation_time_in_ticks = 1.0 / full_rotations_needed_per_tick; + + let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; + + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); + } + _ => {} } } if let Some(e) = game_state_ref.world.get_entity_at(start_pos, data_store_ref) { match e { - Entity::Assembler { info, .. } => { + Entity::Assembler { pos: assembler_pos, ty: assembler_ty, rotation: assembler_rotation, info, .. } => { match info { AssemblerInfo::UnpoweredNoRecipe => {}, AssemblerInfo::Unpowered(_) => {}, @@ -2544,9 +2788,68 @@ pub fn render_ui< let AssemblerOnclickInfo { base_speed, speed_mod, prod_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); let crafting_speed = base_speed * (1.0 + speed_mod); - let time_per_craft = time_per_recipe / crafting_speed; + let mut crafts_per_tick = crafting_speed / time_per_recipe; + + let mut outputs = data_store.recipe_to_items_and_amounts[id.recipe.into_usize()] + .iter() + .filter(|(dir, _, _)| *dir == data::ItemRecipeDir::Out).map(|(_, item ,amount_in_recipe)| (*item, *amount_in_recipe, 0.0)) + .collect_vec(); + + let inserters = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { + x: assembler_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: assembler_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).0, + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).1 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); - let items_produced_per_tick = (*count_in_recipe as f32 * (1.0 + prod_mod)) / time_per_craft; + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime)) + 2.0); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + for (item, amount) in inserters { + outputs.iter_mut().find(|(list_item, _, _)| *list_item == item).unwrap().2 += amount; + } + + for (list_item, amount_in_recipe, amount_removed_by_inserters_per_tick) in outputs { + let recipes_per_tick = amount_removed_by_inserters_per_tick / (f32::from(amount_in_recipe) * (1.0 + prod_mod)); + + if list_item != item && !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { + if recipes_per_tick < crafts_per_tick { + crafts_per_tick = recipes_per_tick; + } + } + } + + let items_produced_per_tick = (*count_in_recipe as f32 * (1.0 + prod_mod)) * crafts_per_tick; // FIXME: Take tech level into consideration let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; @@ -2557,7 +2860,7 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16); + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); }, } }, @@ -2573,6 +2876,34 @@ pub fn render_ui< ActionType::OverrideInserterMovetime { pos, new_movetime: Some(time.try_into().unwrap()) } })) } + + // if ui.button("⚠️Auto Clock Inserters (SLOW!)").clicked() { + // let mut clocking_state = crate::clocking::ClockingState::default(); + + // for assembler in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Assembler { ..})) { + // clocking_state.assume_enough_ingredients(assembler, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + + // for _ in 0..5 { + // for inserter in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Inserter { ..})) { + // clocking_state.get_clocking(inserter, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + + // for assembler in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Assembler { ..})) { + // clocking_state.calculate_machine_slowdown(assembler, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + // } + + // actions.extend(game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Inserter { ..})).map(|inserter| { + // let pos = inserter.get_pos(); + + // let ticks = clocking_state.get_clocking(inserter, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + + + // ActionType::OverrideInserterMovetime { pos, new_movetime: ticks.map(|v| v.try_into().unwrap()) } + // })); + // } + if ui.button("Remove Clocking from all Inserters").clicked() { let inserters_without_values_set = game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { Entity::Inserter { pos, info, .. } => { @@ -2612,12 +2943,40 @@ pub fn render_ui< } else { 'P' }))); + if ui.button("Find additional Grid").clicked() { + let id = game_state_ref.simulation_state.factory.power_grids.power_grids.iter().enumerate().filter(|(_id, grid)| !grid.is_placeholder).nth(1); + + if let Some((id, _grid)) = id { + let pole_pos = game_state_ref.simulation_state.factory.power_grids.pole_pos_to_grid_id.iter().find(|(k, v)| **v as usize == id).unwrap().0; + + state_machine_ref.local_player_pos = (pole_pos.x as f32, pole_pos.y as f32); + } + } + + + #[cfg(feature = "debug-stat-gathering")] + CollapsingHeader::new("Gathered Debug Stats").show(ui, |ui| { + let num_locs_searched = NUM_BELT_LOCS_SEARCHED.load(std::sync::atomic::Ordering::Relaxed); + let num_cache_hits = NUM_BELT_FREE_CACHE_HITS.load(std::sync::atomic::Ordering::Relaxed); + let num_updates = NUM_BELT_UPDATES.load(std::sync::atomic::Ordering::Relaxed); + ui.label(&format!("BeltUpdates: {}, BeltCacheHits: {}, Cache ratio: {:.2}%", num_updates, num_cache_hits,num_cache_hits as f64 / num_updates as f64 * 100.0 )); + ui.label(&format!("BeltLocsSearched: {}, LocsPerUpdate: {:.2}", num_locs_searched, num_locs_searched as f64 / num_updates as f64 )); + + + let inserter_update_calls = NUM_BELT_INSERTER_UPDATES.load(std::sync::atomic::Ordering::Relaxed); + let inserter_update_skips = TIMES_ALL_INCOMING_EARLY_RETURN.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_item = NUM_INSERTER_LOADS_WAITING_FOR_ITEMS.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_space = NUM_INSERTER_LOADS_WAITING_FOR_SPACE.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_space_waster = NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL.load(std::sync::atomic::Ordering::Relaxed); + let inserter_extractions = TIMES_INSERTERS_EXTRACTED.load(std::sync::atomic::Ordering::Relaxed); + ui.label(&format!("Belts updated: {}, percentage skipped: {:.2}", inserter_update_calls, (inserter_update_skips) as f64 / inserter_update_calls as f64)); + ui.label(&format!("Total inserter loads: {}, Avg per belt: {:.2}", inserter_loads_waiting_for_item + inserter_loads_waiting_for_space, (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 / inserter_update_calls as f64)); + ui.label(&format!("Loads waiting for item: {}, {:.2}", inserter_loads_waiting_for_item, inserter_loads_waiting_for_item as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Loads waiting for space: {}, {:.2}", inserter_loads_waiting_for_space, inserter_loads_waiting_for_space as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Loads waiting for space, while space is guaranteed filled: {}, {:.2}", inserter_loads_waiting_for_space_waster, inserter_loads_waiting_for_space_waster as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Extractions: {}, Avg ticks before extraction: {:.2}", inserter_extractions, (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 / inserter_extractions as f64 )); + }); - let num_locs_searched = NUM_BELT_LOCS_SEARCHED.load(std::sync::atomic::Ordering::Relaxed); - let num_cache_hits = NUM_BELT_FREE_CACHE_HITS.load(std::sync::atomic::Ordering::Relaxed); - let num_updates = NUM_BELT_UPDATES.load(std::sync::atomic::Ordering::Relaxed); - ui.label(&format!("BeltUpdates: {}, BeltCacheHits: {}, Cache ratio: {:.2}%", num_updates, num_cache_hits,num_cache_hits as f64 / num_updates as f64 * 100.0 )); - ui.label(&format!("BeltLocsSearched: {}, LocsPerUpdate: {:.2}", num_locs_searched, num_locs_searched as f64 / num_updates as f64 )); if ui.button("Remove Infinity Batteries").clicked() { for entity in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()) { @@ -2632,6 +2991,37 @@ pub fn render_ui< } } + CollapsingHeader::new("Inserter Counts").show(ui, |ui| { + let mut storage_storage = 0; + let mut belt_storage = 0; + let mut belt_belt = 0; + + + for inserter_info in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { + Entity::Inserter { + info: crate::frontend::world::tile::InserterInfo::Attached { + info + }, + .. + } => { + Some(info) + }, + + _ => None, + }) { + match inserter_info { + crate::frontend::world::tile::AttachedInserter::StorageStorage { ..} => storage_storage += 1, + crate::frontend::world::tile::AttachedInserter::BeltStorage { ..} => belt_storage += 1, + crate::frontend::world::tile::AttachedInserter::BeltBelt { ..} => belt_belt += 1, + } + } + + ui.label(&format!("StorageStorage: {}", storage_storage)); + ui.label(&format!("BeltStorage: {}", belt_storage)); + ui.label(&format!("BeltBelt: {}", belt_belt)); + + }); + CollapsingHeader::new("Lab analysis").show(ui, |ui| { let mut items = vec![0u32; data_store.item_names.len()]; @@ -2745,7 +3135,7 @@ pub fn render_ui< .storage_storage_inserters .inserters .iter() - .flat_map(|tree: &std::collections::BTreeMap| tree.values()) + .flat_map(|tree| tree.values()) .map(|(store, )| store.get_load_info()) .map(|(_, _, num_storage_cachelines, num_struct_cachelines)| { num_storage_cachelines + num_struct_cachelines @@ -2914,6 +3304,8 @@ pub fn render_ui< game_state_ref.simulation_state.factory.belts )); }); + + ui.label(&format!("Number of generated chunks: {}", game_state_ref.world.chunks.num_chunks)); }); Window::new("UPS").default_open(true).show(ctx, |ui| { @@ -2977,47 +3369,72 @@ pub fn render_ui< }); }); - Window::new("BP").default_open(false).show(ctx, |ui| { - let bp = if let ActionStateMachineState::Holding(HeldObject::Blueprint(bp)) = - &state_machine_ref.state - { - Some(bp) - } else { - None - }; + Window::new("Blueprint") + .default_open(false) + .show(ctx, |ui| { + let is_wasm = cfg!(target_arch = "wasm32"); - if ui - .add_enabled(bp.is_some(), Button::new("Copy Blueprint String")) - .clicked() - { - let s: BlueprintString = bp.cloned().unwrap().into(); - ctx.copy_text(s.0); - } + if ui + .add_enabled(!is_wasm, Button::new("Import")) + .on_disabled_hover_text("Disabled on WASM for now") + .clicked() + { + #[cfg(not(target_arch = "wasm32"))] + if let Some(path) = rfd::FileDialog::new().pick_file() { + if let Ok(mut file) = File::open(path) { + let mut bp_string = BlueprintString(String::new()); + file.read_to_string(&mut bp_string.0) + .expect("Failed to read from file"); - if ui - .add_enabled(bp.is_some(), Button::new("Write Blueprint String to file")) - .clicked() - { - let s: BlueprintString = bp.cloned().unwrap().into(); - let mut file = File::create("saved.bp").unwrap(); - file.write(s.0.as_bytes()).unwrap(); - } + if let Ok(bp) = bp_string.try_into() { + state_machine_ref.state = + ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); + } + } + } + } - if ui - .add_enabled( - bp.is_some(), - Button::new("Write Blueprint binary data to file"), - ) - .clicked() - { - let v: Vec = bitcode::serialize(bp.unwrap()).unwrap(); - let file = File::create("saved_binary.bp").unwrap(); - let mut encoder = ZlibEncoder::new(file, Compression::best()); - encoder.write_all(&v).unwrap(); - encoder.finish().unwrap(); - } - }); + let bp = if let ActionStateMachineState::Holding(HeldObject::Blueprint(bp)) = + &state_machine_ref.state + { + Some(bp) + } else { + None + }; + + if ui + .add_enabled(bp.is_some(), Button::new("Copy Blueprint String")) + .clicked() + { + let s: BlueprintString = bp.cloned().unwrap().into(); + ctx.copy_text(s.0); + } + + if ui + .add_enabled(bp.is_some(), Button::new("Write Blueprint String to file")) + .clicked() + { + let s: BlueprintString = bp.cloned().unwrap().into(); + let mut file = File::create("saved.bp").unwrap(); + file.write(s.0.as_bytes()).unwrap(); + } + + if ui + .add_enabled( + bp.is_some(), + Button::new("Write Blueprint binary data to file"), + ) + .clicked() + { + let v: Vec = bitcode::serialize(bp.unwrap()).unwrap(); + let file = File::create("saved_binary.bp").unwrap(); + let mut encoder = ZlibEncoder::new(file, Compression::best()); + encoder.write_all(&v).unwrap(); + encoder.finish().unwrap(); + } + }); + #[cfg(debug_assertions)] Window::new("RawData").default_open(false).show(ctx, |ui| { let raw = get_raw_data_test(); @@ -3068,7 +3485,7 @@ pub fn render_ui< // TODO: Once a dropdown with a low number of items is shown, all future dropdowns get cropped to that count :/ ComboBox::new(format!("Recipe list {}", *ty), "Recipes").selected_text(goal_recipe.map(|recipe| data_store_ref.recipe_display_names[usize_from(recipe.id)].as_str()).unwrap_or("Choose a recipe!")).show_ui(ui, |ui| { - data_store_ref.recipe_display_names.iter().enumerate().filter(|(i, recipe_name)| { + data_store_ref.recipe_display_names.iter().enumerate().filter(|(i, _recipe_name)| { (aux_data.settings.show_unresearched_recipes || game_state_ref.simulation_state.tech_state.get_active_recipes()[*i]) && data_store_ref.recipe_allowed_assembling_machines[*i].contains(ty) }).for_each(|(i, recipe_name)| { @@ -3126,6 +3543,9 @@ pub fn render_ui< prod_mod, power_consumption_mod, base_power_consumption, + + #[cfg(feature = "assembler-craft-tracking")] + times_craft_finished } = game_state_ref .simulation_state .factory @@ -3184,6 +3604,10 @@ pub fn render_ui< ui.label(format!("Productivity: {:.1}%", prod_mod * 100.0)); ui.label(format!("Max Consumption: {}({:+.0}%)", Watt((base_power_consumption.0 as f64 * (1.0 + power_consumption_mod as f64)) as u64), power_consumption_mod * 100.0)); + + #[cfg(feature = "assembler-craft-tracking")] + ui.label(format!("Crafts finished: {}", times_craft_finished)); + } } }, @@ -3245,7 +3669,7 @@ pub fn render_ui< }); TableBuilder::new(ui).columns(Column::auto(), 2).body(|body| { - body.rows(1.0, pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()+ pg.num_labs_of_type.len(), |mut row| { + body.rows(1.0, pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()+ pg.num_labs_of_type.len()+ pg.main_accumulator_count.len(), |mut row| { let i = row.index(); if i < pg.num_assemblers_of_type.len() { @@ -3268,8 +3692,15 @@ pub fn render_ui< }); row.col(|ui| {ui.add(Label::new(format!("{}", pg.num_beacons_of_type[i])).extend());}); - } else { + } else if i < pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len() + pg.main_accumulator_count.len() { let i = i - (pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()); + row.col(|ui| { + ui.add(Label::new(&data_store_ref.accumulator_info[i].display_name).extend()); + + }); + row.col(|ui| {ui.add(Label::new(format!("{}", pg.main_accumulator_count[i])).extend());}); + } else { + let i = i - (pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len() + pg.main_accumulator_count.len()); row.col(|ui| { ui.add(Label::new(&data_store_ref.lab_info[i].display_name).extend()); @@ -3384,9 +3815,7 @@ pub fn render_ui< // TODO: }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { - item, - inserter, + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => { ui.label("BeltBelt"); // TODO: @@ -3407,7 +3836,7 @@ pub fn render_ui< if movetime_overridden { let mut movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); - ui.add(egui::Slider::new(&mut movetime, (data_store.inserter_infos[*ty as usize].swing_time_ticks)..=u16::MAX).text("Ticks per half swing")); + ui.add(egui::Slider::new(&mut movetime, (data_store.inserter_infos[*ty as usize].swing_time_ticks)..=NonZero::::MAX).text("Ticks per half swing")); if *user_movetime != Some(movetime.try_into().unwrap()) { actions.push(ActionType::OverrideInserterMovetime { pos: *pos, new_movetime: Some(movetime.try_into().unwrap()) }); @@ -3537,14 +3966,53 @@ pub fn render_ui< }, } }, - Entity::SolarPanel { .. } => { - // TODO + Entity::SolarPanel { ty, .. } => { + ui.label(format!("{}", &data_store.solar_panel_info[*ty as usize].display_name)); + + let current = data_store.solar_panel_info[*ty as usize].power_output.get_at_time(aux_data.current_tick as u32); + let max = data_store.solar_panel_info[*ty as usize].power_output.max(); + + let perc = current.0 as f32 / max.0 as f32; + + ui.add(ProgressBar::new(perc).text(format!("{}/{}", current, max)).corner_radius(0.0)); }, - Entity::Accumulator { .. } => { - // TODO + Entity::Accumulator { ty, pole_position, .. } => { + ui.label(format!("{}", &data_store.accumulator_info[*ty as usize].display_name)); + + let max_charge = data_store.accumulator_info[*ty as usize].max_charge; + let charge = if let Some(pole_pos) = pole_position { + let grid = game_state_ref.simulation_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos.0]; + let grid = &game_state_ref.simulation_state.factory.power_grids.power_grids[grid as usize]; + let charge = grid.main_accumulator_charge[*ty as usize] / grid.main_accumulator_count[*ty as usize]; + + charge + } else { + // FIXME: A unconnected accumulator can still have charge + Joule(0) + }; + + let perc = charge.0 as f32 / max_charge.0 as f32; + + ui.add(ProgressBar::new(perc).text(format!("{}/{}", charge, max_charge)).corner_radius(0.0)); }, - Entity::Beacon { .. } => { - // TODO + Entity::Beacon { ty, modules, .. } => { + ui.label(&data_store.beacon_info[*ty as usize].display_name); + + // Render module slots + let modules = &game_state_ref.world.module_slot_dedup_table[*modules as usize]; + TableBuilder::new(ui).id_salt("Module Slots").columns(Column::auto(), modules.len()).body(|mut body| { + body.row(1.0, |mut row| { + for module in modules.iter() { + row.col(|ui| { + if let Some(module_id) = module { + ui.label(&data_store_ref.module_info[*module_id as usize].display_name); + } else { + ui.label("Empty Module Slot"); + } + }); + } + }); + }); }, Entity::FluidTank { ty, pos, rotation } => { let id = game_state_ref.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; @@ -3611,35 +4079,35 @@ pub fn render_ui< }, } - egui::Area::new("Hotbar".into()) - .anchor(Align2::CENTER_BOTTOM, (0.0, 0.0)) - .show(ui.ctx(), |ui| { - egui_extras::TableBuilder::new(ui) - .columns(Column::auto().resizable(false), 10) - .body(|mut body| { - body.row(30.0, |mut row| { - for i in 0..10 { - if row - .col(|ui| { - let button_response = ui.button(format!("{i}")); - - if button_response.hovered() { - dbg!(i); - } - }) - .1 - .hovered() - { - dbg!(i); - }; - } - }); - }); - }); + // egui::Area::new("Hotbar".into()) + // .anchor(Align2::CENTER_BOTTOM, (0.0, 0.0)) + // .show(ui.ctx(), |ui| { + // egui_extras::TableBuilder::new(ui) + // .columns(Column::auto().resizable(false), 10) + // .body(|mut body| { + // body.row(30.0, |mut row| { + // for i in 0..10 { + // if row + // .col(|ui| { + // let button_response = ui.button(format!("{i}")); + + // if button_response.hovered() { + // dbg!(i); + // } + // }) + // .1 + // .hovered() + // { + // dbg!(i); + // }; + // } + // }); + // }); + // }); Window::new("Technology") .collapsible(false) - .open(&mut state_machine_ref.technology_panel_open) + .open(&mut state_machine_ref.open_windows[action_state_machine::Window::Technology]) .show(ctx, |ui| { let research_actions = game_state_ref .simulation_state @@ -3660,7 +4128,7 @@ pub fn render_ui< Window::new("Statistics") .collapsible(false) - .open(&mut state_machine_ref.statistics_panel_open) + .open(&mut state_machine_ref.open_windows[action_state_machine::Window::Statistics]) .show(ctx, |ui| { let time_scale = match &mut state_machine_ref.statistics_panel { StatisticsPanel::Items(timescale) => timescale, @@ -3950,9 +4418,13 @@ pub fn render_ui< .product::(); let ticks_total = min( ticks_per_sample * NUM_SAMPLES_AT_INTERVALS[time_scale], - (aux_data.statistics.production.num_samples_pushed - - ticks_per_sample - + 1) + (aux_data + .statistics + .production + .num_samples_pushed + .checked_sub(ticks_per_sample) + .map(|v| v + 1) + .unwrap_or(0)) .next_multiple_of(ticks_per_sample), ); diff --git a/src/rendering/temp_assets/krastorio/energy-storage.png b/src/rendering/temp_assets/krastorio/energy-storage.png new file mode 100644 index 0000000..8629c67 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/energy-storage.png differ diff --git a/src/replays/mod.rs b/src/replays/mod.rs index 0b1b03f..48b207a 100644 --- a/src/replays/mod.rs +++ b/src/replays/mod.rs @@ -1,90 +1,77 @@ -use std::borrow::Borrow; -use std::future::Future; -use std::ops::ControlFlow; +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; use std::sync::Arc; -use parking_lot::Mutex; -use std::mem; +use itertools::Itertools; +use log::warn; -use std::path::PathBuf; +mod replay_action; -use genawaiter::GeneratorState::Complete; -use genawaiter::GeneratorState::Yielded; -use genawaiter::rc::{Gen, r#gen}; -use itertools::Itertools; +use crate::example_worlds::{self, ValueValue}; +use crate::progress_info::ProgressInfo; +use crate::replays::replay_action::{ReplayAction, ReplayActionError}; +use crate::{app_state::GameState, data::DataStore, frontend::action::ActionType, item::IdxTrait}; +use crate::{built_info, get_version}; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ProgramInformation { + // The git rev when this was recorded + game_version: String, + git_dirty: bool, + mod_sha: String, + mod_list: Vec<()>, +} +impl ProgramInformation { + pub(crate) fn new(data_store: &DataStore) -> Self { + Self { + game_version: get_version().to_string(), + git_dirty: built_info::GIT_DIRTY.unwrap_or(false), + mod_sha: data_store.checksum.clone(), + mod_list: vec![], + } + } +} -use crate::{ - app_state::GameState, - data::DataStore, - frontend::action::ActionType, - item::{IdxTrait, WeakIdxTrait}, -}; +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenerationInformation { + // The example world (and settings) which were used + pub(crate) example_idx: usize, + pub(crate) example_settings: Vec, +} -// TODO: Keyframe support #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Replay< - ItemIdxType: WeakIdxTrait, - RecipeIdxType: WeakIdxTrait, - DataStor: Borrow>, -> { - /// Compressed binary representation of the starting GameState - starting_state: Box<[u8]>, - pub actions: Vec>, +pub struct Replay { + program_info: ProgramInformation, + generation_info: GenerationInformation, - pub data_store: DataStor, + actions: Vec, current_timestep: u64, - end_timestep: Option, - - storage_location: Option, - - is_dummy: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ReplayAction { +struct ReplayTimedAction { timestamp: u64, - pub action: ActionType, + action: ReplayAction, } -impl< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - DataStor: Borrow>, -> Replay -{ - pub fn new( - game_state: &GameState, - storage_location: Option, - data_store: DataStor, +impl Replay { + pub(crate) fn new( + generation_info: GenerationInformation, + data_store: &DataStore, ) -> Self { - let game_state_bytes = bitcode::serialize(game_state).unwrap(); Self { - starting_state: game_state_bytes.into_boxed_slice(), + program_info: ProgramInformation::new(data_store), + generation_info, actions: vec![], - data_store, current_timestep: 0, end_timestep: None, - - storage_location, - - is_dummy: false, - } - } - - pub fn new_dummy(data_store: DataStor) -> Self { - Self { - starting_state: Box::new([]), - actions: vec![], - data_store, - current_timestep: 0, - end_timestep: None, - - storage_location: None, - - is_dummy: true, } } @@ -95,17 +82,15 @@ impl< .expect("Replay running longer than u64::MAX ticks!"); } - pub fn append_actions( + pub fn append_actions( &mut self, actions: impl IntoIterator>, + data_store: &DataStore, ) { - if self.is_dummy { - return; - } self.actions - .extend(actions.into_iter().map(|a| ReplayAction { + .extend(actions.into_iter().map(|a| ReplayTimedAction { timestamp: self.current_timestep, - action: a, + action: ReplayAction::from_action(a, data_store), })); } @@ -115,165 +100,75 @@ impl< pub fn run( self, - ) -> ReplayViewer< - (GameState, DataStor), - impl Future, DataStor)>, - > { - assert!(!self.is_dummy); - ReplayViewer { - generator: r#gen!({ - let data_store = self.data_store; - - let mut actions = self.actions.into_iter().peekable(); - let mut current_timestep = 0; - - let mut game_state: GameState = - bitcode::deserialize(&*self.starting_state).unwrap(); - - // Free up the memory, so we do not store two copies of the GameState - mem::drop(self.starting_state); - - loop { - let this_ticks_actions = actions - .by_ref() - .peeking_take_while(|a| a.timestamp == current_timestep) - .map(|ra| ra.action); - - // FIXME: - // game_state.apply_actions(this_ticks_actions, data_store.borrow()); - - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - data_store.borrow(), - ); - - // let game_state_opt: Option> = - // yield_!(game_state); - - // game_state = game_state_opt.unwrap(); - - if Some(current_timestep) == self.end_timestep { - break; - } else { - current_timestep += 1; - } - } - - (game_state, data_store) - }), - } - } - - pub fn run_with( - self, - game_state_out: Arc>>, - on_tick: impl Fn(), - ) { - dbg!(&self.end_timestep); - - let data_store = self.data_store; - + game_state: Arc>, + mut on_tick: impl FnMut(&GameState), + data_store: &DataStore, + ) -> Result>, ReplayActionError> { let mut actions = self.actions.into_iter().peekable(); let mut current_timestep = 0; - let game_state: GameState = - bitcode::deserialize(&*self.starting_state).unwrap(); + if data_store.checksum != self.program_info.mod_sha { + warn!("Mod SHA mismatch between replay recording and playback. Sync issues may appear"); + } - // Free up the memory, so we do not store two copies of the GameState - mem::drop(self.starting_state); + if self.program_info.game_version != get_version() && !cfg!(test) { + warn!( + "Game version mismatch between replay recording and playback. Sync issues may appear" + ); + } - *(game_state_out.lock()) = game_state; + let GameState { + world, + simulation_state, + aux_data, + } = &*game_state; + + let new_game_state: GameState = example_worlds::get_builder( + "REPLAY_WORLD".to_string(), + self.generation_info.example_idx, + self.generation_info.example_settings, + )(ProgressInfo::new(), data_store); + + { + *simulation_state.lock() = new_game_state.simulation_state.mutex.into_inner(); + *world.lock() = new_game_state.world.mutex.into_inner(); + *aux_data.lock() = new_game_state.aux_data.mutex.into_inner(); + } loop { let this_ticks_actions: Vec<_> = actions .by_ref() .peeking_take_while(|a| a.timestamp == current_timestep) - .map(|ra| ra.action) - .collect(); - - let game_state = game_state_out.lock(); - - // FIXME: - // GameState::apply_actions( - // game_state.simulation_state, - // game_state.world, - // this_ticks_actions, - // data_store.borrow(), - // ); - - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - data_store.borrow(), - ); - - on_tick(); - - // let game_state_opt: Option> = - // yield_!(game_state); + .map(|ra| ra.action.to_action(data_store)) + .try_collect()?; + + { + let mut sim_state = game_state.simulation_state.lock(); + let mut world = game_state.world.lock(); + + GameState::apply_actions( + &mut *sim_state, + &mut *world, + this_ticks_actions, + data_store, + ); + + GameState::update( + &mut *sim_state, + &mut *game_state.aux_data.lock(), + data_store, + ); + } - // game_state = game_state_opt.unwrap(); + on_tick(&game_state); if Some(current_timestep) == self.end_timestep { - break; + break Ok(game_state); } else { current_timestep += 1; } } } - - // fn save(&self) -> Result<(), ()> - // where - // DataStor: serde::Serialize, - // { - // match &self.storage_location { - // Some(path) => { - // // Ensure the folder exists - // create_dir_all(path).unwrap(); - - // let - - // let start = Instant::now(); - // // If we are in debug mode, save the replay to a file - // let mut file = File::create(path).expect("Could not open file"); - // let ser = bitcode::serialize(self).unwrap(); - // dbg!(start.elapsed()); - // file.write_all(ser.as_slice()) - // .expect("Could not write to file"); - // dbg!(start.elapsed()); - // Ok(()) - // }, - // None => Err(()), - // } - // } } -pub struct ReplayViewer> { - generator: Gen, F>, -} - -impl> ReplayViewer { - pub fn with(mut self, mut every_step: impl FnMut(&V) -> ControlFlow<(), ()>) -> V { - let mut gs = self.generator.resume_with(None); - - while let Yielded(v) = gs { - match every_step(&v) { - ControlFlow::Continue(()) => {}, - ControlFlow::Break(()) => return v, - } - - gs = self.generator.resume_with(Some(v)); - } - - let Complete(v) = gs else { unreachable!() }; - - let _ = every_step(&v); - - v - } -} - -pub fn run_till_finished(_: &V) -> ControlFlow<(), ()> { - ControlFlow::Continue(()) -} +pub fn run_till_finished(_: &GameState) {} diff --git a/src/replays/replay_action.rs b/src/replays/replay_action.rs new file mode 100644 index 0000000..a4b9bd1 --- /dev/null +++ b/src/replays/replay_action.rs @@ -0,0 +1,656 @@ +use std::num::NonZero; + +use petgraph::graph::NodeIndex; + +use crate::Position; +use crate::belt::splitter::SplitterDistributionMode; +use crate::data::DataStore; +use crate::frontend::action::place_entity::{EntityPlaceOptions, PlaceEntityInfo}; +use crate::frontend::action::set_recipe::SetRecipeInfo; +use crate::frontend::world::tile::Dir; +use crate::frontend::world::tile::PlaceEntityType; +use crate::frontend::world::tile::UndergroundDir; +use crate::item::{Indexable, Item, Recipe}; +use crate::{frontend::action::ActionType, item::IdxTrait}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct ReplayPlaceEntity { + pos: Position, + ty: String, + rotation: Dir, + + kind: ReplayPlaceEntityKind, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +enum ReplayPlaceEntityKind { + Assembler {}, + Inserter { + /// The Item the inserter will move, must fit both the in and output side + filter: Option, + + user_movetime: Option>, + }, + Belt {}, + Underground { + underground_dir: UndergroundDir, + }, + PowerPole {}, + Splitter { + in_mode: Option, + out_mode: Option, + }, + Chest {}, + SolarPanel {}, + Accumulator {}, + Lab {}, + Beacon {}, + FluidTank {}, + MiningDrill {}, +} +impl ReplayPlaceEntity { + fn from_entity_place( + ty: &PlaceEntityType, + data_store: &DataStore, + ) -> Self { + use ReplayPlaceEntityKind::*; + match ty.clone() { + PlaceEntityType::Assembler { pos, ty, rotation } => Self { + pos, + ty: data_store.assembler_info[ty as usize].name.to_string(), + rotation, + kind: Assembler {}, + }, + PlaceEntityType::Inserter { + ty, + pos, + dir, + filter, + user_movetime, + } => Self { + pos, + ty: data_store.inserter_infos[ty as usize].name.to_string(), + rotation: dir, + kind: Inserter { + filter: filter.map(|item| data_store.item_names[item.into_usize()].to_string()), + user_movetime, + }, + }, + PlaceEntityType::Belt { pos, direction, ty } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Belt {}, + }, + PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Underground { underground_dir }, + }, + PlaceEntityType::PowerPole { pos, ty } => Self { + pos, + ty: data_store.power_pole_data[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: PowerPole {}, + }, + PlaceEntityType::Splitter { + pos, + direction, + ty, + in_mode, + out_mode, + } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Splitter { in_mode, out_mode }, + }, + PlaceEntityType::Chest { pos, ty } => Self { + pos, + ty: data_store.chest_names[ty as usize].to_string(), + rotation: Dir::default(), + kind: Chest {}, + }, + PlaceEntityType::SolarPanel { pos, ty } => Self { + pos, + ty: data_store.solar_panel_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: SolarPanel {}, + }, + PlaceEntityType::Accumulator { pos, ty } => Self { + pos, + ty: data_store.accumulator_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Accumulator {}, + }, + PlaceEntityType::Lab { pos, ty } => Self { + pos, + ty: data_store.lab_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Lab {}, + }, + PlaceEntityType::Beacon { ty, pos } => Self { + pos, + ty: data_store.beacon_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Beacon {}, + }, + PlaceEntityType::FluidTank { ty, pos, rotation } => Self { + pos, + ty: data_store.fluid_tank_infos[ty as usize].name.to_string(), + rotation, + kind: FluidTank {}, + }, + PlaceEntityType::MiningDrill { ty, pos, rotation } => Self { + pos, + ty: data_store.mining_drill_info[ty as usize].name.to_string(), + rotation, + kind: MiningDrill {}, + }, + } + } + + fn to_entity_place( + self, + data_store: &DataStore, + ) -> Result, ReplayActionError> { + use PlaceEntityType::*; + let Self { + pos, + ty, + rotation, + kind, + } = self; + let ty = match kind { + ReplayPlaceEntityKind::Assembler {} => Assembler { + pos, + ty: data_store + .assembler_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Assembler {}", + ty + )))?, + rotation, + }, + ReplayPlaceEntityKind::Inserter { + filter, + user_movetime, + } => Inserter { + ty: data_store + .inserter_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Inserter {}", + ty + )))?, + pos, + dir: rotation, + filter: filter + .map(|filter| { + data_store + .item_names + .iter() + .position(|info| **info == *ty) + .map(|v| Item::from(A::try_from(v).unwrap())) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Item {}", + filter + ))) + }) + .transpose()?, + user_movetime, + }, + ReplayPlaceEntityKind::Belt {} => Belt { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Underground { underground_dir } => Underground { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + underground_dir, + }, + ReplayPlaceEntityKind::PowerPole {} => PowerPole { + pos, + ty: data_store + .power_pole_data + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Power Pole {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Splitter { in_mode, out_mode } => Splitter { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + in_mode, + out_mode, + }, + ReplayPlaceEntityKind::Chest {} => Chest { + pos, + ty: data_store + .chest_names + .iter() + .position(|info| **info == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Chest {}", + ty + )))?, + }, + ReplayPlaceEntityKind::SolarPanel {} => SolarPanel { + pos, + ty: data_store + .solar_panel_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Solar Panel {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Accumulator {} => Accumulator { + pos, + ty: data_store + .accumulator_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Accumulator {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Lab {} => Lab { + pos, + ty: data_store + .lab_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Lab {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Beacon {} => Beacon { + ty: data_store + .beacon_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Beacon {}", + ty + )))?, + pos, + }, + ReplayPlaceEntityKind::FluidTank {} => FluidTank { + ty: data_store + .fluid_tank_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing FluidTank {}", + ty + )))?, + pos, + rotation, + }, + ReplayPlaceEntityKind::MiningDrill {} => MiningDrill { + ty: data_store + .mining_drill_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Mining Drill {}", + ty + )))?, + pos, + rotation, + }, + }; + + Ok(ty) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(super) enum ReplayAction { + PlaceFloorTile { + pos: Position, + ty: String, + }, + PlaceEntity { + force: bool, + info: ReplayPlaceEntity, + }, + + SetRecipe { + pos: Position, + recipe: String, + }, + + OverrideInserterMovetime { + pos: Position, + new_movetime: Option>, + }, + + Position { + player: String, + pos: (f32, f32), + }, + + AddModules { + pos: Position, + modules: Vec, + }, + RemoveModules { + pos: Position, + indices: Vec, + }, + + SetChestSlotLimit { + pos: Position, + num_slots: u8, + }, + + Remove { + pos: Position, + }, + + AddResearchToQueue { + tech: String, + }, + + RemoveResearchFromQueue { + tech: String, + }, + + CheatUnlockTechnology { + tech: String, + }, + + CheatRelockTechnology { + tech: String, + }, + + PlaceOre { + pos: Position, + ore: String, + amount: u32, + }, + + SpawnPlayer {}, +} + +impl ReplayAction { + pub(super) fn from_action( + action: ActionType, + data_store: &DataStore, + ) -> Self { + match action { + ActionType::PlaceFloorTile(_) => todo!(), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(ty), + force, + }) => Self::PlaceEntity { + force, + info: ReplayPlaceEntity::from_entity_place(&ty, data_store), + }, + ActionType::SetRecipe(set_recipe_info) => Self::SetRecipe { + pos: set_recipe_info.pos, + recipe: data_store.recipe_names[set_recipe_info.recipe.into_usize()].to_string(), + }, + ActionType::OverrideInserterMovetime { pos, new_movetime } => { + Self::OverrideInserterMovetime { pos, new_movetime } + }, + ActionType::Position(id, pos) => Self::Position { + player: format!("{id}"), + pos, + }, + ActionType::AddModules { pos, modules } => Self::AddModules { + pos, + modules: modules + .into_iter() + .map(|module| data_store.module_info[module as usize].name.to_string()) + .collect(), + }, + ActionType::RemoveModules { pos, indices } => Self::RemoveModules { pos, indices }, + ActionType::SetChestSlotLimit { pos, num_slots } => { + Self::SetChestSlotLimit { pos, num_slots } + }, + ActionType::Remove(position) => Self::Remove { pos: position }, + ActionType::AddResearchToQueue { tech } => Self::AddResearchToQueue { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::RemoveResearchFromQueue { tech } => Self::RemoveResearchFromQueue { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::CheatUnlockTechnology { tech } => Self::CheatUnlockTechnology { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::CheatRelockTechnology { tech } => Self::CheatRelockTechnology { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::PlaceOre { pos, ore, amount } => Self::PlaceOre { + pos, + ore: data_store.item_names[ore.into_usize()].to_string(), + amount, + }, + ActionType::Ping(position) => todo!(), + ActionType::SpawnPlayer {} => Self::SpawnPlayer {}, + } + } + + pub(super) fn to_action( + self, + data_store: &DataStore, + ) -> Result, ReplayActionError> { + use ActionType::*; + let action = match self { + ReplayAction::PlaceFloorTile { pos, ty } => todo!(), + ReplayAction::PlaceEntity { force, info } => PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(info.to_entity_place(data_store)?), + force, + }), + ReplayAction::SetRecipe { pos, recipe } => SetRecipe(SetRecipeInfo { + pos, + recipe: Recipe::from( + B::try_from( + data_store + .recipe_names + .iter() + .position(|name| **name == *recipe) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing recipe {}", + recipe + )))?, + ) + .unwrap(), + ), + }), + ReplayAction::OverrideInserterMovetime { pos, new_movetime } => { + OverrideInserterMovetime { pos, new_movetime } + }, + ReplayAction::Position { player, pos } => Position(player.parse().unwrap(), pos), + ReplayAction::AddModules { pos, modules } => AddModules { + pos, + modules: modules + .into_iter() + .map(|mod_name| { + data_store + .module_info + .iter() + .position(|modinfo| *modinfo.name == *mod_name) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing module {}", + mod_name + ))) + }) + .try_collect()?, + }, + ReplayAction::RemoveModules { pos, indices } => RemoveModules { pos, indices }, + ReplayAction::SetChestSlotLimit { pos, num_slots } => { + SetChestSlotLimit { pos, num_slots } + }, + ReplayAction::Remove { pos } => Remove(pos), + ReplayAction::AddResearchToQueue { tech } => AddResearchToQueue { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::RemoveResearchFromQueue { tech } => RemoveResearchFromQueue { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::CheatUnlockTechnology { tech } => CheatUnlockTechnology { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::CheatRelockTechnology { tech } => CheatRelockTechnology { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::PlaceOre { pos, ore, amount } => PlaceOre { + pos, + ore: Item::from( + A::try_from( + data_store + .item_names + .iter() + .position(|name| **name == *ore) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing item {}", + ore + )))?, + ) + .unwrap(), + ), + amount, + }, + ReplayAction::SpawnPlayer {} => SpawnPlayer {}, + }; + + Ok(action) + } +} + +#[derive(Debug)] +pub enum ReplayActionError { + MissingDatastoreEntry(String), +} diff --git a/src/research.rs b/src/research.rs index 3ee004e..d7842d7 100644 --- a/src/research.rs +++ b/src/research.rs @@ -44,7 +44,8 @@ pub type ResearchProgress = u16; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] pub struct TechState { - pub current_technology: Option, + pub research_queue: Vec, + // Map from technologies to how many times they were completed pub finished_technologies: HashMap, pub in_progress_technologies: HashMap, @@ -93,7 +94,7 @@ impl TechState { .collect(); Self { - current_technology: None, + research_queue: vec![], finished_technologies, recipe_active, @@ -120,7 +121,7 @@ impl TechState { return; } - if let Some(current) = &self.current_technology { + if let Some(current) = self.research_queue.first() { let (tech_cost_units, tech_cost_items) = &data_store.technology_costs[current.id as usize]; let mut tech_cost_units = *tech_cost_units; @@ -141,10 +142,7 @@ impl TechState { data::RepeatableCostScaling::Linear { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), - data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, - } => todo!(), + data::RepeatableCostScaling::Exponential { .. } => todo!(), }; tech_cost_units += tech_cost_increase; @@ -212,10 +210,11 @@ impl TechState { { self.recipe_active[recipe.into_usize()] = true; } - if is_repeating { + // TODO: Do not autorepeat always + if is_repeating && self.research_queue.len() == 1 { // Just keep researching the same tech (just one level higher) } else { - self.current_technology = None; + self.research_queue.remove(0); } // Since we only check if a tech is finished at the end of each update, it is possible we produced more science progress in this tick, than was required. @@ -247,6 +246,7 @@ impl TechState { data_store: &DataStore, ) -> Graph, (), Directed, u16, DefaultNodeShape, DefaultEdgeShape> { + // TODO: This seems to be called every frame??? egui_graphs::to_graph_custom::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape>( &data_store.technology_tree, |node| { @@ -331,6 +331,10 @@ impl TechState { data_store: &DataStore, ) -> impl Iterator> + use { + use itertools::Itertools; + + debug_assert!(self.research_queue.iter().all_unique()); + { profiling::scope!("Update Tech Tree colors"); for tech in 0..data_store.technology_costs.len() { @@ -397,7 +401,7 @@ impl TechState { SidePanel::new(egui::panel::Side::Left, "Current Technology Info Sidepanel").show_inside( ui, |ui| { - if let Some(tech) = &self.current_technology { + if let Some(tech) = self.research_queue.first() { let is_repeating = data_store .technology_tree .node_weight(NodeIndex::new(tech.id.into())) @@ -451,8 +455,8 @@ impl TechState { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, + unit_multiplier_per_level_nom: _, + unit_multiplier_per_level_denom: _, } => todo!(), }; @@ -465,10 +469,44 @@ impl TechState { ); if ui.button("Cancel").clicked() { - ret.push(ActionType::SetActiveResearch { tech: None }); + ret.push(ActionType::RemoveResearchFromQueue { tech: *tech }); } else if ui.button("[CHEAT] Unlock Technology").clicked() { ret.push(ActionType::CheatUnlockTechnology { tech: *tech }); } + + ui.separator(); + + use egui_extras::{Column, TableBuilder}; + + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::auto()) + .id_salt("Research Queue") + .body(|body| { + body.rows(1.0, self.research_queue.len() - 1, |mut row| { + let idx = row.index(); + + let tech = &self.research_queue[idx + 1]; + + row.col(|ui| { + ui.label( + &data_store + .technology_tree + .node_weight(NodeIndex::from(tech.id)) + .unwrap() + .name, + ); + }); + + row.col(|ui| { + if ui.button("X").clicked() { + ret.push(ActionType::RemoveResearchFromQueue { + tech: *tech, + }); + } + }); + }); + }); } }, ); @@ -487,10 +525,16 @@ impl TechState { } if !render_graph.selected_nodes().is_empty() { + use crate::data::TechnologyEffect; + let [selected_node] = render_graph.selected_nodes() else { unreachable!("We only allow selecting a single node!"); }; + let selected_tech = Technology { + id: selected_node.index().try_into().unwrap(), + }; + ui.label( &data_store .technology_tree @@ -511,9 +555,7 @@ impl TechState { false } else { self.finished_technologies - .get(&Technology { - id: selected_node.index().try_into().unwrap(), - }) + .get(&selected_tech) .copied() .unwrap_or(0) > 0 @@ -531,10 +573,7 @@ impl TechState { .unwrap_or(0) > 0 }); - - let is_currently_researching = Some(Technology { - id: selected_node.index().try_into().unwrap(), - }) == self.current_technology; + let is_currently_researching = self.research_queue.contains(&selected_tech); if ui .add_enabled( @@ -543,28 +582,40 @@ impl TechState { ) .clicked() { - ret.push(ActionType::SetActiveResearch { - tech: Some(Technology { - id: selected_node.index().try_into().unwrap(), - }), + ret.push(ActionType::AddResearchToQueue { + tech: selected_tech, }); } if is_done { if ui.button("[CHEAT] Undo Technology").clicked() { ret.push(ActionType::CheatRelockTechnology { - tech: Technology { - id: selected_node.index().try_into().unwrap(), - }, + tech: selected_tech, }); } } + + // Render Tech Effect + let TechnologyEffect { unlocked_recipes } = &data_store + .technology_tree + .node_weight(NodeIndex::from(selected_tech.id)) + .unwrap() + .effect; + + // Unlocked Recipes + if !unlocked_recipes.is_empty() { + ui.label("Unlocks Recipes:"); + for recipe in unlocked_recipes { + ui.label(&data_store.recipe_display_names[recipe.into_usize()]); + } + } } }, ); let mut view = GraphView::<_, _, _, _, _, _, LayoutStateTree, LayoutTree>::new(render_graph) + .with_id(Some("Tech Tree".to_string())) .with_navigations( &SettingsNavigation::new() .with_fit_to_screen_enabled(false) diff --git a/src/saving/loading.rs b/src/saving/loading.rs new file mode 100644 index 0000000..7e2654b --- /dev/null +++ b/src/saving/loading.rs @@ -0,0 +1,54 @@ +use std::path::{Path, PathBuf}; + +use crate::saving::{ + LoadError, + save_file_settings::{SaveFileInfo, StoredSaveFileInfo}, + try_load_at, +}; + +#[derive(Debug)] +pub struct SaveFileList { + pub(crate) save_files: Vec>, +} + +#[derive(Debug)] +pub(crate) enum SaveFileError { + CouldNotOpenSaveFile(std::io::Error), + LoadError(LoadError), +} + +impl SaveFileList { + pub(crate) fn generate_from_save_folder(save_folder: &Path) -> Self { + let folder = std::fs::read_dir(save_folder).expect("Could not read save folder"); + + let mut saves: Vec<_> = folder + .filter_map(|save_folder| { + let save = save_folder.unwrap(); + + if save.file_name() == "save_in_progress.lockfile" { + return None; + } + + let info_path = save.path().join("save_file_info"); + let info: Result = try_load_at(info_path); + + Some( + info.map_err(|err| (save.path(), SaveFileError::LoadError(err))) + .map(|stored| SaveFileInfo { + path: save.path(), + stored, + }), + ) + }) + .collect(); + + saves.sort_by(|a, b| match (a, b) { + (Ok(a), Ok(b)) => a.stored.saved_at.cmp(&b.stored.saved_at).reverse(), + (Ok(_), Err(_)) => std::cmp::Ordering::Greater, + (Err(_), Ok(_)) => std::cmp::Ordering::Less, + (Err((a, _)), Err((b, _))) => a.cmp(b), + }); + + SaveFileList { save_files: saves } + } +} diff --git a/src/saving/mod.rs b/src/saving/mod.rs index f274d35..96b5dc5 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -4,6 +4,7 @@ use std::{ io::{Read, Write}, marker::PhantomData, path::PathBuf, + time::Duration, }; use bitcode::Encode; @@ -20,8 +21,19 @@ use crate::{ item::IdxTrait, join_many::join, par_generation::Timer, + saving::save_file_settings::StoredSaveFileInfo, }; +pub mod loading; +mod save_file_settings; + +pub(crate) fn save_folder() -> PathBuf { + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir() + .join("saves") +} + #[derive(Debug, Encode, serde::Deserialize, serde::Serialize)] pub struct SaveGame< ItemIdxType: IdxTrait, @@ -52,7 +64,12 @@ pub fn save_at(value: &V, path: PathBuf) { } pub fn save_at_fork(value: &V, path: PathBuf) { - let mut file = { File::create(path).expect("could not create file") }; + let file = { + File::create(&path).expect(&format!( + "could not create file, tried to create {}", + path.display() + )) + }; // FIXME: It is technically not okay to allocate here. let mut buf_writer = BufWriter::new(file); @@ -63,32 +80,52 @@ pub fn save_at_fork(value: &V, path: PathBuf) { } pub fn load_at serde::Deserialize<'a>>(path: PathBuf) -> V { + try_load_at(path).expect("Failed to load file") +} + +#[derive(Debug)] +pub(crate) enum LoadError { + CouldNotOpenFile(std::io::Error), + DeserializationFailed(bincode::error::DecodeError), +} + +pub fn try_load_at serde::Deserialize<'a>>(path: PathBuf) -> Result { profiling::scope!("Load at", format!("path: {}", path.display())); let file = { profiling::scope!("Open file"); - File::open(&path).expect(&format!("could not open file {:?}", &path)) + File::open(&path).map_err(|err| LoadError::CouldNotOpenFile(err))? }; let mut buf_reader = BufReader::new(file); - { + let loaded = { profiling::scope!("Decompressing and deserializing"); bincode::serde::decode_from_std_read(&mut buf_reader, bincode::config::standard()) - .expect("Deserialization failed") - } + .map_err(|err| LoadError::DeserializationFailed(err))? + }; + + Ok(loaded) } /// # Panics /// If File system stuff fails pub fn save_components( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, data_store: &DataStore, ) { let checksum = data_store.checksum.clone(); - let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); + let save_dir = save_folder(); - create_dir_all(dir.data_dir()).expect("Could not create data dir"); + let lockfile = crate::lockfile::LockfileUnique::create_blocking( + save_dir.join("save_in_progress.lockfile"), + ) + .expect("Locking lockfile failed"); + + create_dir_all(&save_dir).expect("Could not create save dir"); // if let Ok(s) = env::var("FACTORY_SAVE_READABLE") { // if s == "true" { @@ -112,10 +149,28 @@ pub fn save_components( // } // } - let temp_file_dir = dir.data_dir().join("tmp.save"); - let save_file_dir = dir.data_dir().join("save.save"); + let temp_file_dir = save_dir.join("tmp.save"); + let save_file_dir = if let Some(name) = save_name { + save_dir.join(&name) + } else { + save_dir.join("autosave.save") + }; + + let info = StoredSaveFileInfo { + name: if save_name.is_some() { + name.to_string() + } else { + format!("[Autosave] {}", name) + }, + saved_at: chrono::offset::Utc::now(), + playtime: Duration::from_secs(1) / 60 * aux_data.current_tick as u32, + is_autosave: save_name.is_none(), + includes_replay: false, + preview: None, + }; create_dir_all(&temp_file_dir).expect("Could not create temp dir"); + dbg!(&temp_file_dir); // FIXME: What to do, if the size of the Save in memory + on disk exceeds RAM? @@ -136,6 +191,9 @@ pub fn save_components( } = simulation_state; join!( + || { + save_at(&info, temp_file_dir.join("save_file_info")); + }, || { save_at(&checksum, temp_file_dir.join("checksum")); }, @@ -202,12 +260,22 @@ pub fn save_components( // Remove old save if it exists let _ = std::fs::remove_dir_all(&save_file_dir); - std::fs::rename(temp_file_dir, save_file_dir).expect("Could not rename tmp save dir!"); + std::fs::rename(&temp_file_dir, save_file_dir).expect(&format!( + "Could not rename tmp save dir: {}", + temp_file_dir.display() + )); + + lockfile.release().expect("Failed to remove lockfile"); } +pub const FORK_SAVE_STAGES: usize = 13; /// # Panics /// If File system stuff fails +#[cfg(not(target_arch = "wasm32"))] pub fn save_components_fork_safe( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, @@ -215,14 +283,41 @@ pub fn save_components_fork_safe mut send: interprocess::unnamed_pipe::Sender, ) { let checksum = &data_store.checksum; - let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); + let save_dir = save_folder(); - create_dir_all(dir.data_dir()).expect("Could not create data dir"); + let lockfile = crate::lockfile::LockfileUnique::create_blocking( + save_dir.join("save_in_progress.lockfile"), + ) + .expect("Locking lockfile failed"); - let temp_file_dir = dir.data_dir().join("tmp.save"); - let save_file_dir = dir.data_dir().join("save.save"); + create_dir_all(&save_dir).expect("Could not create data dir"); + + // FIXME: Allocation and chrono is prob illegal after a fork + let info = StoredSaveFileInfo { + name: if save_name.is_some() { + name.to_string() + } else { + format!("[Autosave] {}", name) + }, + saved_at: chrono::offset::Utc::now(), + playtime: Duration::from_secs(1) / 60 * aux_data.current_tick as u32, + + is_autosave: save_name.is_none(), + includes_replay: false, + preview: None, + }; + + let temp_file_dir = save_dir.join("tmp.save"); + let save_file_dir = if let Some(name) = save_name { + save_dir.join(&name) + } else { + save_dir.join("autosave.save") + }; + + assert_ne!(temp_file_dir, save_dir); create_dir_all(&temp_file_dir).expect("Could not create temp dir"); + dbg!(&temp_file_dir); { let SimulationState { @@ -240,6 +335,7 @@ pub fn save_components_fork_safe }, } = simulation_state; + save_at_fork(&info, temp_file_dir.join("save_file_info")); send.write(&[0]).expect("Write to pipe failed"); // send.flush().expect("Flushing pipe failed"); save_at_fork(checksum, temp_file_dir.join("checksum")); @@ -307,11 +403,17 @@ pub fn save_components_fork_safe // Remove old save if it exists let _ = std::fs::remove_dir_all(&save_file_dir); std::fs::rename(temp_file_dir, save_file_dir).expect("Could not rename tmp save dir!"); + + lockfile.release().expect("Failed to remove lockfile"); } /// # Panics /// If File system stuff fails +#[cfg(not(target_arch = "wasm32"))] pub fn save_with_fork( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, @@ -322,12 +424,34 @@ pub fn save_with_fork( let (send, recv) = interprocess::unnamed_pipe::pipe().expect("Failed to create unnamed pipe"); match fork::fork() { - Ok(fork::Fork::Parent(child_pid)) => return Some(recv), + Ok(fork::Fork::Parent(child_pid)) => { + log::info!("Started saving fork with pid {}", child_pid); + return Some(recv); + }, Ok(fork::Fork::Child) => {}, - Err(e) => panic!("Failed to fork!"), + Err(e) => { + log::error!("Saving with fork failed: Unable to create fork: {}", e); + save_components( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + ); + return None; + }, } - save_components_fork_safe(world, simulation_state, aux_data, data_store, send); + save_components_fork_safe( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + send, + ); unsafe { libc::_exit(0) } } @@ -342,6 +466,9 @@ pub fn save_with_fork( /// # Panics /// If File system stuff fails pub fn save( + name: &str, + save_name: Option<&str>, + game_state: &GameState, data_store: &DataStore, ) { @@ -365,7 +492,14 @@ pub fn save( &*aux_data.lock() }; - save_components(world, simulation_state, aux_data, data_store); + save_components( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + ); } /// # Panics diff --git a/src/saving/save_file_settings.rs b/src/saving/save_file_settings.rs new file mode 100644 index 0000000..0234f76 --- /dev/null +++ b/src/saving/save_file_settings.rs @@ -0,0 +1,30 @@ +use std::{path::PathBuf, time::Duration}; + +use chrono::Utc; + +#[derive(Debug, Clone)] +pub(crate) struct SaveFileInfo { + pub(crate) path: PathBuf, + pub(crate) stored: StoredSaveFileInfo, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct StoredSaveFileInfo { + pub(crate) name: String, + + pub(crate) saved_at: chrono::DateTime, + pub(crate) playtime: Duration, + pub(super) is_autosave: bool, + + pub(super) includes_replay: bool, + + // scenario: ScenarioInfo, + + // TODO: Do I want a preview? + pub(super) preview: Option<()>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct SaveFileSettings { + save_location: PathBuf, +} diff --git a/src/scenario.rs b/src/scenario.rs new file mode 100644 index 0000000..9f08908 --- /dev/null +++ b/src/scenario.rs @@ -0,0 +1,87 @@ +use std::ops::RangeInclusive; + +use rand::Rng; +use rand_xoshiro::rand_core::SeedableRng; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioInfo { + // The extent of the world. + // Used for ribbon worlds + world_x_range: RangeInclusive, + world_y_range: RangeInclusive, + + // The player will spawn in a random position in this rect + player_spawn_area: [RangeInclusive; 2], + + pre_placed_structures: ScenarioStructureList, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct Seed([u8; 16]); + +impl ScenarioInfo { + fn instantiate( + &self, + seed: Seed, + pre_placed_structure_count: Option, + ) -> ScenarioInfoInstantiation<'_> { + let mut random = rand_xoshiro::Xoroshiro128Plus::from_seed(seed.0); + + let x = random.random_range(self.player_spawn_area[0].clone()); + let y = random.random_range(self.player_spawn_area[1].clone()); + + ScenarioInfoInstantiation { + seed, + + world_x_range: self.world_x_range.clone(), + world_y_range: self.world_y_range.clone(), + player_spawn_area: [x, y], + + pre_placed_structure_count, + pre_placed_structures: &self.pre_placed_structures, + } + } +} + +#[derive(Debug, Clone, serde::Serialize)] +pub(crate) struct ScenarioInfoInstantiation<'a> { + seed: Seed, + + world_x_range: RangeInclusive, + world_y_range: RangeInclusive, + + player_spawn_area: [i32; 2], + + pre_placed_structure_count: Option, + pre_placed_structures: &'a ScenarioStructureList, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioStructureList { + structures: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum ScenarioStructureKind { + Oneshot { + structure: ScenarioStructureDescription, + }, + Tiled { + structure: ScenarioStructureDescription, + tiling_info: (), + + variable_count: Option>, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioStructureDescription {} + +pub(crate) const EMPTY_LAB_WORLD: ScenarioInfo = ScenarioInfo { + world_x_range: -1_000_000..=1_000_000, + world_y_range: -1_000_000..=1_000_000, + + player_spawn_area: [0..=0, 0..=0], + + pre_placed_structures: ScenarioStructureList { structures: vec![] }, +}; diff --git a/src/statistics/consumption.rs b/src/statistics/consumption.rs index 4a3c7e2..cff019f 100644 --- a/src/statistics/consumption.rs +++ b/src/statistics/consumption.rs @@ -4,13 +4,13 @@ use std::{ ops::{Add, AddAssign}, }; -use charts_rs::Series; use itertools::Itertools; use crate::{ NewWithDataStore, data::DataStore, item::{IdxTrait, Indexable, Item}, + statistics::Series, }; use crate::research::LabTickInfo; diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 6c40a86..cf27e5c 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -1,15 +1,9 @@ use std::{array, ops::AddAssign}; -use charts_rs::{LineChart, Series}; use consumption::ConsumptionInfo; use production::ProductionInfo; -use crate::{ - NewWithDataStore, - data::DataStore, - item::{IdxTrait, Item}, - research::ResearchProgress, -}; +use crate::{NewWithDataStore, data::DataStore, item::IdxTrait, research::ResearchProgress}; #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; @@ -21,8 +15,9 @@ mod power; pub mod production; pub mod recipe; pub mod research; +pub mod time_usage; -pub const NUM_DIFFERENT_TIMESCALES: usize = 5; +pub const NUM_DIFFERENT_TIMESCALES: usize = 8; pub const SAMPLES_FOR_SMOOTHING_BASE: usize = 600; const SAMPLES_FOR_SMOOTHING: [usize; NUM_DIFFERENT_TIMESCALES] = { @@ -40,7 +35,8 @@ const SAMPLES_FOR_SMOOTHING: [usize; NUM_DIFFERENT_TIMESCALES] = { } b }; -pub const NUM_SAMPLES_AT_INTERVALS: [usize; NUM_DIFFERENT_TIMESCALES] = [600, 60, 60, 60, 50]; +pub const NUM_SAMPLES_AT_INTERVALS: [usize; NUM_DIFFERENT_TIMESCALES] = + [600, 60, 60, 60, 60, 50, 50, 50]; pub const NUM_SAMPLES_AT_INTERVALS_STORED: [usize; NUM_DIFFERENT_TIMESCALES] = { let mut b = [0; NUM_DIFFERENT_TIMESCALES]; let mut i = 0; @@ -50,20 +46,46 @@ pub const NUM_SAMPLES_AT_INTERVALS_STORED: [usize; NUM_DIFFERENT_TIMESCALES] = { } b }; -pub const NUM_X_AXIS_TICKS: [usize; NUM_DIFFERENT_TIMESCALES] = [10, 6, 6, 10, 10]; -pub const RELATIVE_INTERVAL_MULTS: [usize; NUM_DIFFERENT_TIMESCALES] = [1, 60, 60, 10, 5]; - -pub const TIMESCALE_NAMES: [&'static str; NUM_DIFFERENT_TIMESCALES] = - ["10 seconds", "1 minute", "1 hour", "10 hours", "50 hours"]; +pub const NUM_X_AXIS_TICKS: [usize; NUM_DIFFERENT_TIMESCALES] = [10, 6, 10, 6, 10, 10, 10, 10]; +pub const RELATIVE_INTERVAL_MULTS: [usize; NUM_DIFFERENT_TIMESCALES] = [1, 60, 10, 6, 10, 5, 5, 4]; + +pub const TIMESCALE_NAMES: [&'static str; NUM_DIFFERENT_TIMESCALES] = [ + "10 seconds", + "1 minute", + "10 minute", + "1 hour", + "10 hours", + "50 hours", + "250 hours", + "1000 hours", +]; pub const TIMESCALE_LEGEND: [fn(f64) -> String; NUM_DIFFERENT_TIMESCALES] = [ |t| format!("{:.0}s", t / 60.0), |t| format!("{:.0}s", t), + |t| format!("{:.0}m", t / 6.0), |t| format!("{:.0}m", t), - |t| format!("{:.0}m", t * 10.0), + |t| format!("{:.0}h", t / 60.0), |t| format!("{:.0}h", t), + |t| format!("{:.0}h", t * 5.0), + |t| format!("{:.0}h", t * 20.0), ]; +#[derive(Debug)] +pub(crate) struct Series { + pub name: String, + pub data: Vec, +} + +impl From<(&str, Vec)> for Series { + fn from((name, data): (&str, Vec)) -> Self { + Self { + name: name.into(), + data: data, + } + } +} + #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GenStatistics { @@ -92,25 +114,6 @@ impl GenStatistics { self.consumption.append_single_set_of_samples(samples.1); self.research.append_single_set_of_samples(samples.2 as u64); } - - pub fn get_chart( - &self, - timescale: usize, - data_store: &DataStore, - filter: Option) -> bool>, - ) -> LineChart { - let prod_values: Vec = self - .production - .get_series(timescale, data_store, filter) - .into_iter() - .map(|v| v.1) - .collect(); - - LineChart::new( - prod_values, - vec![".".to_string(); NUM_SAMPLES_AT_INTERVALS[timescale]], - ) - } } pub trait IntoSeries: Sized { diff --git a/src/statistics/power.rs b/src/statistics/power.rs index 154869b..e8b7037 100644 --- a/src/statistics/power.rs +++ b/src/statistics/power.rs @@ -1,10 +1,9 @@ use std::iter; -use charts_rs::Series; - use crate::{ data::DataStore, item::{IdxTrait, Item}, + statistics::Series, }; use super::IntoSeries; diff --git a/src/statistics/production.rs b/src/statistics/production.rs index 6f4fcb0..08c9689 100644 --- a/src/statistics/production.rs +++ b/src/statistics/production.rs @@ -4,13 +4,13 @@ use std::{ ops::{Add, AddAssign}, }; -use charts_rs::Series; use itertools::Itertools; use crate::{ NewWithDataStore, data::DataStore, item::{IdxTrait, Item}, + statistics::Series, }; use super::{IntoSeries, recipe::RecipeTickInfo}; diff --git a/src/statistics/research.rs b/src/statistics/research.rs index 2a8149c..fa23f49 100644 --- a/src/statistics/research.rs +++ b/src/statistics/research.rs @@ -1,6 +1,6 @@ use std::iter; -use crate::{item::IdxTrait, research::ResearchProgress}; +use crate::{item::IdxTrait, statistics::Series}; use super::IntoSeries; @@ -14,7 +14,7 @@ impl IntoSeries<(), ItemIdxType, smoothing_window: usize, filter: Option bool>, _data_store: &crate::data::DataStore, - ) -> impl Iterator { + ) -> impl Iterator { iter::once(( 0, ( diff --git a/src/statistics/time_usage.rs b/src/statistics/time_usage.rs new file mode 100644 index 0000000..320bcd8 --- /dev/null +++ b/src/statistics/time_usage.rs @@ -0,0 +1,80 @@ +use std::{ + collections::BTreeMap, + iter, + ops::{Add, AddAssign}, + time::Duration, +}; + +use crate::{item::IdxTrait, statistics::Series}; + +use super::IntoSeries; + +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; +use itertools::Itertools; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] +pub struct TimeUsageInfo { + full_update_time: Duration, +} + +impl Add<&TimeUsageInfo> for TimeUsageInfo { + type Output = TimeUsageInfo; + + fn add(mut self, rhs: &TimeUsageInfo) -> Self::Output { + self.full_update_time += rhs.full_update_time; + + self + } +} + +impl AddAssign<&TimeUsageInfo> for TimeUsageInfo { + fn add_assign(&mut self, rhs: &TimeUsageInfo) { + self.full_update_time += rhs.full_update_time; + } +} + +impl IntoSeries<(), ItemIdxType, RecipeIdxType> + for TimeUsageInfo +{ + fn into_series( + values: &[Self], + smoothing_window: usize, + _filter: Option bool>, + _data_store: &crate::data::DataStore, + ) -> impl Iterator { + BTreeMap::from_iter( + values + .windows(smoothing_window) + .map(|infos| { + let TimeUsageInfo { full_update_time } = + infos.iter().fold(TimeUsageInfo::default(), |a, b| a + b); + + iter::once(( + (0, "Full Update Time"), + full_update_time.as_secs_f32() * 1000.0, + )) + }) + .flatten() + .into_group_map() + .into_iter() + .map(|(k, v)| (k.0, (k.1, v))), + ) + .into_iter() + .map(move |(time_id, a)| { + ( + time_id, + ( + a.0, + a.1.into_iter() + .map(|v| v as f32 / smoothing_window as f32) + .collect(), + ) + .into(), + ) + }) + } +} diff --git a/src/storage_list.rs b/src/storage_list.rs index 3c2e82b..734e570 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -1,13 +1,17 @@ +use core::panic; use std::iter; +use std::ops::Index; use std::u16; use itertools::Itertools; use rayon::iter::IndexedParallelIterator; use strum::IntoEnumIterator; +use crate::DATA_STORE; use crate::assembler::MultiAssemblerStore; +use crate::assembler::simd::InserterWaitList; use crate::chest::MultiChestStore; -use crate::item::Indexable; +use crate::item::{Indexable, WeakIdxTrait}; use crate::mining_drill::MiningDrillStore; use crate::{ chest::FullChestStore, @@ -20,22 +24,87 @@ use crate::{ split_arbitrary::split_arbitrary_mut_slice, }; -// FIXME: We just yeet 10MB of RAM into the wind here :/ -pub const ALWAYS_FULL: &'static [ITEMCOUNTTYPE] = &[0; 10_000_000]; -pub const PANIC_ON_INSERT: &'static [ITEMCOUNTTYPE] = &[0; 0]; +pub const ALWAYS_FULL: MaxInsertionLimit<'static> = MaxInsertionLimit::Global(0); +pub const PANIC_ON_INSERT: MaxInsertionLimit<'static> = MaxInsertionLimit::PerMachine(&[]); -type SingleGridStorage<'a, 'b> = (&'a [ITEMCOUNTTYPE], &'b mut [ITEMCOUNTTYPE]); +#[derive(Debug)] +pub enum MaxInsertionLimit<'a> { + PerMachine(&'a [ITEMCOUNTTYPE]), + Global(ITEMCOUNTTYPE), +} + +impl<'a> Index for MaxInsertionLimit<'a> { + type Output = ITEMCOUNTTYPE; + fn index(&self, index: usize) -> &Self::Output { + // return &30; + match self { + MaxInsertionLimit::PerMachine(items) => &items[index], + MaxInsertionLimit::Global(value) => value, + } + } +} +impl<'a> MaxInsertionLimit<'a> { + fn get(&self, index: usize) -> Option<&ITEMCOUNTTYPE> { + // return Some(&30); + match self { + MaxInsertionLimit::PerMachine(items) => items.get(index), + MaxInsertionLimit::Global(value) => Some(value), + } + } +} + +#[derive(Debug)] +pub enum InserterWaitLists<'a> { + PerMachine(&'a mut [InserterWaitList], &'a mut [ITEMCOUNTTYPE]), + None, +} + +// impl<'a> Index for InserterWaitLists<'a> { +// type Output = (&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE); +// fn index<'b>(&'b self, index: usize) -> &'b Self::Output { +// match self { +// InserterWaitLists::PerMachine(items, min_needed) => { +// &(&mut items[index], &mut min_needed[index]) +// }, +// InserterWaitLists::None => panic!("No list"), +// } +// } +// } +// impl<'a> IndexMut for InserterWaitLists<'a> { +// fn index_mut(&mut self, index: usize) -> &mut Self::Output { +// &mut self.get_mut(index).unwrap() +// } +// } +impl<'a> InserterWaitLists<'a> { + fn get_mut(&mut self, index: usize) -> Option<(&mut InserterWaitList, &mut ITEMCOUNTTYPE)> { + match self { + InserterWaitLists::PerMachine(items, min_needed) => { + match (items.get_mut(index), min_needed.get_mut(index)) { + (Some(a), Some(b)) => Some((a, b)), + _ => None, + } + }, + InserterWaitLists::None => None, + } + } +} + +type SingleGridStorage<'a, 'b> = ( + MaxInsertionLimit<'a>, + &'b mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, +); pub type SingleItemStorages<'a, 'b> = &'a mut [SingleGridStorage<'b, 'b>]; //[SingleGridStorage; NUM_RECIPES * NUM_GRIDS]; pub type FullStorages<'a, 'b> = Box<[SingleGridStorage<'a, 'b>]>; //[SingleGridStorage; NUM_ITEMS * NUM_RECIPES * NUM_GRIDS]; -fn num_labs( +fn num_labs( item: Item, data_store: &DataStore, ) -> usize { usize::from(data_store.item_is_science[usize_from(item.id)]) } -pub fn num_recipes( +pub fn num_recipes( item: Item, data_store: &DataStore, ) -> usize { @@ -47,7 +116,7 @@ pub fn num_recipes( num_recipes } -pub fn static_size( +pub fn static_size( _item: Item, _data_store: &DataStore, ) -> usize { @@ -62,7 +131,7 @@ pub fn static_size( size } -pub fn grid_size( +pub fn grid_size( item: Item, data_store: &DataStore, ) -> usize { @@ -90,7 +159,11 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( num_recipes: usize, grid_size: usize, static_size: usize, -) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { +) -> ( + &'a ITEMCOUNTTYPE, + &'a mut ITEMCOUNTTYPE, + Option<(&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE)>, +) { let first_grid_offs_in_grids = static_size.div_ceil(grid_size); match storage_id { @@ -109,6 +182,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, Storage::Lab { grid, index } => { @@ -117,6 +191,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, Storage::Static { static_id, index } => { @@ -125,6 +200,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, } @@ -132,10 +208,15 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( #[inline(always)] pub fn index_fake_union<'a, 'b>( + item_id: Option, slice: SingleItemStorages<'a, 'b>, storage_id: FakeUnionStorage, grid_size: usize, -) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { +) -> ( + &'a ITEMCOUNTTYPE, + &'a mut ITEMCOUNTTYPE, + Option<(&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE)>, +) { let (outer, inner) = storage_id.into_inner_and_outer_indices_with_statics_at_zero(grid_size); let len = slice.len(); @@ -144,7 +225,119 @@ pub fn index_fake_union<'a, 'b>( "Out slice was out of bounds for storage_id {storage_id:?}. len was {len}, index was {outer}, grid_size was {grid_size}.", ); }; - (&subslice.0[inner], &mut subslice.1[inner]) + + let Some(max_insert) = subslice.0.get(inner) else { + if let Some(item_id) = item_id { + let item = Item { + id: item_id.try_into().unwrap(), + }; + let static_size: usize = static_size(item, &DATA_STORE); + let is_static = (storage_id.grid_or_static_flag as usize) < static_size; + let index = storage_id.index; + + if !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; + + let recipe = DATA_STORE.recipe_to_translated_index.iter().find( + |((_recipe, found_item), index)| { + item == *found_item + && u16::from(**index) == storage_id.recipe_idx_with_this_item + }, + ); + + if let Some(((r, _), _)) = recipe { + // we are a assembler + panic!( + "Failed FakeUnion Index for item {} for Assembler in grid {}, with recipe {} and index {}", + &DATA_STORE.item_names[item_id], + grid_id, + DATA_STORE.recipe_names[r.into_usize()], + index + ); + } else { + // We are a lab + panic!( + "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", + &DATA_STORE.item_names[item_id], grid_id, index + ); + } + } else { + let static_id = + StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + + panic!( + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index + ); + } + } else { + let grid = storage_id.grid_or_static_flag; + let rec = storage_id.recipe_idx_with_this_item; + let index = storage_id.index; + + panic!( + "Failed FakeUnion Index in grid_or_static_flag {}, with recipe_idx_with_this_item {} and index {}", + grid, rec, index + ); + } + }; + let Some(items) = subslice.1.get_mut(inner) else { + if let Some(item_id) = item_id { + let item = Item { + id: item_id.try_into().unwrap(), + }; + let static_size: usize = static_size(item, &DATA_STORE); + let is_static = (storage_id.grid_or_static_flag as usize) < static_size; + let index = storage_id.index; + + if !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; + + let recipe = DATA_STORE.recipe_to_translated_index.iter().find( + |((_recipe, found_item), index)| { + item == *found_item + && u16::from(**index) == storage_id.recipe_idx_with_this_item + }, + ); + + if let Some(((r, _), _)) = recipe { + // we are a assembler + panic!( + "Failed FakeUnion Index for item {} for Assembler in grid {}, with recipe {} and index {}", + &DATA_STORE.item_names[item_id], + grid_id, + DATA_STORE.recipe_names[r.into_usize()], + index + ); + } else { + // We are a lab + panic!( + "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", + &DATA_STORE.item_names[item_id], grid_id, index + ); + } + } else { + let static_id = + StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + + panic!( + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index + ); + } + } else { + let grid = storage_id.grid_or_static_flag; + let rec = storage_id.recipe_idx_with_this_item; + let index = storage_id.index; + + panic!( + "Failed FakeUnion Index in grid_or_static_flag {}, with recipe_idx_with_this_item {} and index {}", + grid, rec, index + ); + } + }; + let wait_list = subslice.2.get_mut(inner); + (max_insert, items, wait_list) } #[profiling::function] @@ -262,7 +455,7 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( num_power_grids, data_store, ), - (v.0, v.1, v.2.len()), + (v.0, v.1, v.3.len()), ) }) .collect(); @@ -354,13 +547,18 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( static_storages_pre_sorted( item, chest_store, - mining_drill_lists, + ( + mining_drill_lists.0, + mining_drill_lists.1, + InserterWaitLists::None, + ), data_store, ) - .chain( - grid.into_iter() - .map(|(_item, _storage, max_insert, data)| (max_insert, data)), - ) + .chain(grid.into_iter().map( + |(_item, _storage, max_insert, data, wait_list)| { + (max_insert, data, wait_list) + }, + )) }, ) .collect() @@ -381,8 +579,9 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { let all_storages = grids @@ -403,10 +602,19 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( pub fn static_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( item: Item, chest_store: &'a mut MultiChestStore, - drill_lists: (&'a [ITEMCOUNTTYPE], &'a mut [ITEMCOUNTTYPE]), + drill_lists: ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ), data_store: &'b DataStore, -) -> impl Iterator -+ use<'a, 'b, ItemIdxType, RecipeIdxType> { +) -> impl Iterator< + Item = ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ), +> + use<'a, 'b, ItemIdxType, RecipeIdxType> { let grid_size = grid_size(item, data_store); let static_size = static_size(item, data_store); @@ -414,9 +622,13 @@ pub fn static_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: let first_grid_offs = grid_size * first_grid_offs_in_grids; - iter::once(chest_store.storage_list_slices()) + let (chest_max, chest_data, wait_lists) = chest_store.storage_list_slices(); + + iter::once((chest_max, chest_data, wait_lists)) .chain(iter::once(drill_lists)) - .chain(iter::repeat_with(|| (ALWAYS_FULL, [].as_mut_slice()))) + .chain(iter::repeat_with(|| { + (ALWAYS_FULL, [].as_mut_slice(), InserterWaitLists::None) + })) .take(first_grid_offs) } @@ -428,8 +640,9 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { let i = assembler_store @@ -450,7 +663,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([], []), [outputs]) = multi.get_all_mut(); + let (([], [], []), ([outputs], [output_wait])) = multi.get_all_mut(); ( item, @@ -464,6 +677,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + output_wait, ) }) .chain( @@ -498,7 +712,8 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings_max_insert], [ings]), [outputs]) = multi.get_all_mut(); + let (([ings_max_insert], [ings], [ings_wait]), ([outputs], [outputs_wait])) = + multi.get_all_mut(); [ ( @@ -513,6 +728,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings_max_insert, ings, + ings_wait, ), ( item_out, @@ -526,6 +742,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -562,7 +779,10 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs]) = multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ([outputs], [outputs_wait]), + ) = multi.get_all_mut(); [ ( @@ -577,6 +797,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -590,6 +811,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out, @@ -603,6 +825,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -639,8 +862,10 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait let item_out0 = *items_out.next().unwrap(); let item_out1 = *items_out.next().unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs0, outputs1]) = - multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ([outputs0, outputs1], [outputs0_wait, outputs1_wait]), + ) = multi.get_all_mut(); [ ( @@ -655,6 +880,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -668,6 +894,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out0, @@ -681,6 +908,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs0, + outputs0_wait, ), ( item_out1, @@ -694,6 +922,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs1, + outputs1_wait, ), ] }), @@ -731,8 +960,13 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait let item_out1 = *items_out.next().unwrap(); let item_out2 = *items_out.next().unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs0, outputs1, outputs2]) = - multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ( + [outputs0, outputs1, outputs2], + [outputs0_wait, outputs1_wait, outputs2_wait], + ), + ) = multi.get_all_mut(); [ ( @@ -747,6 +981,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -760,6 +995,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out0, @@ -773,6 +1009,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs0, + outputs0_wait, ), ( item_out1, @@ -786,6 +1023,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs1, + outputs1_wait, ), ( item_out2, @@ -799,6 +1037,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs2, + outputs2_wait, ), ] }), @@ -836,8 +1075,14 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings0_max, ings1_max, ings2_max], [ings0, ings1, ings2]), [outputs]) = - multi.get_all_mut(); + let ( + ( + [ings0_max, ings1_max, ings2_max], + [ings0, ings1, ings2], + [ings0_wait, ings1_wait, ings2_wait], + ), + ([outputs], [outputs_wait]), + ) = multi.get_all_mut(); [ ( @@ -852,6 +1097,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -865,6 +1111,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -878,6 +1125,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_out, @@ -891,6 +1139,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -933,8 +1182,9 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ( [ings0_max, ings1_max, ings2_max, ings3_max], [ings0, ings1, ings2, ings3], + [ings0_wait, ings1_wait, ings2_wait, ings3_wait], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -950,6 +1200,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -963,6 +1214,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -976,6 +1228,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -989,6 +1242,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_out, @@ -1002,6 +1256,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -1045,8 +1300,9 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ( [ings0_max, ings1_max, ings2_max, ings3_max, ings4_max], [ings0, ings1, ings2, ings3, ings4], + [ings0_wait, ings1_wait, ings2_wait, ings3_wait, ings4_wait], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -1062,6 +1318,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -1075,6 +1332,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -1088,6 +1346,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -1101,6 +1360,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_in4, @@ -1114,6 +1374,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings4_max, ings4, + ings4_wait, ), ( item_out, @@ -1127,6 +1388,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -1178,8 +1440,16 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ings5_max, ], [ings0, ings1, ings2, ings3, ings4, ings5], + [ + ings0_wait, + ings1_wait, + ings2_wait, + ings3_wait, + ings4_wait, + ings5_wait, + ], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -1195,6 +1465,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -1208,6 +1479,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -1221,6 +1493,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -1234,6 +1507,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_in4, @@ -1247,6 +1521,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings4_max, ings4, + ings4_wait, ), ( item_in5, @@ -1260,6 +1535,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings5_max, ings5, + ings5_wait, ), ( item_out, @@ -1273,10 +1549,12 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), - ); + ) + .map(|(a, b, c, d, e)| (a, b, c, d, InserterWaitLists::PerMachine(e.0, e.1))); i } @@ -1288,8 +1566,9 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { lab_store @@ -1301,8 +1580,13 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( ( item, Storage::Lab { grid, index: 0 }, - max_insert.as_slice(), + // max_insert.as_slice(), + match max_insert.get(0) { + Some(v) => MaxInsertionLimit::Global(*v), + None => PANIC_ON_INSERT, + }, science.as_mut_slice(), + InserterWaitLists::None, ) }) } @@ -1316,8 +1600,9 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { (0..data_store.item_display_names.len()) @@ -1338,7 +1623,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( assert!(first_grid_offs >= static_size); assert!(first_grid_offs % grid_size == 0); - let (max_insert, data) = chest.storage_list_slices(); + let (max_insert, data, wait_lists) = chest.storage_list_slices(); std::iter::repeat(item) .zip( @@ -1349,6 +1634,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max_insert, data, + wait_lists, )) .chain(iter::once(( Storage::Static { @@ -1357,6 +1643,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, drill_lists.0, drill_lists.1, + InserterWaitLists::None, ))) .chain( std::iter::repeat_with(|| (PANIC_ON_INSERT, [].as_mut_slice())) @@ -1369,12 +1656,13 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max, data, + InserterWaitLists::None, ) }), ) .take(first_grid_offs), ) - .map(|(item, (a, b, c))| (item, a, b, c)) + .map(|(a, (b, c, d, e))| (a, b, c, d, e)) }) } @@ -1385,8 +1673,9 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, ItemIdxType, RecipeIdxType> { chest_store @@ -1398,7 +1687,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( id: id.try_into().unwrap(), }; - let (max_insert, data) = multi.storage_list_slices(); + let (max_insert, data, wait_lists) = multi.storage_list_slices(); ( item, @@ -1408,6 +1697,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max_insert, data, + wait_lists, ) }) } diff --git a/src/temp_vec.rs b/src/temp_vec.rs new file mode 100644 index 0000000..5b0ef59 --- /dev/null +++ b/src/temp_vec.rs @@ -0,0 +1,249 @@ +use std::mem::ManuallyDrop; + +#[cfg(feature = "client")] +use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; +#[cfg(feature = "client")] +use get_size2::GetSize; + +pub(crate) struct SmallCapVec { + ptr: *mut T, + len: u32, + capacity: u32, +} + +impl std::fmt::Debug for SmallCapVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.access(|v| v.fmt(f)) + } +} + +impl Clone for SmallCapVec { + fn clone(&self) -> Self { + self.access(|v| v.clone()).try_into().unwrap() + } +} + +unsafe impl Send for SmallCapVec {} +// Send might be overkill here but I dont need it so better safe than sorry +unsafe impl Sync for SmallCapVec {} + +impl Drop for SmallCapVec { + fn drop(&mut self) { + let dropped_vec = + unsafe { Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) }; + std::mem::drop(dropped_vec); + } +} + +impl VecHolder for SmallCapVec { + fn new() -> Self { + let (ptr, len, capacity) = Vec::new().into_raw_parts(); + + assert!( + u16::try_from(capacity).is_ok(), + "SmallCapVec capacity too large at creation" + ); + + Self { + ptr, + len: len.try_into().unwrap(), + capacity: capacity.try_into().unwrap(), + } + } + + #[inline(always)] + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R { + // Wrapping the Vec in ManuallyDrop is needed to avoid a double drop if the user closure panics. + let temp_vec = ManuallyDrop::new(unsafe { + Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) + }); + + let ret = action(&temp_vec); + + assert!( + u16::try_from(temp_vec.capacity()).is_ok(), + "SmallCapVec grew too large" + ); + + let (ptr, len, capacity) = ManuallyDrop::into_inner(temp_vec).into_raw_parts(); + assert!(ptr == self.ptr, "pointer changed with shared ref?"); + assert!(len == self.len as usize, "len changed with shared ref?"); + assert!( + capacity == self.capacity as usize, + "capacity changed with shared ref?" + ); + + ret + } + + #[inline(always)] + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R { + // Wrapping the Vec in ManuallyDrop is needed to avoid a double drop if the user closure panics. + let mut temp_vec = ManuallyDrop::new(unsafe { + Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) + }); + + let ret = action(&mut temp_vec); + + assert!( + u16::try_from(temp_vec.capacity()).is_ok(), + "SmallCapVec grew too large" + ); + + let (ptr, len, capacity) = ManuallyDrop::into_inner(temp_vec).into_raw_parts(); + self.ptr = ptr; + self.len = len.try_into().unwrap(); + self.capacity = capacity.try_into().unwrap(); + + ret + } + + fn into_vec(self) -> Vec { + let Self { ptr, len, capacity } = self; + + unsafe { Vec::from_raw_parts(ptr, len.try_into().unwrap(), capacity.try_into().unwrap()) } + } +} + +impl std::ops::Deref for SmallCapVec { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + unsafe { core::slice::from_raw_parts(self.ptr, self.len as usize) } + } +} + +impl std::ops::DerefMut for SmallCapVec { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len as usize) } + } +} + +impl TryFrom> for SmallCapVec { + type Error = >::Error; + + fn try_from(value: Vec) -> Result { + let (ptr, len, capacity) = value.into_raw_parts(); + + Ok(Self { + ptr, + len: len.try_into()?, + capacity: capacity.try_into()?, + }) + } +} + +impl VecHolder for Vec { + fn new() -> Self { + Vec::new() + } + + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R { + action(self) + } + + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R { + action(self) + } + + fn into_vec(self) -> Vec { + self + } +} + +#[cfg(feature = "client")] +impl GetSize for SmallCapVec { + fn get_heap_size(&self) -> usize { + self.access(|v| v.get_heap_size()) + } +} + +#[cfg(feature = "client")] +impl, E: InfoExtractor, Info: EguiDisplayable> ShowInfo + for SmallCapVec +where + E: InfoExtractor, Info>, +{ + fn show_fields>( + &self, + extractor: &mut E, + ui: &mut egui::Ui, + path: String, + cache: &mut C, + ) { + // TODO: + } + + fn show_info>( + &self, + extractor: &mut E, + ui: &mut egui::Ui, + path: &str, + cache: &mut C, + ) { + let mut path = String::from(path); + path.push_str(std::any::type_name::()); + + let our_info = if let Some(cached) = cache.get(&path) { + cached + } else { + let new = extractor.extract_info(self); + cache.put(path.clone(), &new); + new + }; + + egui::CollapsingHeader::new(std::any::type_name::()) + .id_salt(&path) + .show(ui, |ui| { + our_info.show(ui); + self.show_fields(extractor, ui, path, cache); + }); + } +} +impl serde::Serialize for SmallCapVec { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.access(|v| v.serialize(serializer)) + } +} +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for SmallCapVec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let v = Vec::::deserialize(deserializer)?; + + // FIXME: + Ok(v.try_into().expect("SmallCapVec too long")) + } +} + +pub(crate) trait VecHolder { + fn new() -> Self; + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R; + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R; + fn into_vec(self) -> Vec; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn panic_during_access() { + let mut small = SmallCapVec::::new(); + + small.access_mut(|v| { + v.push(100); + }); + + small.access(|v| { + assert!(v.is_empty()); + }); + + std::mem::drop(small); + } +} diff --git a/src/test_world_harness/assert.rs b/src/test_world_harness/assert.rs new file mode 100644 index 0000000..c784e60 --- /dev/null +++ b/src/test_world_harness/assert.rs @@ -0,0 +1,27 @@ +use crate::{frontend::world::Position, item::IdxTrait}; + +impl super::Test { + pub fn assert_tile_empty(&self, pos: (i32, i32)) { + assert_eq!( + self.game_state + .world + .lock() + .get_entity_at(Position { x: pos.0, y: pos.1 }, &self.data_store), + None, + "{pos:?} was not empty" + ) + } + + pub fn assert_tile_occupied(&self, pos: (i32, i32)) { + assert!( + matches!( + self.game_state + .world + .lock() + .get_entity_at(Position { x: pos.0, y: pos.1 }, &self.data_store), + Some(_), + ), + "{pos:?} was not occupied" + ) + } +} diff --git a/src/test_world_harness/mod.rs b/src/test_world_harness/mod.rs new file mode 100644 index 0000000..9da7f1d --- /dev/null +++ b/src/test_world_harness/mod.rs @@ -0,0 +1,312 @@ +use std::iter; + +use itertools::Itertools; + +use crate::{ + DATA_STORE, + app_state::GameState, + data::DataStore, + frontend::{ + action::{ + ActionType, + action_state_machine::{ + ActionStateMachine, ActionStateMachineState, HeldObject, WIDTH_PER_LEVEL, + }, + }, + input, + world::tile::Dir, + }, + item::IdxTrait, + replays::GenerationInformation, +}; + +mod assert; + +pub struct Test { + data_store: DataStore, + game_state: GameState, + state_machine: ActionStateMachine, + + action_queue: Vec>, +} + +impl Default for Test { + fn default() -> Self { + let data_store = DATA_STORE.clone(); + let game_state = GameState::new( + "TEST_GAMESTATE".to_string(), + GenerationInformation::default(), + &data_store, + ); + let state_machine = ActionStateMachine::new_from_gamestate( + 0, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + &data_store, + ); + Self { + data_store, + game_state, + state_machine, + + action_queue: vec![], + } + } +} + +impl Test { + pub fn clear_hand(&mut self) { + assert!( + matches!( + self.state_machine.state, + ActionStateMachineState::Holding(_) + ), + "Expected to be holding something when calling clear_hand: {:?}", + self.state_machine.state + ); + + self.state_machine.state = ActionStateMachineState::Idle; + } + + // FIXME: This should not exist and is a compat hazard + pub fn hold_bad(&mut self, held: HeldObject) { + self.state_machine.state = ActionStateMachineState::Holding(held); + } + + pub fn hold(&mut self, item: &str) { + let item = ident(item); + let item = self + .data_store + .item_names + .iter() + .enumerate() + .filter_map(|(i, item_name)| item_name.contains(&item).then_some(i)) + .exactly_one() + .expect(&format!("Could not find exclusive match for {}", item)); + + todo!("Get Hand from item {item:?}") + } + + pub fn rotate_holding(&mut self, goal_dir: Dir) { + assert!( + matches!( + self.state_machine.state, + ActionStateMachineState::Holding(_) + ), + "Expected to be holding something when calling rotate_holding: {:?}", + self.state_machine.state + ); + + match &mut self.state_machine.state { + ActionStateMachineState::Holding(held_object) => match held_object { + crate::frontend::action::action_state_machine::HeldObject::Tile(_) => { + unreachable!("Tiles cannot be rotated") + }, + crate::frontend::action::action_state_machine::HeldObject::Entity( + place_entity_type, + ) => match place_entity_type { + crate::frontend::world::tile::PlaceEntityType::Assembler { + rotation, .. + } => *rotation = goal_dir, + crate::frontend::world::tile::PlaceEntityType::Inserter { dir, .. } => { + *dir = goal_dir + }, + crate::frontend::world::tile::PlaceEntityType::Belt { direction, .. } => { + *direction = goal_dir + }, + crate::frontend::world::tile::PlaceEntityType::Underground { + direction, + .. + } => *direction = goal_dir, + crate::frontend::world::tile::PlaceEntityType::PowerPole { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Splitter { + direction, .. + } => *direction = goal_dir, + crate::frontend::world::tile::PlaceEntityType::Chest { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::SolarPanel { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Accumulator { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Lab { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Beacon { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::FluidTank { + rotation, .. + } => *rotation = goal_dir, + crate::frontend::world::tile::PlaceEntityType::MiningDrill { + rotation, .. + } => *rotation = goal_dir, + }, + crate::frontend::action::action_state_machine::HeldObject::OrePlacement { + .. + } => unreachable!("Ore cannot be rotated"), + crate::frontend::action::action_state_machine::HeldObject::Blueprint(_) => { + todo!("Blueprints cannot be rotated") + }, + }, + + _ => unreachable!(), + } + } + + pub fn place(&mut self, pos: (i32, i32)) { + self.click(pos); + self.tick(); + } + + pub fn click(&mut self, pos: (i32, i32)) { + self.left_mouse_up(); + self.mouse_to(pos); + self.left_mouse_down(); + self.left_mouse_up(); + } + + fn left_mouse_down(&mut self) { + self.handle_input(input::Input::LeftClickPressed { + shift: false, + ctrl: false, + }); + } + + fn left_mouse_up(&mut self) { + self.handle_input(input::Input::LeftClickReleased); + } + + pub fn mouse_to(&mut self, pos: (i32, i32)) { + let screen_pos = tile_to_screen( + self.state_machine.zoom_level, + self.state_machine.local_player_pos, + pos, + ) + .expect("TODO: Position is offscreen"); + + self.handle_input(input::Input::MouseMove(screen_pos.0, screen_pos.1)); + } + + fn right_click(&mut self, pos: (i32, i32)) { + self.mouse_to(pos); + self.right_mouse_down(); + self.right_mouse_up(); + } + + pub fn right_mouse_down(&mut self) { + self.handle_input(input::Input::RightClickPressed { shift: false }); + } + + fn right_mouse_up(&mut self) { + self.handle_input(input::Input::RightClickReleased); + } + + fn press(&mut self, key: input::Key) { + self.handle_input(input::Input::KeyPress(key)); + } + fn release(&mut self, key: input::Key) { + self.handle_input(input::Input::KeyRelease(key)); + } + + fn type_key(&mut self, key: input::Key) { + self.press(key); + self.release(key); + } + + fn handle_input(&mut self, input: input::Input) { + let actions = self + .state_machine + .handle_inputs( + iter::once(input), + &*self.game_state.world.lock(), + &*self.game_state.simulation_state.lock(), + &self.data_store, + ) + .collect::>(); + + self.action_queue.extend(actions); + } + + pub fn tick(&mut self) { + self.tick_many(1); + } + + pub fn tick_many(&mut self, count: usize) { + for _ in 0..count { + self.action_queue.extend( + self.state_machine + .once_per_update_actions(&*self.game_state.world.lock(), &self.data_store), + ); + + GameState::apply_actions( + &mut *self.game_state.simulation_state.lock(), + &mut *self.game_state.world.lock(), + self.action_queue.drain(..), + &self.data_store, + ); + + GameState::update( + &mut *self.game_state.simulation_state.lock(), + &mut *self.game_state.aux_data.lock(), + &self.data_store, + ); + } + } +} + +impl Drop for Test { + fn drop(&mut self) { + self.tick_many(20); + } +} + +fn tile_to_screen(zoom_level: f32, camera_pos: (f32, f32), pos: (i32, i32)) -> Option<(f32, f32)> { + let middle = (pos.0 as f32 + 0.5, pos.1 as f32 + 0.5); + + let camera_space = (middle.0 - camera_pos.0, middle.1 - camera_pos.1); + + let mouse_pos = ( + camera_space.0 / 1.5f32.powf(zoom_level) / WIDTH_PER_LEVEL as f32, + camera_space.1 / 1.5f32.powf(zoom_level) / WIDTH_PER_LEVEL as f32, + ); + + Some(mouse_pos) +} + +fn ident(s: &str) -> String { + use convert_case::Casing; + s.to_case(convert_case::Case::Snake) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + blueprint::test::random_position, frontend::action::action_state_machine::WIDTH_PER_LEVEL, + }; + use proptest::*; + + pub fn player_mouse_to_tile( + zoom_level: f32, + camera_pos: (f32, f32), + mouse_pos: (f32, f32), + ) -> (i32, i32) { + let mouse_pos = ( + ((mouse_pos.0) * (WIDTH_PER_LEVEL as f32)) + .mul_add(1.5f32.powf(zoom_level), camera_pos.0), + ((mouse_pos.1) * (WIDTH_PER_LEVEL as f32)) + .mul_add(1.5f32.powf(zoom_level), camera_pos.1), + ); + + (mouse_pos.0.floor() as i32, mouse_pos.1.floor() as i32) + } + + proptest! { + + #[test] + fn mouse_transform_identity(pos in random_position(), zoom_level in 0.0f32..10.0, camera_pos in (-100.0f32..=100.0, -100.0f32..=100.0)) { + let mouse_pos = tile_to_screen(zoom_level, camera_pos, (pos.x, pos.y)); + + prop_assume!(mouse_pos.is_some()); + let mouse_pos = mouse_pos.expect("Assumed"); + + let pos_res = player_mouse_to_tile(zoom_level, camera_pos, mouse_pos); + + prop_assert_eq!((pos.x, pos.y), pos_res); + } + + } +} diff --git a/steel.png b/steel.png new file mode 100644 index 0000000..58be45e Binary files /dev/null and b/steel.png differ diff --git a/test_blueprints/iron_generation_test.bp b/test_blueprints/iron_generation_test.bp new file mode 100644 index 0000000..5459903 --- /dev/null +++ b/test_blueprints/iron_generation_test.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:159caefe3769289f547fadd71bcf57bc4caf6a580b690375e5f6eba564d4a585 +size 140 diff --git a/test_blueprints/lots_of_belts.bp b/test_blueprints/lots_of_belts.bp index 822488a..51d7dcc 100644 --- a/test_blueprints/lots_of_belts.bp +++ b/test_blueprints/lots_of_belts.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:728b246aec89787dc88b73dadfbed119e01261d1eb110c94cede3dbc320e4794 -size 10096 +oid sha256:194a098488a8aacc304ff8654922ca6cf814926b29d04705ec9424ad34f593fa +size 236 diff --git a/test_blueprints/murphy/megabase_new_blueprint_format.bp b/test_blueprints/murphy/megabase_new_blueprint_format.bp index d4aafcd..69cb619 100644 --- a/test_blueprints/murphy/megabase_new_blueprint_format.bp +++ b/test_blueprints/murphy/megabase_new_blueprint_format.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b3cfe5eb02d043c0dcee88f753473e087f7ff3f7ba2c7095e0733d8345086ae -size 8355640 +oid sha256:27b1f01753b19ab9027b512c2d6e3d09d3c59161080cb76e3046cbea2bb55e81 +size 7530204 diff --git a/test_blueprints/murphy/megabase_notes.md b/test_blueprints/murphy/megabase_notes.md deleted file mode 100644 index 8f80ce7..0000000 --- a/test_blueprints/murphy/megabase_notes.md +++ /dev/null @@ -1 +0,0 @@ -I need to fix Petroleum Gas overproduction in the space modules, which deadlocks the entire base. diff --git a/test_blueprints/red_and_green_with_clocking.bp b/test_blueprints/red_and_green_with_clocking.bp deleted file mode 100644 index 2718740..0000000 --- a/test_blueprints/red_and_green_with_clocking.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a08a6159c8b5f4cc40c3c6541ba6ce735a232862755fa4b58bd56e67d99df447 -size 117875 diff --git a/test_blueprints/red_sci.bp b/test_blueprints/red_sci.bp deleted file mode 100644 index 43a502a..0000000 --- a/test_blueprints/red_sci.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:772a709b63a46d70b3d34ba581e9f9ddf5fb168f881339dcb53e649de7c7ba0c -size 6415 diff --git a/test_blueprints/red_sci_with_beacons_and_belts.bp b/test_blueprints/red_sci_with_beacons_and_belts.bp deleted file mode 100644 index 2c3c6ab..0000000 --- a/test_blueprints/red_sci_with_beacons_and_belts.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0be6c5a23c94e49f69580da798630aab2664edc5ef4ace0ba2afe14a10dfb606 -size 37415 diff --git a/test_blueprints/solar_tile.bp b/test_blueprints/solar_tile.bp index 392ce4e..3797c45 100644 --- a/test_blueprints/solar_tile.bp +++ b/test_blueprints/solar_tile.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c00805e515c34c57351071faa8c70b8891f0541ad4bae04c6f4ec0fec70ef02 -size 592 +oid sha256:31bd098354202734055ba9da08dcacfa9d8b76a4fac7b14efcdc3db2be208993 +size 576 diff --git a/tested_ideas/assembler_waitlist.md b/tested_ideas/assembler_waitlist.md new file mode 100644 index 0000000..175a43a --- /dev/null +++ b/tested_ideas/assembler_waitlist.md @@ -0,0 +1,22 @@ +Ensure the assembler waitlist is only read if it produced anything. + +easiest to test by making the "generate" recipes slow and duplicating lots of them and observing the patterns in lag spikes. + +Issue: +Checking the waitlist is a very significant part of the runtime of the assembler update. +Figure out a way to improve this. +Maybe a bitvec to store if there are any inserters in the list? This would be set whenever an inserter enters the waitlist (adding an additional cache line read + write), but would allow skipping the waitlist check if said bit is not set. +Depending on the cache hit rate of this bitset, it could be beneficial or not, since checking this bit is a cacheline (theoretically) on its own. +In practice this seems unlikely, since a cacheline fits 64 * 8 = 512 flags. +The higher the crafting speed/(lower the recipe craft time) the more important this optimization becomes, and also the more likely the bitvec is to be already cached, reducing random access costs. + +This is of course not beneficial if each assembler always has (at least one) inserter per item in the waitlist whenever it finished a craft. + + +If I implement this, one thing I should test is, not using a bitvec, but instead just a bool slice, since it should not require reading the memory when setting the flag. + +This flag would be (potentially) reset, whenever the assembler crafts and subsequently checks the waitlist. Since we already checked the waitlist, checking if it is empty, should not incur any additional memory bandwidth costs. + + +Another thing, that could theoretically help is reducing the size of an inserter waitlist to 32 bytes (maybe by reducing the capacity to 2), and evaluating if this is an improvement or not. +My guess is it is unlikely, since the additional spinning inserters will kill all improvements we could get. \ No newline at end of file diff --git a/tested_ideas/belt_inserter_list.md b/tested_ideas/belt_inserter_list.md new file mode 100644 index 0000000..8075c7b --- /dev/null +++ b/tested_ideas/belt_inserter_list.md @@ -0,0 +1,6 @@ +One quirk I noticed while profiling is, I got no samples for the branch where the inserter was outgoing +and we are at less than old_first_free. + + +While that might be normal (due to this situation immediately stopping itself from happening next tick), +investigate to make sure. \ No newline at end of file diff --git a/tests/visual_replay_tests.rs b/tests/visual_replay_tests.rs new file mode 100644 index 0000000..f04cf29 --- /dev/null +++ b/tests/visual_replay_tests.rs @@ -0,0 +1,171 @@ +use eframe::EventLoopBuilderHook; +use factory::app_state::AppState; +use factory::rendering::eframe_app; +use factory::rendering::window::LoadedGameSized; + +use factory::DATA_STORE; +use factory::replays::GenerationInformation; +use parking_lot::Mutex; +use rstest::fixture; +use rstest::rstest; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::mpsc::channel; +use std::thread::sleep; +use std::thread::spawn; +use std::time::Duration; +use winit::platform::wayland::EventLoopBuilderExtWayland; + +use factory::app_state::GameState; +use factory::frontend::action::action_state_machine::ActionStateMachine; +use factory::rendering::window::LoadedGame; +use factory::rendering::window::LoadedGameInfo; +use factory::replays::Replay; + +use egui::Context; + +#[fixture] +#[once] +fn start_ui() -> (Mutex, Arc>) { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Error) + .env() + .init() + .unwrap(); + let (ctx_send, ctx_recv) = channel(); + + let gs = Arc::new(GameState::new( + "TEST_GAMESTATE".to_string(), + GenerationInformation::default(), + &DATA_STORE, + )); + + let gs_move = gs.clone(); + spawn(move || { + let (send_input, recv) = channel(); + let sm = { + let sim_state = gs_move.simulation_state.lock(); + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + 0, + &*gs_move.world.lock(), + &*sim_state, + &DATA_STORE, + ))) + }; + + let sm_move = sm.clone(); + let gs_move_move = gs_move.clone(); + spawn(move || { + loop { + { + let sim_state = gs_move_move.simulation_state.lock(); + let world = gs_move_move.world.lock(); + for _action in sm_move.lock().handle_inputs( + recv.try_iter(), + &*world, + &*sim_state, + &DATA_STORE, + ) { + // dbg!(_action); + } + for _action in sm_move.lock().once_per_update_actions(&*world, &DATA_STORE) { + // dbg!(_action); + } + } + sleep(Duration::from_millis(16)); + } + }); + + let event_loop_builder: Option = + Some(Box::new(|event_loop_builder| { + event_loop_builder.with_any_thread(true); + })); + let native_options: eframe::NativeOptions = eframe::NativeOptions { + event_loop_builder, + + ..Default::default() + }; + + eprintln!("eframe::run_native"); + eframe::run_native( + format!("FactoryGame Test Runner").as_str(), + native_options, + Box::new(move |cc| { + let mut app = eframe_app::App::new(cc); + + ctx_send.send(cc.egui_ctx.clone()).unwrap(); + + let (send, _recv) = channel(); + + app.currently_loaded_game = Some(LoadedGameInfo { + state: LoadedGame::ItemU8RecipeU8(LoadedGameSized { + state: gs_move, + state_machine: sm, + // FIXME: This test was the only reason this was in a mutex and now I no longer use it + data_store: Arc::new(Mutex::new(DATA_STORE.clone())), + ui_action_sender: send, + stop_update_thread: Default::default(), + }), + tick: Arc::new(AtomicU64::new(0)), + }); + + app.input_sender = Some(send_input); + app.state = AppState::Ingame; + + Ok(Box::new(app)) + }), + ) + .expect("failed to run app"); + eprintln!("eframe::run_native returned??"); + }); + + let ctx_lock = Mutex::new(ctx_recv.recv().unwrap()); + + // FIXME: When the last test finishes we SEGV?!? + (ctx_lock, gs) +} + +#[rstest] +fn crashing_replays_visual( + #[files("crash_replays/*.rep.ron")] path: PathBuf, + start_ui: &(Mutex, Arc>), +) { + use std::{fs::File, io::Read}; + + let _im_running = start_ui.0.lock(); + eprintln!("{path:?}"); + + // Keep running for 30 seconds + const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; + + let mut file = File::open(&path).unwrap(); + + let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); + + file.read_to_end(&mut v).unwrap(); + let str = String::try_from(v).expect("File content not UTF-8"); + + let mut replay: Replay = ron::de::from_str(&str).expect( + format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), + ); + replay.finish(); + + let game_state = replay + .run( + start_ui.1.clone(), + |_current_gs| { + // sleep(Duration::from_millis(1)); + }, + &DATA_STORE, + ) + .expect("Replay ran into an error"); + + for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &DATA_STORE, + ); + } +} diff --git a/tests/visual_test.rs b/tests/visual_test.rs deleted file mode 100644 index 95ce01e..0000000 --- a/tests/visual_test.rs +++ /dev/null @@ -1,158 +0,0 @@ -// use eframe::EventLoopBuilderHook; -// use factory::app_state::AppState; -// use factory::rendering::eframe_app; -// use factory::rendering::window::LoadedGameSized; - -// use factory::DATA_STORE; -// use parking_lot::Mutex; -// use rstest::fixture; -// use rstest::rstest; -// use std::path::PathBuf; -// use std::sync::Arc; -// use std::sync::atomic::AtomicU64; -// use std::sync::mpsc::channel; -// use std::thread::sleep; -// use std::thread::spawn; -// use std::time::Duration; -// use winit::platform::wayland::EventLoopBuilderExtWayland; - -// use factory::app_state::GameState; -// use factory::data::DataStore; -// use factory::frontend::action::action_state_machine::ActionStateMachine; -// use factory::rendering::window::LoadedGame; -// use factory::rendering::window::LoadedGameInfo; -// use factory::replays::Replay; - -// use egui::Context; - -// #[fixture] -// #[once] -// fn start_ui() -> ( -// Mutex, -// Arc>>, -// Arc>>, -// ) { -// let (ctx_send, ctx_recv) = channel(); - -// let ds = Arc::new(Mutex::new(DATA_STORE.clone())); -// let gs = Arc::new(Mutex::new(GameState::new(&DATA_STORE))); - -// let gs_move = gs.clone(); -// let ds_move = ds.clone(); -// spawn(move || { -// let (send, recv) = channel(); -// let sm = Arc::new(Mutex::new(ActionStateMachine::new( -// 0, -// (1600.0, 1600.0), -// &DATA_STORE, -// ))); - -// let sm_move = sm.clone(); -// let gs_move_move = gs_move.clone(); -// let ds_move_move = ds_move.clone(); -// spawn(move || { -// loop { -// { -// let gs = gs_move_move.lock(); -// for action in -// sm_move -// .lock() -// .handle_inputs(&recv, &gs.world, &ds_move_move.lock()) -// { -// dbg!(action); -// } -// } -// sleep(Duration::from_millis(16)); -// } -// }); - -// let event_loop_builder: Option = -// Some(Box::new(|event_loop_builder| { -// event_loop_builder.with_any_thread(true); -// })); -// let native_options: eframe::NativeOptions = eframe::NativeOptions { -// event_loop_builder, - -// ..Default::default() -// }; - -// eframe::run_native( -// format!("FactoryGame Test Runner").as_str(), -// native_options, -// Box::new(move |cc| { -// let mut app = eframe_app::App::new(cc); - -// ctx_send.send(cc.egui_ctx.clone()).unwrap(); - -// let (send, _recv) = channel(); - -// app.currently_loaded_game = Some(LoadedGameInfo { -// state: LoadedGame::ItemU8RecipeU8(LoadedGameSized { -// state: gs_move, -// state_machine: sm, -// data_store: ds_move, -// ui_action_sender: send, -// stop_update_thread: Default::default(), -// }), -// tick: Arc::new(AtomicU64::new(0)), -// }); - -// let (send, _recv) = channel(); - -// app.input_sender = Some(send); -// app.state = AppState::Ingame; - -// Ok(Box::new(app)) -// }), -// ) -// .expect("failed to run app"); -// }); - -// let ctx_lock = Mutex::new(ctx_recv.recv().unwrap()); - -// // FIXME: When the last test finishes we SEGV?!? -// (ctx_lock, ds, gs) -// } - -// #[rstest] -// fn crashing_replays_visual( -// #[files("crash_replays/*.rep")] path: PathBuf, -// start_ui: &( -// Mutex, -// Arc>>, -// Arc>>, -// ), -// ) { -// use std::{fs::File, io::Read}; - -// let _im_running = start_ui.0.lock(); -// let gs = start_ui.2.clone(); - -// // Keep running for 30 seconds -// const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; - -// let mut file = File::open(&path).unwrap(); - -// let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); - -// file.read_to_end(&mut v).unwrap(); - -// // TODO: For non u8 IdxTypes this will fail -// let mut replay: Replay> = bitcode::deserialize(v.as_slice()).expect( -// format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), -// ); -// replay.finish(); - -// *start_ui.1.lock() = replay.data_store.clone(); - -// let gs_move = gs.clone(); -// let ds_move = start_ui.1.clone(); - -// replay.run_with(gs_move.clone(), || { -// sleep(Duration::from_millis(1)); -// }); - -// for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { -// gs_move.lock().update(&ds_move.lock()); -// } -// }