diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4d9dd96 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.bp filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index e2ba191..09f55a9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ *.exe *.temp stats.svg +last_replay.rep +perf.data +perf.data.old +flamegraph.svg +dhat-heap.json +samply.json +vtune-results diff --git a/.vscode/launch.json b/.vscode/launch.json index fef7e2f..bd3056e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -32,6 +32,7 @@ "build", "--bin=factory", "--package=factory", + "--features=graphics", ], "filter": { "name": "factory", @@ -44,54 +45,76 @@ { "type": "lldb", "request": "launch", - "name": "Debug factory client", + "name": "Debug executable 'factory' verbose", "cargo": { "args": [ "build", "--bin=factory", "--package=factory", + "--features=graphics", ], "filter": { "name": "factory", "kind": "bin" } }, - "args": ["--client"], - "cwd": "${workspaceFolder}" + "args": [], + "cwd": "${workspaceFolder}", + "env": { "RUST_LOG": "info" } + },{ + "type": "lldb", + "request": "launch", + "name": "Debug executable 'factory' without tick wait", + "cargo": { + "args": [ + "build", + "--bin=factory", + "--package=factory", + "--features=graphics", + ], + "filter": { + "name": "factory", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}", + "env": { "ZOOM": "true" } }, { "type": "lldb", "request": "launch", - "name": "Debug unit tests in executable 'factory'", + "name": "Debug factory client", "cargo": { "args": [ - "test", - "--no-run", + "build", "--bin=factory", - "--package=factory" + "--package=factory", + "--features=graphics", ], "filter": { "name": "factory", "kind": "bin" } }, - "args": [], + "args": ["--client"], "cwd": "${workspaceFolder}" }, { "type": "lldb", "request": "launch", - "name": "Debug integration test 'test'", + "name": "Debug unit tests in executable 'factory'", "cargo": { "args": [ "test", "--no-run", - "--test=test", - "--package=factory" + "--bin=factory", + "--package=factory", + "--features=graphics", ], "filter": { - "name": "test", - "kind": "test" + "name": "factory", + "kind": "bin" } }, "args": [], diff --git a/Cargo.lock b/Cargo.lock index 270c655..bd053c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" -version = "0.2.29" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -14,21 +14,25 @@ dependencies = [ [[package]] name = "ab_glyph_rasterizer" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" [[package]] name = "accesskit" -version = "0.17.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" +checksum = "e25ae84c0260bdf5df07796d7cc4882460de26a2b406ec0e6c42461a723b271b" +dependencies = [ + "enumn", + "serde", +] [[package]] name = "accesskit_atspi_common" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" +checksum = "29bd41de2e54451a8ca0dd95ebf45b54d349d29ebceb7f20be264eee14e3d477" dependencies = [ "accesskit", "accesskit_consumer", @@ -40,20 +44,19 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" +checksum = "8bfae7c152994a31dc7d99b8eeac7784a919f71d1b306f4b83217e110fd3824c" dependencies = [ "accesskit", "hashbrown", - "immutable-chunkmap", ] [[package]] name = "accesskit_macos" -version = "0.18.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" +checksum = "692dd318ff8a7a0ffda67271c4bd10cf32249656f4e49390db0b26ca92b095f2" dependencies = [ "accesskit", "accesskit_consumer", @@ -65,9 +68,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" +checksum = "c5f7474c36606d0fe4f438291d667bae7042ea2760f506650ad2366926358fc8" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -83,24 +86,23 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.24.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" +checksum = "70a042b62c9c05bf7b616f015515c17d2813f3ba89978d6f4fc369735d60700a" dependencies = [ "accesskit", "accesskit_consumer", "hashbrown", - "paste", "static_assertions", - "windows", - "windows-core", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] name = "accesskit_winit" -version = "0.23.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" +checksum = "5c1f0d3d13113d8857542a4f8d1a1c24d1dc1527b77aee8426127f4901588708" dependencies = [ "accesskit", "accesskit_macos", @@ -110,11 +112,20 @@ dependencies = [ "winit", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -125,6 +136,7 @@ dependencies = [ "cfg-if", "getrandom 0.3.3", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -140,9 +152,12 @@ dependencies = [ [[package]] name = "aligned-vec" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] [[package]] name = "allocator-api2" @@ -157,7 +172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.0", + "bitflags 2.9.1", "cc", "cesu8", "jni", @@ -200,9 +215,9 @@ checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" dependencies = [ "clipboard-win", "image", @@ -232,7 +247,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -262,6 +277,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +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", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -276,9 +313,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -302,9 +339,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" dependencies = [ "async-lock", "blocking", @@ -313,9 +350,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", @@ -324,10 +361,9 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.44", + "rustix 1.0.8", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -341,11 +377,22 @@ dependencies = [ "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.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" dependencies = [ "async-channel", "async-io", @@ -356,8 +403,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 0.38.44", - "tracing", + "rustix 1.0.8", ] [[package]] @@ -368,14 +414,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" dependencies = [ "async-io", "async-lock", @@ -383,10 +429,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.44", + "rustix 1.0.8", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -403,7 +449,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -423,9 +469,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" dependencies = [ "atspi-common", "atspi-connection", @@ -434,9 +480,9 @@ dependencies = [ [[package]] name = "atspi-common" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" dependencies = [ "enumflags2", "serde", @@ -450,9 +496,9 @@ dependencies = [ [[package]] name = "atspi-connection" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" dependencies = [ "atspi-common", "atspi-proxies", @@ -462,27 +508,26 @@ dependencies = [ [[package]] name = "atspi-proxies" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" dependencies = [ "atspi-common", "serde", "zbus", - "zvariant", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" dependencies = [ "anyhow", "arrayvec", @@ -494,13 +539,28 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" dependencies = [ "arrayvec", ] +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base64" version = "0.21.7" @@ -522,6 +582,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -564,7 +633,7 @@ checksum = "42b6b4cb608b8282dc3b53d0f4c9ab404655d562674c682db7e6c0458cc83c23" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -575,9 +644,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -624,11 +693,20 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -645,28 +723,28 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -693,7 +771,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "log", "polling", "rustix 0.38.44", @@ -715,9 +793,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.22" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -742,9 +820,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -763,9 +841,9 @@ dependencies = [ [[package]] name = "charts-rs" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e585cdea469f708028b82b9e16aaaf307e81658251278343fc55c2e47d4e7869" +checksum = "badceab877515b655e5b071d7510ea979e3943d901c5ea5dbfea57ad0cf284af" dependencies = [ "ahash", "arc-swap", @@ -782,35 +860,39 @@ dependencies = [ [[package]] name = "charts-rs-derive" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b92ffcacae7b226114b0cbd2b6c1bd9d5ac3435d977f3827b44c20be00e7315" +checksum = "8e78032941936cf12f78a8b222bc069e41218565a7b2841fa5e10d2c853d16c7" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] [[package]] name = "cobs" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.12", +] [[package]] name = "codespan-reporting" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -862,9 +944,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -920,9 +1002,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -960,9 +1042,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -976,9 +1058,9 @@ dependencies = [ [[package]] name = "cursor-icon" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "data-url" @@ -995,6 +1077,22 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dhat" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827" +dependencies = [ + "backtrace", + "lazy_static", + "mintex", + "parking_lot", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "thousands", +] + [[package]] name = "digest" version = "0.10.7" @@ -1023,7 +1121,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1038,7 +1136,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", + "block2 0.6.1", + "libc", "objc2 0.6.1", ] @@ -1050,7 +1150,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1086,18 +1186,17 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "bytemuck", - "emath", + "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling)", + "serde", ] [[package]] name = "eframe" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "ahash", "bytemuck", @@ -1124,7 +1223,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "web-time", + "web-time 1.1.0", "wgpu", "winapi", "windows-sys 0.59.0", @@ -1134,24 +1233,25 @@ dependencies = [ [[package]] name = "egui" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "accesskit", "ahash", - "bitflags 2.9.0", - "emath", + "bitflags 2.9.1", + "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling)", "epaint", "log", "nohash-hasher", "profiling", + "ron 0.10.1", + "serde", + "unicode-segmentation", ] [[package]] name = "egui-wgpu" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "ahash", "bytemuck", @@ -1162,7 +1262,7 @@ dependencies = [ "profiling", "thiserror 1.0.69", "type-map", - "web-time", + "web-time 1.1.0", "wgpu", "winit", ] @@ -1170,8 +1270,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "accesskit_winit", "ahash", @@ -1182,7 +1281,7 @@ dependencies = [ "profiling", "raw-window-handle", "smithay-clipboard", - "web-time", + "web-time 1.1.0", "webbrowser", "winit", ] @@ -1199,13 +1298,13 @@ dependencies = [ "log", "mime_guess2", "profiling", + "serde", ] [[package]] name = "egui_glow" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "ahash", "bytemuck", @@ -1219,6 +1318,18 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_graphs" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4779b6849bda09572523f4dedbfbf80179253f801f4eb1aaa0ef231894634ba6" +dependencies = [ + "egui", + "petgraph", + "rand 0.9.2", + "serde", +] + [[package]] name = "egui_plot" version = "0.32.1" @@ -1227,7 +1338,7 @@ checksum = "14ae092b46ea532f6c69d3e71036fb3b688fd00fd09c2a1e43d17051a8ae43e6" dependencies = [ "ahash", "egui", - "emath", + "emath 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1241,8 +1352,14 @@ name = "emath" version = "0.31.1" 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?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -1281,14 +1398,14 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -1296,38 +1413,68 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "enumn" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "epaint" version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", - "emath", + "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling)", "epaint_default_fonts", "log", "nohash-hasher", "parking_lot", "profiling", + "serde", ] [[package]] name = "epaint_default_fonts" version = "0.31.1" +source = "git+https://github.com/BloodStainedCrow/egui?branch=removeProfiling#4e11a02615078f509d9acc474ae13a66b411eaf7" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "equivalent" @@ -1337,12 +1484,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1351,6 +1498,15 @@ 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.0" @@ -1395,28 +1551,40 @@ dependencies = [ "bitcode", "bitvec", "charts-rs", + "dhat", "directories", "eframe", + "egui", "egui_extras", + "egui_graphs", "egui_plot", "enum-map", + "flate2", "genawaiter", "hex", "image", "itertools 0.14.0", "log", + "noise", + "parking_lot", "petgraph", "postcard", + "profiling", "proptest", - "rand", + "puffin", + "puffin_egui", + "rand 0.8.5", "rayon", - "ron", + "rfd", + "ron 0.8.1", + "rstest", "serde", + "serde_path_to_error", "sha2", "simple_logger", "spin_sleep_util", "static_assertions", - "strum 0.27.1", + "strum 0.27.2", "take_mut", "tilelib", "winit", @@ -1445,11 +1613,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1521,7 +1690,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1545,6 +1714,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -1578,20 +1756,20 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] -name = "futures-sink" +name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] -name = "futures-task" -version = "0.3.31" +name = "futures-timer" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" @@ -1602,7 +1780,6 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", - "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1668,7 +1845,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -1685,14 +1862,20 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1706,9 +1889,15 @@ dependencies = [ [[package]] name = "glam" -version = "0.30.3" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11" + +[[package]] +name = "glob" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b46b9ca4690308844c644e7c634d68792467260e051c8543e0c7871662b3ba7" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "glow" @@ -1728,7 +1917,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "cgl", "dispatch2", @@ -1794,7 +1983,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "gpu-alloc-types", ] @@ -1804,7 +1993,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -1816,16 +2005,16 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows", + "windows 0.58.0", ] [[package]] name = "gpu-descriptor" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "gpu-descriptor-types", "hashbrown", ] @@ -1836,7 +2025,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -1847,6 +2036,7 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", + "num-traits", ] [[package]] @@ -1860,13 +2050,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", "foldhash", + "rayon", ] [[package]] @@ -1891,9 +2082,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1907,15 +2098,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -1965,9 +2147,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -1981,9 +2163,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -2048,9 +2230,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" dependencies = [ "byteorder-lite", "quick-error 2.0.1", @@ -2068,24 +2250,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", "rayon", + "serde", ] [[package]] @@ -2096,7 +2270,16 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] @@ -2157,9 +2340,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" @@ -2190,11 +2373,12 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" dependencies = [ "arrayvec", + "euclid", "smallvec", ] @@ -2212,15 +2396,15 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -2228,12 +2412,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -2244,13 +2428,22 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.15", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", ] [[package]] @@ -2279,12 +2472,13 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2302,6 +2496,12 @@ dependencies = [ "imgref", ] +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2323,15 +2523,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] @@ -2351,7 +2551,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "block", "core-graphics-types", "foreign-types", @@ -2386,43 +2586,58 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] +[[package]] +name = "mintex" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" + [[package]] name = "naga" -version = "24.0.0" +version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "codespan-reporting", + "half", + "hashbrown", "hexf-parse", "indexmap", "log", - "rustc-hash", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "termcolor", "thiserror 2.0.12", - "unicode-xid", + "unicode-ident", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -2463,11 +2678,11 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -2480,6 +2695,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "noise" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rand_xorshift 0.3.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -2520,7 +2746,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2550,27 +2776,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2622,8 +2850,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -2638,7 +2866,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", + "block2 0.6.1", "objc2 0.6.1", "objc2-core-foundation", "objc2-core-graphics", @@ -2651,8 +2880,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2664,7 +2893,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2675,8 +2904,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2687,7 +2916,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "dispatch2", "objc2 0.6.1", ] @@ -2698,7 +2927,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "dispatch2", "objc2 0.6.1", "objc2-core-foundation", @@ -2711,7 +2940,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2723,7 +2952,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2741,8 +2970,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -2754,7 +2983,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "objc2 0.6.1", "objc2-core-foundation", ] @@ -2765,7 +2994,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "objc2 0.6.1", "objc2-core-foundation", ] @@ -2776,7 +3005,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -2788,8 +3017,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2800,8 +3029,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2823,8 +3052,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -2844,7 +3073,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2855,13 +3084,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2919,9 +3157,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2929,13 +3167,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.12", + "redox_syscall 0.5.15", "smallvec", "windows-targets 0.52.6", ] @@ -2954,11 +3192,11 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +version = "0.8.2" +source = "git+https://github.com/petgraph/petgraph#46c4cb8e59294ab1330010de6d4b3744928b07e5" dependencies = [ "fixedbitset", + "hashbrown", "indexmap", "rayon", "serde", @@ -2982,7 +3220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -2995,7 +3233,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "unicase", ] @@ -3032,7 +3270,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3079,17 +3317,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -3098,11 +3335,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "postcard" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +checksum = "6c1de96e20f51df24ca73cafcc4690e044854d803259db27a00a461cb3b9d17a" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -3193,43 +3436,77 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", + "puffin", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "proptest" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.0", + "bitflags 2.9.1", "lazy_static", "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] +[[package]] +name = "puffin" +version = "0.19.2" +source = "git+https://github.com/BloodStainedCrow/puffin#a3f7ff67ec940258ca010609b256d4126e560002" +dependencies = [ + "anyhow", + "bincode", + "byteorder", + "cfg-if", + "itertools 0.10.5", + "lz4_flex", + "once_cell", + "parking_lot", + "serde", +] + +[[package]] +name = "puffin_egui" +version = "0.29.1" +source = "git+https://github.com/BloodStainedCrow/puffin#a3f7ff67ec940258ca010609b256d4126e560002" +dependencies = [ + "egui", + "egui_extras", + "indexmap", + "natord", + "once_cell", + "parking_lot", + "puffin", + "time", + "vec1", + "web-time 0.2.4", +] + [[package]] name = "qoi" version = "0.4.1" @@ -3253,9 +3530,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", "serde", @@ -3281,9 +3558,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -3298,8 +3575,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -3309,7 +3596,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -3321,13 +3618,31 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", ] [[package]] @@ -3362,8 +3677,8 @@ dependencies = [ "once_cell", "paste", "profiling", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "simd_helpers", "system-deps", "thiserror 1.0.69", @@ -3373,9 +3688,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.12" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", @@ -3423,11 +3738,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -3470,6 +3785,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3490,11 +3811,35 @@ dependencies = [ "usvg", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] @@ -3506,23 +3851,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.0", + "bitflags 2.9.1", "serde", "serde_derive", ] +[[package]] +name = "ron" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" +dependencies = [ + "base64 0.22.1", + "bitflags 2.9.1", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.104", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3538,7 +3938,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3547,22 +3947,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rusty-fork" @@ -3582,7 +3982,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytemuck", "core_maths", "log", @@ -3657,14 +4057,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -3672,6 +4072,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -3680,29 +4090,18 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -3773,12 +4172,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slotmap" @@ -3791,9 +4187,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" @@ -3801,7 +4197,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3842,23 +4238,23 @@ dependencies = [ [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3872,11 +4268,11 @@ dependencies = [ [[package]] name = "spin_sleep" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17db5ecef7e0bebeb8bf8bc4c4b554e05e0205d7008f10bb37787892e7a6507b" +checksum = "14ac0e4b54d028c2000a13895bcd84cd02a1d63c4f78e08e4ec5ec8f53efd4b9" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3894,7 +4290,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.9.0", + "bitflags 2.9.1", ] [[package]] @@ -3929,11 +4325,11 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.27.1", + "strum_macros 0.27.2", ] [[package]] @@ -3946,20 +4342,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3994,9 +4389,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -4022,7 +4417,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4065,7 +4460,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -4104,7 +4499,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4115,9 +4510,15 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "tiff" version = "0.9.1" @@ -4132,7 +4533,7 @@ dependencies = [ [[package]] name = "tilelib" version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/tilelib.git#6769d4b9983bb668f5cde702d9ffad926f889489" +source = "git+https://github.com/BloodStainedCrow/tilelib.git#aa44854b8b9c571894a4a83b3ff65aecb6f78614" dependencies = [ "bytemuck", "egui", @@ -4232,9 +4633,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -4244,18 +4645,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -4277,20 +4678,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -4312,11 +4713,11 @@ dependencies = [ [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -4398,15 +4799,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "url" @@ -4417,8 +4812,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "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" @@ -4454,15 +4856,21 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "v_frame" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" dependencies = [ "aligned-vec", "num-traits", "wasm-bindgen", ] +[[package]] +name = "vec1" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" + [[package]] name = "version-compare" version = "0.2.0" @@ -4496,9 +4904,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -4531,7 +4939,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -4566,7 +4974,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4600,7 +5008,7 @@ version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "rustix 0.38.44", "wayland-backend", "wayland-scanner", @@ -4612,7 +5020,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cursor-icon", "wayland-backend", ] @@ -4634,7 +5042,7 @@ version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4646,7 +5054,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4659,7 +5067,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4699,6 +5107,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -4711,12 +5129,11 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e" +checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" dependencies = [ - "core-foundation 0.10.0", - "home", + "core-foundation 0.10.1", "jni", "log", "ndk-context", @@ -4728,24 +5145,26 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "24.0.3" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35904fb00ba2d2e0a4d002fcbbb6e1b89b574d272a50e5fc95f6e81cf281c245" +checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "document-features", + "hashbrown", "js-sys", "log", "naga", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "smallvec", @@ -4760,42 +5179,76 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "24.0.2" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c25545d479b47d3f0a8e373aceb2060b67c6eb841b24ac8c32348151c7a0c" +checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", + "bit-set", "bit-vec", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "document-features", + "hashbrown", "indexmap", "log", "naga", "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.12", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", ] +[[package]] +name = "wgpu-core-deps-apple" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-hal" -version = "24.0.4" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.9.0", + "bitflags 2.9.1", "block", "bytemuck", + "cfg-if", "cfg_aliases", "core-graphics-types", "glow", @@ -4803,6 +5256,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", + "hashbrown", "js-sys", "khronos-egl", "libc", @@ -4812,32 +5266,33 @@ dependencies = [ "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", - "once_cell", "ordered-float", "parking_lot", + "portable-atomic", "profiling", "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", "smallvec", "thiserror 2.0.12", "wasm-bindgen", "web-sys", "wgpu-types", - "windows", - "windows-core", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "24.0.0" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", + "bytemuck", "js-sys", "log", + "thiserror 2.0.12", "web-sys", ] @@ -4878,23 +5333,69 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -4903,7 +5404,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] @@ -4914,7 +5426,34 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", ] [[package]] @@ -4926,16 +5465,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -4972,6 +5529,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5020,9 +5586,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -5034,6 +5600,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -5216,15 +5791,15 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.10" +version = "0.30.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d05bd8908e14618c9609471db04007e644fd9cce6529756046cfc577f9155e" +checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.0", - "block2", + "bitflags 2.9.1", + "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", @@ -5259,7 +5834,7 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time", + "web-time 1.1.0", "windows-sys 0.52.0", "x11-dl", "x11rb", @@ -5268,9 +5843,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -5281,7 +5856,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -5333,19 +5908,9 @@ checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xkbcommon-dl" @@ -5353,7 +5918,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "dlib", "log", "once_cell", @@ -5368,9 +5933,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmlwriter" @@ -5398,19 +5963,18 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] [[package]] name = "zbus" -version = "4.4.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -5421,20 +5985,16 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", + "futures-lite", "hex", "nix", "ordered-stream", - "rand", "serde", "serde_repr", - "sha1", - "static_assertions", "tracing", "uds_windows", - "windows-sys 0.52.0", - "xdg-home", + "windows-sys 0.59.0", + "winnow", "zbus_macros", "zbus_names", "zvariant", @@ -5442,9 +6002,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" dependencies = [ "zbus_xml", "zvariant", @@ -5452,13 +6012,13 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "zbus-lockstep", "zbus_xml", "zvariant", @@ -5466,35 +6026,38 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", + "winnow", "zvariant", ] [[package]] name = "zbus_xml" -version = "4.0.0" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" dependencies = [ - "quick-xml 0.30.0", + "quick-xml 0.36.2", "serde", "static_assertions", "zbus_names", @@ -5503,22 +6066,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5538,7 +6101,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "synstructure", ] @@ -5572,9 +6135,15 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", ] +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + [[package]] name = "zune-core" version = "0.4.12" @@ -5592,46 +6161,51 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.14" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] [[package]] name = "zvariant" -version = "4.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", + "url", + "winnow", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.104", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "serde", + "static_assertions", + "syn 2.0.104", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index de1a30e..7d1c407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,8 @@ [package] name = "factory" version = "0.1.0" -edition = "2021" -rust-version = "1.81" +edition = "2024" +rust-version = "1.85" [dependencies] enum-map = { version = "2.7.3", features = ["serde"] } @@ -22,7 +22,7 @@ take_mut = "0.2.2" static_assertions = "1.1.0" itertools = "0.14.0" genawaiter = "0.99.1" -petgraph = { version = "0.7.1", features = ["rayon", "serde", "serde-1", "serde_derive"] } +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"] } @@ -35,8 +35,30 @@ egui_extras = "0.31.1" egui_plot = "0.32.1" rand = "0.8.5" bitcode = { version = "0.6.6", features = ["serde"] } +egui = { version = "0.31.1", features = ["serde"] } +flate2 = {version = "1.1.1", features = ["zlib-rs"]} +rstest = "0.25.0" +parking_lot = { version = "0.12.3", features = ["serde"] } +profiling = { version = "1.0.16" } +puffin_egui = "0.29" +puffin = "0.19" +dhat = "0.3.3" +noise = { version = "0.9.0", features = ["std"] } +rfd = "0.15.3" +egui_graphs = "0.25.1" +serde_path_to_error = "0.1.17" -[dev-dependencies] +[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", branch = "removeProfiling" } +eframe = { git = "https://github.com/BloodStainedCrow/egui", branch = "removeProfiling" } +egui-wgpu = { git = "https://github.com/BloodStainedCrow/egui", branch = "removeProfiling" } + +petgraph = { git = "https://github.com/petgraph/petgraph" } + +# [patch."https://github.com/BloodStainedCrow/tilelib"] +# tilelib = { path = "../tilelib" } [lints.rust] # TODO: @@ -50,12 +72,21 @@ unwrap_used = { level = "deny", priority = -1 } wildcard_enum_match_arm = { level = "deny", priority = -1 } 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 } [profile.release-with-debug] inherits = "release" debug = true +strip = false + +[profile.release-with-debug-asserts] +inherits = "release" +debug = true +strip = false +debug-assertions = true [profile.fast-compile] inherits = "dev" @@ -65,8 +96,13 @@ strip = "none" lto = false codegen-units = 256 incremental = true -# codegen-backend = "cranelift" - -[profile.release] -lto = true +# lto = true # codegen-units = 1 + +[features] +default = ["profiler", "graphics"] +# Use Krastorio2 graphics. Since I have not properly added licensing information, I currently do not push them, therefore this feature is broken +graphics = [] +# dhat-rs memory profiling (https://docs.rs/dhat/latest/dhat/) +dhat-heap = [] +profiler = ["profiling/profile-with-puffin"] \ No newline at end of file diff --git a/README.md b/README.md index 5f94dd0..c19a723 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,32 @@ I was playing the above games and started being unable to expand due to performa ## 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 +- ~~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 +- ~~Debug inserters~~ - ~~Production Graphs~~ +- ~~Liquids~~ +- ~~Map View~~ +- ~~Technology~~ - Mining Drills -- Liquids +- ~~Underground belts~~ +- Fix Underground Pipe connection breaking/overlap - Place Steam Turbines -- Splitters -- Ore Generation -- Bots \ No newline at end of file +- ~~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 diff --git a/addr2line-rs/default.nix b/addr2line-rs/default.nix new file mode 100644 index 0000000..4eed125 --- /dev/null +++ b/addr2line-rs/default.nix @@ -0,0 +1,27 @@ +{ lib, rustPlatform, fetchCrate }: + +rustPlatform.buildRustPackage rec { + pname = "addr2line-rs"; + version = "0.25.0"; + + src = fetchCrate { + pname = "addr2line"; + version = "0.25.0"; + hash = "sha256-ZDgASG5pLbavXJFoJnuSq+Y4dX9vRo/IOhbTij5kOuI="; + }; + + cargoHash = "sha256-cdWOqGL5f1v4j0OVd/wAF9UwR7ELF8ysAvgPROoM5qc="; + + useFetchCargoVendor = true; + + cargoFeatures = [ "bin" ]; + +dontBuild = true; + + installPhase = '' + mkdir -p $out/bin + cargo install --path . --root $out --features bin --locked +''; + + +} \ No newline at end of file diff --git a/codium.nix b/codium.nix index 197d619..99b1a4d 100644 --- a/codium.nix +++ b/codium.nix @@ -1,10 +1,12 @@ 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; @@ -12,8 +14,15 @@ pkgs.mkShell { 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 b/crash_replays/001.rep new file mode 100644 index 0000000..dd59792 Binary files /dev/null and b/crash_replays/001.rep differ diff --git a/proptest-regressions/chest.txt b/proptest-regressions/chest.txt new file mode 100644 index 0000000..c397b64 --- /dev/null +++ b/proptest-regressions/chest.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 c644ded4ba7f8ee584a609c70355e6e511082bc91e41b152615791321cbc7afc # shrinks to inout = 0, storage = 1, max_items = 0 diff --git a/proptest-regressions/frontend/action/belt_placement.txt b/proptest-regressions/frontend/action/belt_placement.txt index 3149309..5b7c2e3 100644 --- a/proptest-regressions/frontend/action/belt_placement.txt +++ b/proptest-regressions/frontend/action/belt_placement.txt @@ -7,3 +7,5 @@ cc 74ea13e43011615c7d46bccaafe02551048c07c2b4b57161e2f666660245af27 # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Chest { pos: Position { x: 0, y: 0 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 1, y: 0 }, dir: East, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 2, y: 0 }, direction: East }) })] cc df1b2c1c6bb7f5443f4e38cac4eaa0256201964ffa8d48b3de90f4390f278692 # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 2, y: 2 }, dir: North, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 2, y: 1 }, direction: East }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 3 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 2 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Assembler(Position { x: 0, y: 3 })) })] cc 72ed0a55ddee234e6d60f0d1cc9c9ccd65c0fdeea2bb42cbacb5bff177ad139f # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Assembler(Position { x: 0, y: 3 })) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 3 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 2, y: 2 }, dir: North, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 2 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 2, y: 1 }, direction: East }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 3, y: 1 }, direction: East }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 4, y: 1 }, direction: South }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 4, y: 0 }, direction: South }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 4, y: 2 }, direction: South }) })] +cc 8ad0a0b41c051215c6fc613471fba468eb4a746bdf1f4690f53beec99d5b9468 # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 0, y: 3 }, ty: 0 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 3 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 2 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(SolarPanel { pos: Position { x: 1, y: 0 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 2, y: 2 }, dir: North, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 2, y: 1 }, direction: East }) })] +cc 6993f4605e5a60a23e9dc330fd073ce23d4464611f51ba0bb1651a1ecc0bd8ab # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 0, y: 3 }, ty: 0 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 3 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 2, y: 2 }, dir: North, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 2, y: 1 }, direction: East }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 2 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(SolarPanel { pos: Position { x: 1, y: 0 }, ty: 0 }) })] diff --git a/proptest-regressions/inserter/mod.txt b/proptest-regressions/inserter/mod.txt new file mode 100644 index 0000000..12d51b2 --- /dev/null +++ b/proptest-regressions/inserter/mod.txt @@ -0,0 +1,8 @@ +# 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 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 }) diff --git a/proptest-regressions/mining_drill.txt b/proptest-regressions/mining_drill.txt new file mode 100644 index 0000000..39af333 --- /dev/null +++ b/proptest-regressions/mining_drill.txt @@ -0,0 +1,10 @@ +# 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 1f99ee923251526dc93e3f0c7e0c7c36cba5c91c5d2ec80794404e7e9232de5c # shrinks to value = 0 +cc c243b12cf125cb7e2dff30cfd3313f6dcc0797681ed6ec04025816bd0ee939c1 # shrinks to value = 834, value2 = 834 +cc 56de79a153e522912fbec46d01f521601e2b01444858ac5184a7222c02dc44cc # shrinks to value = 0, value2 = 1 +cc f61f7c7ed447c3e522e886177907b86d99bda48aff00a813ad5021dd43365867 # shrinks to to_remove = 0, old_values = [716, 1, 1, 1, 716] diff --git a/proptest-regressions/mining_drill/with_shared_ore.txt b/proptest-regressions/mining_drill/with_shared_ore.txt new file mode 100644 index 0000000..be8c735 --- /dev/null +++ b/proptest-regressions/mining_drill/with_shared_ore.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 c77f3e4f74a8c9fb91d785a3f78870b27bd1ff6996ddcf1a7f99eb2f7c4504ac # shrinks to to_remove = 0, (old_values, mined_items) = ([1, 1, 1, 1, 1], 5) diff --git a/proptest-regressions/rendering/app_state.txt b/proptest-regressions/rendering/app_state.txt index 568ac27..18eb72a 100644 --- a/proptest-regressions/rendering/app_state.txt +++ b/proptest-regressions/rendering/app_state.txt @@ -19,3 +19,9 @@ cc 4f190a2321ba0573d7ad5a9ca5149177d323023b8da0c817ab58e746adcafd71 # shrinks to cc 7c49f346af89a36b78de90185555ca1f7a5e30cffe4e46dc8c062e0dd65c6b8f # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 10 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 10, y: 0 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 0 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 1 }, ty: 2 }) })] }, time = 0 cc e21d0d639f05876814aebd7508efbc664856f08d929d6cfee848cc8299d7ba8c # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 8, y: 0 }, ty: 3 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Lab { pos: Position { x: 5, y: 4 }, ty: 0 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 9, y: 7 }, dir: North, filter: Some(Item { id: 0 }) }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 8, y: 6 }, dir: West, filter: Some(Item { id: 0 }) }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Chest { pos: Position { x: 9, y: 8 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 9, y: 6 }, direction: North }) })] } cc 348fec1b654f14111d2d477e8656e30ec6077b4c9d84538f2fbe698e8e88dae4 # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [PlaceEntity(PlaceEntityInfo { entities: Single(Chest { pos: Position { x: 4, y: 15 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 4, y: 13 }, direction: North }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 4, y: 14 }, dir: North, filter: Some(Item { id: 1 }) }) })] }, time = 1 +cc fd5920a96adfe306b1e3a781cc96c05f3c0be26154e852a1323a0718c304022b # shrinks to actions = [AddModules { pos: Position { x: 16, y: 16 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 29, y: 23 } }) }), AddModules { pos: Position { x: 12, y: 16 }, modules: [0, 0] }, AddModules { pos: Position { x: 9, y: 16 }, modules: [0, 0] }, AddModules { pos: Position { x: 19, y: 13 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 19, y: 26 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 22, y: 19 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 26, y: 26 } }) }), AddModules { pos: Position { x: 24, y: 21 }, modules: [1, 1, 1, 1] }, PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 29, y: 26 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 19, y: 19 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 22, y: 26 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(SolarPanel { pos: Position { x: 19, y: 13 }, ty: 0 }) }), AddModules { pos: Position { x: 1609, y: 1613 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 28, y: 25 }, ty: 0 }) }), AddModules { pos: Position { x: 19, y: 16 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 27, y: 22 }, dir: East, filter: None }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 24, y: 21 }, ty: 2 }) }), AddModules { pos: Position { x: 16, y: 6 }, modules: [0, 0] }, AddModules { pos: Position { x: 1609, y: 1606 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 28, y: 19 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 29, y: 19 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 26, y: 16 } }) }), AddModules { pos: Position { x: 1609, y: 1609 }, modules: [0, 0] }, AddModules { pos: Position { x: 19, y: 6 }, modules: [0, 0] }, SetRecipe(SetRecipeInfo { pos: Position { x: 24, y: 21 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 22, y: 16 } }) }), AddModules { pos: Position { x: 1612, y: 1606 }, modules: [0, 0] }, AddModules { pos: Position { x: 19, y: 9 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 29, y: 16 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Chest { pos: Position { x: 28, y: 22 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 19, y: 16 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 19, y: 23 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 22, y: 25 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 22, y: 15 }, ty: 0 }) })] +cc 2f00f18e3512a8f148822ea6e1f7665f7d5ad9c836c6f1a4d19b74dee99d8fb7 # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 14, y: 11 }, direction: North }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 14, y: 10 }, dir: North, filter: Some(Item { id: 0 }) }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 14, y: 9 }, direction: North }) })] }, time = 0 +cc fc8994d22c9d40c0a2ebc3467a79afb5ce79b5caac8b43555b4b70f7db58e356 # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 5, y: 1 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 11, y: 0 }, ty: 3 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 0 }, ty: 0 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 5, y: 1 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 2, y: 0 }, ty: 1 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 5, y: 1 }, recipe: Recipe { id: 0 } })] }, time = 0 +cc 9cc305166ad25c9e3c6074efa0adfb325844a161066519695e8d80db4bad4f88 # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Inserter { pos: Position { x: 7, y: 1 }, dir: East, filter: Some(Item { id: 0 }) }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 6, y: 1 }, direction: North }) }), PlaceEntity(PlaceEntityInfo { entities: Single(Belt { pos: Position { x: 8, y: 1 }, direction: North }) })] } +cc e13a5b3c25b2c00bfed7f5d3560eb4b343faca287ba01e26fa44b0ff58948e29 # shrinks to base_pos = Position { x: 1600, y: 1600 }, blueprint = Blueprint { actions: [SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 12, y: 2 }, ty: 0 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 0, y: 0 }, recipe: Recipe { id: 0 } }), SetRecipe(SetRecipeInfo { pos: Position { x: 12, y: 2 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 4, y: 12 }, ty: 3 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 5, y: 3 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 0, y: 3 }, ty: 1 }) })] }, time = 0 +cc dd3ae3423dc449512b526f1afa7a28fc9d604329f754338705dba7571669ac65 # shrinks to actions = [PlaceEntity(PlaceEntityInfo { entities: Single(Assembler { pos: Position { x: 1600, y: 1600 }, ty: 2 }) }), SetRecipe(SetRecipeInfo { pos: Position { x: 1600, y: 1600 }, recipe: Recipe { id: 0 } }), PlaceEntity(PlaceEntityInfo { entities: Single(Beacon { ty: 0, pos: Position { x: 1603, y: 1600 } }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 1603, y: 1599 }, ty: 0 }) }), AddModules { pos: Position { x: 1603, y: 1600 }, modules: [0, 0] }, PlaceEntity(PlaceEntityInfo { entities: Single(SolarPanel { pos: Position { x: 1600, y: 1597 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 1613, y: 1599 }, ty: 0 }) }), PlaceEntity(PlaceEntityInfo { entities: Single(PowerPole { pos: Position { x: 1608, y: 1599 }, ty: 0 }) })] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3653e33..9fe5ede 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2025-04-10" +channel = "nightly-2025-05-27" components = ["clippy", "rustfmt"] diff --git a/shell.nix b/shell.nix index 484efc9..6bcf3da 100644 --- a/shell.nix +++ b/shell.nix @@ -1,15 +1,9 @@ let - rust_overlay = import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"); - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/929116e316068c7318c54eb4d827f7d9756d5e9c.tar.gz")) { overlays = [ rust_overlay ]; }; - rust = pkgs.rust-bin.nightly."2025-04-10".default.override { - extensions = [ - "rust-src" # for rust-analyzer - "rust-analyzer" - ]; - }; + pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/929116e316068c7318c54eb4d827f7d9756d5e9c.tar.gz")) { overlays = [ ]; }; buildInputs = [ - rust ] ++ (with pkgs; [ + rustup + pkg-config # perf for cargo-flamegraph diff --git a/src/assembler.rs b/src/assembler.rs index 932be42..97f47bd 100644 --- a/src/assembler.rs +++ b/src/assembler.rs @@ -1,26 +1,26 @@ -use std::{array, cmp::max, simd::Simd}; - -use itertools::Itertools; +use std::{array, i32, simd::Simd, u8}; use crate::{ data::{DataStore, ItemRecipeDir}, - frontend::world::{tile::AssemblerID, Position}, - item::{IdxTrait, Item, Recipe, WeakIdxTrait, ITEMCOUNTTYPE}, + frontend::world::{Position, tile::AssemblerID}, + inserter::HAND_SIZE, + item::{ITEMCOUNTTYPE, IdxTrait, Item, Recipe, WeakIdxTrait}, power::{ - power_grid::{IndexUpdateInfo, PowerGridEntity, PowerGridIdentifier, MAX_POWER_MULT}, - Joule, Watt, + Watt, + power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridEntity, PowerGridIdentifier}, }, }; +use itertools::Itertools; +use std::cmp::max; pub type Simdtype = Simd; -// TODO: Is u8 bit enough? pub type TIMERTYPE = u16; -// TODO: Do I want these generics or just get it at runtime? // FIXME: We store the same slice length n times! -// TODO: Do I want to use SimdTypes for this? // 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! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MultiAssemblerStore< RecipeIdxType: WeakIdxTrait, @@ -43,6 +43,7 @@ pub struct MultiAssemblerStore< /// Power Consumption in 5% increments /// i.e. 28 => 140% Crafting speed /// Maximum is 1275% x Base Power Consumption + // TODO: We could just store base_power_consumption * power_consumption_modifier instead of doing this calculation every tick power_consumption_modifier: Box<[u8]>, raw_speed_mod: Box<[i16]>, @@ -61,7 +62,8 @@ pub struct MultiAssemblerStore< prod_timers: Box<[TIMERTYPE]>, holes: Vec, - positions: Vec, + positions: Box<[Position]>, + types: Box<[u8]>, len: usize, } @@ -70,6 +72,12 @@ pub struct FullAssemblerStore { pub assemblers_0_1: Box<[MultiAssemblerStore]>, pub assemblers_1_1: Box<[MultiAssemblerStore]>, pub assemblers_2_1: Box<[MultiAssemblerStore]>, + pub assemblers_2_2: Box<[MultiAssemblerStore]>, + pub assemblers_2_3: Box<[MultiAssemblerStore]>, + pub assemblers_3_1: Box<[MultiAssemblerStore]>, + pub assemblers_4_1: Box<[MultiAssemblerStore]>, + pub assemblers_5_1: Box<[MultiAssemblerStore]>, + pub assemblers_6_1: Box<[MultiAssemblerStore]>, } #[derive(Debug, Clone)] @@ -78,6 +86,11 @@ pub struct AssemblerOnclickInfo { pub outputs: Vec<(Item, ITEMCOUNTTYPE)>, pub timer_percentage: f32, pub prod_timer_percentage: f32, + pub base_speed: f32, + pub speed_mod: f32, + pub prod_mod: f32, + pub power_consumption_mod: f32, + pub base_power_consumption: Watt, } impl FullAssemblerStore { @@ -104,11 +117,59 @@ impl FullAssemblerStore { .iter() .map(|r| MultiAssemblerStore::new(*r)) .collect(); + let assemblers_2_2 = data_store + .ing_out_num_to_recipe + .get(&(2, 2)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); + let assemblers_2_3 = data_store + .ing_out_num_to_recipe + .get(&(2, 3)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); + let assemblers_3_1 = data_store + .ing_out_num_to_recipe + .get(&(3, 1)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); + let assemblers_4_1 = data_store + .ing_out_num_to_recipe + .get(&(4, 1)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); + let assemblers_5_1 = data_store + .ing_out_num_to_recipe + .get(&(5, 1)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); + let assemblers_6_1 = data_store + .ing_out_num_to_recipe + .get(&(6, 1)) + .unwrap() + .iter() + .map(|r| MultiAssemblerStore::new(*r)) + .collect(); 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, } } @@ -120,7 +181,8 @@ impl FullAssemblerStore { data_store: &DataStore, ) -> ( Self, - impl IntoIterator>, + impl IntoIterator> + + use, ) { // TODO: This just works with box::into_iter in edition 2024 let (assemblers_0_1, assemblers_0_1_updates): (Vec<_>, Vec<_>) = self @@ -146,11 +208,63 @@ impl FullAssemblerStore { .zip(other.assemblers_2_1.into_vec()) .map(|(a, b)| a.join(b, new_grid_id, data_store)) .unzip(); + let (assemblers_2_2, assemblers_2_2_updates): (Vec<_>, Vec<_>) = self + .assemblers_2_2 + .into_vec() + .into_iter() + .zip(other.assemblers_2_2.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); + let (assemblers_2_3, assemblers_2_3_updates): (Vec<_>, Vec<_>) = self + .assemblers_2_3 + .into_vec() + .into_iter() + .zip(other.assemblers_2_3.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); + + let (assemblers_3_1, assemblers_3_1_updates): (Vec<_>, Vec<_>) = self + .assemblers_3_1 + .into_vec() + .into_iter() + .zip(other.assemblers_3_1.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); + + let (assemblers_4_1, assemblers_4_1_updates): (Vec<_>, Vec<_>) = self + .assemblers_4_1 + .into_vec() + .into_iter() + .zip(other.assemblers_4_1.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); + + let (assemblers_5_1, assemblers_5_1_updates): (Vec<_>, Vec<_>) = self + .assemblers_5_1 + .into_vec() + .into_iter() + .zip(other.assemblers_5_1.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); + + let (assemblers_6_1, assemblers_6_1_updates): (Vec<_>, Vec<_>) = self + .assemblers_6_1 + .into_vec() + .into_iter() + .zip(other.assemblers_6_1.into_vec()) + .map(|(a, b)| a.join(b, new_grid_id, data_store)) + .unzip(); let ret = Self { assemblers_0_1: assemblers_0_1.into_boxed_slice(), assemblers_1_1: assemblers_1_1.into_boxed_slice(), assemblers_2_1: assemblers_2_1.into_boxed_slice(), + assemblers_2_2: assemblers_2_2.into_boxed_slice(), + assemblers_2_3: assemblers_2_3.into_boxed_slice(), + assemblers_3_1: assemblers_3_1.into_boxed_slice(), + assemblers_4_1: assemblers_4_1.into_boxed_slice(), + assemblers_5_1: assemblers_5_1.into_boxed_slice(), + assemblers_6_1: assemblers_6_1.into_boxed_slice(), }; ( @@ -159,7 +273,13 @@ impl FullAssemblerStore { .into_iter() .flatten() .chain(assemblers_1_1_updates.into_iter().flatten()) - .chain(assemblers_2_1_updates.into_iter().flatten()), + .chain(assemblers_2_1_updates.into_iter().flatten()) + .chain(assemblers_2_2_updates.into_iter().flatten()) + .chain(assemblers_2_3_updates.into_iter().flatten()) + .chain(assemblers_3_1_updates.into_iter().flatten()) + .chain(assemblers_4_1_updates.into_iter().flatten()) + .chain(assemblers_5_1_updates.into_iter().flatten()) + .chain(assemblers_6_1_updates.into_iter().flatten()), ) } @@ -202,6 +322,66 @@ impl FullAssemblerStore { .get_info(assembler_id.assembler_index, data_store) }, + (2, 2) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + + (2, 3) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + + (3, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + + (4, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + + (5, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + + (6, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]].recipe + ); + + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_info(assembler_id.assembler_index, data_store) + }, + _ => unreachable!(), } } @@ -241,8 +421,8 @@ impl FullAssemblerStore { // } pub struct AssemblerRemovalInfo { - ings: Vec, - outputs: Vec, + pub ings: Vec, + pub outputs: Vec, } // TODO: Maybe also add a defragmentation routine to mend the ineffeciencies left by deconstruction large amounts of assemblers @@ -272,7 +452,8 @@ impl base_power_consumption: vec![].into_boxed_slice(), holes: vec![], - positions: vec![], + positions: vec![].into_boxed_slice(), + types: vec![].into_boxed_slice(), len: 0, } } @@ -285,9 +466,32 @@ impl data_store: &DataStore, ) -> ( Self, - impl IntoIterator>, + impl IntoIterator> + + use, ) { - let old_len = self.positions.len(); + #[cfg(debug_assertions)] + { + for (i, pos) in self.positions.iter().enumerate() { + assert_eq!( + (self.holes.contains(&i) || i >= self.len), + (pos.x == i32::MAX) + ); + } + + for (i, pos) in other.positions.iter().enumerate() { + assert_eq!( + (other.holes.contains(&i) || i >= other.len), + (pos.x == i32::MAX) + ); + } + } + + let old_len_stored = self.positions.len(); + + for i in self.len..old_len_stored { + self.holes.push(i); + } + self.len = old_len_stored; let new_ings_max: [Box<[u8]>; NUM_INGS] = self .ings_max_insert @@ -299,6 +503,7 @@ impl o.into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -317,6 +522,7 @@ impl o.into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -334,6 +540,7 @@ impl o.into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -348,6 +555,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -359,6 +567,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -370,6 +579,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -381,6 +591,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -392,6 +603,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -403,6 +615,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -414,6 +627,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -425,6 +639,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -436,6 +651,7 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); @@ -447,35 +663,98 @@ impl .into_vec() .into_iter() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); - self.positions.extend( + let mut new_positions = self.positions.into_vec(); + new_positions.extend( other .positions .iter() .copied() .enumerate() + .take(other.len) .filter(|(i, _)| !other.holes.contains(i)) .map(|(_, v)| v), ); - let updates = other - .positions - .into_iter() + let mut new_types = self.types.into_vec(); + new_types.extend( + other + .types + .iter() + .copied() + .enumerate() + .take(other.len) + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + 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() - .map(move |(new_index_offs, (old_index, pos))| IndexUpdateInfo { - position: pos, - new_storage: PowerGridEntity::Assembler { - recipe: self.recipe, - index: (old_len + new_index_offs).try_into().unwrap(), - }, - new_grid: new_grid_id, + .map(move |(new_index_offs, (old_index, (pos, ty)))| { + assert!(new_index_offs <= old_index); + IndexUpdateInfo { + position: pos, + old_pg_entity: PowerGridEntity::Assembler { + ty, + recipe: self.recipe, + index: old_index.try_into().unwrap(), + }, + new_pg_entity: PowerGridEntity::Assembler { + ty, + recipe: self.recipe, + index: (old_len_stored + new_index_offs).try_into().unwrap(), + }, + new_grid: new_grid_id, + } }); + // #[cfg(debug_assertions)] + // let updates = { + // let updates = updates.collect::>(); + + // assert_eq!(updates.len(), other.len - other.holes.len()); + + // assert!(updates.iter().all(|update| { + // let IndexUpdateInfo { + // new_pg_entity: PowerGridEntity::Assembler { index, .. }, + // .. + // } = update + // else { + // unreachable!() + // }; + + // !self.holes.contains(&(*index as usize)) + // })); + + // assert_eq!( + // new_positions.len(), + // old_len_stored + (other.len - other.holes.len()) + // ); + + // assert!(updates.iter().all(|update| { + // let IndexUpdateInfo { + // new_pg_entity: PowerGridEntity::Assembler { index, .. }, + // .. + // } = update + // else { + // unreachable!() + // }; + + // new_positions[*index as usize].x < i32::MAX + // })); + + // updates + // }; + let ret = Self { recipe: self.recipe, ings_max_insert: new_ings_max, @@ -484,7 +763,7 @@ impl timers: new_timers.into(), prod_timers: new_prod_timers.into(), holes: self.holes, - len: self.positions.len(), + len: new_positions.len(), base_speed: new_base_speed.into_boxed_slice(), combined_speed_mod: new_speed.into_boxed_slice(), bonus_productivity: new_prod.into_boxed_slice(), @@ -495,15 +774,26 @@ impl raw_power_consumption_modifier: new_raw_power_consumption_modifier.into_boxed_slice(), base_power_consumption: new_base_power_consumption.into_boxed_slice(), - positions: self.positions, + positions: new_positions.into_boxed_slice(), + types: new_types.into_boxed_slice(), }; + #[cfg(debug_assertions)] + { + for (i, pos) in ret.positions.iter().enumerate() { + assert_eq!( + (ret.holes.contains(&i) || i >= ret.len), + (pos.x == i32::MAX) + ); + } + } + (ret, updates) } fn get_info( &self, - index: u16, + index: u32, data_store: &DataStore, ) -> AssemblerOnclickInfo { let items = data_store.recipe_to_items.get(&self.recipe).unwrap(); @@ -546,6 +836,13 @@ impl timer_percentage: f32::from(self.timers[index as usize]) / f32::from(TIMERTYPE::MAX), prod_timer_percentage: f32::from(self.prod_timers[index as usize]) / f32::from(TIMERTYPE::MAX), + base_speed: f32::from(self.base_speed[index as usize]) * 0.05, + speed_mod: f32::from(self.raw_speed_mod[index as usize]) * 0.05, + prod_mod: f32::from(self.bonus_productivity[index as usize]) * 0.01, + power_consumption_mod: f32::from(self.power_consumption_modifier[index as usize]) + * 0.05 + - 1.0, + base_power_consumption: self.base_power_consumption[index as usize], } } @@ -651,10 +948,7 @@ impl // } // } - #[inline(never)] - // TODO: Do i want this to also do the power calculation, or will that be done in another step? // TODO: Currently power demand and supply are offset by a single tick. Is this acceptable? - // TODO: Write tests to ensure this works as expected. /// # Panics /// If `power_mult` > `MAX_POWER_MULT` = 64 pub fn update_branchless( @@ -664,59 +958,30 @@ impl recipe_ings: &[[ITEMCOUNTTYPE; NUM_INGS]], recipe_outputs: &[[ITEMCOUNTTYPE; NUM_OUTPUTS]], times: &[TIMERTYPE], - ) -> (Joule, u32, u32) { - // FIXME: These depend on which machine we are. - const POWER_DRAIN: Watt = Watt(2_500); - const POWER_CONSUMPTION: Watt = Watt(75_000); - + ) -> (Watt, u32, u32) { let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; let our_ings: &[ITEMCOUNTTYPE; NUM_INGS] = &recipe_ings[ing_idx]; let our_outputs: &[ITEMCOUNTTYPE; NUM_OUTPUTS] = &recipe_outputs[out_idx]; - // TODO: For SOME reason, this is actually faster if this is a u32. - // It is also better, since it allows possibly having more than u16::max assembers of a single recipe - let mut running: u32 = 0; - let mut times_ings_used = 0; let mut num_finished_crafts = 0; - // TODO: With power calculations being done on the fly, we cannot return early, since we then do not know the power demands of the base :( - // It might be fine, since it only applies if the power is so low, NOTHING happens and as soon as any power is connected it will start running again. - // My guess is that returning 0 (or just the drain power) would lead to flickering. - // if power_mult == 0 { - // return; - // } - - debug_assert!(power_mult <= MAX_POWER_MULT); - - // FIXME: - // assert_eq!(self.outputs.len(), self.timers.len()); - // assert_eq!(self.input1.len(), self.timers.len()); - // assert!(self.outputs.len() % Simdtype::LEN == 0); + assert!(power_mult <= MAX_POWER_MULT); - // TODO: This does not round correctly - let increase: TIMERTYPE = (TIMERTYPE::from(power_mult) - * (TIMERTYPE::MAX / TIMERTYPE::from(MAX_POWER_MULT))) - / times[self.recipe.id.into()]; - - // TODO: I don't think this holds anymore, now that we cannot bail early at 0 power_mult - // debug_assert!(increase > 0); - - let ings_arr = ZipArray { - array: self.ings.each_mut().map(|r| r.iter_mut()), - }; + // TODO: Is this amount of accuracy enough? + let increase: TIMERTYPE = (u32::from(power_mult) * u32::from(TIMERTYPE::MAX) + / u32::from(MAX_POWER_MULT) + / u32::from(times[self.recipe.id.into()])) + .try_into() + .unwrap_or(TIMERTYPE::MAX); - let outputs_arr = ZipArray { - array: self.outputs.each_mut().map(|r| r.iter_mut()), - }; + let mut power = Watt(0); - for ( - mut outputs, - (mut ings, (timer, (prod_timer, (speed_mod, (bonus_prod, (base_power, power_mod)))))), - ) in outputs_arr.zip( - ings_arr.zip( - self.timers.iter_mut().zip( + for (index, (timer, (prod_timer, (speed_mod, (bonus_prod, (base_power, power_mod)))))) in + self.timers + .iter_mut() + .zip( self.prod_timers.iter_mut().zip( self.combined_speed_mod.iter().copied().zip( self.bonus_productivity.iter().copied().zip( @@ -727,58 +992,65 @@ impl ), ), ), - ), - ), - ) { - let increase = increase * (speed_mod as u16) / 20; + ) + .enumerate() + { + // ~~Remove the items from the ings at the start of the crafting process~~ + // We will do this as part of the frontend ui! - let ing_mul = ings - .iter() - .zip(our_ings.iter()) - .fold(1, |acc, (have, want)| acc * u16::from(**have >= *want)); - let new_timer_output_space = timer.wrapping_add(increase * ing_mul); - let new_timer_output_full = timer.saturating_add(increase * ing_mul); - - let space_mul: u8 = - outputs - .iter() - .zip(our_outputs.iter()) - .fold(1, |acc, (have, new_from_recipe)| { - // TODO: 100 output amount hardcoded!!!! - acc * u8::from((have.saturating_add(*new_from_recipe)) <= 100) - }); + let increase = (u32::from(increase) * u32::from(speed_mod) / 20) as u16; + + let mut ing_mul: u8 = 1; + for i in 0..NUM_INGS { + ing_mul *= u8::from(self.ings[i][index] >= our_ings[i]); + } + + let mut ing_mul_for_two_crafts: u16 = 1; + for i in 0..NUM_INGS { + ing_mul_for_two_crafts *= u16::from(self.ings[i][index] >= our_ings[i] * 2); + } + + let new_timer_output_space = timer.wrapping_add(increase * u16::from(ing_mul)); + let new_timer_output_full = timer.saturating_add(increase * u16::from(ing_mul)); + + let mut space_mul: u8 = 1; + for i in 0..NUM_OUTPUTS { + space_mul *= + u8::from((self.outputs[i][index].saturating_add(our_outputs[i])) <= 100); + } let new_timer = new_timer_output_space * u16::from(space_mul) + new_timer_output_full * (1 - u16::from(space_mul)); - let new_prod_timer = - prod_timer.wrapping_add(new_timer.wrapping_sub(*timer) * (bonus_prod as u16) / 100); + let timer_mul: u8 = u8::from(new_timer < *timer); + + // if we have enough items for another craft keep the wrapped value, else clamp it to 0 + let new_timer = u16::from(timer_mul) * (ing_mul_for_two_crafts * new_timer) + + (1 - u16::from(timer_mul)) * new_timer; + + let new_prod_timer = prod_timer.wrapping_add( + (u32::from(new_timer.wrapping_sub(*timer)) * (bonus_prod as u32) / 100) as u16, + ); - let timer_mul: u8 = (new_timer < *timer).into(); let prod_timer_mul: u8 = (new_prod_timer < *prod_timer).into(); // Power calculation // We use power if any work was done - running += u32::from(ing_mul * u16::from(space_mul)); + power = power + base_power * u64::from(ing_mul * space_mul) * u64::from(power_mod) / 20; *timer = new_timer; *prod_timer = new_prod_timer; - outputs - .iter_mut() - .zip(our_outputs.iter()) - .for_each(|(output, new)| **output += (timer_mul + prod_timer_mul) * new); - ings.iter_mut() - .zip(our_ings.iter()) - .for_each(|(ing, used)| **ing -= timer_mul * used); + for i in 0..NUM_OUTPUTS { + self.outputs[i][index] += (timer_mul + prod_timer_mul) * our_outputs[i]; + } + for i in 0..NUM_INGS { + self.ings[i][index] -= timer_mul * our_ings[i]; + } times_ings_used += u32::from(timer_mul); num_finished_crafts += u32::from(timer_mul + prod_timer_mul); } - ( - POWER_CONSUMPTION.joules_per_tick() * u64::from(running), - times_ings_used, - num_finished_crafts, - ) + (power, times_ings_used, num_finished_crafts) } pub fn get_all_mut( @@ -807,44 +1079,41 @@ impl self.ings.each_mut().map(|b| &mut **b) } - pub fn get_outputs_mut(&mut self, idx: usize) -> &mut [ITEMCOUNTTYPE] { - &mut self.outputs[idx] + pub fn get_outputs_mut(&mut self, idx: u32) -> &mut [ITEMCOUNTTYPE] { + &mut self.outputs[idx as usize] } - pub fn get_ings_mut(&mut self, idx: usize) -> &mut [ITEMCOUNTTYPE] { - &mut self.ings[idx] + pub fn get_ings_mut(&mut self, idx: u32) -> &mut [ITEMCOUNTTYPE] { + &mut self.ings[idx as usize] } - pub fn get_output_mut( - &mut self, - output_idx: usize, - index: usize, - ) -> Option<&mut ITEMCOUNTTYPE> { - if index < self.len { - Some(&mut self.outputs[output_idx][index]) + pub fn get_output_mut(&mut self, output_idx: usize, index: u32) -> Option<&mut ITEMCOUNTTYPE> { + if (index as usize) < self.len { + Some(&mut self.outputs[output_idx][index as usize]) } else { None } } - pub fn get_output(&self, output_idx: usize, index: usize) -> Option<&ITEMCOUNTTYPE> { - if index < self.len { - Some(&self.outputs[output_idx][index]) + pub fn get_output(&self, output_idx: usize, index: u32) -> Option<&ITEMCOUNTTYPE> { + if (index as usize) < self.len { + Some(&self.outputs[output_idx][index as usize]) } else { None } } - pub fn get_ing_mut(&mut self, input_idx: usize, index: usize) -> Option<&mut ITEMCOUNTTYPE> { - if index < self.len { - Some(&mut self.ings[input_idx][index]) + pub fn get_ing_mut(&mut self, input_idx: usize, index: u32) -> Option<&mut ITEMCOUNTTYPE> { + if (index as usize) < self.len { + Some(&mut self.ings[input_idx][index as usize]) } else { None } } /// The caller must make sure, that this index is not used in any other machine, since it will either crash/work on a nonexistant Assembler or be reused for another machine! - pub fn remove_assembler(&mut self, index: usize) -> AssemblerRemovalInfo { + pub fn remove_assembler(&mut self, index: u32) -> AssemblerRemovalInfo { + let index = index as usize; debug_assert!(!self.holes.contains(&index)); self.holes.push(index); @@ -858,13 +1127,54 @@ impl for out in &mut self.outputs { out[index] = ITEMCOUNTTYPE::MAX; } + self.timers[index] = 0; + self.prod_timers[index] = 0; + self.positions[index] = Position { + x: i32::MAX, + y: i32::MAX, + }; ret } - fn remove_assembler_data( + pub fn remove_assembler_data( &mut self, - index: usize, + index: u32, + ) -> ( + Vec, + Vec, + Vec, + TIMERTYPE, + TIMERTYPE, + Watt, + i16, + i16, + u8, + i16, + u8, + Position, + ) { + let data = self.remove_assembler_data_inner(index); + + ( + data.0.into(), + data.1.into(), + data.2.into(), + data.3.into(), + data.4.into(), + data.5.into(), + data.6.into(), + data.7.into(), + data.8.into(), + data.9.into(), + data.10.into(), + data.11.into(), + ) + } + + fn remove_assembler_data_inner( + &mut self, + index: u32, ) -> ( [ITEMCOUNTTYPE; NUM_INGS], [ITEMCOUNTTYPE; NUM_INGS], @@ -876,8 +1186,10 @@ impl i16, u8, i16, + u8, Position, ) { + let index = index as usize; debug_assert!(!self.holes.contains(&index)); self.holes.push(index); @@ -901,6 +1213,7 @@ impl self.raw_bonus_productivity[index], self.base_speed[index], self.raw_speed_mod[index], + self.types[index], self.positions[index], ); for ing in &mut self.ings { @@ -909,7 +1222,13 @@ impl for out in &mut self.outputs { out[index] = ITEMCOUNTTYPE::MAX; } + self.timers[index] = 0; + self.prod_timers[index] = 0; self.base_power_consumption[index] = Watt(0); + self.positions[index] = Position { + x: i32::MAX, + y: i32::MAX, + }; ret } @@ -919,8 +1238,10 @@ impl ty: u8, modules: &[Option], position: Position, + recipe_lookup: &[(usize, usize)], + recipe_ings: &[[ITEMCOUNTTYPE; NUM_INGS]], data_store: &DataStore, - ) -> usize { + ) -> u32 { assert_eq!( modules.len(), data_store.assembler_info[usize::from(ty)].num_module_slots as usize @@ -953,9 +1274,18 @@ impl .map(|module| i16::from(data_store.module_info[module].power_mod)) .sum(); + let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; + + let our_ings: &[ITEMCOUNTTYPE; NUM_INGS] = &recipe_ings[ing_idx]; + self.add_assembler_with_data( // TODO: Make the automatic insertion limit dependent on the speed of the machine and recipe - array::from_fn(|ing| 10), + array::from_fn(|ing| { + max( + HAND_SIZE, + our_ings[ing].saturating_mul(3).saturating_add(12), + ) + }), array::from_fn(|_| 0), array::from_fn(|_| 0), 0, @@ -965,6 +1295,7 @@ impl prod, base_speed, speed_mod, + ty, position, data_store, ) @@ -972,19 +1303,19 @@ impl pub fn move_assembler( &mut self, - index: usize, + index: u32, dest: &mut Self, data_store: &DataStore, - ) -> usize { - let data = self.remove_assembler_data(index); + ) -> u32 { + let data = self.remove_assembler_data_inner(index); dest.add_assembler_with_data( data.0, data.1, data.2, data.3, data.4, data.5, data.6, data.7, data.8, data.9, - data.10, data_store, + data.10, data.11, data_store, ) } - fn add_assembler_with_data( + pub fn add_assembler_with_data( &mut self, ings_max_insert: [ITEMCOUNTTYPE; NUM_INGS], ings: [ITEMCOUNTTYPE; NUM_INGS], @@ -996,9 +1327,10 @@ impl bonus_productiviy: i16, base_speed: u8, speed_mod: i16, + ty: u8, position: Position, data_store: &DataStore, - ) -> usize { + ) -> u32 { let len = self.timers.len(); // debug_assert!(len % Simdtype::LEN == 0); @@ -1043,8 +1375,9 @@ impl .try_into() .expect("Value clamped already"); + self.types[hole_index] = ty; self.positions[hole_index] = position; - return hole_index; + return hole_index.try_into().unwrap(); } if self.len == self.timers.len() { @@ -1159,6 +1492,30 @@ impl speed.resize(new_len, 0); speed.into_boxed_slice() }); + + take_mut::take(&mut self.combined_speed_mod, |speed| { + let mut speed = speed.into_vec(); + speed.resize(new_len, 0); + speed.into_boxed_slice() + }); + + take_mut::take(&mut self.positions, |pos| { + let mut pos = pos.into_vec(); + pos.resize( + new_len, + Position { + x: i32::MAX, + y: i32::MAX, + }, + ); + pos.into_boxed_slice() + }); + + take_mut::take(&mut self.types, |ty| { + let mut ty = ty.into_vec(); + ty.resize(new_len, u8::MAX); + ty.into_boxed_slice() + }); } for (output, new_val) in self.outputs.iter_mut().zip(out) { @@ -1192,28 +1549,22 @@ impl .try_into() .expect("Values already clamped"); - self.positions.reserve(1); - assert_eq!( - self.positions.len(), - self.len, - "{:?}, {:?}", - self.positions, - self.len - ); - self.positions.push(position); + self.positions[self.len] = position; + self.types[self.len] = ty; self.len += 1; - self.len - 1 + (self.len - 1).try_into().unwrap() } pub fn modify_modifiers( &mut self, - index: u16, + index: u32, speed: i16, prod: i16, power: i16, data_store: &DataStore, ) { + let index = index as usize; self.raw_speed_mod[usize::from(index)] = self.raw_speed_mod[usize::from(index)] .checked_add(speed) .expect("Over/Underflowed"); @@ -1239,7 +1590,7 @@ impl self.combined_speed_mod[usize::from(index)] = ((self.raw_speed_mod[usize::from(index)] + 20) * i16::from(self.base_speed[usize::from(index)]) - / 10) + / 20) .clamp(0, u8::MAX.into()) .try_into() .expect("Values already clamped"); @@ -1262,13 +1613,13 @@ impl Iterator for ZipArray { } } -mod arrays { +pub mod arrays { use std::{convert::TryInto, marker::PhantomData}; use serde::{ + Deserialize, Deserializer, Serialize, Serializer, de::{SeqAccess, Visitor}, ser::SerializeTuple, - Deserialize, Deserializer, Serialize, Serializer, }; pub fn serialize( data: &[T; N], diff --git a/src/belt/belt.rs b/src/belt/belt.rs index f324bbc..edbfb47 100644 --- a/src/belt/belt.rs +++ b/src/belt/belt.rs @@ -1,25 +1,21 @@ -use std::{ - error::Error, - fmt::{Display, Write}, - marker::PhantomData, -}; +use std::{error::Error, fmt::Display}; -use crate::item::{self, IdxTrait, Item, WeakIdxTrait}; +use crate::item::{IdxTrait, Item, WeakIdxTrait}; -use super::smart::Side; +use super::{smart::Side, splitter::SushiSplitter}; pub type BeltLenType = u16; pub trait Belt { - fn query_item(&self, pos: BeltLenType) -> Option>; - fn get_front(&self) -> Option> { + fn query_item(&self, pos: BeltLenType) -> Option>; + fn get_front(&self) -> Option> { self.query_item(0) } - fn get_back(&self) -> Option> { + fn get_back(&self) -> Option> { let len = self.get_len(); self.query_item(len - 1) } - fn remove_item(&mut self, pos: BeltLenType) -> Option>; + fn remove_item(&mut self, pos: BeltLenType) -> Option>; /// # Errors /// When there is no space at `pos` fn try_insert_item( @@ -30,13 +26,13 @@ pub trait Belt { // TODO: I need to choose here. using impl Trait means no dyn-compatibility, // but a vec adds another allocation - // fn items(&self) -> impl IntoIterator>>; - fn items(&self) -> Vec>>; + // fn items(&self) -> impl Iterator>>; + fn items(&self) -> impl Iterator>>; fn get_len(&self) -> BeltLenType; fn add_length(&mut self, amount: BeltLenType, side: Side) -> BeltLenType; - fn update(&mut self); + fn update(&mut self, splitter_list: &[SushiSplitter]); fn item_hint(&self) -> Option>>; } @@ -46,26 +42,6 @@ pub enum ItemInfo { Sushi(Item), } -struct PrintMe>(T, PhantomData); - -impl Display for PrintMe -where - T: Belt, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = String::new(); - - for i in 0..self.0.get_len() { - // match self.query_item(i) { - // Some(item) => f.write_str(&item.print()), - // None => f.write_char("."), - // } - } - - write!(f, "{s}") - } -} - #[derive(Debug)] pub struct NoSpaceError; diff --git a/src/belt/mod.rs b/src/belt/mod.rs index 1cbbc2f..39146b5 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -7,47 +7,63 @@ pub mod splitter; mod sushi; use std::{ + cell::UnsafeCell, collections::{HashMap, HashSet}, iter::once, marker::PhantomData, mem, usize, }; +use crate::inserter::HAND_SIZE; + +use serde::ser::{SerializeSeq, SerializeStruct}; +use strum::IntoEnumIterator; + use crate::{ data::DataStore, inserter::{ - belt_belt_inserter::{BeltBeltInserter, SushiBeltBeltInserter}, + belt_belt_inserter::BeltBeltInserter, belt_storage_inserter::{BeltStorageInserter, Dir}, - Storage, }, - item::{usize_from, Item}, - storage_list::{grid_size, num_recipes, SingleItemStorages}, + item::{Item, usize_from}, + storage_list::{SingleItemStorages, grid_size}, +}; +use crate::{ + inserter::{FakeUnionStorage, Storage}, + item::Indexable, }; use belt::{Belt, BeltLenType}; -use itertools::Itertools; +use itertools::{Either, Itertools}; use log::info; use petgraph::{ + Direction::Outgoing, graph::NodeIndex, prelude::StableDiGraph, - visit::{EdgeRef, NodeRef}, - Direction::Outgoing, + visit::{EdgeRef, IntoNodeReferences}, }; use rayon::iter::IntoParallelRefMutIterator; use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -use smart::{InserterAdditionError, Side, SmartBelt, SpaceOccupiedError}; -use splitter::{Splitter, SplitterDistributionMode, SushiSplitter}; +use smart::{BeltInserterInfo, InserterAdditionError, Side, SmartBelt, SpaceOccupiedError}; +use splitter::{PureSplitter, SplitterDistributionMode, SplitterSide, SushiSplitter}; use sushi::{SushiBelt, SushiInfo}; +use parking_lot::Mutex; + #[derive(Debug, PartialEq, Clone, Copy, serde::Deserialize, serde::Serialize)] enum FreeIndex { FreeIndex(BeltLenType), OldFreeIndex(BeltLenType), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +struct SplitterID { + index: u32, +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum Inserter { - Out(BeltStorageInserter), - In(BeltStorageInserter), +enum Inserter { + Out(BeltStorageInserter<{ Dir::BeltToStorage }>), + In(BeltStorageInserter<{ Dir::StorageToBelt }>), } #[derive( @@ -87,8 +103,8 @@ fn do_update_test_bools(items: &mut [bool]) { } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct BeltStore { - inner: InnerBeltStore, +pub struct BeltStore { + pub inner: InnerBeltStore, any_belts: Vec>, any_belt_holes: Vec, @@ -96,12 +112,12 @@ pub struct BeltStore { any_splitters: Vec>, any_splitter_holes: Vec, - belt_graph: StableDiGraph, BeltGraphConnection>, - belt_graph_lookup: HashMap, NodeIndex>, + pub belt_graph: StableDiGraph, BeltGraphConnection>, + pub belt_graph_lookup: HashMap, NodeIndex>, } #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] -enum BeltGraphConnection { +pub enum BeltGraphConnection { Sideload { dest_belt_pos: BeltLenType, }, @@ -110,44 +126,131 @@ enum BeltGraphConnection { dest_belt_pos: BeltLenType, filter: Item, }, - Connected, // Used for handling extremely long belts and splitters - // Always connects the end of the source to the beginning of the destination + /// Used for handling extremely long belts and splitters + /// Always connects the end of the source to the beginning of the destination + Connected { + filter: Option>, + }, } -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct InnerBeltStore { - sushi_belts: Vec>, +#[derive(Debug, serde::Deserialize)] +pub struct InnerBeltStore { + sushi_belts: Vec>, sushi_belt_holes: Vec, - smart_belts: Box<[MultiBeltStore]>, + smart_belts: Box<[MultiBeltStore]>, - belt_belt_inserters: BeltBeltInserterStore, + pub belt_belt_inserters: BeltBeltInserterStore, pure_splitters: Box<[SplitterStore]>, - sushi_splitters: Vec, + sushi_splitters: Vec>, + sushi_splitter_connections: Mutex; 2]; 2]>>, sushi_splitter_holes: Vec, + + pub belt_update_timers: Box<[u8]>, + pub belt_update_timers_cumulative: Box<[u32]>, +} + +impl Clone for InnerBeltStore { + fn clone(&self) -> Self { + Self { + sushi_belts: self.sushi_belts.clone(), + sushi_belt_holes: self.sushi_belt_holes.clone(), + smart_belts: self.smart_belts.clone(), + belt_belt_inserters: self.belt_belt_inserters.clone(), + pure_splitters: self.pure_splitters.clone(), + sushi_splitters: self + .sushi_splitters + .iter() + .map(|sushi_splitter| unsafe { + // SAFETY: + // The only point in the code, where we use a & for mutating the unsafe cell is during the belt update. + // The Belt update requires a &mut of either self.smart_belts or self.sushi_belts. + // We currently hold a & of both of them, so no &mut can exist. + sushi_splitter.unsafe_clone() + }) + .collect(), + sushi_splitter_connections: Mutex::new(self.sushi_splitter_connections.lock().clone()), + sushi_splitter_holes: self.sushi_splitter_holes.clone(), + belt_update_timers: self.belt_update_timers.clone(), + belt_update_timers_cumulative: self.belt_update_timers_cumulative.clone(), + } + } +} + +impl serde::Serialize for InnerBeltStore +where + ItemIdxType: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + /// CONSTRUCTING THIS IS unsafe, since it requires a precondition or the trait impl will cause UB + struct SushiSplitterWrapper<'a, ItemIdxType: WeakIdxTrait> { + inner: &'a Vec>, + } + + impl<'a, ItemIdxType: WeakIdxTrait> serde::Serialize for SushiSplitterWrapper<'a, ItemIdxType> + where + ItemIdxType: serde::Serialize, + { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.inner.len()))?; + for sushi_splitter in self.inner { + // SAFETY: + // This is safe, since SushiSplitterWrapper may only be constructed when no writes on the inner SushiSplitters are occuring + unsafe { + sushi_splitter.unsafe_serialize_elem::(&mut seq)?; + } + } + seq.end() + } + } + + let mut state = serializer.serialize_struct("InnerBeltStore", 10)?; + state.serialize_field("sushi_belts", &self.sushi_belts)?; + state.serialize_field("sushi_belt_holes", &self.sushi_belt_holes)?; + state.serialize_field("smart_belts", &self.smart_belts)?; + state.serialize_field("belt_belt_inserters", &self.belt_belt_inserters)?; + state.serialize_field("pure_splitters", &self.pure_splitters)?; + state.serialize_field( + "sushi_splitters", + // SAFETY: + // The only point in the code, where we use a & for mutating the unsafe cell is during the belt update. + // The Belt update requires a &mut of either self.smart_belts or self.sushi_belts. + // We currently hold a & of both of them, so no &mut can exist. + &SushiSplitterWrapper { + inner: &self.sushi_splitters, + }, + )?; + state.serialize_field( + "sushi_splitter_connections", + &self.sushi_splitter_connections, + )?; + state.serialize_field("sushi_splitter_holes", &self.sushi_splitter_holes)?; + state.serialize_field("belt_update_timers", &self.belt_update_timers)?; + state.serialize_field( + "belt_update_timers_cumulative", + &self.belt_update_timers_cumulative, + )?; + state.end() + } } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct SplitterStore { // TODO: Holes - pub pure_splitters: Vec, + pub pure_splitters: Vec, item: PhantomData, } -impl SplitterStore { - pub fn get_splitter_belt_ids<'a>( - &'a self, - id: usize, - ) -> [impl IntoIterator + use<'a, ItemIdxType>; 2] { - [ - self.pure_splitters[id].input_belts.iter().copied(), - self.pure_splitters[id].output_belts.iter().copied(), - ] - } -} +impl SplitterStore {} #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] enum AnyBeltBeltInserter { @@ -158,7 +261,7 @@ enum AnyBeltBeltInserter { } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct BeltBeltInserterStore { +pub struct BeltBeltInserterStore { // FIXME: This is likely VERY slow pub pure_to_pure_inserters: Box< [Vec< @@ -232,11 +335,8 @@ pub struct BreakBeltResultInfo { pub new_belt: Option<(BeltTileId, Side)>, } -impl InnerBeltStore { - fn remove_smart_belt( - &mut self, - id: BeltId, - ) -> SmartBelt { +impl InnerBeltStore { + fn remove_smart_belt(&mut self, id: BeltId) -> SmartBelt { self.smart_belts[usize_from(id.item.id)].remove_belt(id.index) } @@ -244,45 +344,167 @@ impl InnerBeltStore, id: usize, - ) -> [impl IntoIterator> + use<'a, ItemIdxType, RecipeIdxType>; 2] - { - let index_to_id = move |index| BeltId { item, index }; + ) -> [[AnyBelt; 2]; 2] { + info!("Had to search for splitter belt ids!"); + todo!() + } - let [input, output] = self.pure_splitters[usize_from(item.id)].get_splitter_belt_ids(id); + fn get_sushi_splitter_belt_ids(&self, id: SplitterID) -> [[AnyBelt; 2]; 2] { + let maybe_ret = &mut self.sushi_splitter_connections.lock()[id.index as usize]; - [ - input.into_iter().map(index_to_id), - output.into_iter().map(index_to_id), - ] + for side in SplitterSide::iter() { + let input_belt = &mut maybe_ret[0][usize::from(bool::from(side))]; + + let needs_update = match input_belt { + AnyBelt::Smart(belt_id) => { + if let Some((out_id, out_side)) = self.get_smart(*belt_id).output_splitter { + out_id != id || out_side != side + } else { + true + } + }, + AnyBelt::Sushi(sushi_index) => { + if let Some((out_id, out_side)) = self.get_sushi(*sushi_index).output_splitter { + out_id != id || out_side != side + } else { + true + } + }, + }; + + if needs_update { + *input_belt = self.get_sushi_splitter_belt_ids_uncached_input(id, side); + } + } + + for side in SplitterSide::iter() { + let output_belt = &mut maybe_ret[1][usize::from(bool::from(side))]; + + let needs_update = match output_belt { + AnyBelt::Smart(belt_id) => { + if let Some((in_id, in_side)) = self.get_smart(*belt_id).input_splitter { + in_id != id || in_side != side + } else { + true + } + }, + AnyBelt::Sushi(sushi_index) => { + if let Some((in_id, in_side)) = self.get_sushi(*sushi_index).input_splitter { + in_id != id || in_side != side + } else { + true + } + }, + }; + + if needs_update { + *output_belt = self.get_sushi_splitter_belt_ids_uncached_output(id, side); + } + } + + maybe_ret.clone() } - fn get_sushi_splitter_belt_ids<'a>( - &'a self, - id: usize, - ) -> [impl IntoIterator + use<'a, ItemIdxType, RecipeIdxType>; 2] { - [ - self.sushi_splitters[id].input_belts.iter().copied(), - self.sushi_splitters[id].output_belts.iter().copied(), - ] + fn get_sushi_splitter_belt_ids_uncached_input( + &self, + id: SplitterID, + side: SplitterSide, + ) -> AnyBelt { + info!("Had to search for splitter belt ids!"); + + for (item_index, store) in self.smart_belts.iter().enumerate() { + let item = Item { + id: ItemIdxType::try_from(item_index).unwrap(), + }; + + let index = store.belts.iter().position(|belt| { + if let Some((splitter_id, splitter_side)) = belt.output_splitter { + splitter_id == id && splitter_side == side + } else { + false + } + }); + + if let Some(index) = index { + return AnyBelt::Smart(BeltId { item, index }); + } + } + + let index = self.sushi_belts.iter().position(|belt| { + if let Some((splitter_id, splitter_side)) = belt.output_splitter { + splitter_id == id && splitter_side == side + } else { + false + } + }); + + if let Some(index) = index { + return AnyBelt::Sushi(index); + } + + unreachable!("Splitter is not connected to any belt (including its internal one)"); } - fn remove_sushi_belt(&mut self, id: usize) -> SushiBelt { - let mut temp = SushiBelt::new(1); + fn get_sushi_splitter_belt_ids_uncached_output( + &self, + id: SplitterID, + side: SplitterSide, + ) -> AnyBelt { + info!("Had to search for splitter belt ids!"); + + for (item_index, store) in self.smart_belts.iter().enumerate() { + let item = Item { + id: ItemIdxType::try_from(item_index).unwrap(), + }; + + let index = store.belts.iter().position(|belt| { + if let Some((splitter_id, splitter_side)) = belt.input_splitter { + splitter_id == id && splitter_side == side + } else { + false + } + }); + + if let Some(index) = index { + return AnyBelt::Smart(BeltId { item, index }); + } + } + + let index = self.sushi_belts.iter().position(|belt| { + if let Some((splitter_id, splitter_side)) = belt.input_splitter { + splitter_id == id && splitter_side == side + } else { + false + } + }); + + if let Some(index) = index { + return AnyBelt::Sushi(index); + } + + unreachable!("Splitter is not connected to any belt (including its internal one)"); + } + + fn remove_sushi_belt(&mut self, id: usize) -> SushiBelt { + let mut temp = SushiBelt::new(0, 1); + temp.make_circular(); mem::swap(&mut temp, &mut self.sushi_belts[id]); + self.sushi_belt_holes.push(id); temp } - fn get_smart( - &self, - smart_belt_id: BeltId, - ) -> &SmartBelt { + fn try_get_smart(&self, smart_belt_id: BeltId) -> Option<&SmartBelt> { + self.smart_belts + .get(usize_from(smart_belt_id.item.id))? + .belts + .get(smart_belt_id.index) + } + + fn get_smart(&self, smart_belt_id: BeltId) -> &SmartBelt { &self.smart_belts[usize_from(smart_belt_id.item.id)].belts[smart_belt_id.index] } - fn get_smart_mut( - &mut self, - smart_belt_id: BeltId, - ) -> &mut SmartBelt { + fn get_smart_mut(&mut self, smart_belt_id: BeltId) -> &mut SmartBelt { &mut self.smart_belts[usize_from(smart_belt_id.item.id)].belts[smart_belt_id.index] } @@ -290,21 +512,18 @@ impl InnerBeltStore, indices: [usize; N], - ) -> [&mut SmartBelt; N] { + ) -> [&mut SmartBelt; N] { self.smart_belts[usize_from(item.id)] .belts .get_disjoint_mut(indices) .unwrap() } - fn get_sushi(&self, sushi_belt_id: usize) -> &SushiBelt { + fn get_sushi(&self, sushi_belt_id: usize) -> &SushiBelt { &self.sushi_belts[sushi_belt_id] } - fn get_sushi_mut( - &mut self, - sushi_belt_id: usize, - ) -> &mut SushiBelt { + fn get_sushi_mut(&mut self, sushi_belt_id: usize) -> &mut SushiBelt { &mut self.sushi_belts[sushi_belt_id] } @@ -315,132 +534,189 @@ impl InnerBeltStore { - let Some(old) = ins.take() else { - unreachable!() - }; + match (is_source, is_dest) { + (true, true) => { + let hole_idx = self + .belt_belt_inserters + .sushi_to_sushi_inserters + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, + ((new_id, source_pos), (new_id, dest_pos), movetime, filter), + )); - self.belt_belt_inserters - .sushi_to_sushi_inserters - .push(Some(( - old.0, + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx] + .is_none() + ); + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx] = + new_val; + } else { + self.belt_belt_inserters + .sushi_to_sushi_inserters + .push(new_val); + } + }, + (true, false) => { + let new_val = ( + ins, ( - (new_id, old.1 .0 .1), - (new_id, old.1 .1 .1), - old.1 .2, - Some(id.item), + (new_id, source_pos), + ( + BeltId { + item: id.item, + index: dest, + }, + dest_pos, + ), + movetime, + filter, ), - ))); - }, - (true, false) => { - let Some(old) = ins.take() else { - unreachable!() - }; - - // This inserter starts on the (now sushi) belt and end on a smart belt. This means that the destination belt HAS to be changed to a sushi belt later - self.belt_belt_inserters - .temp_sushi_to_smart_inserters - .push(( - old.0, + ); + self.belt_belt_inserters + .temp_sushi_to_smart_inserters + .push(new_val); + }, + (false, true) => { + let hole_idx = self + .belt_belt_inserters + .pure_to_sushi_inserters + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, ( - (new_id, old.1 .0 .1), ( BeltId { item: id.item, - index: old.1 .1 .0, + index: source, }, - old.1 .1 .1, + source_pos, ), - old.1 .2, - old.1 .3, + (new_id, dest_pos), + movetime, + filter, ), )); - }, - (false, true) => { - let Some(old) = ins.take() else { - unreachable!() - }; - // This inserter starts on the (now sushi) belt and end on a smart belt. This means that the destination belt HAS to be changed to a sushi belt later - self.belt_belt_inserters.pure_to_sushi_inserters.push(Some(( - old.0, - ( - ( - BeltId { - item: id.item, - index: old.1 .0 .0, - }, - old.1 .0 .1, - ), - (new_id, old.1 .1 .1), - old.1 .2, - old.1 .3, - ), - ))); - }, - (false, false) => {}, + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.pure_to_sushi_inserters[hole_idx] + .is_none() + ); + self.belt_belt_inserters.pure_to_sushi_inserters[hole_idx] = + new_val; + } else { + self.belt_belt_inserters + .pure_to_sushi_inserters + .push(new_val); + } + }, + (false, false) => { + // Unrelated + unreachable!(); + }, + } } } } - for ins in &mut self.belt_belt_inserters.pure_to_sushi_inserters { - if let Some(inner_ins) = ins { - if inner_ins.1 .0 .0 == id { - let Some(old) = ins.take() else { - unreachable!() + for ins in self.belt_belt_inserters.pure_to_sushi_inserters.iter_mut() { + if let Some((_, ((source, _), (dest, _), _, _))) = ins { + if *source == id { + let Some((ins, ((source, source_pos), (dest, dest_pos), movetime, filter))) = + ins.take() + else { + unreachable!(); }; - self.belt_belt_inserters + let hole_idx = self + .belt_belt_inserters .sushi_to_sushi_inserters - .push(Some(( - old.0, - ( - (new_id, old.1 .0 .1), - (old.1 .1 .0, old.1 .1 .1), - old.1 .2, - old.1 .3, - ), - ))); + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, + ((new_id, source_pos), (dest, dest_pos), movetime, filter), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx].is_none() + ); + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx] = new_val; + } else { + self.belt_belt_inserters + .sushi_to_sushi_inserters + .push(new_val); + } + } else { + // Unrelated, do nothing } } } + self.belt_belt_inserters + .temp_sushi_to_smart_inserters + .retain( + |(ins, ((source, source_pos), (dest, dest_pos), movetime, filter))| { + if *dest == id { + let hole_idx = self + .belt_belt_inserters + .sushi_to_sushi_inserters + .iter() + .position(Option::is_none); + + let new_val = Some(( + *ins, + ( + (*source, *source_pos), + (new_id, *dest_pos), + *movetime, + *filter, + ), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx] + .is_none() + ); + self.belt_belt_inserters.sushi_to_sushi_inserters[hole_idx] = new_val; + } else { + self.belt_belt_inserters + .sushi_to_sushi_inserters + .push(new_val); + } + + // Now pure pure + false + } else { + // Unrelated, keep + true + } + }, + ); + new_id } @@ -451,12 +727,182 @@ impl InnerBeltStore { + let hole_idx = self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)] + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, + ( + (new_id.index, source_pos), + (new_id.index, dest_pos), + movetime, + filter, + ), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)][hole_idx] + .is_none() + ); + self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)][hole_idx] = new_val; + } else { + self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)] + .push(new_val); + } + }, + (true, false) => { + let hole_idx = self + .belt_belt_inserters + .pure_to_sushi_inserters + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, + ((new_id, source_pos), (dest, dest_pos), movetime, filter), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.pure_to_sushi_inserters[hole_idx] + .is_none() + ); + self.belt_belt_inserters.pure_to_sushi_inserters[hole_idx] = + new_val; + } else { + self.belt_belt_inserters + .pure_to_sushi_inserters + .push(new_val); + } + }, + (false, true) => { + let new_val = ( + ins, + ((source, source_pos), (new_id, dest_pos), movetime, filter), + ); + + self.belt_belt_inserters + .temp_sushi_to_smart_inserters + .push(new_val); + }, + (false, false) => { + // Unrelated + unreachable!(); + }, + } + } + } + } + + for ins in self.belt_belt_inserters.pure_to_sushi_inserters.iter_mut() { + if let Some((_, ((_, _), (dest, _), _, _))) = ins { + if *dest == index { + let Some((ins, ((source, source_pos), (dest, dest_pos), movetime, filter))) = + ins.take() + else { + unreachable!(); + }; + + let hole_idx = self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)] + .iter() + .position(Option::is_none); + + let new_val = Some(( + ins, + ( + (source.index, source_pos), + (new_id.index, dest_pos), + movetime, + filter, + ), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)] + [hole_idx] + .is_none() + ); + self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)] + [hole_idx] = new_val; + } else { + self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)] + .push(new_val); + } + } else { + // Unrelated, do nothing + } + } + } + + self.belt_belt_inserters + .temp_sushi_to_smart_inserters + .retain( + |(ins, ((source, source_pos), (dest, dest_pos), movetime, filter))| { + if *source == index { + let hole_idx = self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)] + .iter() + .position(Option::is_none); + + let new_val = Some(( + *ins, + ( + (new_id.index, *source_pos), + (dest.index, *dest_pos), + *movetime, + *filter, + ), + )); + + if let Some(hole_idx) = hole_idx { + assert!( + self.belt_belt_inserters.pure_to_pure_inserters + [usize_from(item.id)][hole_idx] + .is_none() + ); + self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)] + [hole_idx] = new_val; + } else { + self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)] + .push(new_val); + } + + // Now pure pure + false + } else { + // Unrelated, keep + true + } + }, + ); // TODO: Update id whereever necessary new_id } - fn add_sushi_belt(&mut self, belt: SushiBelt) -> usize { + fn add_sushi_belt(&mut self, belt: SushiBelt) -> usize { let sushi_idx = if let Some(hole) = self.sushi_belt_holes.pop() { self.sushi_belts[hole] = belt; hole @@ -468,7 +914,7 @@ impl InnerBeltStore) -> BeltId { + fn add_belt(&mut self, belt: SmartBelt) -> BeltId { let item = belt.item; let smart_idx = self.smart_belts[usize_from(belt.item.id)].add_belt(belt); @@ -509,38 +955,132 @@ impl InnerBeltStore, back: BeltId) -> () { if front.item != back.item { todo!("Item mismatch. Do I want to error or panic?"); panic!("We defintively cannot continue."); } + let item = front.item; + if front == back { - todo!("Make circular"); + self.get_smart_mut(front).make_circular(); return; } + let mut front_len = None; + let back_belt = self.remove_smart_belt(back); take_mut::take(self.get_smart_mut(front), |front| { + front_len = Some(front.get_len()); SmartBelt::join(front, back_belt) }); + + let front_len = front_len.unwrap(); + // FIXME: We need to fix inserter ids and offsets! + + for ins in self.belt_belt_inserters.pure_to_pure_inserters[usize_from(item.id)].iter_mut() { + if let Some((_, ((source, source_pos), (dest, dest_pos), _, _))) = ins { + if *source == back.index { + *source = front.index; + *source_pos = (*source_pos) + .checked_add(front_len) + .expect("Belt too long!"); + } + + if *dest == back.index { + *dest = front.index; + *dest_pos = (*dest_pos).checked_add(front_len).expect("Belt too long!"); + } + } + } + + for ins in self.belt_belt_inserters.pure_to_sushi_inserters.iter_mut() { + if let Some((_, ((source, source_pos), (des_, _), _, _))) = ins { + if *source == back { + *source = front; + *source_pos = (*source_pos) + .checked_add(front_len) + .expect("Belt too long!"); + } + } + } + + for ins in self + .belt_belt_inserters + .temp_sushi_to_smart_inserters + .iter_mut() + { + let (_, ((_, _), (dest, dest_pos), _, _)) = ins; + if *dest == back { + *dest = front; + *dest_pos = (*dest_pos).checked_add(front_len).expect("Belt too long!"); + } + } } // TODO: What does this return? + #[profiling::function] fn merge_sushi_belts(&mut self, front: usize, back: usize) { if front == back { - todo!("Make circular"); + self.get_sushi_mut(front).make_circular(); + return; + } + + let mut front_len = None; + + let back_belt = self.remove_sushi_belt(back); + + take_mut::take(self.get_sushi_mut(front), |front| { + front_len = Some(front.get_len()); + SushiBelt::join(front, back_belt) + }); + + let front_len = front_len.unwrap(); + + for ins in self.belt_belt_inserters.sushi_to_sushi_inserters.iter_mut() { + if let Some((_, ((source, source_pos), (dest, dest_pos), _, _))) = ins { + if *source == back { + *source = front; + *source_pos = (*source_pos) + .checked_add(front_len) + .expect("Belt too long!"); + } - return; + if *dest == back { + *dest = front; + *dest_pos = (*dest_pos).checked_add(front_len).expect("Belt too long!"); + } + } } - let back_belt = self.remove_sushi_belt(back); + for ins in self.belt_belt_inserters.pure_to_sushi_inserters.iter_mut() { + if let Some((_, ((_, _), (dest, dest_pos), _, _))) = ins { + if *dest == back { + *dest = front; + *dest_pos = (*dest_pos).checked_add(front_len).expect("Belt too long!"); + } + } + } - take_mut::take(self.get_sushi_mut(front), |front| { - SushiBelt::join(front, back_belt) - }); + for ins in self + .belt_belt_inserters + .temp_sushi_to_smart_inserters + .iter_mut() + { + let (_, ((source, source_pos), (des_, _), _, _)) = ins; + if *source == back { + *source = front; + *source_pos = (*source_pos) + .checked_add(front_len) + .expect("Belt too long!"); + } + } + + // FIXME: We need to fix splitter ids and offsets! } // TODO: What does this return? @@ -560,6 +1100,8 @@ impl InnerBeltStore InnerBeltStore { Pure(Item, usize), - Sushi(usize), + Sushi(SplitterID), } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] @@ -605,22 +1149,25 @@ enum MakePureError { ErrorSushi, } -impl BeltStore { - pub fn new(data_store: &DataStore) -> Self { +impl BeltStore { + pub fn new( + data_store: &DataStore, + ) -> Self { Self { inner: InnerBeltStore { sushi_belts: vec![], sushi_belt_holes: vec![], smart_belts: vec![ MultiBeltStore { + belt_ty: vec![], belts: vec![], holes: vec![] }; - data_store.item_names.len() + data_store.item_display_names.len() ] .into_boxed_slice(), belt_belt_inserters: BeltBeltInserterStore { - pure_to_pure_inserters: vec![vec![]; data_store.item_names.len()] + pure_to_pure_inserters: vec![vec![]; data_store.item_display_names.len()] .into_boxed_slice(), pure_to_sushi_inserters: vec![], sushi_to_sushi_inserters: vec![], @@ -632,12 +1179,17 @@ impl BeltStore BeltStore( - store: &mut BeltStore, + fn check_incoming_edges( + store: &mut BeltStore, tile_id: BeltTileId, mut goal_item: Option>, incoming_edges: &[(NodeIndex, BeltGraphConnection)], @@ -695,7 +1247,7 @@ impl BeltStore { match connection_type { BeltGraphConnection::Sideload { dest_belt_pos: _ } - | BeltGraphConnection::Connected => { + | BeltGraphConnection::Connected { filter: None } => { // Every item on the belt could end up on out belt! match store.try_make_belt_pure(idx, *source_tile_id, bias) { @@ -715,7 +1267,10 @@ impl BeltStore BeltStore { - if *belt_items == inserter_item { + if belt_items == inserter_item { // All items are the correct type! } else { return Err(MakePureError::ErrorSushi); @@ -807,13 +1362,13 @@ impl BeltStore { - if *belt_items == *incoming_belts_item.get_or_insert(*belt_items) { + if belt_items == *incoming_belts_item.get_or_insert(belt_items) { // All items are the correct type! - *belt_items + belt_items } else { return Err(MakePureError::ErrorSushi); } @@ -850,11 +1405,24 @@ impl BeltStore) { + let mut done_propagating = HashSet::default(); + let mut dedup = HashSet::default(); + let mut done = HashMap::default(); + + self.propagate_sushi_if_necessary(tile_id, &mut done_propagating, &mut dedup, &mut done); + } + fn merge_and_fix( &mut self, front_tile_id: BeltTileId, back_tile_id: BeltTileId, ) { + // if !self.belt_graph_lookup.contains_key(&back_tile_id) { + // // We assume we already merged them in the graph + // assert!(self.belt_graph_lookup.contains_key(&front_tile_id)); + // return; + // } let front_len = self.get_len(front_tile_id); let inner_edges: Vec<_> = self @@ -868,21 +1436,42 @@ impl BeltStore {}, - BeltGraphConnection::BeltBeltInserter { .. } => {}, - BeltGraphConnection::Connected => {}, + BeltGraphConnection::Sideload { .. } => { + // TODO: Fix sideload inserter + todo!() + }, + BeltGraphConnection::BeltBeltInserter { .. } => { + // TODO: Fix BeltBeltInserter inserter + todo!() + }, + BeltGraphConnection::Connected { .. } => { + // TODO: Fix Connection inserter + todo!() + }, } } let edges: Vec<_> = self .belt_graph - .edges(self.belt_graph_lookup[&back_tile_id]) + .edges_directed( + self.belt_graph_lookup[&back_tile_id], + petgraph::Direction::Incoming, + ) .map(|edge| (edge.source(), edge.target(), edge.id())) + .chain( + self.belt_graph + .edges_directed( + self.belt_graph_lookup[&back_tile_id], + petgraph::Direction::Outgoing, + ) + .map(|edge| (edge.source(), edge.target(), edge.id())), + ) .collect(); for (source, target, id) in edges { let conn = self.belt_graph.remove_edge(id).unwrap(); + // FIXME: Fix the simulation entities as well!!!! let new_conn = match conn { BeltGraphConnection::Sideload { dest_belt_pos } => { if source == self.belt_graph_lookup[&back_tile_id] { @@ -920,7 +1509,14 @@ impl BeltStore BeltGraphConnection::Connected, + // TODO: Is this correct? + BeltGraphConnection::Connected { filter } => { + assert_eq!( + target, self.belt_graph_lookup[&back_tile_id], + "Any end of belt connections must be at the end of the back belt, since the front is being merged" + ); + BeltGraphConnection::Connected { filter } + }, }; if source == self.belt_graph_lookup[&back_tile_id] { @@ -928,7 +1524,7 @@ impl BeltStore BeltStore BeltStore BeltStore, dedup: &mut HashSet>, @@ -1065,7 +1690,7 @@ impl BeltStore match self.any_belts[index] { AnyBelt::Smart(belt_id) => { let belt = self.inner.get_smart(belt_id); - let items_all_empty = belt.items().iter().all(|loc| loc.is_none()); + 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]), @@ -1081,7 +1706,7 @@ impl BeltStore BeltStore { + | BeltGraphConnection::Connected { filter: None } => { self.get_items_which_could_end_up_on_that_belt(*belt, dedup, done); done.get(belt) .unwrap_or(&vec![]) @@ -1117,7 +1742,10 @@ impl BeltStore>() }, - BeltGraphConnection::BeltBeltInserter { + BeltGraphConnection::Connected { + filter: Some(filter), + } + | BeltGraphConnection::BeltBeltInserter { source_belt_pos: _, dest_belt_pos: _, filter, @@ -1135,16 +1763,89 @@ impl BeltStore( + #[profiling::function] + pub fn update<'a, 'b, RecipeIdxType: IdxTrait>( &mut self, - num_grids_total: usize, storages_by_item: impl IndexedParallelIterator>, data_store: &DataStore, ) where 'b: 'a, { + #[cfg(debug_assertions)] + { + assert!( + self.belt_graph_lookup + .iter() + .all(|(belt_tile_id, _)| { self.get_len(*belt_tile_id) > 0 }) + ); + + assert!( + self.any_belt_holes + .iter() + .map(|hole| BeltTileId::AnyBelt(*hole, PhantomData)) + .all(|belt_tile_id| { !self.belt_graph_lookup.contains_key(&belt_tile_id) }) + ); + + assert!( + self.any_belt_holes + .iter() + .map(|hole| BeltTileId::AnyBelt(*hole, PhantomData)) + .all(|belt_tile_id| { + !self.belt_graph.node_weights().contains(&belt_tile_id) + }) + ); + + assert!( + (0..self.any_splitters.len()) + .filter(|idx| !self.any_splitter_holes.contains(idx)) + .map(|any_splitter_idx| self + .get_splitter_belt_ids(SplitterTileId::Any(any_splitter_idx))) + .all(|[inputs, outputs]| { + inputs.into_iter().all(|id| { + self.belt_graph + .edges_directed( + self.belt_graph_lookup[&id], + petgraph::Direction::Outgoing, + ) + .count() + >= 2 + }) && outputs.into_iter().all(|id| { + self.belt_graph + .edges_directed( + self.belt_graph_lookup[&id], + petgraph::Direction::Incoming, + ) + .count() + >= 2 + }) + }) + ); + } // TODO: Once every (maybe more or less) check a single belt and check if it still needs to be sushi + // Increase the belt timers + for ((current_timer, increase), cumulative) in self + .inner + .belt_update_timers + .iter_mut() + .zip(data_store.belt_infos.iter().map(|info| info.timer_increase)) + .zip(self.inner.belt_update_timers_cumulative.iter_mut()) + { + *current_timer = (*current_timer) + .checked_add(increase) + .expect("Belt Timer wrapped!"); + + *cumulative += u32::from(increase); + } + + { + profiling::scope!("Update Splitters"); + self.inner + .sushi_splitters + .par_iter_mut() + .for_each(|splitter| splitter.update()); + } + // Update all the "Pure Belts" self.inner .smart_belts @@ -1163,58 +1864,74 @@ impl BeltStore= 120 { + belt.update(&self.inner.sushi_splitters); + } + belt.update_inserters(item_storages, grid_size); + } + } + // { + // profiling::scope!("Update BeltStorageInserters"); + // for belt in &mut belt_store.belts { - belt.get_two([(*source_pos).into(), (*dest_pos).into()]) - } else { - let [inp, out] = - belt_store.belts.get_disjoint_mut([*source, *dest]).unwrap(); + // } + // } - [inp.get_mut(*source_pos), out.get_mut(*dest_pos)] - }; + { + profiling::scope!("Update PurePure Inserters"); + for (ins, ((source, source_pos), (dest, dest_pos), cooldown, filter)) in + pure_to_pure_inserters.iter_mut().flatten() + { + let [source_loc, 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()]) + } else { + let [inp, out] = + belt_store.belts.get_disjoint_mut([*source, *dest]).unwrap(); - if *cooldown == 0 { - ins.update_instant(source_loc, dest_loc); - } else { - ins.update(source_loc, dest_loc, *cooldown, (), |_| { - filter - .map(|filter_item| filter_item == item) - .unwrap_or(true) - }); - } + [inp.get_mut(*source_pos), out.get_mut(*dest_pos)] + }; - let source_loc = *source_loc; - let dest_loc = *dest_loc; + if *cooldown == 0 { + ins.update_instant(source_loc, dest_loc); + } else { + ins.update(source_loc, dest_loc, *cooldown, HAND_SIZE, (), |_| { + filter + .map(|filter_item| filter_item == item) + .unwrap_or(true) + }); + } - if !source_loc { - belt_store.belts[*source].update_first_free_pos(*source_pos); - } + let source_loc = *source_loc; + let dest_loc = *dest_loc; - if dest_loc { - belt_store.belts[*dest].remove_first_free_pos_maybe(*dest_pos); + { + profiling::scope!("Update update_first_free_pos"); + if !source_loc { + belt_store.belts[*source].update_first_free_pos(*source_pos); + } + + if dest_loc { + belt_store.belts[*dest].remove_first_free_pos_maybe(*dest_pos); + } + } } } @@ -1225,11 +1942,44 @@ impl BeltStore= 120 { + sushi_belt.update(&self.inner.sushi_splitters); + } + }); + } - // TODO: Update inserters! - }); + { + profiling::scope!("SushiBelt Inserter Update"); + 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( @@ -1238,36 +1988,14 @@ impl BeltStore [[BeltTileId; 2]; 2] { let belts: [[AnyBelt; 2]; 2] = match splitter_id { SplitterTileId::Any(index) => match self.any_splitters[index] { - AnySplitter::Pure(item, id) => self - .inner - .get_pure_splitter_belt_ids(item, id) - .into_iter() - .map(|v| { - v.into_iter() - .map(|belt_id| AnyBelt::Smart(belt_id)) - .collect_array() - .unwrap() - }) - .collect_array() - .unwrap(), - AnySplitter::Sushi(id) => self - .inner - .get_sushi_splitter_belt_ids(id) - .into_iter() - .map(|v| { - v.into_iter() - .map(|index| AnyBelt::Sushi(index)) - .collect_array() - .unwrap() - }) - .collect_array() - .unwrap(), + AnySplitter::Pure(item, id) => self.inner.get_pure_splitter_belt_ids(item, id), + AnySplitter::Sushi(id) => self.inner.get_sushi_splitter_belt_ids(id), }, }; let [inputs, outputs] = belts; - // FIXME: This is O(n) over the number of splitters :/ + // FIXME: This is O(n) over the number of belts :/ [ inputs .into_iter() @@ -1303,7 +2031,7 @@ impl BeltStore) -> BeltTileId { + fn add_belt(&mut self, belt: SmartBelt) -> BeltTileId { let id = self.inner.add_belt(belt); let new_id = self.add_smart_to_any_list(id); @@ -1314,8 +2042,8 @@ impl BeltStore BeltTileId { - let sushi_idx = self.inner.add_sushi_belt(SushiBelt::new(len)); + pub fn add_empty_belt(&mut self, ty: u8, len: u16) -> BeltTileId { + let sushi_idx = self.inner.add_sushi_belt(SushiBelt::new(ty, len)); let new_id = self.add_sushi_to_any_list(sushi_idx); @@ -1325,10 +2053,7 @@ impl BeltStore, - ) -> BeltTileId { + fn add_sushi_belt(&mut self, belt: SushiBelt) -> BeltTileId { let sushi_idx = self.inner.add_sushi_belt(belt); let new_id = self.add_sushi_to_any_list(sushi_idx); @@ -1428,11 +2153,10 @@ impl BeltStore, id: BeltTileId, pos: BeltLenType, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> 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); match id { BeltTileId::AnyBelt(index, _) => { @@ -1445,7 +2169,7 @@ impl BeltStore { - return Err(SpaceOccupiedError) + unreachable!(); }, Err(InserterAdditionError::ItemMismatch) => { // We need to transition to sushi belt @@ -1459,13 +2183,6 @@ impl BeltStore BeltStore return Err(SpaceOccupiedError), + Err(SpaceOccupiedError) => unreachable!(), } }, } }, } + self.fix_graph(id); + return Ok(()); } @@ -1491,11 +2210,10 @@ impl BeltStore, id: BeltTileId, pos: BeltLenType, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> 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); match id { BeltTileId::AnyBelt(index, _) => { @@ -1508,7 +2226,7 @@ impl BeltStore { - return Err(SpaceOccupiedError) + return Err(SpaceOccupiedError); }, Err(InserterAdditionError::ItemMismatch) => { // We need to transition to sushi belt @@ -1592,9 +2310,23 @@ impl BeltStore unreachable!( - "If a sushi belt sideloads onto a smart belt, it can never be pure" - ), + (AnyBelt::Sushi(source_idx), AnyBelt::Smart(dest_id)) => { + self.inner + .belt_belt_inserters + .temp_sushi_to_smart_inserters + .push(( + BeltBeltInserter::new(), + ((*source_idx, 0), (*dest_id, dest.1), 0, None), + )); + + self.inner + .belt_belt_inserters + .temp_sushi_to_smart_inserters + .len() + }, + // unreachable!( + // "If a sushi belt sideloads onto a smart belt, it can never be pure" + // ), (AnyBelt::Sushi(source_index), AnyBelt::Sushi(dest_index)) => { self.inner .belt_belt_inserters @@ -1624,8 +2356,8 @@ impl BeltStore, ) -> ( - impl IntoIterator> + Clone + use<'a, ItemIdxType, RecipeIdxType>, - impl IntoIterator> + Clone + use<'a, ItemIdxType, RecipeIdxType>, + impl IntoIterator> + Clone + use<'a, ItemIdxType>, + impl IntoIterator> + Clone + use<'a, ItemIdxType>, ) { // FIXME: Consider splitters!!!! ( @@ -1659,12 +2391,15 @@ impl BeltStore SushiInfo::Pure(Some(*filter)), BeltGraphConnection::Sideload { dest_belt_pos: _ } - | BeltGraphConnection::Connected => { + | BeltGraphConnection::Connected { filter: None } => { match self.belt_graph.node_weight(edge.source()).unwrap() { BeltTileId::AnyBelt(index, _) => match self.any_belts[*index] { AnyBelt::Smart(belt_id) => SushiInfo::Pure(Some(belt_id.item)), @@ -1716,6 +2451,104 @@ impl BeltStore, + belt_pos: u16, + ) -> Option { + match belt { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => self.inner.smart_belts[belt_id.item.into_usize()].belts + [belt_id.index as usize] + .get_inserter_info_at(belt_pos), + AnyBelt::Sushi(index) => { + self.inner.sushi_belts[index].get_inserter_info_at(belt_pos) + }, + }, + } + } + + pub fn get_inserter_item( + &self, + belt: BeltTileId, + belt_pos: u16, + ) -> Item { + match belt { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => belt_id.item, + AnyBelt::Sushi(index) => self.inner.sushi_belts[index].get_inserter_item(belt_pos), + }, + } + } + + pub fn update_belt_storage_inserter_src( + &mut self, + belt: BeltTileId, + belt_pos: u16, + src_item: Item, + new_src: Storage, + data_store: &DataStore, + ) { + match belt { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => { + assert_eq!(src_item, belt_id.item); + self.inner.smart_belts[belt_id.item.into_usize()].belts[belt_id.index] + .set_inserter_storage_id( + belt_pos, + FakeUnionStorage::from_storage_with_statics_at_zero( + belt_id.item, + new_src, + data_store, + ), + ); + }, + AnyBelt::Sushi(index) => { + self.inner.sushi_belts[index].set_inserter_storage_id( + belt_pos, + FakeUnionStorage::from_storage_with_statics_at_zero( + src_item, new_src, data_store, + ), + ); + }, + }, + } + } + + pub fn update_belt_storage_inserter_dest( + &mut self, + belt: BeltTileId, + belt_pos: u16, + dest_item: Item, + new_dest: Storage, + data_store: &DataStore, + ) { + match belt { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => { + assert_eq!(dest_item, belt_id.item); + self.inner.smart_belts[belt_id.item.into_usize()].belts[belt_id.index] + .set_inserter_storage_id( + belt_pos, + FakeUnionStorage::from_storage_with_statics_at_zero( + belt_id.item, + new_dest, + data_store, + ), + ); + }, + AnyBelt::Sushi(index) => { + self.inner.sushi_belts[index].set_inserter_storage_id( + belt_pos, + FakeUnionStorage::from_storage_with_statics_at_zero( + dest_item, new_dest, data_store, + ), + ); + }, + }, + } + } + pub fn add_belt_belt_inserter( &mut self, from: (BeltTileId, u16), @@ -1799,7 +2632,7 @@ impl BeltStore { + Ok((_, new_id)) => { assert_eq!(id, new_id); }, Err(_) => { @@ -1815,7 +2648,7 @@ impl BeltStore match &self.any_belts[index] { AnyBelt::Smart(smart_belt) => Some(smart_belt.item), - AnyBelt::Sushi(sushi_belt) => None, + AnyBelt::Sushi(_) => None, }, } } @@ -1823,11 +2656,31 @@ impl BeltStore, - ) -> impl IntoIterator>> { + ) -> Either< + impl Iterator>>, + impl Iterator>>, + > { match id { BeltTileId::AnyBelt(index, _) => match &self.any_belts[index] { - AnyBelt::Smart(smart_belt) => self.inner.get_smart(*smart_belt).items(), - AnyBelt::Sushi(sushi_belt) => self.inner.get_sushi(*sushi_belt).items(), + AnyBelt::Smart(smart_belt) => { + Either::Left(self.inner.get_smart(*smart_belt).items()) + }, + AnyBelt::Sushi(sushi_belt) => { + Either::Right(self.inner.get_sushi(*sushi_belt).items()) + }, + }, + } + } + + pub fn get_belt_progress(&self, ty: u8) -> u8 { + self.inner.belt_update_timers[usize::from(ty)] + } + + pub fn get_last_moved_pos(&self, id: BeltTileId) -> BeltLenType { + match id { + BeltTileId::AnyBelt(index, _) => match &self.any_belts[index] { + AnyBelt::Smart(smart_belt) => self.inner.get_smart(*smart_belt).last_moving_spot, + AnyBelt::Sushi(sushi_belt) => self.inner.get_sushi(*sushi_belt).last_moving_spot, }, } } @@ -1846,21 +2699,33 @@ impl BeltStore( &mut self, front_tile_id: BeltTileId, back_tile_id: BeltTileId, data_store: &DataStore, ) -> (BeltTileId, BeltLenType) { + // self.merge_and_fix(front_tile_id, back_tile_id); + if front_tile_id == back_tile_id { - todo!("Make circular") + // Make them cicular + match front_tile_id { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => self.inner.merge_smart_belts(belt_id, belt_id), + AnyBelt::Sushi(idx) => self.inner.merge_sushi_belts(idx, idx), + }, + } + return (front_tile_id, self.get_len(front_tile_id)); } - match (front_tile_id, back_tile_id) { + let ret = match (front_tile_id, back_tile_id) { (BeltTileId::AnyBelt(front, _), BeltTileId::AnyBelt(back, _)) => { assert_ne!(front, back); match self.any_belts.get_disjoint_mut([front, back]).unwrap() { - [AnyBelt::Smart(front_smart_belt), AnyBelt::Smart(back_smart_belt)] => { + [ + AnyBelt::Smart(front_smart_belt), + AnyBelt::Smart(back_smart_belt), + ] => { if front_smart_belt.item == back_smart_belt.item { self.inner .merge_smart_belts(*front_smart_belt, *back_smart_belt); @@ -1885,27 +2750,22 @@ impl BeltStore { + [ + AnyBelt::Smart(front_smart_belt), + AnyBelt::Sushi(back_sushi_belt), + ] => { let front_smart_belt = *front_smart_belt; - let back_sushi_belt = *back_sushi_belt; - let bias = Some(front_smart_belt.item); - match self.try_make_belt_pure(back_sushi_belt, back_tile_id, bias) { - Ok(_) => { - // We now have two smart belts, retry: - self.merge_belts(front_tile_id, back_tile_id, data_store) - }, - Err(MakePureError::ErrorEmpty) => unreachable!(), - Err(MakePureError::ErrorSushi) => { - let front_sushi_idx = self.inner.make_sushi(front_smart_belt); - self.any_belts[front] = AnyBelt::Sushi(front_sushi_idx); + let front_sushi_idx = self.inner.make_sushi(front_smart_belt); + self.any_belts[front] = AnyBelt::Sushi(front_sushi_idx); - // We now have two Sushi belts, retry: - self.merge_belts(front_tile_id, back_tile_id, data_store) - }, - } + // We now have two Sushi belts, retry: + self.merge_belts(front_tile_id, back_tile_id, data_store) }, - [AnyBelt::Sushi(front_sushi_belt), AnyBelt::Smart(back_smart_belt)] => { + [ + AnyBelt::Sushi(front_sushi_belt), + AnyBelt::Smart(back_smart_belt), + ] => { let front_sushi_belt = *front_sushi_belt; let back_smart_belt = *back_smart_belt; @@ -1925,7 +2785,10 @@ impl BeltStore { + [ + AnyBelt::Sushi(front_sushi_belt), + AnyBelt::Sushi(back_sushi_belt), + ] => { let front_sushi_belt = *front_sushi_belt; self.inner @@ -1942,14 +2805,256 @@ impl BeltStore 0 }) + ); + + assert!( + self.any_belt_holes + .iter() + .map(|hole| BeltTileId::AnyBelt(*hole, PhantomData)) + .all(|belt_tile_id| { !self.belt_graph_lookup.contains_key(&belt_tile_id) }) + ); + + assert!( + self.any_belt_holes + .iter() + .map(|hole| BeltTileId::AnyBelt(*hole, PhantomData)) + .all(|belt_tile_id| { + !self.belt_graph.node_weights().contains(&belt_tile_id) + }) + ); + + assert!( + (0..self.any_splitters.len()) + .filter(|idx| !self.any_splitter_holes.contains(idx)) + .map(|any_splitter_idx| self + .get_splitter_belt_ids(SplitterTileId::Any(any_splitter_idx))) + .all(|[inputs, outputs]| { + inputs.into_iter().all(|id| { + self.belt_graph + .edges_directed( + self.belt_graph_lookup[&id], + petgraph::Direction::Outgoing, + ) + .count() + >= 2 + }) && outputs.into_iter().all(|id| { + self.belt_graph + .edges_directed( + self.belt_graph_lookup[&id], + petgraph::Direction::Incoming, + ) + .count() + >= 2 + }) + }) + ); + + assert!( + self.inner + .belt_belt_inserters + .pure_to_pure_inserters + .iter() + .enumerate() + .all(|(item, ins)| { + { + ins.iter().flatten().all( + |( + _ins_info, + ((source, _source_pos), (dest, _dest_pos), _movetime, _filter), + )| { + !self.inner.smart_belts[item].holes.contains(source) + && !self.inner.smart_belts[item].holes.contains(dest) + }, + ) + } + }), + "{:?}", + self.inner.belt_belt_inserters.pure_to_pure_inserters[0] + ); + assert!( + self.inner + .belt_belt_inserters + .sushi_to_sushi_inserters + .iter() + .flatten() + .all( + |( + _ins_info, + ((source, _source_pos), (dest, _dest_pos), _movetime, _filter), + )| { + !self.inner.sushi_belt_holes.contains(source) + && !self.inner.sushi_belt_holes.contains(dest) + }, + ) + ); + assert!( + self.inner + .belt_belt_inserters + .pure_to_sushi_inserters + .iter() + .flatten() + .all( + |( + _ins_info, + ((source, _source_pos), (dest, _dest_pos), _movetime, _filter), + )| { + !self.inner.smart_belts[usize_from(source.item.id)] + .holes + .contains(&source.index) + && !self.inner.sushi_belt_holes.contains(dest) + }, + ) + ); + assert!( + self.inner + .belt_belt_inserters + .temp_sushi_to_smart_inserters + .iter() + .all( + |( + _ins_info, + ((source, _source_pos), (dest, _dest_pos), _movetime, _filter), + )| { + !self.inner.sushi_belt_holes.contains(source) + && !self.inner.smart_belts[usize_from(dest.item.id)] + .holes + .contains(&dest.index) + }, + ) + ); } + + ret } pub fn add_splitter(&mut self, info: SplitterInfo) -> SplitterTileId { - todo!() + let new_splitter = SushiSplitter { + in_mode: info.in_mode, + out_mode: info.out_mode, + inputs: [UnsafeCell::new(None), UnsafeCell::new(None)], + outputs: [UnsafeCell::new(None), UnsafeCell::new(None)], + }; + + // match info.filter { + // FIXME: Even with this splitter seems to still be transmuting items + for input in &info.input_belts { + for output in &info.output_belts { + self.add_graph_connection_and_fix( + *input, + *output, + BeltGraphConnection::Connected { filter: None }, + ); + } + } + + // } + + let new_splitter_connections = [ + info.input_belts.map(|any| match any { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => AnyBelt::Smart(belt_id), + AnyBelt::Sushi(index) => AnyBelt::Sushi(index), + }, + }), + info.output_belts.map(|any| match any { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => AnyBelt::Smart(belt_id), + AnyBelt::Sushi(index) => AnyBelt::Sushi(index), + }, + }), + ]; + + let index = if let Some(hole_idx) = self.inner.sushi_splitter_holes.pop() { + self.inner.sushi_splitters[hole_idx] = new_splitter; + self.inner.sushi_splitter_connections.lock()[hole_idx] = new_splitter_connections; + hole_idx + } else { + self.inner.sushi_splitters.push(new_splitter); + self.inner + .sushi_splitter_connections + .lock() + .push(new_splitter_connections); + self.inner.sushi_splitters.len() - 1 + }; + + let any_idx = if let Some(hole_idx) = self.any_splitter_holes.pop() { + self.any_splitters[hole_idx] = AnySplitter::Sushi(SplitterID { + index: index.try_into().unwrap(), + }); + hole_idx + } else { + self.any_splitters.push(AnySplitter::Sushi(SplitterID { + index: index.try_into().unwrap(), + })); + self.any_splitters.len() - 1 + }; + + // Since we distribute the SplitterID once for each side, no two belts point to the same UnsafeCell in the splitter, maintaining the safety invariant + for side in SplitterSide::iter() { + let belt_id = info.input_belts[usize::from(bool::from(side))]; + + match belt_id { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => { + self.inner.get_smart_mut(belt_id).add_output_splitter( + SplitterID { + index: index.try_into().unwrap(), + }, + side, + ) + }, + AnyBelt::Sushi(sushi_index) => { + self.inner.get_sushi_mut(sushi_index).add_output_splitter( + SplitterID { + index: index.try_into().unwrap(), + }, + side, + ) + }, + }, + } + } + + for side in SplitterSide::iter() { + // This is flipped from the add_x_splitter calls below since this is from the POV of the splitter and those calls are from the pow of the belt + let belt_id = info.output_belts[usize::from(bool::from(side))]; + + match belt_id { + BeltTileId::AnyBelt(idx, _) => match self.any_belts[idx] { + AnyBelt::Smart(belt_id) => { + self.inner.get_smart_mut(belt_id).add_input_splitter( + SplitterID { + index: index.try_into().unwrap(), + }, + side, + ) + }, + AnyBelt::Sushi(sushi_index) => { + self.inner.get_sushi_mut(sushi_index).add_input_splitter( + SplitterID { + index: index.try_into().unwrap(), + }, + side, + ) + }, + }, + } + } + + SplitterTileId::Any(any_idx) } - pub fn remove_splitter(&mut self, index: usize) { + /// 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!() } @@ -1964,47 +3069,47 @@ impl BeltStore { - pub belts: Vec>, +pub struct MultiBeltStore { + pub belt_ty: Vec, + pub belts: Vec>, pub holes: Vec, } -impl Default - for MultiBeltStore -{ +impl Default for MultiBeltStore { fn default() -> Self { Self { + belt_ty: vec![], belts: vec![], holes: vec![], } } } -impl MultiBeltStore { - pub fn belts_mut( - &mut self, - ) -> impl IntoIterator> { +impl MultiBeltStore { + pub fn belts_mut(&mut self) -> impl Iterator> { self.belts .iter_mut() .enumerate() .filter_map(|(i, b)| (!self.holes.contains(&i)).then_some(b)) } - pub fn add_belt(&mut self, belt: SmartBelt) -> usize { + pub fn add_belt(&mut self, belt: SmartBelt) -> usize { if let Some(hole) = self.holes.pop() { + self.belt_ty[hole] = belt.ty; self.belts[hole] = belt; hole } else { + self.belt_ty.push(belt.ty); self.belts.push(belt); self.belts.len() - 1 } } - pub fn remove_belt(&mut self, belt: usize) -> SmartBelt { + pub fn remove_belt(&mut self, belt: usize) -> SmartBelt { self.holes.push(belt); - let mut temp = SmartBelt::new(1, self.belts[belt].item); + let mut temp = SmartBelt::new(0, 1, self.belts[belt].item); temp.make_circular(); mem::swap(&mut temp, &mut self.belts[belt]); temp diff --git a/src/belt/smart.rs b/src/belt/smart.rs index aedae84..89bdc89 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1,52 +1,63 @@ -use std::{cmp::min, iter::repeat}; - -use log::info; +use std::iter::repeat; use crate::{ - inserter::{ - belt_storage_inserter::{BeltStorageInserter, Dir}, - InserterState, Storage, MOVETIME, - }, + inserter::{InserterState, MOVETIME, belt_storage_inserter::BeltStorageInserter}, item::{IdxTrait, Item, WeakIdxTrait}, storage_list::SingleItemStorages, }; +use log::trace; +use std::mem; use super::{ - belt::{Belt, BeltLenType, ItemInfo, NoSpaceError}, + FreeIndex, Inserter, SplitterID, + belt::{Belt, BeltLenType, NoSpaceError}, + splitter::{SplitterSide, SushiSplitter}, sushi::{SushiBelt, SushiInserterStore}, - FreeIndex, Inserter, }; +use crate::inserter::FakeUnionStorage; +use crate::inserter::HAND_SIZE; + +type TEST = SmartBelt; #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct SmartBelt { +pub struct SmartBelt { + pub(super) ty: u8, + pub(super) is_circular: bool, pub(super) first_free_index: FreeIndex, /// Important, zero_index must ALWAYS be used using mod len pub(super) zero_index: BeltLenType, pub(super) locs: Box<[bool]>, - pub(super) inserters: InserterStore, + pub(super) inserters: InserterStore, pub(super) item: Item, + + pub last_moving_spot: BeltLenType, + + pub(super) input_splitter: Option<(SplitterID, SplitterSide)>, + pub(super) output_splitter: Option<(SplitterID, SplitterSide)>, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)] pub struct EmptyBelt { + ty: u8, + is_circular: bool, pub len: u16, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct InserterStore { - pub(super) inserters: Vec>, - pub(super) offsets: Vec, +pub struct InserterStore { + pub(super) inserters: Box<[Inserter]>, + pub(super) offsets: Box<[u16]>, } #[derive(Debug)] -pub struct BeltInserterInfo { +pub struct BeltInserterInfo { pub outgoing: bool, pub state: InserterState, - pub connection: Storage, + pub connection: FakeUnionStorage, } const MIN_INSERTER_SPACING: usize = 8; @@ -59,20 +70,27 @@ pub(super) enum InserterAdditionError { ItemMismatch, } -impl SmartBelt { +impl SmartBelt { #[must_use] - pub fn new(len: u16, item: Item) -> Self { + pub fn new(ty: u8, len: u16, item: Item) -> Self { Self { + ty, + is_circular: false, first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, locs: vec![false; len.into()].into_boxed_slice(), inserters: InserterStore { - inserters: vec![], - offsets: vec![], + inserters: vec![].into_boxed_slice(), + offsets: vec![].into_boxed_slice(), }, item, + + last_moving_spot: len, + + input_splitter: None, + output_splitter: None, } } @@ -80,20 +98,29 @@ impl SmartBelt SushiBelt { + pub(super) fn into_sushi_belt(self) -> SushiBelt { let Self { + ty, + is_circular, first_free_index, zero_index, locs, inserters: InserterStore { inserters, offsets }, item, + + last_moving_spot, + + input_splitter, + output_splitter, } = self; SushiBelt { + ty, + is_circular, locs: locs - .into_iter() + .iter() .map(|loc| if *loc { Some(item) } else { None }) .collect(), first_free_index, @@ -105,11 +132,59 @@ impl SmartBelt Option<(SplitterID, SplitterSide)> { + self.input_splitter.take() + } + + pub fn remove_output_splitter(&mut self) -> Option<(SplitterID, SplitterSide)> { + self.output_splitter.take() } // pub fn get_take_item_fn<'a>( @@ -185,11 +260,7 @@ impl SmartBelt, - new: Storage, - ) { + pub fn change_inserter_storage_id(&mut self, old: FakeUnionStorage, new: FakeUnionStorage) { for inserter in &mut self.inserters.inserters { match inserter { Inserter::Out(inserter) => { @@ -206,7 +277,7 @@ impl SmartBelt) { + pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { let mut pos = 0; for (offset, inserter) in self @@ -225,14 +296,19 @@ impl SmartBelt= belt_pos { - unreachable!() + 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.offsets + ); } + pos += 1; } } #[must_use] - pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option> { + pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { let mut pos = 0; for (offset, inserter) in self @@ -264,7 +340,7 @@ impl SmartBelt Storage { + pub fn remove_inserter(&mut self, pos: BeltLenType) -> FakeUnionStorage { assert!( usize::from(pos) < self.locs.len(), "Bounds check {pos} >= {}", @@ -278,7 +354,9 @@ impl SmartBelt 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::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 => { @@ -288,10 +366,24 @@ impl SmartBelt inserter.storage_id, @@ -309,7 +401,7 @@ impl SmartBelt, index: u16, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> Result<(), InserterAdditionError> { assert!( usize::from(index) < self.locs.len(), @@ -340,10 +432,16 @@ impl SmartBelt SmartBelt, index: u16, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> Result<(), InserterAdditionError> { assert!( usize::from(index) < self.locs.len(), @@ -395,10 +493,16 @@ impl SmartBelt SmartBelt Option<&Inserter> { + fn get_inserter(&self, index: u16) -> Option<&Inserter> { let mut pos_after_last_inserter = 0; for (i, offset) in self.inserters.offsets.iter().enumerate() { @@ -430,12 +534,8 @@ impl SmartBelt( &mut self, storages: SingleItemStorages<'a, 'b>, - num_grids_total: usize, - num_recipes: usize, grid_size: usize, ) { - // FIXME: This has a critical bug. FreeIndex does not get set correctly, - // which could result in parts of the belt not working correctly debug_assert_eq!(self.inserters.inserters.len(), self.inserters.offsets.len()); let mut items_mut_iter = Self::items_mut(&mut self.locs, self.zero_index); @@ -460,22 +560,12 @@ impl SmartBelt { let old = *loc; match ins { - Inserter::Out(inserter) => inserter.update( - loc, - storages, - MOVETIME, - num_grids_total, - num_recipes, - grid_size, - ), - Inserter::In(inserter) => inserter.update( - loc, - storages, - MOVETIME, - num_grids_total, - num_recipes, - grid_size, - ), + Inserter::Out(inserter) => { + inserter.update(loc, storages, MOVETIME, HAND_SIZE, grid_size) + }, + Inserter::In(inserter) => { + inserter.update(loc, storages, MOVETIME, HAND_SIZE, grid_size) + }, } // TODO: Make sure this is actually correct @@ -532,7 +622,8 @@ impl SmartBelt BeltLenType { + // #[profiling::function] + fn find_and_update_real_first_free_index(&mut self) -> Option { let new_free_index = match self.first_free_index { FreeIndex::FreeIndex(index) => { debug_assert!( @@ -543,7 +634,7 @@ impl SmartBelt { - info!("HAD TO SEARCH FOR FIRST FREE INDEX!"); + trace!("HAD TO SEARCH FOR FIRST FREE INDEX!"); let search_start_index = index; @@ -559,6 +650,15 @@ impl SmartBelt SmartBelt SmartBelt SmartBelt SmartBelt SmartBelt assert!(output_splitter.is_none()), + Side::BACK => assert!(input_splitter.is_none()), + } + + assert_eq!(ty, empty.ty); + // Important, first_free_index must ALWAYS be used using mod len let zero_index = zero_index % len; @@ -751,12 +914,22 @@ impl SmartBelt 0, + Side::BACK => last_moving_spot, + }, + + input_splitter, + output_splitter, }; new.find_and_update_real_first_free_index(); @@ -767,6 +940,8 @@ impl SmartBelt 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; @@ -811,15 +986,32 @@ impl SmartBelt SmartBelt SmartBelt Self { + pub const fn new(ty: u8, len: u16) -> Self { Self { + ty, + is_circular: false, len, } @@ -846,24 +1045,28 @@ impl EmptyBelt { #[must_use] #[allow(clippy::needless_pass_by_value)] - pub const fn join(front: Self, back: Self) -> Self { + pub fn join(front: Self, back: Self) -> Self { assert!(!front.is_circular); assert!(!back.is_circular); + assert_eq!(front.ty, back.ty); + Self { + ty: front.ty, + is_circular: false, len: front.len + back.len, } } - pub fn into_smart_belt( + pub fn into_smart_belt( self, item: Item, - ) -> SmartBelt { - SmartBelt::new(self.len.into(), item) + ) -> SmartBelt { + SmartBelt::new(self.ty, self.len.into(), item) } - pub fn add_length(&mut self, amount: u16, side: Side) -> u16 { + pub fn add_length(&mut self, amount: u16, _side: Side) -> u16 { self.len += amount; self.len } @@ -876,6 +1079,8 @@ impl EmptyBelt { let old_len = self.len; self.len = pos_to_break_at; Self { + ty: self.ty, + is_circular: false, len: old_len - pos_to_break_at, } @@ -892,12 +1097,13 @@ pub enum Side { BACK, } -impl Belt - for SmartBelt -{ +impl Belt for SmartBelt { fn add_length(&mut self, amount: BeltLenType, side: Side) -> BeltLenType { assert!(!self.is_circular); - take_mut::take(self, |s| s.join_with_empty(EmptyBelt::new(amount), side)); + let ty = self.ty; + take_mut::take(self, |s| { + s.join_with_empty(EmptyBelt::new(ty, amount), side) + }); self.get_len() } @@ -906,6 +1112,11 @@ impl Belt pos: BeltLenType, item: Item, ) -> Result<(), super::belt::NoSpaceError> { + debug_assert_eq!( + self.item, item, + "Tried to insert wrong item onto SmartBelt, resulting in item transmutation" + ); + if Belt::::query_item(self, pos).is_none() { self.locs[self.into_loc_index(pos)] = true; @@ -926,14 +1137,51 @@ impl Belt } #[allow(clippy::bool_assert_comparison)] - fn update(&mut self) { + fn update(&mut self, splitter_list: &[SushiSplitter]) { if self.is_circular { - // Correctness: Since we always % len whenever we access using self.zero_index, we do not need to % len here + // Correctness: Since we always % len henever we access using self.zero_index, we do not need to % len here // TODO: This could overflow after usize::MAX ticks which is 9749040289 Years. Should be fine! self.zero_index += 1; return; } + if let Some((output_id, side)) = &self.output_splitter { + if let Some(item) = self.query_item(0) { + let splitter_loc = + &splitter_list[output_id.index as usize].inputs[usize::from(bool::from(*side))]; + + // SAFETY: + // This is the only place where we modify splitter_list from a &. + // This can never race since only one belt ever has the same values for output_id and side, so only a single belt will ever modify each splitter loc + let splitter_loc = unsafe { &mut *splitter_loc.get() }; + + if splitter_loc.is_none() { + *splitter_loc = Some(item); + let _ = self.remove_item(0); + } + } + } + + if let Some((input_id, side)) = &self.input_splitter { + // Last pos + if self.query_item(self.get_len() - 1).is_none() { + let splitter_loc = + &splitter_list[input_id.index as usize].outputs[usize::from(bool::from(*side))]; + + // SAFETY: + // This is the only place where we modify splitter_list from a &. + // This can never race since only one belt ever has the same values for output_id and side, so only a single belt will ever modify each splitter loc + let splitter_loc = unsafe { &mut *splitter_loc.get() }; + + if let Some(item) = *splitter_loc { + *splitter_loc = None; + let _ = self + .try_insert_item(self.get_len() - 1, item) + .expect("Should never fail!"); + } + } + } + match self.first_free_index { FreeIndex::FreeIndex(idx) | FreeIndex::OldFreeIndex(idx) => { debug_assert!(idx <= self.get_len()); @@ -942,7 +1190,9 @@ impl Belt if Belt::::query_item(self, 0).is_none() { // Correctness: Since we always % len whenever we access using self.zero_index, we do not need to % len here // TODO: This could overflow after usize::MAX ticks which is 9749040289 Years. Should be fine! + self.zero_index %= self.get_len(); self.zero_index += 1; + self.last_moving_spot = 0; match self.first_free_index { FreeIndex::FreeIndex(0) | FreeIndex::OldFreeIndex(0) => { if Belt::::query_item(self, 0).is_none() { @@ -952,10 +1202,14 @@ impl Belt } }, FreeIndex::FreeIndex(_) => { - unreachable!("FreeIndex should always point at the earliest known empty spot and we know that index 0 WAS an empty spot") + unreachable!( + "FreeIndex should always point at the earliest known empty spot and we know that index 0 WAS an empty spot" + ) }, FreeIndex::OldFreeIndex(_) => { - unreachable!("OldFreeIndex should always point at the earliest potential empty spot and we know that index 0 WAS an empty spot") + unreachable!( + "OldFreeIndex should always point at the earliest potential empty spot and we know that index 0 WAS an empty spot" + ) }, } return; @@ -967,10 +1221,12 @@ impl Belt let len = self.get_len(); - if first_free_index_real == len { + self.last_moving_spot = first_free_index_real.unwrap_or(len); + + let Some(first_free_index_real) = first_free_index_real else { // All slots are full return; - } + }; assert!( first_free_index_real < len, @@ -1036,24 +1292,25 @@ impl Belt self.locs.len().try_into().unwrap() } - fn query_item(&self, pos: BeltLenType) -> Option> { + fn query_item(&self, pos: BeltLenType) -> Option> { if self.locs[self.into_loc_index(pos)] { - Some(ItemInfo::Implicit) + Some(self.item) } else { None } } - fn remove_item(&mut self, pos: BeltLenType) -> Option> { + fn remove_item(&mut self, pos: BeltLenType) -> Option> { if self.locs[self.into_loc_index(pos)] { self.locs[self.into_loc_index(pos)] = false; - Some(ItemInfo::Implicit) + self.update_first_free_pos(pos); + Some(self.item) } else { None } } - fn items(&self) -> Vec>> { + fn items(&self) -> impl Iterator>> { let (start, end) = self .locs .split_at(usize::from(self.zero_index % self.get_len())); @@ -1065,499 +1322,9 @@ impl Belt end.iter() .chain(start.iter()) .map(|loc| if *loc { Some(self.item) } else { None }) - .collect() } fn item_hint(&self) -> Option>> { Some(vec![self.item]) } } - -// TODO -#[cfg(todotest)] -mod tests { - - extern crate test; - - use std::cmp::min; - - use proptest::{prelude::prop, prop_assert, prop_assert_eq, proptest}; - use rand::random; - use test::Bencher; - - use crate::{belt::do_update_test_bools, inserter::StorageID, item::ITEMCOUNTTYPE}; - - use super::*; - - const MAX_LEN: u16 = 50_000; - proptest! { - - #[test] - fn test_belt_moves_item_forward(item_pos in 0..MAX_LEN) { - let mut belt = SmartBelt::::new(MAX_LEN); - - let ret = belt.try_insert_item(item_pos); - - // Since the whole belt is empty, it should not fail to put an item in - assert!(ret.is_ok()); - - belt.update(); - - if item_pos > 0 { - // The item should have moved - for i in 0..MAX_LEN { - if i == item_pos - 1 { - prop_assert_eq!(belt.query_item(i), Some(())); - } else { - prop_assert_eq!(belt.query_item(i), None); - } - } - } else { - // The item should NOT have moved - for i in 0..MAX_LEN { - if i == item_pos { - prop_assert_eq!(belt.query_item(i), Some(())); - } else { - prop_assert_eq!(belt.query_item(i), None); - } - } - } - } - - #[test] - fn test_smart_belt_agrees_with_functional(mut items in prop::collection::vec(prop::bool::ANY, 1..100)) { - let mut belt = SmartBelt::::new(items.len().try_into().unwrap()); - - for (i, item_opt) in items.iter().enumerate() { - - if *item_opt { - belt.try_insert_item(i.try_into().unwrap()).expect("Since the belt starts empty this should never fail"); - } else { - assert!(belt.remove_item(i).is_none()); - } - } - - for _update_count in 0..items.len() * 2 { - belt.update(); - - do_update_test_bools(&mut items); - - for (i, should_be_filled) in items.iter().enumerate() { - let correct = if *should_be_filled { - Some(()) - } else { - None - }; - prop_assert_eq!(belt.query_item(i.try_into().unwrap()), correct); - } - } - } - - #[test] - fn test_join_belt_length(front_len in 0..MAX_LEN, back_len in 0..MAX_LEN) { - let back = SmartBelt::::new(back_len); - let front = SmartBelt::new(front_len); - - prop_assert_eq!(SmartBelt::join(front, back).get_len(), front_len + back_len); - } - - #[test] - fn test_join_belt_items(front_items in prop::collection::vec(prop::bool::ANY, 1..100), back_items in prop::collection::vec(prop::bool::ANY, 1..100)) { - let mut back = SmartBelt::::new(back_items.len().try_into().unwrap()); - let mut front = SmartBelt::new(front_items.len().try_into().unwrap()); - - for (i, item) in front_items.iter().enumerate() { - if *item { - assert!(front.try_insert_item(i.try_into().unwrap()).is_ok()); - } - } - - for (i, item) in back_items.iter().enumerate() { - if *item { - assert!(back.try_insert_item(i.try_into().unwrap()).is_ok()); - } - } - - let new_belt = SmartBelt::join(front, back); - - for (i, item) in front_items.iter().chain(back_items.iter()).enumerate() { - prop_assert_eq!(new_belt.query_item(i.try_into().unwrap()).is_some(), *item, "{:?}", new_belt); - } - } - - #[test] - fn test_join_belt_first_free_index(front_items in prop::collection::vec(prop::bool::ANY, 1..100), back_items in prop::collection::vec(prop::bool::ANY, 1..100)) { - let mut back = SmartBelt::::new(back_items.len().try_into().unwrap()); - let mut front = SmartBelt::new(front_items.len().try_into().unwrap()); - - for (i, item) in front_items.iter().enumerate() { - if *item { - assert!(front.try_insert_item(i.try_into().unwrap()).is_ok()); - } - } - - for (i, item) in back_items.iter().enumerate() { - if *item { - assert!(back.try_insert_item(i.try_into().unwrap()).is_ok()); - } - } - - let new_belt = SmartBelt::join(front, back); - - let Some((index, _)) = front_items.iter().chain(back_items.iter()).enumerate().find(|(_i, item)| !**item) else { - return Ok(()); - }; - - match new_belt.first_free_index { - FreeIndex::FreeIndex(join_index) => prop_assert_eq!(join_index, index), - FreeIndex::OldFreeIndex(join_index) => prop_assert!(join_index <= index), - } - } - - // #[test] - // fn test_join_belt_inserters(front_inserters in prop::collection::vec(prop::bool::ANY, 1..100), back_inserters in prop::collection::vec(prop::bool::ANY, 1..100)) { - // let mut back = SmartBelt::new(back_inserters.len()); - // let mut front = SmartBelt::new(front_inserters.len()); - - // for (i, inserter) in front_inserters.iter().enumerate() { - // if *inserter { - // assert!(front.add_out_inserter(i.try_into().expect("Hardcoded"), StorageID { recipe: 0, grid: 0, storage: i.try_into().expect("Hardcoded") }).is_ok()); - // } - // } - - // for (i, inserter) in back_inserters.iter().enumerate() { - // if *inserter { - // dbg!(i); - // assert!(back.add_out_inserter(i.try_into().expect("Hardcoded"), StorageID { recipe: 0, grid: 0, storage: i.try_into().expect("Hardcoded") }).is_ok()); - // } - // } - - // let new_belt = SmartBelt::join(front, back); - - // for (i, (storage_i, inserter)) in front_inserters.iter().enumerate().chain(back_inserters.iter().enumerate()).enumerate() { - // let expected_id = StorageID { recipe: 0, grid: 0, storage: storage_i.try_into().expect("Hardcoded") }; - - // match new_belt.get_inserter(i.try_into().expect("Hardcoded")) { - // Some(Inserter::Out(ins)) => prop_assert_eq!(ins.storage_id.clone(), expected_id, "{:?}", new_belt), - // None => prop_assert!(!*inserter, "{:?}", new_belt), - // _ => prop_assert!(false, "Out inserter became In ?!?"), - // } - // } - // } - - // #[test] - // fn test_add_inserter(inserters in prop::collection::vec(prop::bool::ANY, 1..100)) { - // let mut belt = SmartBelt::new(inserters.len()); - - // for (i, inserter) in inserters.iter().enumerate() { - // if *inserter { - // let id = StorageID { recipe: 0, grid: 0, storage: i.try_into().expect("Hardcoded") }; - // prop_assert!(belt.add_out_inserter(i.try_into().expect("Hardcoded"), id).is_ok()); - // } - // } - - // for (i, inserter) in inserters.iter().enumerate() { - // let expected_id = StorageID { recipe: 0, grid: 0, storage: i.try_into().expect("Hardcoded") }; - - // match belt.get_inserter(i.try_into().expect("Hardcoded")) { - // Some(Inserter::Out(ins)) => prop_assert_eq!(ins.storage_id.clone(), expected_id, "{:?}", belt), - // None => prop_assert!(!*inserter, "{:?}", belt), - // _ => prop_assert!(false, "Out inserter became In ?!?"), - // } - // } - // } - } - - #[test] - fn test_smart_belt_does_not_set_free_index_wrong() { - let mut belt = SmartBelt::new(MAX_LEN); - - // OutInserter::create_and_add(Arc::downgrade(&storage.storage), &mut belt, MAX_LEN - 1); - - belt.try_insert_item(0).expect("Expected insert to work"); - - for _ in 0..1_000 { - belt.update(); - belt.remove_item(1); - } - } - - #[test] - fn test_smart_belt_with_inserters() { - let mut belt = SmartBelt::new(10_000); - - let storage_unused = ITEMCOUNTTYPE::default(); - - let storage_source = 30; - let storage_dest = ITEMCOUNTTYPE::default(); - - let id = StorageID { - storage_list_idx: todo!(), - machine_idx: todo!(), - phantom: std::marker::PhantomData, - }; - belt.add_out_inserter(5, id); - - let mut steel_producer_storages = [storage_unused, storage_source, storage_dest]; - - for _ in 0..20 { - belt.update(); - belt.update_inserters(&mut [&mut [&mut steel_producer_storages]]); - // let _ = belt.remove_item(5); - - let _ = belt.try_insert_item(9); - - println!("{}", &belt as &dyn Belt); - println!("{:?}", steel_producer_storages[2]); - } - } - - #[bench] - fn bench_smart_belt_with_inserters(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - - let storage_unused = ITEMCOUNTTYPE::default(); - - let storage_source = 30; - let storage_dest = ITEMCOUNTTYPE::default(); - - let id = StorageID { - recipe: 0, - grid: 0, - storage: 2, - }; - belt.add_out_inserter(5, id); - - let mut steel_producer_storages = [storage_unused, storage_source, storage_dest]; - - b.iter(|| { - belt.update(); - belt.update_inserters(&mut [&mut [&mut steel_producer_storages]]); - - let _ = belt.try_insert_item(9); - }); - } - - #[bench] - fn bench_smart_belt_with_10000_inserters(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - - let storage_unused = ITEMCOUNTTYPE::default(); - - let storage_source = 30; - let storage_dest = ITEMCOUNTTYPE::default(); - - for i in 0..10_000 { - let id = StorageID { - recipe: 0, - grid: 0, - storage: random(), - }; - belt.add_out_inserter(i, id); - } - - let mut storages: Vec = - [storage_unused.clone(), storage_source, storage_dest].into(); - - for _ in 0..100_000 { - storages.push(storage_unused.clone()); - } - - b.iter(|| { - belt.update(); - belt.update_inserters(&mut [&mut [&mut storages]]); - - let _ = belt.try_insert_item(9); - }); - } - - #[bench] - fn bench_smart_belt_with_10000_inserters_belt_empty(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - - let storage_unused = ITEMCOUNTTYPE::default(); - - let storage_source = 0; - let storage_dest = ITEMCOUNTTYPE::default(); - - for i in 0..10_000 { - let id = StorageID { - recipe: 0, - grid: 0, - storage: random(), - }; - belt.add_out_inserter(i, id); - } - - let mut storages: Vec = - [storage_unused.clone(), storage_source, storage_dest].into(); - - for _ in 0..100_000 { - storages.push(storage_unused.clone()); - } - - b.iter(|| { - belt.update(); - belt.update_inserters(&mut [&mut [&mut storages]]); - }); - } - - #[test] - fn test_debug() { - let mut belt = SmartBelt::new(MAX_LEN); - - for _ in 0..100_000 { - let bb = test::black_box(&mut belt); - bb.update(); - } - } - - #[bench] - fn bench_smart_belt_update_free_flowing(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - // belt.belt_storage - // .try_put_item_in_pos(Item::Iron, MAX_LEN - 1); - - b.iter(|| { - let bb = test::black_box(&mut belt); - bb.update(); - }); - - // println!("{belt}"); - } - - #[bench] - fn bench_smart_belt_update_stuck(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - belt.try_insert_item(MAX_LEN - 1) - .expect("Expected insert to work"); - - belt.try_insert_item(0).expect("Expected insert to work"); - - b.iter(|| { - let bb = test::black_box(&mut belt); - bb.update(); - }); - - // println!("{belt}"); - } - - #[bench] - fn bench_smart_belt_update_full(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - - for i in 0..MAX_LEN { - assert!(belt.try_insert_item(i).is_ok()); - } - - b.iter(|| { - let bb = test::black_box(&mut belt); - bb.update(); - }); - - // println!("{belt}"); - } - - #[bench] - fn bench_smart_belt_worst_case(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - - for i in 0..MAX_LEN / 2 { - assert!(belt.try_insert_item(i).is_ok()); - } - - for _ in (MAX_LEN / 2)..MAX_LEN { - // This spot is empty - } - - // Insert a single item at the end - assert!(belt.try_insert_item(MAX_LEN - 1).is_ok()); - - b.iter(|| { - let bb = test::black_box(&mut belt); - bb.update(); - }); - - // println!("{belt:?}"); - let mut num_items = 0; - let mut last_spot_with_items: Option = None; - for i in 0..MAX_LEN { - if belt.query_item(i).is_some() { - num_items += 1; - last_spot_with_items = Some(i); - } - } - println!("{num_items}"); - println!("{last_spot_with_items:?}"); - } - - // TODO: Check on these benchmarks, I saw the 1k belt one be slower than the 50k one?!? - #[bench] - fn bench_extend_belt_by_one_front_1_000(b: &mut Bencher) { - let mut belt = Some(SmartBelt::new(1_000)); - - b.iter(move || { - let back = belt.take().expect("Hardcoded"); - let front = SmartBelt::new(1); - - belt = Some(SmartBelt::join(front, back)); - }); - } - - #[bench] - fn bench_extend_belt_by_one_back_1_000(b: &mut Bencher) { - let mut belt = Some(SmartBelt::new(1_000)); - - b.iter(move || { - let front = belt.take().expect("Hardcoded"); - let back = SmartBelt::new(1); - - belt = Some(SmartBelt::join(front, back)); - }); - } - - #[bench] - fn bench_extend_belt_by_one_front_50_000(b: &mut Bencher) { - let mut belt = Some(SmartBelt::new(50_000)); - - b.iter(move || { - let back = belt.take().expect("Hardcoded"); - let front = SmartBelt::new(1); - - belt = Some(SmartBelt::join(front, back)); - }); - } - - #[bench] - fn bench_extend_belt_by_one_back_50_000(b: &mut Bencher) { - let mut belt = Some(SmartBelt::new(50_000)); - - b.iter(move || { - let front = belt.take().expect("Hardcoded"); - let back = SmartBelt::new(1); - - belt = Some(SmartBelt::join(front, back)); - }); - } - - #[bench] - fn bench_add_inserter(b: &mut Bencher) { - let mut belt = SmartBelt::new(MAX_LEN); - let mut i = 0; - - b.iter(|| { - let _ = belt.add_in_inserter( - min( - (MAX_LEN - 1).try_into().expect("MAX_LEN too large for u16"), - i, - ), - StorageID { - recipe: 0, - grid: 0, - storage: 1, - }, - ); - i += 1; - }); - } -} diff --git a/src/belt/splitter.rs b/src/belt/splitter.rs index 9a384d7..81d9d9f 100644 --- a/src/belt/splitter.rs +++ b/src/belt/splitter.rs @@ -1,19 +1,17 @@ use std::cmp::min; -use itertools::Itertools; +use std::cell::UnsafeCell; -use crate::item::{IdxTrait, Item}; +use itertools::Itertools; +use serde::ser::SerializeSeq; -use super::{ - belt::{Belt, ItemInfo}, - MultiBeltStore, SushiBelt, -}; +use crate::item::{IdxTrait, Item, WeakIdxTrait}; -type BeltBeltInserterID = u32; +use strum::EnumIter; pub const SPLITTER_BELT_LEN: u16 = 2; -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize, EnumIter)] pub enum SplitterSide { Left, Right, @@ -38,7 +36,7 @@ impl From for usize { } impl SplitterSide { - fn switch(self) -> Self { + pub fn switch(self) -> Self { match self { SplitterSide::Left => SplitterSide::Right, SplitterSide::Right => SplitterSide::Left, @@ -62,44 +60,21 @@ impl Default for SplitterDistributionMode { } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct Splitter { +pub struct PureSplitter { pub in_mode: SplitterDistributionMode, pub out_mode: SplitterDistributionMode, /// 0 is left - pub input_belts: [usize; 2], + pub inputs: [bool; 2], /// 0 is left - pub output_belts: [usize; 2], + pub outputs: [bool; 2], } -impl Splitter { +impl PureSplitter { // TODO: Test this - pub fn update( - &mut self, - belts: &mut MultiBeltStore, - ) { - // FIXME: Handle the case where an input and output are the same belt! - let [input_1, input_2, output_1, output_2] = belts - .belts - .get_disjoint_mut([ - self.input_belts[0], - self.input_belts[1], - self.output_belts[0], - self.output_belts[1], - ]) - .expect("Inputs or outputs overlap (or something is out of bounds)"); - let mut inputs: [&mut super::smart::SmartBelt; 2] = - [input_1, input_2]; - let mut outputs = [output_1, output_2]; - - let num_items_possible_to_input = inputs - .iter() - .filter(|belt| belt.get_front().is_some()) - .count(); - let num_items_possible_to_output = outputs - .iter() - .filter(|belt| belt.get_back().is_none()) - .count(); + pub fn update(&mut self) { + let num_items_possible_to_input = self.inputs.iter().filter(|belt| **belt).count(); + let num_items_possible_to_output = self.outputs.iter().filter(|belt| !**belt).count(); let num_items = min(num_items_possible_to_input, num_items_possible_to_output); @@ -112,21 +87,23 @@ impl Splitter { }; if should_switch_in { - inputs.rotate_left(1); + self.inputs.rotate_left(1); } - let should_switch_out = match self.out_mode { + let should_switch_out: bool = match self.out_mode { SplitterDistributionMode::Fair { next } => next.into(), SplitterDistributionMode::Priority(splitter_side) => splitter_side.into(), }; + let mut outputs = self.outputs.each_mut(); + if should_switch_out { outputs.rotate_left(1); } - for (i, input) in inputs.iter_mut().enumerate() { - let old = input.get_front().is_some(); - *input.get_front_mut() = false; + for (i, input) in self.inputs.iter_mut().enumerate() { + let old = *input; + *input = false; if old { let original_index = (i + usize::from(should_switch_in)) % 2; if let SplitterDistributionMode::Fair { next } = &mut self.in_mode { @@ -139,8 +116,8 @@ impl Splitter { } for (i, output) in outputs.iter_mut().enumerate() { - let old = output.get_back().is_some(); - *output.get_back_mut() = true; + let old = **output; + **output = true; if !old { let original_index = (i + usize::from(should_switch_in)) % 2; if let SplitterDistributionMode::Fair { next } = &mut self.out_mode { @@ -153,57 +130,119 @@ impl Splitter { } }, 2 => { - for input in inputs { - debug_assert_eq!(*input.get_front_mut(), true); - *input.get_front_mut() = false; - } - - for output in outputs { - debug_assert_eq!(*output.get_back_mut(), false); - *output.get_back_mut() = true; - } + self.inputs = [false, false]; + self.outputs = [true, true]; }, _ => unreachable!("A Splitter can move at most 2 items every tick"), } } } -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct SushiSplitter { +#[derive(Debug)] +pub(super) struct SushiSplitter { pub in_mode: SplitterDistributionMode, pub out_mode: SplitterDistributionMode, /// 0 is left - pub input_belts: [usize; 2], + pub(super) inputs: [UnsafeCell>>; 2], /// 0 is left - pub output_belts: [usize; 2], + pub(super) outputs: [UnsafeCell>>; 2], +} + +// SAFETY: +// Since all accesses to the UnsafeCells are tightly controlled inside the super module, we can share this between threads +unsafe impl Sync for SushiSplitter {} + +#[derive(serde::Deserialize, serde::Serialize)] +struct SushiSplitterReplacement { + pub in_mode: SplitterDistributionMode, + pub out_mode: SplitterDistributionMode, + + /// 0 is left + pub inputs: [Option>; 2], + /// 0 is left + pub outputs: [Option>; 2], +} + +impl<'de, ItemIdxType: WeakIdxTrait> serde::Deserialize<'de> for SushiSplitter +where + ItemIdxType: serde::Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + SushiSplitterReplacement::deserialize(deserializer).map( + |SushiSplitterReplacement { + in_mode, + out_mode, + inputs, + outputs, + }| SushiSplitter { + in_mode, + out_mode, + inputs: inputs.map(|v| UnsafeCell::new(v)), + outputs: outputs.map(|v| UnsafeCell::new(v)), + }, + ) + } } -impl SushiSplitter { +impl SushiSplitter { + /// The caller must ensure, reads from self cannot race on the unsafe cell (i.e. that this is not called during a belt update) + pub unsafe fn unsafe_clone(&self) -> Self { + // SAFETY: + // This is safe, since the caller is responsible + unsafe { + Self { + in_mode: self.in_mode.clone(), + out_mode: self.out_mode.clone(), + inputs: [ + UnsafeCell::new(*self.inputs[0].get()), + UnsafeCell::new(*self.inputs[1].get()), + ], + outputs: [ + UnsafeCell::new(*self.outputs[0].get()), + UnsafeCell::new(*self.outputs[1].get()), + ], + } + } + } + + /// The caller must ensure, reads from self cannot race on the unsafe cell (i.e. that this is not called during a belt update) + pub unsafe fn unsafe_serialize_elem( + &self, + seq: &mut ::SerializeSeq, + ) -> Result<(), S::Error> + where + S: serde::Serializer, + ItemIdxType: serde::Serialize, + { + // SAFETY: + // This is safe, since the caller is responsible + unsafe { + seq.serialize_element(&SushiSplitterReplacement { + in_mode: self.in_mode, + out_mode: self.out_mode, + inputs: [*self.inputs[0].get(), *self.inputs[1].get()], + outputs: [*self.outputs[0].get(), *self.outputs[1].get()], + }) + } + } +} + +impl SushiSplitter { // TODO: Test this - pub fn update( - &mut self, - belts: &mut [SushiBelt], - ) { - // FIXME: Handle the case where an input and output are the same belt! - let [input_1, input_2, output_1, output_2] = belts - .get_disjoint_mut([ - self.input_belts[0], - self.input_belts[1], - self.output_belts[0], - self.output_belts[1], - ]) - .expect("Inputs or outputs overlap (or something is out of bounds)"); - let mut inputs: [&mut SushiBelt; 2] = [input_1, input_2]; - let mut outputs: [&mut SushiBelt; 2] = [output_1, output_2]; - - let num_items_possible_to_input = inputs - .iter() - .filter(|belt| belt.get_front().is_some()) + pub fn update(&mut self) { + let num_items_possible_to_input = self + .inputs + .iter_mut() + .filter_map(|i| i.get_mut().is_some().then_some(())) .count(); - let num_items_possible_to_output = outputs - .iter() - .filter(|belt| belt.get_back().is_none()) + let num_items_possible_to_output = self + .outputs + .iter_mut() + .filter_map(|i| i.get_mut().is_none().then_some(())) .count(); let num_items = min(num_items_possible_to_input, num_items_possible_to_output); @@ -217,7 +256,7 @@ impl SushiSplitter { }; if should_switch_in { - inputs.rotate_left(1); + self.inputs.rotate_left(1); } let should_switch_out = match self.out_mode { @@ -225,48 +264,45 @@ impl SushiSplitter { SplitterDistributionMode::Priority(splitter_side) => splitter_side.into(), }; - if should_switch_out { - outputs.rotate_left(1); - } - #[cfg(debug_assertions)] let mut removed = false; - let items: [Item; 1] = inputs + let items: [Item; 1] = self + .inputs .iter_mut() .enumerate() .flat_map(|(i, input)| { - if input.get_front().is_some() { + if input.get_mut().is_some() { let original_index = (i + usize::from(should_switch_in)) % 2; if let SplitterDistributionMode::Fair { next } = &mut self.in_mode { if Into::::into(*next) == original_index { *next = next.switch(); } } + #[cfg(debug_assertions)] + { + assert!(!removed); + removed = true; + } } - #[cfg(debug_assertions)] - { - assert!(!removed); - removed = true; - } - input.remove_item(0) + input.get_mut().take() }) .take(1) - .map(|info| { - let ItemInfo::Sushi(item) = info else { - unreachable!() - }; - item - }) .collect_array() .expect("We already checked that has to be at least one item"); + let mut outputs = self.outputs.each_mut(); + + if should_switch_out { + outputs.rotate_left(1); + } + for (i, output) in outputs.iter_mut().enumerate() { - if !output.get_back().is_some() { + if output.get_mut().is_none() { // There is space here - output.try_insert_item(output.get_len() - 1, items[0]); - let original_index = (i + usize::from(should_switch_in)) % 2; + *output.get_mut() = Some(items[0]); + let original_index = (i + usize::from(should_switch_out)) % 2; if let SplitterDistributionMode::Fair { next } = &mut self.out_mode { if Into::::into(*next) == original_index { *next = next.switch(); @@ -277,27 +313,19 @@ impl SushiSplitter { } }, 2 => { - let items: [Item; 2] = inputs + let items: [Item; 2] = self + .inputs .iter_mut() - .enumerate() - .map(|(i, input)| input.remove_item(0).unwrap()) - .map(|info| { - let ItemInfo::Sushi(item) = info else { - unreachable!() - }; - item - }) + .map(|input| input.get_mut().take().unwrap()) .collect_array() .expect("We already checked that has to be 2 items to pick up"); // TODO: How do I want to distribute these items? // Currently if both input lines are full the items are not shuffled. - for (output, item) in outputs.iter_mut().zip_eq(items) { - debug_assert!(output.get_back().is_none()); - output - .try_insert_item(output.get_len() - 1, item) - .expect("There should be space on the belt!"); + for (output, item) in self.outputs.iter_mut().zip_eq(items) { + debug_assert!(output.get_mut().is_none()); + *output.get_mut() = Some(item); } }, _ => unreachable!("A Splitter can move at most 2 items every tick"), diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index 5549aa7..b3ade61 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -4,30 +4,40 @@ use itertools::Itertools; use crate::{ belt::belt::NoSpaceError, - inserter::{belt_storage_inserter::BeltStorageInserter, Storage}, + inserter::belt_storage_inserter::BeltStorageInserter, item::{IdxTrait, Item, WeakIdxTrait}, }; use super::{ + FreeIndex, Inserter, SplitterID, belt::{Belt, BeltLenType}, - smart::{InserterStore, Side, SmartBelt, SpaceOccupiedError}, - FreeIndex, Inserter, + smart::{BeltInserterInfo, InserterStore, Side, SmartBelt, SpaceOccupiedError}, + splitter::{SplitterSide, SushiSplitter}, }; +use crate::inserter::FakeUnionStorage; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub(super) struct SushiBelt { +pub(super) struct SushiBelt { + pub(super) ty: u8, + pub(super) is_circular: bool, pub(super) locs: Box<[Option>]>, pub(super) first_free_index: FreeIndex, /// Important, zero_index must ALWAYS be used using mod len + // FIXME: This will overflow! pub(super) zero_index: BeltLenType, - pub(super) inserters: SushiInserterStore, + pub(super) inserters: SushiInserterStore, + + pub last_moving_spot: BeltLenType, + + pub(super) input_splitter: Option<(SplitterID, SplitterSide)>, + pub(super) output_splitter: Option<(SplitterID, SplitterSide)>, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub(super) struct SushiInserterStore { - pub(super) inserters: Vec<(Inserter, Item)>, - pub(super) offsets: Vec, +pub(super) struct SushiInserterStore { + pub(super) inserters: Box<[(Inserter, Item)]>, + pub(super) offsets: Box<[u16]>, } #[derive(Debug, PartialEq, Eq)] @@ -36,17 +46,24 @@ pub(super) enum SushiInfo { Pure(Option>), } -impl SushiBelt { - pub fn new(len: BeltLenType) -> Self { +impl SushiBelt { + pub fn new(ty: u8, len: BeltLenType) -> Self { Self { + ty, + is_circular: false, locs: vec![None; usize::from(len)].into_boxed_slice(), first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, inserters: SushiInserterStore { - inserters: vec![], - offsets: vec![], + inserters: vec![].into_boxed_slice(), + offsets: vec![].into_boxed_slice(), }, + + last_moving_spot: len, + + input_splitter: None, + output_splitter: None, } } @@ -54,7 +71,7 @@ impl SushiBelt, pos: BeltLenType, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -81,11 +98,19 @@ impl SushiBelt SushiBelt, pos: BeltLenType, - storage_id: Storage, + storage_id: FakeUnionStorage, ) -> Result<(), SpaceOccupiedError> { assert!( usize::from(pos) < self.locs.len(), @@ -127,11 +152,19 @@ impl SushiBelt SushiBelt Option { + let mut pos = 0; + + for (offset, inserter) in self + .inserters + .offsets + .iter() + .zip(self.inserters.inserters.iter()) + { + pos += offset; + if pos == belt_pos { + return Some(match &inserter.0 { + Inserter::Out(belt_storage_inserter) => BeltInserterInfo { + outgoing: true, + state: belt_storage_inserter.state, + connection: belt_storage_inserter.storage_id, + }, + Inserter::In(belt_storage_inserter) => BeltInserterInfo { + outgoing: false, + state: belt_storage_inserter.state, + connection: belt_storage_inserter.storage_id, + }, + }); + } else if pos > belt_pos { + return None; + } + pos += 1; + } + + None + } + + pub fn get_inserter_item(&self, belt_pos: u16) -> Item { + assert!( + usize::from(belt_pos) < self.locs.len(), + "Bounds check {belt_pos} >= {}", + self.locs.len() + ); + + let mut pos_after_last_inserter = 0; + let mut i = 0; + + for offset in &self.inserters.offsets { + 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 + } + + pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { + let mut pos = 0; + + for (offset, inserter) in self + .inserters + .offsets + .iter() + .zip(self.inserters.inserters.iter_mut()) + { + pos += offset; + if pos == belt_pos { + match &mut inserter.0 { + Inserter::Out(belt_storage_inserter) => { + belt_storage_inserter.storage_id = new; + }, + Inserter::In(belt_storage_inserter) => { + belt_storage_inserter.storage_id = new; + }, + } + return; + } else if pos >= belt_pos { + unreachable!() + } + pos += 1; + } + } + pub fn remove_inserter(&mut self, pos: BeltLenType) { assert!( usize::from(pos) < self.locs.len(), @@ -156,7 +278,9 @@ impl SushiBelt 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::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 => { @@ -166,8 +290,18 @@ impl SushiBelt SushiBelt) -> SmartBelt { + pub fn into_smart_belt(self, item: Item) -> SmartBelt { let found_item = match self.locs.iter().copied().flatten().all_equal_value() { - Ok(found_item) => { - assert_eq!(found_item, item); - found_item - }, + Ok(found_item) => found_item, Err(None) => item, Err(Some(_)) => panic!("Belt is not pure!"), }; + assert_eq!(found_item, item); + let Self { + ty, + is_circular, locs, first_free_index, zero_index, inserters: SushiInserterStore { inserters, offsets }, + + last_moving_spot, + + input_splitter, + output_splitter, } = self; SmartBelt { + ty, + is_circular, first_free_index, zero_index, locs: locs - .into_iter() + .iter() .map(|loc| { if let Some(found_item) = loc { debug_assert_eq!(*found_item, item); @@ -266,13 +408,59 @@ impl SushiBelt Option<(SplitterID, SplitterSide)> { + self.input_splitter.take() + } + + pub fn remove_output_splitter(&mut self) -> Option<(SplitterID, SplitterSide)> { + 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 { @@ -320,14 +508,31 @@ impl SushiBelt SushiBelt Self { let front_len = front.get_len() as usize; let _back_len = back.get_len() as usize; @@ -352,23 +568,45 @@ impl SushiBelt SushiBelt SushiBelt BeltLenType { + pub fn update_first_free_pos(&mut self, now_empty_pos: BeltLenType) { + match self.first_free_index { + FreeIndex::OldFreeIndex(index) | FreeIndex::FreeIndex(index) => { + if now_empty_pos <= index { + self.first_free_index = FreeIndex::FreeIndex(now_empty_pos); + } + }, + } + } + + fn find_and_update_real_first_free_index(&mut self) -> Option { let new_free_index = match self.first_free_index { FreeIndex::FreeIndex(index) => index, FreeIndex::OldFreeIndex(index) => { - // println!("HAD TO SEARCH FOR FIRST FREE INDEX!"); - let search_start_index = index; let mut iter = self @@ -446,33 +709,36 @@ impl SushiBelt Belt - for SushiBelt -{ - fn query_item( - &self, - pos: super::belt::BeltLenType, - ) -> Option> { +impl Belt for SushiBelt { + fn query_item(&self, pos: super::belt::BeltLenType) -> Option> { let pos = usize::from((self.zero_index + pos) % self.get_len()); - self.locs[pos].map(|item| super::belt::ItemInfo::Sushi(item)) + self.locs[pos].map(|item| item) } - fn remove_item( - &mut self, - pos: super::belt::BeltLenType, - ) -> Option> { - let pos = usize::from((self.zero_index + pos) % self.get_len()); + fn remove_item(&mut self, pos: super::belt::BeltLenType) -> Option> { + let pos_size = usize::from((self.zero_index + pos) % self.get_len()); - self.locs[pos] - .take() - .map(|item| super::belt::ItemInfo::Sushi(item)) + if self.locs[pos_size].is_some() { + self.update_first_free_pos(pos); + } + + self.locs[pos_size].take().map(|item| item) } fn try_insert_item( @@ -491,7 +757,7 @@ impl Belt } } - fn items(&self) -> Vec>> { + fn items(&self) -> impl Iterator>> { let len = self.locs.len(); let (start, end) = self .locs @@ -501,18 +767,61 @@ impl Belt end.iter() .chain(start.iter()) .map(|loc| loc.map(|item| item)) - .collect() } fn get_len(&self) -> super::belt::BeltLenType { BeltLenType::try_from(self.locs.len()).expect("Belt too long!") } - fn update(&mut self) { + fn update(&mut self, splitter_list: &[SushiSplitter]) { + if self.is_circular { + self.zero_index += 1; + return; + } + + if let Some((output_id, side)) = &self.output_splitter { + if let Some(item) = self.query_item(0) { + let splitter_loc = + &splitter_list[output_id.index as usize].inputs[usize::from(bool::from(*side))]; + + // SAFETY: + // This is the only place where we modify splitter_list from a &. + // This can never race since only one belt ever has the same values for output_id and side, so only a single belt will ever modify each splitter loc + let splitter_loc = unsafe { &mut *splitter_loc.get() }; + + if splitter_loc.is_none() { + *splitter_loc = Some(item); + let _ = self.remove_item(0); + } + } + } + + if let Some((input_id, side)) = &self.input_splitter { + // Last pos + if self.query_item(self.get_len() - 1).is_none() { + let splitter_loc = + &splitter_list[input_id.index as usize].outputs[usize::from(bool::from(*side))]; + + // SAFETY: + // This is the only place where we modify splitter_list from a &. + // This can never race since only one belt ever has the same values for output_id and side, so only a single belt will ever modify each splitter loc + let splitter_loc = unsafe { &mut *splitter_loc.get() }; + + if let Some(item) = *splitter_loc { + *splitter_loc = None; + let _ = self + .try_insert_item(self.get_len() - 1, item) + .expect("Should never fail!"); + } + } + } + if self.query_item(0).is_none() { // Correctness: Since we always % len whenever we access using self.zero_index, we do not need to % len here // TODO: This could overflow after usize::MAX ticks which is 9749040289 Years. Should be fine! + self.zero_index %= self.get_len(); self.zero_index += 1; + self.last_moving_spot = 0; match self.first_free_index { FreeIndex::FreeIndex(0) | FreeIndex::OldFreeIndex(0) => { if self.query_item(0).is_none() { @@ -522,10 +831,14 @@ impl Belt } }, FreeIndex::FreeIndex(_) => { - unreachable!("FreeIndex should always point at the earliest known empty spot and we know that index 0 WAS an empty spot") + unreachable!( + "FreeIndex should always point at the earliest known empty spot and we know that index 0 WAS an empty spot" + ) }, FreeIndex::OldFreeIndex(_) => { - unreachable!("OldFreeIndex should always point at the earliest potential empty spot and we know that index 0 WAS an empty spot") + unreachable!( + "OldFreeIndex should always point at the earliest potential empty spot and we know that index 0 WAS an empty spot" + ) }, } return; @@ -538,6 +851,13 @@ impl Belt let len = self.get_len(); + self.last_moving_spot = first_free_index_real.unwrap_or(len); + + let Some(first_free_index_real) = first_free_index_real else { + // All spots are filled + return; + }; + let slice = &mut self.locs; let (end_slice, start_slice) = slice.split_at_mut(self.zero_index.into()); @@ -656,13 +976,25 @@ impl Belt 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; @@ -696,17 +1028,26 @@ impl Belt }; if side == Side::FRONT { - inserters.offsets[0] = inserters.offsets[0] - .checked_add(front_extension_amount) - .expect("Max length of belt (u16::MAX) reached"); + if !inserters.offsets.is_empty() { + inserters.offsets[0] = inserters.offsets[0] + .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, } }); diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 6ce275d..664ca88 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -1,49 +1,483 @@ -use std::{borrow::Borrow, ops::Range}; - +use log::error; use proptest::{ - prelude::{prop, Just, Strategy}, + prelude::{Just, Strategy, prop}, prop_oneof, }; +use std::num::NonZero; +use std::{borrow::Borrow, ops::Range}; +use tilelib::types::{DrawInstance, Layer}; + +use crate::{ + belt::splitter::SplitterDistributionMode, frontend::world::tile::DirRelative, item::Indexable, +}; +use crate::{frontend::world::tile::UndergroundDir, item::WeakIdxTrait}; use crate::{ data::DataStore, frontend::{ action::{ + ActionType, place_entity::{EntityPlaceOptions, PlaceEntityInfo}, place_tile::{PlaceFloorTileByHandInfo, PlaceFloorTileGhostInfo, PositionInfo}, set_recipe::SetRecipeInfo, - ActionType, }, world::{ - tile::{ - AssemblerID, AssemblerInfo, AttachedInserter, Dir, FloorTile, InserterInfo, - PlaceEntityType, World, - }, Position, + tile::{AssemblerID, AssemblerInfo, Dir, FloorTile, PlaceEntityType, World}, }, }, - item::{IdxTrait, Item, Recipe, WeakIdxTrait}, - rendering::app_state::GameState, + item::{IdxTrait, Item, Recipe}, + rendering::{TextureAtlas, app_state::GameState}, replays::Replay, - statistics::recipe, }; // For now blueprint will just be a list of actions -#[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct Blueprint { - pub actions: Vec>, +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Blueprint { + actions: Vec, +} + +#[derive(Debug, Clone)] +pub struct ReusableBlueprint { + actions: Vec>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum BlueprintAction { + PlaceEntity(BlueprintPlaceEntity), + + SetRecipe { + pos: Position, + recipe: String, + }, + + OverrideInserterMovetime { + pos: Position, + new_movetime: Option>, + }, + + AddModules { + pos: Position, + modules: Vec, + }, + + SetChestSlotLimit { + pos: Position, + num_slots: u8, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum BlueprintPlaceEntity { + Assembler { + pos: Position, + ty: String, + #[serde(default = "Dir::default")] + rotation: Dir, + }, + Inserter { + pos: Position, + dir: Dir, + /// The Item the inserter will move, must fit both the in and output side + filter: Option, + }, + Belt { + pos: Position, + direction: Dir, + ty: String, + }, + Underground { + pos: Position, + direction: Dir, + ty: String, + underground_dir: UndergroundDir, + }, + PowerPole { + pos: Position, + ty: String, + }, + Splitter { + pos: Position, + direction: Dir, + ty: String, + + in_mode: Option, + out_mode: Option, + }, + Chest { + pos: Position, + ty: String, + }, + SolarPanel { + pos: Position, + ty: String, + }, + Lab { + pos: Position, + ty: String, + }, + Beacon { + ty: String, + pos: Position, + }, + FluidTank { + ty: String, + pos: Position, + rotation: Dir, + }, + MiningDrill { + ty: String, + pos: Position, + rotation: Dir, + }, } -impl Blueprint { +impl BlueprintAction { + fn from( + action: &ActionType, + data_store: &DataStore, + ) -> Self { + match action { + ActionType::PlaceFloorTile(_) => unimplemented!(), + ActionType::PlaceEntity(place_entity_info) => { + match place_entity_info.entities.clone() { + EntityPlaceOptions::Single(place_entity_type) => { + let ty = match place_entity_type { + PlaceEntityType::Assembler { pos, ty, rotation } => { + BlueprintPlaceEntity::Assembler { + pos, + ty: data_store.assembler_info[ty as usize].name.clone(), + rotation, + } + }, + PlaceEntityType::Inserter { pos, dir, filter } => { + BlueprintPlaceEntity::Inserter { + pos, + dir, + filter: filter.map(|item| { + data_store.item_names[item.into_usize()].clone() + }), + } + }, + PlaceEntityType::Belt { pos, direction, ty } => { + BlueprintPlaceEntity::Belt { + pos, + direction, + ty: data_store.belt_infos[ty as usize].name.clone(), + } + }, + PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + } => BlueprintPlaceEntity::Underground { + pos, + direction, + ty: data_store.belt_infos[ty as usize].name.clone(), + underground_dir, + }, + PlaceEntityType::PowerPole { pos, ty } => { + BlueprintPlaceEntity::PowerPole { + pos, + ty: data_store.power_pole_data[ty as usize].name.clone(), + } + }, + PlaceEntityType::Splitter { + pos, + direction, + ty, + in_mode, + out_mode, + } => BlueprintPlaceEntity::Splitter { + pos, + direction, + // FIXME: + ty: "FIXME".to_string(), + in_mode, + out_mode, + }, + PlaceEntityType::Chest { pos, ty } => BlueprintPlaceEntity::Chest { + pos, + ty: data_store.chest_names[ty as usize].clone(), + }, + PlaceEntityType::SolarPanel { pos, ty } => { + BlueprintPlaceEntity::SolarPanel { + pos, + ty: data_store.solar_panel_info[ty as usize].name.clone(), + } + }, + PlaceEntityType::Lab { pos, ty } => BlueprintPlaceEntity::Lab { + pos, + ty: data_store.lab_info[ty as usize].name.clone(), + }, + PlaceEntityType::Beacon { ty, pos } => BlueprintPlaceEntity::Beacon { + pos, + ty: data_store.beacon_info[ty as usize].name.clone(), + }, + PlaceEntityType::FluidTank { ty, pos, rotation } => { + BlueprintPlaceEntity::FluidTank { + pos, + ty: data_store.fluid_tank_infos[ty as usize].name.clone(), + rotation, + } + }, + PlaceEntityType::MiningDrill { ty, pos, rotation } => { + BlueprintPlaceEntity::MiningDrill { + pos, + ty: data_store.mining_drill_info[ty as usize].name.clone(), + rotation, + } + }, + }; + Self::PlaceEntity(ty) + }, + EntityPlaceOptions::Multiple(_) => unimplemented!(), + } + }, + ActionType::SetRecipe(set_recipe_info) => Self::SetRecipe { + pos: set_recipe_info.pos, + recipe: data_store.recipe_names[set_recipe_info.recipe.into_usize()].clone(), + }, + ActionType::OverrideInserterMovetime { pos, new_movetime } => { + Self::OverrideInserterMovetime { + pos: *pos, + new_movetime: *new_movetime, + } + }, + ActionType::Position(_, _) => unimplemented!(), + ActionType::AddModules { pos, modules } => Self::AddModules { + pos: *pos, + modules: modules + .iter() + .map(|module| data_store.module_info[*module].name.clone()) + .collect(), + }, + ActionType::RemoveModules { .. } => unimplemented!(), + ActionType::SetChestSlotLimit { pos, num_slots } => Self::SetChestSlotLimit { + pos: *pos, + num_slots: *num_slots, + }, + ActionType::Remove(_) => unimplemented!(), + ActionType::SetActiveResearch { .. } => unimplemented!(), + ActionType::CheatUnlockTechnology { .. } => unimplemented!(), + ActionType::Ping(_) => unimplemented!(), + } + } + + fn try_into_real_action( + &self, + data_store: &DataStore, + ) -> Result, ()> { + let action = match self { + BlueprintAction::PlaceEntity(blueprint_place_entity) => { + let entity = match blueprint_place_entity { + BlueprintPlaceEntity::Assembler { pos, ty, rotation } => { + PlaceEntityType::Assembler { + pos: *pos, + ty: data_store + .assembler_info + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + rotation: *rotation, + } + }, + BlueprintPlaceEntity::Inserter { pos, dir, filter } => { + PlaceEntityType::Inserter { + pos: *pos, + dir: *dir, + filter: filter + .as_ref() + .map(|item| { + data_store + .item_names + .iter() + .position(|ds_item| ds_item == item) + .map(|index| Item { + id: index.try_into().unwrap(), + }) + .ok_or(()) + }) + .transpose()?, + } + }, + BlueprintPlaceEntity::Belt { pos, direction, ty } => PlaceEntityType::Belt { + pos: *pos, + direction: *direction, + ty: data_store + .belt_infos + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::Underground { + pos, + direction, + ty, + underground_dir, + } => PlaceEntityType::Underground { + pos: *pos, + direction: *direction, + ty: data_store + .belt_infos + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + underground_dir: *underground_dir, + }, + BlueprintPlaceEntity::PowerPole { pos, ty } => PlaceEntityType::PowerPole { + pos: *pos, + ty: data_store + .power_pole_data + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::Splitter { + pos, + direction, + ty, + in_mode, + out_mode, + } => PlaceEntityType::Splitter { + pos: *pos, + direction: *direction, + // FIXME: + ty: 0, + in_mode: *in_mode, + out_mode: *out_mode, + }, + BlueprintPlaceEntity::Chest { pos, ty } => PlaceEntityType::Chest { + pos: *pos, + ty: data_store + .chest_names + .iter() + .position(|name| name == ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::SolarPanel { pos, ty } => PlaceEntityType::SolarPanel { + pos: *pos, + ty: data_store + .solar_panel_info + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::Lab { pos, ty } => PlaceEntityType::Lab { + pos: *pos, + ty: data_store + .lab_info + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::Beacon { ty, pos } => PlaceEntityType::Beacon { + pos: *pos, + ty: data_store + .beacon_info + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + }, + BlueprintPlaceEntity::FluidTank { ty, pos, rotation } => { + PlaceEntityType::FluidTank { + pos: *pos, + ty: data_store + .fluid_tank_infos + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + rotation: *rotation, + } + }, + BlueprintPlaceEntity::MiningDrill { ty, pos, rotation } => { + PlaceEntityType::MiningDrill { + pos: *pos, + ty: data_store + .mining_drill_info + .iter() + .position(|info| info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(())?, + rotation: *rotation, + } + }, + }; + + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(entity), + }) + }, + BlueprintAction::SetRecipe { pos, recipe } => ActionType::SetRecipe(SetRecipeInfo { + pos: *pos, + recipe: data_store + .recipe_names + .iter() + .position(|ds_recipe| ds_recipe == recipe) + .map(|index| Recipe { + id: index.try_into().unwrap(), + }) + .ok_or(())?, + }), + BlueprintAction::OverrideInserterMovetime { pos, new_movetime } => { + ActionType::OverrideInserterMovetime { + pos: *pos, + new_movetime: *new_movetime, + } + }, + BlueprintAction::AddModules { pos, modules } => ActionType::AddModules { + pos: *pos, + modules: modules + .iter() + .map(|action_module| { + data_store + .module_info + .iter() + .position(|module| module.name == *action_module) + }) + .collect::>() + .ok_or(())?, + }, + BlueprintAction::SetChestSlotLimit { pos, num_slots } => { + ActionType::SetChestSlotLimit { + pos: *pos, + num_slots: *num_slots, + } + }, + }; + + Ok(action) + } +} + +impl ReusableBlueprint { pub fn apply( &self, base_pos: Position, game_state: &mut GameState, data_store: &DataStore, ) { - // FIXME: currently base_pos is unused! - game_state.apply_actions( - self.actions.iter().map(|a| match a { + game_state.apply_actions(self.actions_with_base_pos(base_pos), data_store); + } + + pub fn actions_with_base_pos( + &self, + base_pos: Position, + ) -> impl Iterator> { + self.actions + .iter() + .cloned() + .map(move |a| match a { ActionType::PlaceFloorTile(PlaceFloorTileByHandInfo { ghost_info: PlaceFloorTileGhostInfo { @@ -53,7 +487,7 @@ impl Blueprint ActionType::PlaceFloorTile(PlaceFloorTileByHandInfo { ghost_info: PlaceFloorTileGhostInfo { - tile: *tile, + tile, position: PositionInfo::Single { pos: Position { x: base_pos.x + pos.x, @@ -61,38 +495,61 @@ impl Blueprint ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { pos: Position { x: base_pos.x + pos.x, y: base_pos.y + pos.y, }, - ty: *ty, + ty, + rotation, }), }), ActionType::PlaceEntity(PlaceEntityInfo { - entities: EntityPlaceOptions::Single(PlaceEntityType::Belt { pos, direction }), + entities: + EntityPlaceOptions::Single(PlaceEntityType::Belt { pos, direction, ty }), }) => ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Belt { pos: Position { x: base_pos.x + pos.x, y: base_pos.y + pos.y, }, - direction: *direction, + direction, + ty, + }), + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: + EntityPlaceOptions::Single(PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + }), + }) => ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Underground { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + direction, + ty, + underground_dir, }), }), ActionType::PlaceEntity(PlaceEntityInfo { - entities: EntityPlaceOptions::Single(PlaceEntityType::Chest { pos }), + entities: EntityPlaceOptions::Single(PlaceEntityType::Chest { pos, ty }), }) => ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Chest { pos: Position { x: base_pos.x + pos.x, y: base_pos.y + pos.y, }, + ty, }), }), ActionType::PlaceEntity(PlaceEntityInfo { @@ -104,8 +561,8 @@ impl Blueprint Blueprint Blueprint Blueprint Blueprint Blueprint ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Beacon { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + ty, + }), + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::FluidTank { pos, ty, rotation }), + }) => ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::FluidTank { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + ty, + rotation, }), }), ActionType::SetRecipe(SetRecipeInfo { pos, recipe }) => { @@ -166,64 +648,146 @@ impl Blueprint ActionType::Remove(Position { x: base_pos.x + position.x, y: base_pos.y + position.y, }), - _ => unreachable!(), - }), - data_store, - ); + ActionType::AddModules { pos, modules } => ActionType::AddModules { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + modules: modules.clone(), + }, + ActionType::SetChestSlotLimit { pos, num_slots } => ActionType::SetChestSlotLimit { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + num_slots, + }, + ActionType::OverrideInserterMovetime { pos, new_movetime } => { + ActionType::OverrideInserterMovetime { + pos: Position { + x: base_pos.x + pos.x, + y: base_pos.y + pos.y, + }, + new_movetime, + } + }, + a => unreachable!("{:?}", a), + }) + } +} + +impl Blueprint { + pub fn get_reusable( + &self, + data_store: &DataStore, + ) -> ReusableBlueprint { + ReusableBlueprint { + actions: self + .actions + .iter() + .map(|bp_action| { + bp_action.try_into_real_action(data_store).expect( + format!("Action not possible with current mod set: {:?}", bp_action) + .as_str(), + ) + }) + .collect(), + } + } + + pub fn apply( + &self, + base_pos: Position, + game_state: &mut GameState, + data_store: &DataStore, + ) { + let reusable = self.get_reusable(data_store); + game_state.apply_actions(reusable.actions_with_base_pos(base_pos), data_store); } - pub fn from_replay>>( + pub fn from_replay< + ItemIdxType: IdxTrait, + RecipeIdxType: IdxTrait, + DS: Borrow>, + >( replay: &Replay, ) -> Self { Self { - actions: replay.actions.iter().map(|ra| ra.action.clone()).collect(), + actions: replay + .actions + .iter() + .map(|ra| BlueprintAction::from(&ra.action, replay.data_store.borrow())) + .collect(), } } - pub fn from_area( + pub fn from_area( world: &World, - area: [Range; 2], + area: [Range; 2], data_store: &DataStore, ) -> Self { let mut bp = Self { actions: vec![] }; + let entities: Vec<_> = world + .get_entities_colliding_with( + Position { + x: area[0].start, + y: area[1].start, + }, + ( + (area[0].end - area[0].start).try_into().unwrap(), + (area[1].end - area[1].start).try_into().unwrap(), + ), + data_store, + ) + .into_iter() + .collect(); + + if entities.is_empty() { + return Self { actions: vec![] }; + } + let base_pos = Position { - x: area[0].start as usize, - y: area[1].start as usize, + x: entities.iter().map(|e| e.get_pos().x).min().unwrap(), + y: entities.iter().map(|e| e.get_pos().y).min().unwrap(), }; - let entities = world.get_entities_colliding_with( - base_pos, - (area[0].end - area[0].start, area[1].end - area[1].start), - data_store, - ); - // FIXME: This could be unreproducable if the power connection order matters // FIXME: This will underflow if a entity extends past the edge of the selected area for e in entities { let actions = match e { - // FIXME: Insert Modules crate::frontend::world::tile::Entity::Assembler { ty, pos, info: AssemblerInfo::PoweredNoRecipe(_) | AssemblerInfo::UnpoweredNoRecipe, modules, - } => vec![ActionType::PlaceEntity(PlaceEntityInfo { - entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + rotation, + } => vec![ + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + ty: *ty, + rotation: *rotation, + }), + }), + ActionType::AddModules { pos: Position { x: pos.x - base_pos.x, y: pos.y - base_pos.y, }, - ty: *ty, - }), - })], + modules: modules.iter().flatten().copied().collect(), + }, + ], crate::frontend::world::tile::Entity::Assembler { ty, pos, @@ -234,6 +798,7 @@ impl Blueprint vec![ ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { @@ -242,6 +807,7 @@ impl Blueprint Blueprint { vec![ActionType::PlaceEntity(PlaceEntityInfo { @@ -263,7 +836,9 @@ impl Blueprint { + crate::frontend::world::tile::Entity::Belt { + pos, ty, direction, .. + } => { vec![ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Belt { pos: Position { @@ -271,6 +846,7 @@ impl Blueprint Blueprint todo!(), - crate::frontend::world::tile::Entity::Splitter { pos, direction, id } => todo!(), - crate::frontend::world::tile::Entity::Inserter { pos, direction, .. } => { + ty, + .. + } => { vec![ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Underground { + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + direction: *direction, + ty: *ty, + underground_dir: *underground_dir, + }), + })] + }, + crate::frontend::world::tile::Entity::Splitter { pos, direction, id } => { + vec![ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Splitter { + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + direction: *direction, + // FIXME: + ty: 0, + in_mode: None, + out_mode: None, + }), + })] + }, + crate::frontend::world::tile::Entity::Inserter { + user_movetime, + pos, + direction, + .. + } => { + let mut ret = vec![ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Inserter { pos: Position { x: pos.x - base_pos.x, @@ -292,15 +899,28 @@ impl Blueprint { + crate::frontend::world::tile::Entity::Chest { pos, ty, .. } => { vec![ActionType::PlaceEntity(PlaceEntityInfo { entities: EntityPlaceOptions::Single(PlaceEntityType::Chest { pos: Position { x: pos.x - base_pos.x, y: pos.y - base_pos.y, }, + ty: *ty, }), })] }, @@ -333,26 +953,285 @@ impl Blueprint { + vec![ + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Beacon { + ty: *ty, + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + }), + }), + ActionType::AddModules { + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + modules: modules.into_iter().copied().flatten().collect(), + }, + ] + }, + crate::frontend::world::tile::Entity::FluidTank { ty, pos, rotation } => { + vec![ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::FluidTank { + ty: *ty, + pos: Position { + x: pos.x - base_pos.x, + y: pos.y - base_pos.y, + }, + rotation: *rotation, + }), + })] + }, }; - bp.actions.extend(actions); + bp.actions.extend( + actions + .into_iter() + .map(|action| BlueprintAction::from(&action, data_store)), + ); } bp } + + pub fn flip_horizontal( + &mut self, + data_store: &DataStore, + ) { + self.modify( + |pos, e_size| Position { + x: -pos.x - e_size[0], + y: pos.y, + }, + |dir| { + if dir.compare(Dir::North) == DirRelative::Turned { + dir.reverse() + } else { + *dir + } + }, + data_store, + ); + } + + pub fn flip_vertical( + &mut self, + data_store: &DataStore, + ) { + self.modify( + |pos, e_size| Position { + x: pos.x, + y: -pos.y - e_size[1], + }, + |dir| { + if dir.compare(Dir::East) == DirRelative::Turned { + dir.reverse() + } else { + *dir + } + }, + data_store, + ); + } + + pub fn turn_right( + &mut self, + data_store: &DataStore, + ) { + self.modify( + |pos, e_size| Position { + x: -pos.y - (e_size[1] - 1), + y: pos.x, + }, + |dir| dir.turn_right(), + data_store, + ); + } + + fn modify( + &mut self, + pos_fn: impl Fn(&Position, [i32; 2]) -> Position, + rotation_fn: impl Fn(&Dir) -> Dir, + data_store: &DataStore, + ) { + for action in self.actions.iter_mut() { + let e_size: [i32; 2] = action + .try_into_real_action(data_store) + .unwrap() + .get_building_size(data_store) + .unwrap_or([1, 1]) + .map(|v| v.into()); + match action { + BlueprintAction::PlaceEntity(blueprint_place_entity) => { + match blueprint_place_entity { + BlueprintPlaceEntity::Assembler { pos, rotation, .. } => { + *pos = pos_fn(pos, e_size); + *rotation = rotation_fn(&rotation); + }, + BlueprintPlaceEntity::Inserter { pos, dir, .. } => { + *pos = pos_fn(pos, e_size); + *dir = rotation_fn(&dir); + }, + BlueprintPlaceEntity::Belt { pos, direction, .. } => { + *pos = pos_fn(pos, e_size); + *direction = rotation_fn(&direction); + }, + BlueprintPlaceEntity::Underground { pos, direction, .. } => { + *pos = pos_fn(pos, e_size); + *direction = rotation_fn(&direction); + }, + BlueprintPlaceEntity::PowerPole { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintPlaceEntity::Splitter { pos, direction, .. } => { + *pos = pos_fn(pos, e_size); + *direction = rotation_fn(&direction); + }, + BlueprintPlaceEntity::Chest { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintPlaceEntity::SolarPanel { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintPlaceEntity::Lab { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintPlaceEntity::Beacon { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintPlaceEntity::FluidTank { pos, rotation, .. } => { + *pos = pos_fn(pos, e_size); + *rotation = rotation_fn(&rotation); + }, + BlueprintPlaceEntity::MiningDrill { pos, rotation, .. } => { + *pos = pos_fn(pos, e_size); + *rotation = rotation_fn(&rotation); + }, + } + }, + BlueprintAction::SetRecipe { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintAction::OverrideInserterMovetime { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintAction::AddModules { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + BlueprintAction::SetChestSlotLimit { pos, .. } => { + *pos = pos_fn(pos, e_size); + }, + } + } + } + + pub fn draw( + &self, + base_pos: (f32, f32), + camera_pos: (f32, f32), + layer: &mut Layer, + texture_atlas: &TextureAtlas, + data_store: &DataStore, + ) { + // let bottom_right = Position { + // x: self + // .actions + // .iter() + // .flat_map(|e| e.get_pos()) + // .map(|pos| pos.x) + // .max() + // .unwrap(), + // y: self + // .actions + // .iter() + // .flat_map(|e| e.get_pos()) + // .map(|pos| pos.y) + // .max() + // .unwrap(), + // }; + + // let raw_base_pos = Position { + // x: self + // .actions + // .iter() + // .flat_map(|e| e.get_pos()) + // .map(|pos| pos.x) + // .min() + // .unwrap(), + // y: self + // .actions + // .iter() + // .flat_map(|e| e.get_pos()) + // .map(|pos| pos.y) + // .min() + // .unwrap(), + // }; + + // layer.draw_sprite( + // &texture_atlas.dark_square, + // DrawInstance { + // position: [ + // raw_base_pos.x as f32 - camera_pos.0 + base_pos.0, + // raw_base_pos.y as f32 - camera_pos.1 + base_pos.1, + // ], + // size: [ + // bottom_right.x as f32 - raw_base_pos.x as f32, + // bottom_right.y as f32 - raw_base_pos.y as f32, + // ], + // animation_frame: 0, + // }, + // ); + + for action in &self.actions { + let Ok(action) = action.try_into_real_action(data_store) else { + error!("Could not draw blueprint!"); + return; + }; + let pos = action.get_pos(); + let size = action.get_building_size(data_store); + + let (Some(pos), Some(size)) = (pos, size) else { + continue; + }; + + layer.draw_sprite( + &texture_atlas.dark_square, + DrawInstance { + position: [ + pos.x as f32 - camera_pos.0 + base_pos.0, + pos.y as f32 - camera_pos.1 + base_pos.1, + ], + size: [size[0] as f32, size[1] as f32], + animation_frame: 0, + }, + ); + } + } } -pub fn random_blueprint_strategy( +pub fn random_blueprint_strategy<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( len_range: Range, - data_store: &DataStore, -) -> impl Strategy> { - prop::collection::vec(random_blueprint_action(data_store), len_range) - .prop_map(|actions| Blueprint { actions }) + data_store: &'a DataStore, +) -> impl Strategy + use<'a, ItemIdxType, RecipeIdxType> { + prop::collection::vec(random_blueprint_action(data_store), len_range).prop_map(|actions| { + Blueprint { + actions: actions + .into_iter() + .map(|v| BlueprintAction::from(&v, data_store)) + .collect(), + } + }) } pub fn random_action( data_store: &DataStore, -) -> impl Strategy> { +) -> impl Strategy> + use +{ prop_oneof![ random_position().prop_map(|pos| ActionType::Ping(pos)), random_position().prop_map(|pos| ActionType::Position(0, (pos.x as f32, pos.y as f32))), @@ -376,7 +1255,8 @@ pub fn random_action( pub fn random_blueprint_action( data_store: &DataStore, -) -> impl Strategy> { +) -> impl Strategy> + use +{ prop_oneof![ // random_blueprint_offs().prop_map(|pos| ActionType::PlaceFloorTile( // PlaceFloorTileByHandInfo { @@ -400,18 +1280,30 @@ pub fn random_blueprint_action( pub fn random_entity_to_place( data_store: &DataStore, -) -> impl Strategy> { +) -> impl Strategy> + use { prop_oneof![ - random_blueprint_offs().prop_map(|pos| PlaceEntityType::Assembler { pos, ty: 0 }), - random_blueprint_offs().prop_map(|pos| PlaceEntityType::Chest { pos }), + random_blueprint_offs().prop_map(|pos| PlaceEntityType::Assembler { + pos, + ty: 0, + rotation: Dir::North + }), + (random_blueprint_offs(), 0..data_store.chest_num_slots.len()).prop_map(|(pos, ty)| { + PlaceEntityType::Chest { + pos, + ty: ty.try_into().unwrap(), + } + }), ( random_blueprint_offs() // 0..(data_store.solar_panel_sizes.len().try_into().unwrap()) ) - .prop_map(|(pos)| PlaceEntityType::SolarPanel { pos, ty: 0 }), + .prop_map(|pos| PlaceEntityType::SolarPanel { pos, ty: 0 }), (random_blueprint_offs(), random_dir()).prop_map(|(pos, dir)| PlaceEntityType::Belt { pos, - direction: dir + direction: dir, + + // TODO: + ty: 0, }), (random_blueprint_offs(), 0..data_store.power_pole_data.len()).prop_map(|(pos, ty)| { PlaceEntityType::PowerPole { @@ -431,7 +1323,10 @@ pub fn random_entity_to_place( filter: Some(filter), } }), - (random_blueprint_offs(), (0u8..1)) + ( + random_blueprint_offs(), + (0u8..(data_store.lab_info.len().try_into().unwrap())) + ) .prop_map(|(pos, ty)| { PlaceEntityType::Lab { pos, ty } }), // (random_blueprint_offs(), random_dir()).prop_map(|(pos, dir)| { // PlaceEntityType::Splitter { @@ -457,7 +1352,7 @@ pub fn random_dir() -> impl Strategy { pub fn random_recipe( data_store: &DataStore, -) -> impl Strategy> { +) -> impl Strategy> + use { (0..data_store.recipe_timers.len()) .prop_map(|recipe_idx| RecipeIdxType::try_from(recipe_idx).unwrap()) .prop_map(|id| Recipe { id }) @@ -465,22 +1360,22 @@ pub fn random_recipe( pub fn random_item( data_store: &DataStore, -) -> impl Strategy> { - (0..data_store.item_names.len()) +) -> impl Strategy> + use { + (0..data_store.item_display_names.len()) .prop_map(|item_idx| ItemIdxType::try_from(item_idx).unwrap()) .prop_map(|id| Item { id }) } pub fn random_blueprint_offs() -> impl Strategy { ((0u32..16), (0u32..16)).prop_map(|(x, y)| Position { - x: x as usize, - y: y as usize, + x: x as i32, + y: y as i32, }) } pub fn random_position() -> impl Strategy { ((1600u32..=1602u32), (1600u32..=1602u32)).prop_map(|(x, y)| Position { - x: x as usize, - y: y as usize, + x: x as i32, + y: y as i32, }) } diff --git a/src/bot_system.rs b/src/bot_system.rs index d71c04d..2539c4d 100644 --- a/src/bot_system.rs +++ b/src/bot_system.rs @@ -2,27 +2,33 @@ use std::{cmp::max, cmp::min, collections::BTreeMap, marker::PhantomData}; // I will render bots as "particles" with a fixed size instanced gpu buffer // This is fine as long as we do not override bots, which are still flying -const GOAL_ITEMS_PER_TICK: usize = 40000; +const GOAL_ITEMS_PER_MINUTE: usize = 1_000_000_000; +const GOAL_ITEMS_PER_TICK: usize = GOAL_ITEMS_PER_MINUTE / 60 / 60; const BOT_HAND_SIZE: usize = 4; const BOT_SPEED: usize = 120 * 1000 / 60 / 60; // tiles / s const MAX_BASE_SIZE: usize = 3000; -const AVG_TRIP_LENGTH: usize = 300; -const MAX_BOT_TRAVEL_TIME_TICKS: usize = MAX_BASE_SIZE * 60 / BOT_SPEED; +const BOT_BATTERY_LIFE_KJ: usize = 1500; +const BOT_KJ_PER_TILE: usize = 5; +const BOT_KJ_PER_SEC: usize = 3; +const BOT_FLIGHT_COST_PER_SEC: usize = BOT_SPEED * BOT_KJ_PER_TILE + BOT_KJ_PER_SEC; +const BOT_BATTERY_LIFE_TICKS: usize = 1500 * 60 / BOT_FLIGHT_COST_PER_SEC; +const AVG_TRIP_LENGTH: usize = 600; +const MAX_BOT_TRAVEL_TIME_TICKS: usize = 2 * BOT_BATTERY_LIFE_TICKS; const AVG_BOT_TRAVEL_TIME_TICKS: usize = AVG_TRIP_LENGTH * 60 / BOT_SPEED; const AVG_NEW_BOTS_PER_TICK: usize = GOAL_ITEMS_PER_TICK / (BOT_HAND_SIZE); const REQUIRED_BOTS: usize = GOAL_ITEMS_PER_TICK * AVG_BOT_TRAVEL_TIME_TICKS / BOT_HAND_SIZE; +const BOT_UPDATE_COUNT_CPU_PER_TICK: usize = REQUIRED_BOTS / BOT_BATTERY_LIFE_TICKS; const REQUIRED_DRAW_SLOTS: usize = MAX_BOT_TRAVEL_TIME_TICKS * AVG_NEW_BOTS_PER_TICK; const REQUIRED_MEMORY_SEND_TO_GPU_PER_TICK: usize = AVG_NEW_BOTS_PER_TICK * (2 * 3 * (2 + 1)) * 4; const REQUIRED_PCI_BANDWIDTH_MBS: usize = REQUIRED_MEMORY_SEND_TO_GPU_PER_TICK * 60 / 1_000_000; const REQUIRED_VRAM: usize = REQUIRED_DRAW_SLOTS * (2 * 3 * (2 + 1)); const REQUIRED_VRAM_MB: usize = REQUIRED_VRAM / 1_000_000; -const OVERDRAW_RATIO: usize = MAX_BASE_SIZE / AVG_TRIP_LENGTH; use log::info; use crate::{ - frontend::world::{tile::World, Position}, - item::{usize_from, IdxTrait, Item, WeakIdxTrait, ITEMCOUNTTYPE}, + frontend::world::{Position, tile::World}, + item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait, usize_from}, network_graph::{Network, WeakIndex}, power::{Joule, Watt}, rendering::app_state::SimulationState, @@ -310,7 +316,7 @@ struct MultiStorageInfo {} enum BotUpdate { DoGoal(BotGoal), - // TODO: To allow removing of robotports I will need generational indices! + // TODO: To allow removing of roboports I will need generational indices! TryStartChargingAndContinue(u32, (f32, f32), BotGoal), TryStartChargingAndEnter(u32, Joule, (f32, f32)), Enter(u32), @@ -318,7 +324,7 @@ enum BotUpdate { #[derive(Debug)] enum BotGoal { - DepositItem { + TryDepositItem { current_charge: Joule, item: Item, amount: ITEMCOUNTTYPE, @@ -340,7 +346,7 @@ const MAX_ROBOPORT_CHARGE_RATE: Watt = Watt(2_000_000); const BOT_CHARGING_RATE: Watt = Watt(500_000); const BOT_MAX_CHARGE: Joule = Joule(3_000_000); -const LOGIBOT_SPEED_CHARGED: f32 = 0.05; +const LOGIBOT_SPEED_CHARGED: f32 = 3.0 / 60.0; const LOGIBOT_SPEED_EMPTY: f32 = LOGIBOT_SPEED_CHARGED / 5.0; const LOGIBOT_POWER_CONSUMPTION: Watt = Watt(63_750); @@ -462,7 +468,7 @@ impl BotNetwork BotNetwork, id: u16) -> (f32, f32) { todo!() } + // TODO: Handle Logibot types fn get_closest_roboport_with_free_slot_and_reserve_it( &mut self, pos: (f32, f32), - ) -> Option { + ) -> Option<(u32, (f32, f32))> { todo!() } - fn get_closest_roboport_with_idle_bot_and_take_it(&mut self, pos: (f32, f32)) -> Option { + // TODO: Handle Logibot types + fn get_closest_roboport_with_idle_bot_and_take_it( + &mut self, + pos: (f32, f32), + ) -> Option<(u32, (f32, f32))> { todo!() } // This is effectively a lookup into a voronoi texture! + // Except it is not, since this would lead to all robots flying to the same roboport :/ fn get_closest_roboport(&self, pos: (f32, f32)) -> u32 { todo!() } @@ -627,8 +640,9 @@ impl BotNetwork impl IntoIterator, ITEMCOUNTTYPE, u16, u16)> { - vec![todo!()] + ) -> impl Iterator, ITEMCOUNTTYPE, u16, u16)> + + use { + vec![todo!()].into_iter() } fn add_requester_chest( @@ -653,6 +667,10 @@ impl BotNetwork BotNetwork, - ) -> impl IntoIterator { + ) -> impl Iterator + use { let mut render_infos = vec![]; // Handle logibots, which are now done with their job @@ -722,7 +740,7 @@ impl BotNetwork BotNetwork { - let robotport_pos = self.get_roboport_position(roboport_id); + Some((roboport_id, robotport_pos)) => { let (arrival_time, remaining_charge) = Self::path_result_keep_going_when_empty( current_bot_pos, @@ -765,12 +782,26 @@ impl BotNetwork { match self.try_find_storage_to_get_rid_of_and_reserve(item, amount) { Ok(id) => { - let ((time, update), render) = self.go_deposit(current_bot_pos, current_charge, LOGIBOT_SPEED_CHARGED, LOGIBOT_SPEED_EMPTY, LOGIBOT_POWER_CONSUMPTION, item, amount, id); + let ((time, update), render) = self.go_deposit( + current_bot_pos, + current_charge, + LOGIBOT_SPEED_CHARGED, + LOGIBOT_SPEED_EMPTY, + LOGIBOT_POWER_CONSUMPTION, + item, + amount, + id, + ); - self.bot_jobs.entry(self.current_tick + time).or_default().push(update); + self.bot_jobs + .entry(self.current_tick + time) + .or_default() + .push(update); render_infos.push(render); }, - Err(()) => todo!("Handle not having anywhere to dump items (i.e.) all storage chests are full"), + Err(()) => todo!( + "Handle not having anywhere to dump items (i.e.) all storage chests are full" + ), } }, } @@ -797,7 +828,7 @@ impl BotNetwork BotNetwork BotNetwork BotNetwork { + // TODO: Handle the case where the roboport was removed? + // Or do I want to loop through all jobs to avoid having to check on the hot path? let charge_needed = BOT_MAX_CHARGE - current_charge; if self.roboports[idx as usize].current_charge < charge_needed { // Requeue charging later @@ -952,7 +987,7 @@ impl BotNetwork BotNetwork { + // FIXME: This will break if an inserter fills up a roboport before the bot arrives and will panic!! self.roboports[idx as usize].logibots_idle = self.roboports[idx as usize] .logibots_idle .checked_add(1) @@ -1014,8 +1050,7 @@ impl BotNetwork BotNetwork BotNetwork, world: &mut World, ) -> ( - impl IntoIterator, - impl IntoIterator, + impl IntoIterator + use, + impl IntoIterator + use, ) { // self.bot_jobs. (vec![todo!()], vec![todo!()]) } + // FIXME: This assumes all roboports are connected to a single power network. + // TODO: This might be a case where the single tick delay of the power grid power_mult could lead to problems fn update_roboports(&mut self, last_power_mult: u8) -> Joule { let mut power_needed = Joule(0); diff --git a/src/chest.rs b/src/chest.rs index d13e598..15383b9 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -1,15 +1,19 @@ +use std::cmp::max; use std::{cmp::min, u8}; +use rayon::iter::IndexedParallelIterator; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use crate::{ data::DataStore, - item::{usize_from, IdxTrait, Item, WeakIdxTrait, ITEMCOUNTTYPE}, + item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait, usize_from}, }; const CHEST_GOAL_AMOUNT: ITEMCOUNTTYPE = ITEMCOUNTTYPE::MAX / 2; -const MAX_INSERT_AMOUNT: &'static [ITEMCOUNTTYPE] = &[ITEMCOUNTTYPE::MAX; 10_000_000]; +// TODO: Add specilised chests for different sizes +pub type ChestSize = u32; +pub type SignedChestSize = i32; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct FullChestStore { @@ -17,32 +21,43 @@ pub struct FullChestStore { } impl FullChestStore { - pub fn update(&mut self) { - self.stores.par_iter_mut().for_each(|store| store.update()); + #[profiling::function] + pub fn update( + &mut self, + data_store: &DataStore, + ) { + self.stores + .par_iter_mut() + .enumerate() + .for_each(|(item_id, store)| { + profiling::scope!( + "Chest Update", + format!("Item: {}", data_store.item_display_names[item_id]).as_str() + ); + store.update_simd() + }); } } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct MultiChestStore { item: Item, - max_insert: Option>, + max_insert: Vec, pub inout: Vec, - storage: Vec, + storage: Vec, // TODO: Any way to not have to store this a billion times? - max_items: Vec, + max_items: Vec, holes: Vec, } impl MultiChestStore { - pub fn new( - item: Item, - data_store: &DataStore, - ) -> Self { + #[must_use] + pub fn new(item: Item) -> Self { Self { item, inout: vec![], storage: vec![], - max_insert: None, + max_insert: vec![], max_items: vec![], holes: vec![], } @@ -51,62 +66,72 @@ impl MultiChestStore { pub fn add_chest( &mut self, ty: u8, + slot_limit: u8, data_store: &DataStore, - ) -> usize { + ) -> u32 { let stack_size = data_store.item_stack_sizes[usize_from(self.item.id)]; - let num_stacks = data_store.chest_num_slots[usize::from(ty)]; - let max_items = u16::from(stack_size) * u16::from(num_stacks); + assert!(slot_limit <= data_store.chest_num_slots[usize::from(ty)]); + let num_stacks = slot_limit; + let max_items = ChestSize::from(stack_size) * ChestSize::from(num_stacks); - if max_items < u16::from(ITEMCOUNTTYPE::MAX) { - if self.max_insert.is_none() { - self.max_insert = Some(vec![ITEMCOUNTTYPE::MAX; self.max_items.len()]); - } + if let Some(hole) = self.holes.pop() { + self.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)); + hole.try_into().unwrap() + } else { + self.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.inout.len() - 1).try_into().unwrap() } + } + pub fn add_custom_chest(&mut self, max_items: ChestSize) -> u32 { if let Some(hole) = self.holes.pop() { self.inout[hole] = 0; self.storage[hole] = 0; - if let Ok(max_insert) = max_items.try_into() { - self.max_insert.as_mut().unwrap()[hole] = max_insert; - } + self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); - self.max_items[hole] = max_items.saturating_sub(u16::from(ITEMCOUNTTYPE::MAX)); - hole + self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + hole.try_into().unwrap() } else { self.inout.push(0); self.storage.push(0); - if let Ok(max_insert) = max_items.try_into() { - self.max_insert.as_mut().unwrap().push(max_insert); - } + self.max_insert + .push(max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX)); self.max_items - .push(max_items.saturating_sub(u16::from(ITEMCOUNTTYPE::MAX))); - self.inout.len() - 1 + .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); + (self.inout.len() - 1).try_into().unwrap() } } - pub fn remove_chest( - &mut self, - index: usize, - data_store: &DataStore, - ) -> u16 { + pub fn remove_chest(&mut self, index: u32) -> ChestSize { + let index = index as usize; self.holes.push(index); - let items = self.inout[index] as u16 + self.storage[index]; + let items = ChestSize::from(self.inout[index]) + self.storage[index]; self.inout[index] = 0; self.storage[index] = 0; - self.storage[index] = 0; + self.max_items[index] = 0; items } - pub fn get_chest(&self, index: usize) -> (u16, u16) { + pub fn get_chest(&self, index: u32) -> (ChestSize, ChestSize) { ( - self.storage[index] + u16::from(self.inout[index]), - self.max_items[index], + self.storage[index as usize] + ChestSize::from(self.inout[index as usize]), + self.max_items[index as usize] + ChestSize::from(self.max_insert[index as usize]), ) } - pub fn update(&mut self) { + pub fn update_naive(&mut self) { for (inout, (storage, max_items)) in self .inout .iter_mut() @@ -115,30 +140,215 @@ impl MultiChestStore { let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); if *inout >= CHEST_GOAL_AMOUNT { - let moved: ITEMCOUNTTYPE = min(to_move as u16, max_items - *storage) + let moved: ITEMCOUNTTYPE = min(ChestSize::from(to_move), max_items - *storage) .try_into() .expect("since to_move was a ITEMCOUNTTYPE, this always fits"); *inout -= moved; - *storage += moved as u16; + *storage += ChestSize::from(to_move); debug_assert!(*storage <= max_items); } else { - let moved: ITEMCOUNTTYPE = min(to_move as u16, *storage) + let moved: ITEMCOUNTTYPE = min(ChestSize::from(to_move), *storage) .try_into() .expect("since to_move was a ITEMCOUNTTYPE, this always fits"); *inout += moved; - *storage -= moved as u16; + *storage -= ChestSize::from(to_move); } } } + pub fn update_simd(&mut self) { + 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; + + debug_assert!(*storage <= max_items); + } + } + + pub fn add_items_to_chest( + &mut self, + index: u32, + new_items: ChestSize, + ) -> Result<(), ChestSize> { + let index = index as usize; + + let storage_size = self.max_items[index] + ChestSize::from(self.max_insert[index]); + let current_items = self.inout[index] as ChestSize + self.storage[index]; + + if current_items + new_items > storage_size { + let not_inserted = (current_items + new_items) - storage_size; + self.storage[index] = self.max_items[index]; + self.inout[index] = self.max_insert[index]; + + Err(not_inserted) + } else { + let free_in_storage = self.max_items[index] - self.storage[index]; + + if new_items > free_in_storage { + self.storage[index] = self.max_items[index]; + self.inout[index] = self.inout[index] + .checked_add(u8::try_from(new_items - free_in_storage).unwrap()) + .unwrap(); + assert!(self.inout[index] <= self.max_insert[index]); + } else { + self.storage[index] += new_items; + } + + Ok(()) + } + } + + pub fn remove_items_from_chest( + &mut self, + index: u32, + to_remove: ChestSize, + ) -> Result<(), ChestSize> { + let index = index as usize; + + let current_items = self.inout[index] as ChestSize + self.storage[index]; + + if current_items >= to_remove { + if self.storage[index] >= to_remove { + self.storage[index] -= to_remove; + } else { + self.inout[index] -= u8::try_from(to_remove - self.storage[index]).unwrap(); + self.storage[index] = 0; + } + + Ok(()) + } else { + let missing = to_remove - current_items; + self.storage[index] = 0; + self.inout[index] = 0; + + Err(missing) + } + } + + /// Returns the number of items no longer part of the box + pub fn change_chest_size(&mut self, index: u32, new_size: ChestSize) -> ChestSize { + let index = index as usize; + + let removed_items = + if new_size < max(self.max_items[index], self.max_insert[index] as ChestSize) { + let current_items = self.inout[index] as ChestSize + self.storage[index]; + + if current_items > new_size { + let items_to_remove = current_items - new_size; + + if self.storage[index] >= items_to_remove { + self.storage[index] -= items_to_remove; + } else { + self.inout[index] = self.inout[index] + .checked_sub( + items_to_remove + .checked_sub(self.storage[index]) + .unwrap() + .try_into() + .unwrap(), + ) + .unwrap(); + self.storage[index] = 0; + } + + items_to_remove + } else { + 0 + } + } else { + 0 + }; + + self.max_items[index] = new_size.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + self.max_insert[index] = new_size.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); + + removed_items + } + pub fn storage_list_slices(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { - ( - self.max_insert - .as_ref() - .map(|v| v.as_slice()) - .unwrap_or(MAX_INSERT_AMOUNT), - self.inout.as_mut_slice(), - ) + (self.max_insert.as_slice(), self.inout.as_mut_slice()) + } +} + +#[cfg(test)] +mod test { + use std::cmp::min; + + use proptest::prelude::Just; + use proptest::prelude::Strategy; + use proptest::prop_assert_eq; + use proptest::proptest; + + use crate::chest::CHEST_GOAL_AMOUNT; + use crate::chest::ChestSize; + use crate::chest::ITEMCOUNTTYPE; + use crate::chest::SignedChestSize; + + fn max_items_and_storage() -> impl Strategy { + (0..ChestSize::MAX).prop_flat_map(|max_items| (Just(max_items), 0..max_items)) + } + + proptest! { + + + #[test] + fn simd_always_same_as_naive(inout in 0..ITEMCOUNTTYPE::MAX, (max_items, storage) in max_items_and_storage()) { + let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); + + let mut storage_naive = storage; + let mut inout_naive = inout; + + if inout_naive >= CHEST_GOAL_AMOUNT { + let moved: ITEMCOUNTTYPE = min(ChestSize::from(to_move), max_items - storage) + .try_into() + .expect("since to_move was a ITEMCOUNTTYPE, this always fits"); + inout_naive -= moved; + storage_naive += ChestSize::from(moved); + + debug_assert!(storage_naive <= max_items); + } else { + let moved: ITEMCOUNTTYPE = min(ChestSize::from(to_move), storage) + .try_into() + .expect("since to_move was a ITEMCOUNTTYPE, this always fits"); + inout_naive += moved; + storage_naive -= ChestSize::from(moved); + } + + let mut storage_simd = storage; + let mut inout_simd = inout; + + let switch = ChestSize::from(inout_simd >= CHEST_GOAL_AMOUNT); + + let moved: SignedChestSize = (switch as SignedChestSize + (1 - switch as SignedChestSize) * -1) + * (min( + to_move as ChestSize, + (max_items - storage_simd) * switch + (1 - switch) * storage_simd, + ) as SignedChestSize); + + inout_simd = (inout_simd as ChestSize).checked_sub_signed(moved).unwrap() as u8; + storage_simd = (storage_simd as ChestSize).checked_add_signed(moved).unwrap(); + + debug_assert!(storage_simd <= max_items); + + prop_assert_eq!(inout_naive, inout_simd, "inout"); + prop_assert_eq!(storage_naive, storage_simd, "storage"); + } + } } diff --git a/src/data.rs b/src/data.rs deleted file mode 100644 index f68ee39..0000000 --- a/src/data.rs +++ /dev/null @@ -1,1127 +0,0 @@ -use std::{array, collections::HashMap}; - -use eframe::egui::Color32; -use itertools::Itertools; -use log::warn; -use rand::random; -use sha2::{Digest, Sha256}; -use strum::IntoEnumIterator; - -use crate::{ - assembler::TIMERTYPE, - frontend::world::tile::{AssemblerID, Dir}, - inserter::{StaticID, Storage}, - item::{IdxTrait, Item, Recipe, WeakIdxTrait, ITEMCOUNTTYPE}, - power::{power_grid::PowerGridIdentifier, Joule, Watt}, -}; - -type ItemString = String; -type AssemblingMachineString = String; -type InserterString = String; -type EngineString = String; - -#[must_use] -pub fn get_raw_data_test() -> RawDataStore { - RawDataStore { - recipes: vec![ - RawRecipeData { - name: "factory_game::iron_ore_generation".to_string(), - display_name: "Generate Iron Ore from nothing".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![].into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::iron_ore".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 60, - is_intermediate: false, - }, - RawRecipeData { - name: "factory_game::copper_ore_generation".to_string(), - display_name: "Generate Copper Ore from nothing".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![].into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::copper_ore".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 60, - is_intermediate: false, - }, - RawRecipeData { - name: "factory_game::iron_smelting".to_string(), - display_name: "Smelt Iron Ore into Iron Plates".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![RawItemStack { - item: "factory_game::iron_ore".to_string(), - amount: 1, - }] - .into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::iron_plate".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 300, - is_intermediate: true, - }, - RawRecipeData { - name: "factory_game::copper_smelting".to_string(), - display_name: "Smelt Copper Ore into Copper Plates".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![RawItemStack { - item: "factory_game::copper_ore".to_string(), - amount: 1, - }] - .into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::copper_plate".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 300, - is_intermediate: true, - }, - RawRecipeData { - name: "factory_game::gears".to_string(), - display_name: "Gears".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![RawItemStack { - item: "factory_game::iron_plate".to_string(), - amount: 2, - }] - .into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::gear".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 600, - - is_intermediate: true, - }, - RawRecipeData { - name: "factory_game::red_science".to_string(), - display_name: "Automation Science".to_string(), - possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), - ings: vec![ - RawItemStack { - item: "factory_game::gear".to_string(), - amount: 1, - }, - RawItemStack { - item: "factory_game::copper_plate".to_string(), - amount: 1, - }, - ] - .into_boxed_slice(), - output: vec![RawItemStack { - item: "factory_game::red_science".to_string(), - amount: 1, - }] - .into_boxed_slice(), - time_to_craft: 300, - - is_intermediate: true, - }, - ], - items: vec![ - RawItem { - name: "factory_game::iron_ore".to_string(), - display_name: "Iron Ore".to_string(), - stack_size: 100, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: None, - is_fluid: false, - }, - RawItem { - name: "factory_game::copper_ore".to_string(), - display_name: "Copper Ore".to_string(), - stack_size: 100, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: None, - is_fluid: false, - }, - RawItem { - name: "factory_game::iron_plate".to_string(), - display_name: "Iron Plate".to_string(), - stack_size: 100, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: None, - is_fluid: false, - }, - RawItem { - name: "factory_game::copper_plate".to_string(), - display_name: "Copper Plate".to_string(), - stack_size: 100, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: None, - is_fluid: false, - }, - RawItem { - name: "factory_game::gear".to_string(), - display_name: "Gear".to_string(), - stack_size: 50, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: None, - is_fluid: false, - }, - RawItem { - name: "factory_game::red_science".to_string(), - display_name: "Automation Science Pack".to_string(), - stack_size: 200, - placed_as: None, - burnable_in: vec![].into_boxed_slice(), - science_data: Some(()), - is_fluid: false, - }, - ], - machines: vec![RawAssemblingMachine { - name: "factory_game::assembler2".to_string(), - display_name: "Assembling Machine 2".to_string(), - tile_size: (3, 3), - working_power_draw: Watt(150_000), - fluid_connection_offsets: vec![], - base_bonus_prod: 0, - base_speed: 15, - num_module_slots: 2, - }], - miners: vec![], - power_poles: vec![ - RawPowerPole { - name: "factory_game::small_power_pole".to_string(), - display_name: "Small Power Pole".to_string(), - tile_size: (1, 1), - power_range: 2, - // TODO: - connection_range: 7, - }, - RawPowerPole { - name: "factory_game::medium_power_pole".to_string(), - display_name: "Medium Power Pole".to_string(), - tile_size: (1, 1), - power_range: 3, - // TODO: - connection_range: 9, - }, - RawPowerPole { - name: "factory_game::large_power_pole".to_string(), - display_name: "Large Power Pole".to_string(), - tile_size: (2, 2), - power_range: 1, - // TODO: - connection_range: 32, - }, - RawPowerPole { - name: "factory_game::substation".to_string(), - display_name: "Substation".to_string(), - tile_size: (2, 2), - power_range: 8, - // TODO: - connection_range: 16, - }, - ], - modules: vec![ - RawModule { - name: "factory_game::prod_mod".to_string(), - display_name: "Productivity Module".to_string(), - item: "factory_game::prod_mod".to_string(), - productivity_effect: 10, - speed_effect: -2, - power_effect: 8, - allowed_in: AllowedIn::OnlyIntermediate, - }, - RawModule { - name: "factory_game::speed_mod".to_string(), - display_name: "Speed Module".to_string(), - item: "factory_game::speed_mod".to_string(), - productivity_effect: 0, - speed_effect: 5, - power_effect: 7, - allowed_in: AllowedIn::AllIncludingBeacons, - }, - ], - - chests: vec![ - RawChest { - name: "factory_game::wooden_chest".to_string(), - display_name: "Wooden Chest".to_string(), - tile_size: (1, 1), - number_of_slots: 16, - }, - RawChest { - name: "factory_game::iron_chest".to_string(), - display_name: "Iron Chest".to_string(), - tile_size: (1, 1), - number_of_slots: 32, - }, - RawChest { - name: "factory_game::steel_chest".to_string(), - display_name: "Steel Chest".to_string(), - tile_size: (1, 1), - number_of_slots: 48, - }, - ], - technologies: vec![RawTechnology { - name: "factory_game::automation".to_string(), - display_name: "Automation".to_string(), - cost_of_single_research_unit: vec![RawItemStack { - item: "factory_game::red_science".to_string(), - amount: 1, - }], - num_units: 10, - precursors: vec![], - }], - } -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct RawRecipeData { - /// The fully qualified name of the recipe - name: String, - display_name: String, - possible_machines: Box<[AssemblingMachineString]>, - ings: Box<[RawItemStack]>, - output: Box<[RawItemStack]>, - time_to_craft: TIMERTYPE, - is_intermediate: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawItemStack { - item: ItemString, - amount: ITEMCOUNTTYPE, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawChest { - name: String, - display_name: String, - tile_size: (u8, u8), - number_of_slots: u8, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawAssemblingMachine { - name: String, - display_name: String, - tile_size: (u8, u8), - working_power_draw: Watt, - fluid_connection_offsets: Vec, - - num_module_slots: u8, - - /// Base bonus productivity in % - base_bonus_prod: u8, - /// Speed multiplier compared to "baseline" in 5% - base_speed: u8, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawFluidConnection { - fluid_dir: ItemRecipeDir, - offs: (u8, u8), - pipe_connection_direction: Dir, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawInserter { - name: String, - display_name: String, - time_per_trip: TIMERTYPE, - handsize: ITEMCOUNTTYPE, - pickup_offs: (i8, i8), - dropoff_offs: (i8, i8), -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct RawDataStore { - recipes: Vec, - items: Vec, - machines: Vec, - miners: Vec, - power_poles: Vec, - modules: Vec, - chests: Vec, - technologies: Vec, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawTechnology { - name: String, - display_name: String, - cost_of_single_research_unit: Vec, - num_units: u64, - precursors: Vec, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawModule { - name: String, - display_name: String, - item: String, - // TODO: Document the units of these modifiers - // TODO: Maybe in percent? - productivity_effect: i8, - speed_effect: i8, - power_effect: i8, - allowed_in: AllowedIn, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum AllowedIn { - AllIncludingBeacons, - AllNoBeacons, - OnlyIntermediate, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawPowerPole { - name: String, - display_name: String, - tile_size: (u8, u8), - power_range: u8, - connection_range: u8, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawMiner { - name: String, - display_name: String, - timer: TIMERTYPE, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct RawItem { - name: String, - display_name: String, - stack_size: ITEMCOUNTTYPE, - placed_as: Option, - burnable_in: Box<[EngineString]>, - science_data: Option<()>, - is_fluid: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum RawEntity { - AssemblingMachine(AssemblingMachineString), - Inserter(InserterString), - Belt(()), -} - -#[derive(Debug, Clone)] -pub struct AssemblerInfo { - pub size: (u8, u8), - pub num_module_slots: u8, - - /// Base Speed as a numerator and divisor. All module modifiers apply multiplicatively - pub base_speed: u8, - pub base_prod: u8, - pub base_power_consumption: Watt, -} - -#[derive(Debug, Clone)] -pub struct ModuleInfo { - pub name: String, - - pub speed_mod: i8, - pub prod_mod: i8, - pub power_mod: i8, -} - -#[derive(Debug, Clone)] -pub struct DataStore { - pub checksum: String, - - pub max_entity_size: (usize, usize), - - pub assembler_info: Vec, - pub module_info: Vec, - - /// In 5% steps - pub min_power_mod: u8, - - pub recipe_names: Vec, - - pub recipe_num_ing_lookup: Vec, - pub recipe_num_out_lookup: Vec, - pub recipe_to_ing_out_combo_idx: Vec, - pub ing_out_num_to_recipe: HashMap<(usize, usize), Vec>>, - - pub recipe_item_store_to_item: Vec>, - - recipe_item_to_storage_list_idx: - HashMap<(Recipe, Item, ItemRecipeDir), u16>, - - /// A lookup from recipe to its ing and out idxs - pub recipe_index_lookups: Vec<(usize, usize)>, - /// A lookup from recipe_ing_idx to its ingredient counts - pub recipe_ings: RecipeIngLookups, - /// A lookup from recipe_out_idx to its output counts - pub recipe_outputs: RecipeOutputLookups, - - pub recipe_timers: Box<[TIMERTYPE]>, - - pub recipe_to_items: HashMap, Vec<(ItemRecipeDir, Item)>>, - - pub science_bottle_items: Vec>, - - pub item_to_recipe_count_where_its_ingredient: Vec, ITEMCOUNTTYPE)>>, - pub item_to_recipe_where_its_output: Vec, ITEMCOUNTTYPE)>>, - - pub lazy_power_machine_infos: Vec>, - - pub item_names: Vec, - - pub item_is_fluid: Vec, - - pub power_pole_data: Vec, - - pub max_power_search_range: u8, - - pub max_inserter_search_range: u8, - - pub num_different_static_containers: usize, - - /// use Item to index, gives how many recipes have this item as an ingredient - pub num_recipes_with_item: Vec, - - pub lab_infos: Vec<()>, - pub item_is_science: Vec, - - pub item_stack_sizes: Vec, - pub chest_num_slots: Vec, - pub chest_tile_sizes: Vec<(u8, u8)>, - - pub recipe_to_translated_index: - HashMap<(Recipe, Item), RecipeIdxType>, - - pub item_to_colour: Vec, - - pub technology_costs: Vec<(u64, Box<[ITEMCOUNTTYPE]>)>, -} - -#[derive(Debug, Clone)] -pub struct LazyPowerMachineInfo { - pub ingredient: Item, - pub power_per_item: Joule, - pub max_power_per_tick: Joule, -} - -#[derive(Debug, Clone)] -pub struct PowerPoleData { - pub size: (u8, u8), - pub power_range: u8, - pub connection_range: u8, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Deserialize, serde::Serialize)] -pub enum ItemRecipeDir { - Ing, - Out, -} - -#[derive(Debug)] -pub enum DataStoreOptions { - ItemU8RecipeU8(DataStore), - ItemU8RecipeU16(DataStore), - ItemU16RecipeU8(DataStore), - ItemU16RecipeU16(DataStore), -} - -impl DataStoreOptions { - pub fn assume_simple(self) -> DataStore { - match self { - DataStoreOptions::ItemU8RecipeU8(data_store) => data_store, - _ => unreachable!(), - } - } -} - -struct RecipeIndexLookup { - ing: usize, - out: usize, -} - -#[derive(Debug, Clone)] -pub struct RecipeIngLookups { - pub ing0: Vec<[ITEMCOUNTTYPE; 0]>, - pub ing1: Vec<[ITEMCOUNTTYPE; 1]>, - pub ing2: Vec<[ITEMCOUNTTYPE; 2]>, - pub ing3: Vec<[ITEMCOUNTTYPE; 3]>, - pub ing4: Vec<[ITEMCOUNTTYPE; 4]>, -} - -#[derive(Debug, Clone)] -pub struct RecipeOutputLookups { - pub out1: Vec<[ITEMCOUNTTYPE; 1]>, - pub out2: Vec<[ITEMCOUNTTYPE; 2]>, - pub out3: Vec<[ITEMCOUNTTYPE; 3]>, - pub out4: Vec<[ITEMCOUNTTYPE; 4]>, -} - -impl RawDataStore { - #[must_use] - pub fn process(self) -> DataStoreOptions { - match (self.items.len(), self.recipes.len()) { - (items, recipes) if items <= u8::MAX.into() && recipes <= u8::MAX.into() => { - DataStoreOptions::ItemU8RecipeU8(self.turn::()) - }, - (items, recipes) if items <= u8::MAX.into() && recipes <= u16::MAX.into() => { - DataStoreOptions::ItemU8RecipeU16(self.turn::()) - }, - (items, recipes) if items <= u16::MAX.into() && recipes <= u8::MAX.into() => { - DataStoreOptions::ItemU16RecipeU8(self.turn::()) - }, - (items, recipes) if items <= u16::MAX.into() && recipes <= u16::MAX.into() => { - DataStoreOptions::ItemU16RecipeU16(self.turn::()) - }, - - _ => unimplemented!("Too many items or recipes, u16::MAX is the max allowed amount (currently) (Btw, are you joking? Who are you trying to torture with this many options? xD)"), - } - } - - #[allow(clippy::too_many_lines)] - pub fn turn( - self, - ) -> DataStore { - let checksum = self.get_checksum(); - warn!("Parsing game data with checksum {}", checksum); - - // TODO: Stop cloning the item names - let item_names = self.items.iter().map(|i| i.display_name.clone()).collect(); - - let mut ing_out_num_to_recipe: HashMap<(usize, usize), Vec>> = - HashMap::new(); - let mut recipe_to_ing_out_combo_idx = vec![]; - - let item_lookup: HashMap<&str, ItemIdxType> = self - .items - .iter() - .map(|i| i.name.as_str()) - .enumerate() - .map(|(i, v)| { - ( - v, - i.try_into() - .unwrap_or_else(|_| panic!("item idx did not fit!!!")), - ) - }) - .collect(); - - let reverse_item_lookup: HashMap = - item_lookup.iter().map(|(k, v)| (*v, *k)).collect(); - - let science_bottle_items: Vec> = self - .items - .iter() - .filter(|i| i.science_data.is_some()) - .map(|r| { - item_lookup - .get(r.name.as_str()) - .expect("Science Item not in lookup?!?") - }) - .map(|idx| Item { id: *idx }) - .collect(); - - let item_to_recipe_where_its_ingredient: Vec> = self - .items - .iter() - .map(|item| { - self.recipes - .iter() - .enumerate() - .filter_map(|(recipe_idx, r)| { - r.ings - .iter() - .find(|stack| stack.item == item.name) - .map(|found_stack| { - ( - Recipe { - id: recipe_idx.try_into().unwrap_or_else(|_| todo!()), - }, - found_stack.amount, - ) - }) - }) - .collect() - }) - .collect(); - - let item_to_recipe_where_its_output: Vec> = - self.items - .iter() - .map(|item| { - self.recipes - .iter() - .enumerate() - .filter_map(|(recipe_idx, r)| { - r.output.iter().find(|stack| stack.item == item.name).map( - |found_stack| { - ( - Recipe { - id: recipe_idx.try_into().unwrap_or_else(|_| todo!()), - }, - found_stack.amount, - ) - }, - ) - }) - .collect() - }) - .collect(); - - for (i, raw_recipe) in self.recipes.iter().enumerate() { - let recipe = Recipe { - id: RecipeIdxType::try_from(i) - .unwrap_or_else(|_| panic!("recipe idx did not fit!!!")), - }; - - let prev_list = - ing_out_num_to_recipe.get_mut(&(raw_recipe.ings.len(), raw_recipe.output.len())); - - if let Some(prev) = prev_list { - recipe_to_ing_out_combo_idx.push(prev.len()); - prev.push(recipe); - } else { - recipe_to_ing_out_combo_idx.push(0); - ing_out_num_to_recipe.insert( - (raw_recipe.ings.len(), raw_recipe.output.len()), - vec![recipe], - ); - } - } - - let mut recipe_item_store_to_item = vec![]; - - let mut recipe_item_to_storage_list_idx = HashMap::new(); - - for num_ings in 0..10 { - for num_out in 1..10 { - if let Some(recipes) = ing_out_num_to_recipe.get(&(num_ings, num_out)) { - for recipe in recipes { - let recipe_id: usize = recipe.id.into(); - for ing in &self.recipes[recipe_id].ings { - let item = *item_lookup.get(ing.item.as_str()).unwrap_or_else(|| { - panic!("Item does not exist: {}", ing.item.as_str()) - }); - - recipe_item_to_storage_list_idx.insert( - (*recipe, Item { id: item }, ItemRecipeDir::Ing), - recipe_item_store_to_item - .iter() - .filter(|i| **i == Item { id: item }) - .count() - .try_into() - .unwrap_or_else(|_| panic!("Too many storage type with item")), - ); - recipe_item_store_to_item.push(Item { id: item }); - } - for output in &self.recipes[recipe_id].output { - let item = - *item_lookup.get(output.item.as_str()).unwrap_or_else(|| { - panic!("Item does not exist: {}", output.item.as_str()) - }); - - recipe_item_to_storage_list_idx.insert( - (*recipe, Item { id: item }, ItemRecipeDir::Out), - recipe_item_store_to_item - .iter() - .filter(|i| **i == Item { id: item }) - .count() - .try_into() - .unwrap_or_else(|_| panic!("Too many storage type with item")), - ); - recipe_item_store_to_item.push(Item { id: item }); - } - } - } - } - } - - let recipe_num_ing_lookup = self.recipes.iter().map(|r| r.ings.len()).collect(); - let recipe_num_out_lookup = self.recipes.iter().map(|r| r.output.len()).collect(); - - let mut recipe_index_lookups = vec![]; - - let mut recipe_ings = RecipeIngLookups { - ing0: vec![], - ing1: vec![], - ing2: vec![], - ing3: vec![], - ing4: vec![], - }; - - let mut recipe_outputs = RecipeOutputLookups { - out1: vec![], - out2: vec![], - out3: vec![], - out4: vec![], - }; - - for (i, recipe) in self.recipes.iter().enumerate() { - let ing_idx = match recipe.ings.len() { - 0 => { - recipe_ings - .ing0 - .push(array::from_fn(|i| recipe.ings[i].amount)); - recipe_ings.ing0.len() - }, - 1 => { - recipe_ings - .ing1 - .push(array::from_fn(|i| recipe.ings[i].amount)); - recipe_ings.ing1.len() - }, - 2 => { - recipe_ings - .ing2 - .push(array::from_fn(|i| recipe.ings[i].amount)); - recipe_ings.ing2.len() - }, - 3 => { - recipe_ings - .ing3 - .push(array::from_fn(|i| recipe.ings[i].amount)); - recipe_ings.ing3.len() - }, - 4 => { - recipe_ings - .ing4 - .push(array::from_fn(|i| recipe.ings[i].amount)); - recipe_ings.ing4.len() - }, - n => { - unimplemented!("{n} ingredients in a single recipe are currently unsupported!!") - }, - } - 1; - - let out_idx = match recipe.output.len() { - 0 => unimplemented!("Recipes without outputs are currently not supported!"), - 1 => { - recipe_outputs - .out1 - .push(array::from_fn(|i| recipe.output[i].amount)); - recipe_outputs.out1.len() - }, - 2 => { - recipe_outputs - .out2 - .push(array::from_fn(|i| recipe.output[i].amount)); - recipe_outputs.out2.len() - }, - 3 => { - recipe_outputs - .out3 - .push(array::from_fn(|i| recipe.output[i].amount)); - recipe_outputs.out3.len() - }, - 4 => { - recipe_outputs - .out4 - .push(array::from_fn(|i| recipe.output[i].amount)); - recipe_outputs.out4.len() - }, - n => { - unimplemented!("{n} outputs in a single recipe are currently unsupported!!") - }, - } - 1; - - recipe_index_lookups.push((ing_idx, out_idx)); - } - - let recipe_timers = self.recipes.iter().map(|r| r.time_to_craft).collect(); - - let recipe_to_items = self - .recipes - .iter() - .enumerate() - .map(|(i, r)| { - let mut v = vec![]; - - for ing in &r.ings { - v.push(( - ItemRecipeDir::Ing, - Item::from(*item_lookup.get(ing.item.as_str()).unwrap()), - )); - } - - for out in &r.output { - v.push(( - ItemRecipeDir::Out, - Item::from(*item_lookup.get(out.item.as_str()).unwrap()), - )); - } - - ( - Recipe { - id: RecipeIdxType::try_from(i).unwrap_or_else(|_| panic!()), - }, - v, - ) - }) - .collect(); - - let power_pole_data = self - .power_poles - .iter() - .map(|p| PowerPoleData { - size: p.tile_size, - power_range: p.power_range, - connection_range: p.connection_range, - }) - .collect(); - - let num_recipes_with_item = item_to_recipe_where_its_ingredient - .iter() - .map(Vec::len) - .zip(item_to_recipe_where_its_output.iter().map(Vec::len)) - .map(|(ing, out)| ing + out) - .collect(); - - assert!( - self.technologies - .iter() - .flat_map(|tech| tech.cost_of_single_research_unit.iter()) - .all(|stack| science_bottle_items.contains(&Item { - id: *item_lookup - .get(stack.item.as_str()) - .expect("Could not find item") - })), - "Some research item in a technology is not designated as science item" - ); - - let technology_costs = self - .technologies - .iter() - .map(|tech| { - ( - tech.num_units, - science_bottle_items - .iter() - .map(|item| reverse_item_lookup[&item.id]) - .map(|raw_item_name| { - tech.cost_of_single_research_unit - .iter() - .find_map(|stack| { - (stack.item == raw_item_name).then_some(stack.amount) - }) - .unwrap_or(0) - }) - .collect(), - ) - }) - .collect(); - - DataStore { - checksum, - - assembler_info: self - .machines - .iter() - .map(|m| AssemblerInfo { - size: m.tile_size, - num_module_slots: m.num_module_slots, - // TODO: Hm, this seems rather silly - base_speed: m.base_speed, - base_prod: m.base_bonus_prod, - base_power_consumption: m.working_power_draw, - }) - .collect(), - - module_info: self - .modules - .iter() - .map(|module| ModuleInfo { - name: module.display_name.clone(), - speed_mod: module.speed_effect, - prod_mod: module.productivity_effect, - power_mod: module.power_effect, - }) - .collect(), - - min_power_mod: 4, - - // TODO: - max_entity_size: (4, 4), - - recipe_names: self - .recipes - .iter() - .map(|r| r.display_name.clone()) - .collect(), - - recipe_num_ing_lookup, - recipe_num_out_lookup, - recipe_to_ing_out_combo_idx, - ing_out_num_to_recipe, - recipe_item_store_to_item, - recipe_item_to_storage_list_idx, - recipe_index_lookups, - recipe_ings, - - recipe_outputs, - - recipe_timers, - - recipe_to_items, - - science_bottle_items, - - item_to_recipe_count_where_its_ingredient: item_to_recipe_where_its_ingredient, - item_to_recipe_where_its_output, - - item_names, - - power_pole_data, - - item_is_fluid: self.items.iter().map(|i| i.is_fluid).collect(), - - lazy_power_machine_infos: vec![], - - max_power_search_range: self - .power_poles - .iter() - .map(|p| p.power_range) - .max() - .expect("At least one type of power pole must exist"), - - max_inserter_search_range: 2, - - item_is_science: self - .items - .iter() - .map(|i| i.science_data.is_some()) - .collect(), - lab_infos: vec![], - num_different_static_containers: StaticID::iter().count(), - num_recipes_with_item, - - item_stack_sizes: self.items.iter().map(|item| item.stack_size).collect(), - chest_num_slots: self - .chests - .iter() - .map(|chest| chest.number_of_slots) - .collect(), - chest_tile_sizes: self.chests.iter().map(|chest| chest.tile_size).collect(), - - recipe_to_translated_index: (0..self.recipes.len()) - .cartesian_product( - self.items - .iter() - .enumerate() - .map(|(item_id, item_name)| (item_id, &item_name.name)), - ) - .map(|(recipe_id, (item_id, item_name))| { - ( - ( - Recipe { - id: recipe_id.try_into().unwrap(), - }, - Item { - id: item_id.try_into().unwrap(), - }, - ), - self.recipes - .iter() - .take(recipe_id) - .filter(|recipe| { - recipe - .ings - .iter() - .map(|stack| &stack.item) - .any(|item| item == item_name) - | recipe - .output - .iter() - .map(|stack| &stack.item) - .any(|item| item == item_name) - }) - .count() - .try_into() - .unwrap(), - ) - }) - .collect(), - - item_to_colour: self - .items - .iter() - .map(|item_name| Color32::from_rgb(random(), random(), random())) - .collect(), - - technology_costs, - } - } - - pub fn get_checksum(&self) -> String { - let mut hasher = Sha256::new(); - - hasher.update(postcard::to_allocvec(self).unwrap()); - - hex::encode_upper(hasher.finalize()) - } -} - -impl DataStore { - pub fn get_storage_id_for_assembler( - &self, - dir: ItemRecipeDir, - item: Item, - assembler_id: AssemblerID, - ) -> Result, ()> { - let Some(storage_list_idx) = - self.recipe_item_to_storage_list_idx - .get(&(assembler_id.recipe, item, dir)) - else { - return Err(()); - }; - - // Ok(StorageID { - // grid: assembler_id.grid, - // storage_list_idx: *storage_list_idx, - // machine_idx: assembler_id.assembler_index, - - // phantom: PhantomData, - // }) - Ok(Storage::Assembler { - grid: assembler_id.grid, - recipe_idx_with_this_item: assembler_id.recipe.id, - index: assembler_id.assembler_index, - }) - } - - pub fn get_storage_id_for_lab_science( - &self, - grid: PowerGridIdentifier, - lab_idx: u16, - ) -> Storage { - // let num_entries_for_assemblers = self.recipe_item_to_storage_list_idx.len(); - // let science_idx = self - // .science_bottle_items - // .iter() - // .position(|i| *i == item) - // .expect("Science item for lab is not in science list"); - - Storage::Lab { - grid, - index: lab_idx, - } - // StorageID { - // grid, - // storage_list_idx: (num_entries_for_assemblers + science_idx) - // .try_into() - // .expect("More than u16::MAX assemblers"), - // machine_idx: lab_idx, - - // phantom: PhantomData, - // } - } -} diff --git a/src/data/factorio_1_1.fgmod b/src/data/factorio_1_1.fgmod new file mode 100644 index 0000000..51c239a --- /dev/null +++ b/src/data/factorio_1_1.fgmod @@ -0,0 +1,3293 @@ +( + recipes: [ + ( + name: "factory_game::iron_ore_generation", + display_name: "Generate Iron Ore from nothing", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [], + output: [ + ( + item: "factory_game::iron_ore", + amount: 1, + ), + ], + time_to_craft: 1, + is_intermediate: false, + ), + ( + name: "factory_game::copper_ore_generation", + display_name: "Generate Copper Ore from nothing", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [], + output: [ + ( + item: "factory_game::copper_ore", + amount: 1, + ), + ], + time_to_craft: 1, + is_intermediate: false, + ), + ( + name: "factory_game::coal_generation", + display_name: "Generate Coal from nothing", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [], + output: [ + ( + item: "factory_game::coal", + amount: 1, + ), + ], + time_to_craft: 1, + is_intermediate: false, + ), + ( + name: "factory_game::stone_generation", + display_name: "Generate Stone from nothing", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [], + output: [ + ( + item: "factory_game::stone", + amount: 1, + ), + ], + time_to_craft: 1, + is_intermediate: false, + ), + ( + name: "factory_game::iron_smelting", + display_name: "Smelt Iron Ore into Iron Plates", + possible_machines: [ + "factory_game::electric_furnace", + ], + ings: [ + ( + item: "factory_game::iron_ore", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + time_to_craft: 192, + is_intermediate: true, + ), + ( + name: "factory_game::copper_smelting", + display_name: "Smelt Copper Ore into Copper Plates", + possible_machines: [ + "factory_game::electric_furnace", + ], + ings: [ + ( + item: "factory_game::copper_ore", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::copper_plate", + amount: 1, + ), + ], + time_to_craft: 192, + is_intermediate: true, + ), + ( + name: "factory_game::gears", + display_name: "Gears", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::gear", + amount: 1, + ), + ], + time_to_craft: 30, + is_intermediate: true, + ), + ( + name: "factory_game::red_science", + display_name: "Automation Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::gear", + amount: 1, + ), + ( + item: "factory_game::copper_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + time_to_craft: 300, + is_intermediate: true, + ), + ( + name: "factory_game::copper_wire", + display_name: "Copper Wire", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::copper_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::copper_wire", + amount: 2, + ), + ], + time_to_craft: 30, + is_intermediate: true, + ), + ( + name: "factory_game::green_chip", + display_name: "Green Chip", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::copper_wire", + amount: 3, + ), + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::green_chip", + amount: 1, + ), + ], + time_to_craft: 30, + is_intermediate: true, + ), + ( + name: "factory_game::yellow_belt", + display_name: "Yellow Belt", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::gear", + amount: 1, + ), + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::yellow_belt", + amount: 2, + ), + ], + time_to_craft: 30, + is_intermediate: false, + ), + ( + name: "factory_game::inserter", + display_name: "Inserter", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::green_chip", + amount: 1, + ), + ( + item: "factory_game::gear", + amount: 1, + ), + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::inserter", + amount: 1, + ), + ], + time_to_craft: 30, + is_intermediate: false, + ), + ( + name: "factory_game::green_science", + display_name: "Green Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::yellow_belt", + amount: 1, + ), + ( + item: "factory_game::inserter", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + time_to_craft: 360, + is_intermediate: true, + ), + ( + name: "factory_game::water_generation", + display_name: "Generate Water from nothing", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [], + output: [ + ( + item: "factory_game::water", + amount: 30, + ), + ], + time_to_craft: 5, + is_intermediate: false, + ), + ( + name: "factory_game::sulfuric_acid", + display_name: "Sulfuric Acid", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ( + item: "factory_game::sulfur", + amount: 5, + ), + ( + item: "factory_game::water", + amount: 100, + ), + ], + output: [ + ( + item: "factory_game::sulfuric_acid", + amount: 50, + ), + ], + time_to_craft: 60, + is_intermediate: true, + ), + ( + name: "factory_game::blue_science", + display_name: "Blue Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::red_chip", + amount: 3, + ), + ( + item: "factory_game::sulfur", + amount: 1, + ), + ( + item: "factory_game::engine", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::blue_science", + amount: 2, + ), + ], + time_to_craft: 1440, + is_intermediate: true, + ), + ( + name: "factory_game::red_chip", + display_name: "Advanced Circuit", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::copper_wire", + amount: 4, + ), + ( + item: "factory_game::green_chip", + amount: 2, + ), + ( + item: "factory_game::plastic", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::red_chip", + amount: 1, + ), + ], + time_to_craft: 360, + is_intermediate: true, + ), + ( + name: "factory_game::plastic", + display_name: "Plastic", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::coal", + amount: 1, + ), + ( + item: "factory_game::petroleum_gas", + amount: 20, + ), + ], + output: [ + ( + item: "factory_game::plastic", + amount: 2, + ), + ], + time_to_craft: 60, + is_intermediate: true, + ), + ( + name: "factory_game::engine", + display_name: "Engine", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::gear", + amount: 1, + ), + ( + item: "factory_game::pipe", + amount: 2, + ), + ( + item: "factory_game::steel", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::engine", + amount: 1, + ), + ], + time_to_craft: 600, + is_intermediate: true, + ), + ( + name: "factory_game::pipe", + display_name: "Pipe", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::pipe", + amount: 1, + ), + ], + time_to_craft: 30, + is_intermediate: false, + ), + ( + name: "factory_game::steel", + display_name: "Steel", + possible_machines: [ + "factory_game::electric_furnace", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 5, + ), + ], + output: [ + ( + item: "factory_game::steel", + amount: 1, + ), + ], + time_to_craft: 960, + is_intermediate: true, + ), + ( + name: "factory_game::sulfur", + display_name: "Sulfur", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::petroleum_gas", + amount: 30, + ), + ( + item: "factory_game::water", + amount: 30, + ), + ], + output: [ + ( + item: "factory_game::sulfur", + amount: 2, + ), + ], + time_to_craft: 60, + is_intermediate: true, + ), + ( + name: "factory_game::basic_oil_processing", + display_name: "Basic Oil Processing", + possible_machines: [ + "factory_game::refinery", + ], + ings: [ + ( + item: "factory_game::raw_oil", + amount: 100, + ), + ], + output: [ + ( + item: "factory_game::petroleum_gas", + amount: 45, + ), + ], + time_to_craft: 300, + is_intermediate: true, + ), + ( + name: "factory_game::raw_oil_generation", + display_name: "Generate Raw Oil", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [], + output: [ + ( + item: "factory_game::raw_oil", + amount: 30, + ), + ], + time_to_craft: 30, + is_intermediate: true, + ), + ( + name: "factory_game::advanced_oil_processing", + display_name: "Advanced Oil Processing", + possible_machines: [ + "factory_game::refinery", + ], + ings: [ + ( + item: "factory_game::raw_oil", + amount: 100, + ), + ( + item: "factory_game::water", + amount: 50, + ), + ], + output: [ + ( + item: "factory_game::petroleum_gas", + amount: 55, + ), + ( + item: "factory_game::light_oil", + amount: 45, + ), + ( + item: "factory_game::heavy_oil", + amount: 25, + ), + ], + time_to_craft: 300, + is_intermediate: true, + ), + ( + name: "factory_game::heavy_oil_cracking", + display_name: "Heavy Oil Cracking", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::heavy_oil", + amount: 40, + ), + ( + item: "factory_game::water", + amount: 30, + ), + ], + output: [ + ( + item: "factory_game::light_oil", + amount: 30, + ), + ], + time_to_craft: 120, + is_intermediate: true, + ), + ( + name: "factory_game::light_oil_cracking", + display_name: "Heavy Oil Cracking", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::light_oil", + amount: 30, + ), + ( + item: "factory_game::water", + amount: 30, + ), + ], + output: [ + ( + item: "factory_game::petroleum_gas", + amount: 20, + ), + ], + time_to_craft: 120, + is_intermediate: true, + ), + ( + name: "factory_game::purple_science", + display_name: "Purple Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::electric_furnace", + amount: 1, + ), + ( + item: "factory_game::prod_module_1", + amount: 1, + ), + ( + item: "factory_game::rail", + amount: 30, + ), + ], + output: [ + ( + item: "factory_game::purple_science", + amount: 3, + ), + ], + time_to_craft: 1260, + is_intermediate: true, + ), + ( + name: "factory_game::electric_furnace", + display_name: "Electric Furnace", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::red_chip", + amount: 5, + ), + ( + item: "factory_game::steel", + amount: 5, + ), + ( + item: "factory_game::stone_brick", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::electric_furnace", + amount: 1, + ), + ], + time_to_craft: 300, + is_intermediate: false, + ), + ( + name: "factory_game::stone_brick", + display_name: "Stone Brick", + possible_machines: [ + "factory_game::electric_furnace", + ], + ings: [ + ( + item: "factory_game::stone", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::stone_brick", + amount: 1, + ), + ], + time_to_craft: 192, + is_intermediate: true, + ), + ( + name: "factory_game::prod_module_1", + display_name: "Productivity Module 1", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::red_chip", + amount: 5, + ), + ( + item: "factory_game::green_chip", + amount: 5, + ), + ], + output: [ + ( + item: "factory_game::prod_module_1", + amount: 1, + ), + ], + time_to_craft: 900, + is_intermediate: false, + ), + ( + name: "factory_game::rail", + display_name: "Rail", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::iron_stick", + amount: 1, + ), + ( + item: "factory_game::steel", + amount: 1, + ), + ( + item: "factory_game::stone", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::rail", + amount: 2, + ), + ], + time_to_craft: 30, + is_intermediate: false, + ), + ( + name: "factory_game::iron_stick", + display_name: "Iron Stick", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::iron_stick", + amount: 2, + ), + ], + time_to_craft: 30, + is_intermediate: true, + ), + ( + name: "factory_game::lds", + display_name: "Low Density Structure", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::copper_plate", + amount: 20, + ), + ( + item: "factory_game::plastic", + amount: 5, + ), + ( + item: "factory_game::steel", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::lds", + amount: 1, + ), + ], + time_to_craft: 900, + is_intermediate: true, + ), + ( + name: "factory_game::blue_chip", + display_name: "Processing Unit", + possible_machines: [ + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::red_chip", + amount: 2, + ), + ( + item: "factory_game::green_chip", + amount: 20, + ), + ( + item: "factory_game::sulfuric_acid", + amount: 5, + ), + ], + output: [ + ( + item: "factory_game::blue_chip", + amount: 1, + ), + ], + time_to_craft: 600, + is_intermediate: true, + ), + ( + name: "factory_game::battery", + display_name: "Battery", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::iron_plate", + amount: 1, + ), + ( + item: "factory_game::copper_plate", + amount: 1, + ), + ( + item: "factory_game::sulfuric_acid", + amount: 20, + ), + ], + output: [ + ( + item: "factory_game::battery", + amount: 1, + ), + ], + time_to_craft: 240, + is_intermediate: true, + ), + ( + name: "factory_game::lube", + display_name: "Lubricant", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::heavy_oil", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::lube", + amount: 10, + ), + ], + time_to_craft: 60, + is_intermediate: true, + ), + ( + name: "factory_game::electric_engine", + display_name: "Electric Engine", + possible_machines: [ + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::green_chip", + amount: 2, + ), + ( + item: "factory_game::engine", + amount: 1, + ), + ( + item: "factory_game::lube", + amount: 15, + ), + ], + output: [ + ( + item: "factory_game::electric_engine", + amount: 1, + ), + ], + time_to_craft: 600, + is_intermediate: true, + ), + ( + name: "factory_game::flying_robot_frame", + display_name: "Flying Robot Frame", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::battery", + amount: 2, + ), + ( + item: "factory_game::electric_engine", + amount: 1, + ), + ( + item: "factory_game::green_chip", + amount: 3, + ), + ( + item: "factory_game::steel", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::flying_robot_frame", + amount: 1, + ), + ], + time_to_craft: 1200, + is_intermediate: true, + ), + ( + name: "factory_game::yellow_science", + display_name: "Utility Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::flying_robot_frame", + amount: 1, + ), + ( + item: "factory_game::lds", + amount: 3, + ), + ( + item: "factory_game::blue_chip", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::yellow_science", + amount: 3, + ), + ], + time_to_craft: 1260, + is_intermediate: true, + ), + ( + name: "factory_game::white_science", + display_name: "White Science", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::rocket_result", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::white_science", + amount: 100, + ), + ], + time_to_craft: 1, + is_intermediate: false, + ), + ( + name: "factory_game::rocket_launch", + display_name: "Rocket Launch", + possible_machines: [ + "factory_game::rocket_silo", + ], + ings: [ + ( + item: "factory_game::rocket_part", + amount: 100, + ), + ], + output: [ + // TODO: Recipes without outputs are currently not supported! + ( + item: "factory_game::gear", + amount: 1, + ), + ], + time_to_craft: 1200, + is_intermediate: false, + ), + ( + name: "factory_game::rocket_launch_with_satellite", + display_name: "Rocket Launch With Satellite", + possible_machines: [ + "factory_game::rocket_silo", + ], + ings: [ + ( + item: "factory_game::rocket_part", + amount: 100, + ), + ( + item: "factory_game::satellite", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::rocket_result", + amount: 10, + ), + ], + time_to_craft: 1200, + is_intermediate: false, + ), + ( + name: "factory_game::rocket_part", + display_name: "Rocket Part", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::lds", + amount: 10, + ), + ( + item: "factory_game::rcu", + amount: 10, + ), + ( + item: "factory_game::rocket_fuel", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::rocket_part", + amount: 1, + ), + ], + time_to_craft: 180, + is_intermediate: true, + ), + ( + name: "factory_game::rcu", + display_name: "Rocket Control Unit", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::speed_module_1", + amount: 1, + ), + ( + item: "factory_game::blue_chip", + amount: 1, + ), + ], + output: [ + ( + item: "factory_game::rcu", + amount: 1, + ), + ], + time_to_craft: 1800, + is_intermediate: true, + ), + ( + name: "factory_game::speed_module_1", + display_name: "Speed Module 1", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::red_chip", + amount: 5, + ), + ( + item: "factory_game::green_chip", + amount: 5, + ), + ], + output: [ + ( + item: "factory_game::speed_module_1", + amount: 1, + ), + ], + time_to_craft: 900, + is_intermediate: false, + ), + ( + name: "factory_game::rocket_fuel", + display_name: "Rocket Fuel", + possible_machines: [ + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::solid_fuel", + amount: 10, + ), + ( + item: "factory_game::light_oil", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::rocket_fuel", + amount: 1, + ), + ], + time_to_craft: 900, + is_intermediate: true, + ), + ( + name: "factory_game::solid_fuel_from_light_oil", + display_name: "Solid Fuel From Light Oil", + possible_machines: [ + "factory_game::chemical_plant", + ], + ings: [ + ( + item: "factory_game::light_oil", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::solid_fuel", + amount: 1, + ), + ], + time_to_craft: 120, + is_intermediate: true, + ), + ( + name: "factory_game::satellite", + display_name: "Satellite", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::accumulator", + amount: 100, + ), + ( + item: "factory_game::lds", + amount: 100, + ), + ( + item: "factory_game::blue_chip", + amount: 100, + ), + ( + item: "factory_game::radar", + amount: 5, + ), + ( + item: "factory_game::rocket_fuel", + amount: 50, + ), + ( + item: "factory_game::solar_panel", + amount: 100, + ), + ], + output: [ + ( + item: "factory_game::satellite", + amount: 1, + ), + ], + time_to_craft: 300, + is_intermediate: false, + ), + ( + name: "factory_game::accumulator", + display_name: "Accumulator", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::battery", + amount: 5, + ), + ( + item: "factory_game::iron_plate", + amount: 2, + ), + ], + output: [ + ( + item: "factory_game::accumulator", + amount: 1, + ), + ], + time_to_craft: 600, + is_intermediate: false, + ), + ( + name: "factory_game::solar_panel", + display_name: "Solar Panel", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::copper_plate", + amount: 5, + ), + ( + item: "factory_game::green_chip", + amount: 15, + ), + ( + item: "factory_game::steel", + amount: 5, + ), + ], + output: [ + ( + item: "factory_game::solar_panel", + amount: 1, + ), + ], + time_to_craft: 600, + is_intermediate: false, + ), + ( + name: "factory_game::radar", + display_name: "Radar", + possible_machines: [ + "factory_game::assembler1", + "factory_game::assembler2", + "factory_game::assembler3", + ], + ings: [ + ( + item: "factory_game::gear", + amount: 5, + ), + ( + item: "factory_game::green_chip", + amount: 5, + ), + ( + item: "factory_game::iron_plate", + amount: 10, + ), + ], + output: [ + ( + item: "factory_game::radar", + amount: 1, + ), + ], + time_to_craft: 30, + is_intermediate: false, + ), + ], + items: [ + ( + name: "factory_game::iron_ore", + display_name: "Iron Ore", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::copper_ore", + display_name: "Copper Ore", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::iron_plate", + display_name: "Iron Plate", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::copper_plate", + display_name: "Copper Plate", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::gear", + display_name: "Gear", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::red_science", + display_name: "Automation Science Pack", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::copper_wire", + display_name: "Copper Wire", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::green_chip", + display_name: "Green Chip", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::yellow_belt", + display_name: "Yellow Belt", + stack_size: 100, + placed_as: Some(Belt(())), + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::inserter", + display_name: "Inserter", + stack_size: 50, + placed_as: Some(Inserter("inserter")), + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::green_science", + display_name: "Green Science", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::water", + display_name: "Water", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::sulfur", + display_name: "Sulfur", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::sulfuric_acid", + display_name: "Sulfuric Acid", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::blue_science", + display_name: "Blue Science", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::red_chip", + display_name: "Advanced Circuit", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::plastic", + display_name: "Plastic", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::coal", + display_name: "Coal", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::petroleum_gas", + display_name: "Petroleum Gas", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::light_oil", + display_name: "Light Oil", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::heavy_oil", + display_name: "Heavy Oil", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::lube", + display_name: "Lubricant", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::raw_oil", + display_name: "Raw Oil", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: true, + ), + ( + name: "factory_game::engine", + display_name: "Engine", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::steel", + display_name: "Steel", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::pipe", + display_name: "Pipe", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::purple_science", + display_name: "Purple Science", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::electric_furnace", + display_name: "Electric Furnace", + stack_size: 50, + placed_as: Some(AssemblingMachine("factory_game::electric_furnace")), + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::stone", + display_name: "Stone", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::stone_brick", + display_name: "Stone Brick", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::prod_module_1", + display_name: "Productivity Module 1", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::rail", + display_name: "Rail", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::iron_stick", + display_name: "Iron Stick", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::battery", + display_name: "Battery", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::electric_engine", + display_name: "Electric Engine", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::flying_robot_frame", + display_name: "Flying Robot Frame", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::lds", + display_name: "Low Density Structure", + stack_size: 10, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::blue_chip", + display_name: "Processing Unit", + stack_size: 100, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::yellow_science", + display_name: "Utility Science Pack", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::white_science", + display_name: "Space Science Pack", + stack_size: 200, + placed_as: None, + burnable_in: [], + science_data: Some(()), + is_fluid: false, + ), + ( + name: "factory_game::rocket_result", + display_name: "Rocket Result", + stack_size: 5, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::rocket_part", + display_name: "Rocket Part", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::rcu", + display_name: "Rocket Control Unit", + stack_size: 10, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::speed_module_1", + display_name: "Speed Module 1", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::rocket_fuel", + display_name: "Rocket Fuel", + stack_size: 10, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::solid_fuel", + display_name: "Solid Fuel", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::satellite", + display_name: "Satellite", + stack_size: 1, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::accumulator", + display_name: "Accumulator", + stack_size: 50, + placed_as: Some(Accumulator("factory_game::accumulator")), + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::radar", + display_name: "Radar", + stack_size: 50, + placed_as: None, + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ( + name: "factory_game::solar_panel", + display_name: "Solar Panel", + stack_size: 50, + placed_as: Some(SolarPanel("factory_game::solar_panel")), + burnable_in: [], + science_data: None, + is_fluid: false, + ), + ], + machines: [ + ( + name: "factory_game::assembler1", + display_name: "Assembling Machine", + tile_size: (3, 3), + working_power_draw: (150000), + fluid_connection_offsets: [], + fluid_connection_flowthrough: [], + num_module_slots: 0, + base_bonus_prod: 0, + base_speed: 10, + ), + ( + name: "factory_game::assembler2", + display_name: "Assembling Machine 2", + tile_size: (3, 3), + working_power_draw: (75000), + fluid_connection_offsets: [ + ( + allowed_fluid_directions: Both( + preferred: Out + ), + offset: (1, 0), + pipe_connection_direction: North, + connection_type: Direct, + ) + ], + fluid_connection_flowthrough: [], + num_module_slots: 2, + base_bonus_prod: 0, + base_speed: 15, + ), + ( + name: "factory_game::assembler3", + display_name: "Assembling Machine 3", + tile_size: (3, 3), + working_power_draw: (375000), + fluid_connection_offsets: [ + ( + allowed_fluid_directions: Both( + preferred: Out + ), + offset: (1, 0), + pipe_connection_direction: North, + connection_type: Direct, + ) + ], + fluid_connection_flowthrough: [], + num_module_slots: 4, + base_bonus_prod: 0, + base_speed: 25, + ), + ( + name: "factory_game::rocket_silo", + display_name: "Rocket Silo", + tile_size: (9, 9), + working_power_draw: (4_000_000), + fluid_connection_offsets: [], + fluid_connection_flowthrough: [], + num_module_slots: 4, + base_bonus_prod: 0, + base_speed: 20, + ), + ( + name: "factory_game::electric_furnace", + display_name: "Electric Furnace", + tile_size: (3, 3), + working_power_draw: (180000), + fluid_connection_offsets: [], + fluid_connection_flowthrough: [], + num_module_slots: 2, + base_bonus_prod: 0, + base_speed: 40, + ), + ( + name: "factory_game::chemical_plant", + display_name: "Chemical Plant", + tile_size: (3, 3), + working_power_draw: (210000), + fluid_connection_offsets: [ + ( + allowed_fluid_directions: Single( + Out + ), + offset: (0, 0), + pipe_connection_direction: North, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Out + ), + offset: (2, 0), + pipe_connection_direction: North, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Ing + ), + offset: (0, 2), + pipe_connection_direction: South, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Ing + ), + offset: (2, 2), + pipe_connection_direction: South, + connection_type: Direct, + ), + ], + fluid_connection_flowthrough: [], + num_module_slots: 3, + base_bonus_prod: 0, + base_speed: 20, + ), + ( + name: "factory_game::refinery", + display_name: "Oil Refinery", + tile_size: (5, 5), + working_power_draw: (420000), + fluid_connection_offsets: [ + ( + allowed_fluid_directions: Single( + Out + ), + offset: (0, 0), + pipe_connection_direction: North, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Out + ), + offset: (2, 0), + pipe_connection_direction: North, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Out + ), + offset: (4, 0), + pipe_connection_direction: North, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Ing + ), + offset: (1, 4), + pipe_connection_direction: South, + connection_type: Direct, + ), + ( + allowed_fluid_directions: Single( + Ing + ), + offset: (3, 4), + pipe_connection_direction: South, + connection_type: Direct, + ), + ], + fluid_connection_flowthrough: [], + num_module_slots: 3, + base_bonus_prod: 0, + base_speed: 20, + ), + ], + labs: [ + ( + name: "factory_game::lab", + display_name: "Lab", + tile_size: (3, 3), + working_power_draw: (60000), + fluid_connection_offsets: [], + fluid_connection_flowthrough: [], + num_module_slots: 2, + base_bonus_prod: 0, + base_speed: 1, + ), + ], + beacons: [ + ( + name: "factory_game::beacon", + display_name: "Beacon", + tile_size: (3, 3), + working_power_draw: (480000), + num_module_slots: 2, + effectiveness: (1, 2), + effect_size: (9, 9), + ), + ], + miners: [], + power_poles: [ + ( + name: "factory_game::small_power_pole", + display_name: "Small Power Pole", + tile_size: (1, 1), + power_range: 2, + connection_range: 7, + ), + ( + name: "factory_game::medium_power_pole", + display_name: "Medium Power Pole", + tile_size: (1, 1), + power_range: 3, + connection_range: 9, + ), + ( + name: "factory_game::large_power_pole", + display_name: "Large Power Pole", + tile_size: (2, 2), + power_range: 1, + connection_range: 32, + ), + ( + name: "factory_game::substation", + display_name: "Substation", + tile_size: (2, 2), + power_range: 8, + connection_range: 17, + ), + ], + modules: [ + ( + name: "factory_game::speed_mod", + display_name: "Speed Module", + item: "factory_game::speed_mod", + productivity_effect: 0, + speed_effect: 10, + power_effect: 14, + allowed_in: AllIncludingBeacons, + ), + ( + name: "factory_game::prod_mod", + display_name: "Productivity Module", + item: "factory_game::prod_mod", + productivity_effect: 10, + speed_effect: -3, + power_effect: 16, + allowed_in: OnlyIntermediate, + ), + ], + chests: [ + ( + name: "factory_game::wooden_chest", + display_name: "Wooden Chest", + tile_size: (1, 1), + number_of_slots: 16, + ), + ( + name: "factory_game::iron_chest", + display_name: "Iron Chest", + tile_size: (1, 1), + number_of_slots: 32, + ), + ( + name: "factory_game::steel_chest", + display_name: "Steel Chest", + tile_size: (1, 1), + number_of_slots: 48, + ), + ], + technologies: [ + ( + name: "factory_game::sink_red", + display_name: "Sink Red", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 1_000_000_000_000, + precursors: [], + + pre_unlocked: false, + + infinite: None, + effects: [ + ], + ), + ( + name: "factory_game::sink_red_green", + display_name: "Sink Red Green", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 1_000_000_000_000, + precursors: [], + + pre_unlocked: false, + + infinite: None, + effects: [ + ], + ), + ( + name: "factory_game::sink_red_green_blue", + display_name: "Sink Red Green Blue", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 1_000_000_000_000, + precursors: [], + + pre_unlocked: false, + + infinite: None, + effects: [ + ], + ), + ( + name: "factory_game::sink_red_green_blue_purple", + display_name: "Sink Red Green Blue Purple", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ( + item: "factory_game::purple_science", + amount: 1, + ), + ], + num_units: 1_000_000_000_000, + precursors: [], + + pre_unlocked: false, + + infinite: None, + effects: [ + ], + ), + ( + name: "factory_game::debug", + display_name: "DEBUG", + cost_of_single_research_unit: [], + num_units: 0, + precursors: [], + + pre_unlocked: true, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::iron_ore_generation"), + RecipeUnlock("factory_game::copper_ore_generation"), + RecipeUnlock("factory_game::coal_generation"), + 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"), + ], + ), + ( + name: "factory_game::smelting", + display_name: "Smelting", + cost_of_single_research_unit: [], + num_units: 0, + precursors: [], + + pre_unlocked: true, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::iron_smelting"), + RecipeUnlock("factory_game::gears"), + RecipeUnlock("factory_game::copper_smelting"), + RecipeUnlock("factory_game::yellow_belt"), + RecipeUnlock("factory_game::stone_brick"), + ], + ), + ( + name: "factory_game::electronics", + display_name: "Electronics", + cost_of_single_research_unit: [], + num_units: 0, + precursors: [ + "factory_game::smelting", + ], + + pre_unlocked: true, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::copper_wire"), + RecipeUnlock("factory_game::green_chip"), + RecipeUnlock("factory_game::inserter"), + // RecipeUnlock("factory_game::lab"), + // RecipeUnlock("factory_game::power_pole"), + ], + ), + ( + name: "factory_game::steam_power", + display_name: "Steam power", + cost_of_single_research_unit: [], + num_units: 0, + precursors: [ + "factory_game::smelting", + ], + + pre_unlocked: true, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::pipe"), + // RecipeUnlock("factory_game::underground_pipe"), + // RecipeUnlock("factory_game::boiler"), + // RecipeUnlock("factory_game::offshore_pump"), + // RecipeUnlock("factory_game::steam_engine"), + ], + ), + ( + name: "factory_game::automation_science_pack", + display_name: "Automation Science Pack", + cost_of_single_research_unit: [], + num_units: 0, + precursors: [ + "factory_game::electronics", + "factory_game::steam_power", + ], + + pre_unlocked: true, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::red_science"), + ], + ), + ( + name: "factory_game::automation", + display_name: "Automation", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 10, + precursors: [ + "factory_game::automation_science_pack" + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::assembling_machine"), + // RecipeUnlock("factory_game::red_inserter"), + ], + ), + ( + name: "factory_game::logistics_science_pack", + display_name: "Logistics Science Pack", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::automation_science_pack" + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::green_science"), + ], + ), + ( + name: "factory_game::steel", + display_name: "Steel Processing", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 500_000_000, + precursors: [ + "factory_game::automation_science_pack" + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::steel"), + ], + ), + ( + name: "factory_game::automation_2", + display_name: "Automation 2", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 40, + precursors: [ + "factory_game::automation", + "factory_game::logistics_science_pack", + "factory_game::steel", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::assembling_machine_2"), + ], + ), + ( + name: "factory_game::fluid_handling", + display_name: "Fluid Handling", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 50, + precursors: [ + "factory_game::automation_2", + "factory_game::engine", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::fluid_tank"), + // RecipeUnlock("factory_game::pump"), + ], + ), + ( + name: "factory_game::engine", + display_name: "Engine", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 100, + precursors: [ + "factory_game::steel", + "factory_game::logistics_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::engine"), + ], + ), + ( + name: "factory_game::oil_gathering", + display_name: "Oil Gathering", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 100, + precursors: [ + "factory_game::fluid_handling", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::refinery"), + // RecipeUnlock("factory_game::chemical_plant"), + // RecipeUnlock("factory_game::solid_fuel_from_petroluem_gas"), + RecipeUnlock("factory_game::basic_oil_processing"), + ], + ), + ( + name: "factory_game::plastics", + display_name: "Plastics", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 200, + precursors: [ + "factory_game::oil_gathering", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::plastic"), + ], + ), + ( + name: "factory_game::advanced_circuit", + display_name: "Advanced Circuit", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 200, + precursors: [ + "factory_game::plastics", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::red_chip"), + ], + ), + ( + name: "factory_game::sulfur", + display_name: "Sulfur", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 150, + precursors: [ + "factory_game::oil_gathering", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::sulfur"), + RecipeUnlock("factory_game::sulfuric_acid"), + ], + ), + ( + name: "factory_game::chemical_science_pack", + display_name: "Chemical Science Pack", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::advanced_circuit", + "factory_game::sulfur", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::blue_science"), + ], + ), + ( + name: "factory_game::steel_furnace", + display_name: "Advanced material processing", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::logistics_science_pack", + "factory_game::steel", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::steel_furnace"), + ], + ), + ( + name: "factory_game::electric_furnace", + display_name: "Advanced material processing 2", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 250, + precursors: [ + "factory_game::steel_furnace", + "factory_game::chemical_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::electric_furnace"), + ], + ), + ( + name: "factory_game::modules", + display_name: "Modules", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 100, + precursors: [ + "factory_game::advanced_circuit", + ], + + pre_unlocked: false, + + infinite: None, + effects: [], + ), + ( + name: "factory_game::prod_module", + display_name: "Productivity Module", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 50, + precursors: [ + "factory_game::modules", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::prod_module_1"), + ], + ), + ( + name: "factory_game::railway", + display_name: "Railway", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::engine", + "factory_game::logistics_2", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::iron_stick"), + RecipeUnlock("factory_game::rail"), + // RecipeUnlock("factory_game::cargo_wagon"), + // RecipeUnlock("factory_game::locomotive"), + ], + ), + ( + name: "factory_game::logistics_2", + display_name: "Logistics 2", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 200, + precursors: [ + "factory_game::logistics", + "factory_game::logistics_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::red_belt"), + // RecipeUnlock("factory_game::red_underground"), + // RecipeUnlock("factory_game::red_splitter"), + ], + ), + ( + name: "factory_game::logistics", + display_name: "Logistics", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 20, + precursors: [ + "factory_game::automation_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::yellow_underground"), + // RecipeUnlock("factory_game::yellow_splitter"), + ], + ), + ( + name: "factory_game::purple_science", + display_name: "Production Science Pack", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 100, + precursors: [ + "factory_game::electric_furnace", + "factory_game::railway", + "factory_game::prod_module", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::purple_science"), + ], + ), + ( + name: "factory_game::low_density_structure", + display_name: "Low Density Structure", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 300, + precursors: [ + "factory_game::chemical_science_pack", + "factory_game::steel_furnace", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::lds"), + ], + ), + ( + name: "factory_game::processing_unit", + display_name: "Processing Unit", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 300, + precursors: [ + "factory_game::chemical_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::blue_chip"), + ], + ), + ( + name: "factory_game::battery", + display_name: "Battery", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 150, + precursors: [ + "factory_game::sulfur", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::battery"), + ], + ), + ( + name: "factory_game::advanced_oil_processing", + display_name: "Advanced Oil Processing", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::chemical_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::advanced_oil_processing"), + RecipeUnlock("factory_game::heavy_oil_cracking"), + RecipeUnlock("factory_game::light_oil_cracking"), + ], + ), + ( + name: "factory_game::lube", + display_name: "Lubricant", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 50, + precursors: [ + "factory_game::advanced_oil_processing", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::lube"), + ], + ), + ( + name: "factory_game::electric_engine", + display_name: "Electric Engine", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 50, + precursors: [ + "factory_game::lube", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::electric_engine"), + ], + ), + ( + name: "factory_game::robotics", + display_name: "Robotics", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 75, + precursors: [ + "factory_game::battery", + "factory_game::electric_engine", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::flying_robot_frame"), + ], + ), + ( + name: "factory_game::utility_science_pack", + display_name: "Utility Science Pack", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 100, + precursors: [ + "factory_game::low_density_structure", + "factory_game::processing_unit", + "factory_game::robotics", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::yellow_science"), + ], + ), + ( + name: "factory_game::radar", + display_name: "Radar", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ], + num_units: 20, + precursors: [ + "factory_game::automation_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::radar"), + ], + ), + ( + name: "factory_game::solar_energy", + display_name: "Solar Energy", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 250, + precursors: [ + "factory_game::steel", + "factory_game::logistics_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::solar_panel"), + ], + ), + ( + name: "factory_game::electric_energy_distribution_1", + display_name: "Electric Energy Distribution 1", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 120, + precursors: [ + "factory_game::steel", + "factory_game::logistics_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::iron_stick"), + // RecipeUnlock("factory_game::medium_power_pole"), + // RecipeUnlock("factory_game::large_power_pole"), + ], + ), + ( + name: "factory_game::accumulator", + display_name: "Accumulators", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 150, + precursors: [ + "factory_game::battery", + "factory_game::electric_energy_distribution_1", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::accumulator"), + ], + ), + ( + name: "factory_game::concrete", + display_name: "Concrete", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ], + num_units: 250, + precursors: [ + "factory_game::steel_furnace", + "factory_game::automation_2", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::iron_stick"), + // RecipeUnlock("factory_game::concrete"), + ], + ), + ( + name: "factory_game::rocket_fuel", + display_name: "Rocket Fuel", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ], + num_units: 300, + precursors: [ + "factory_game::advanced_oil_processing", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::rocket_fuel"), + ], + ), + ( + name: "factory_game::rocket_silo", + display_name: "Rocket Silo", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ( + item: "factory_game::purple_science", + amount: 1, + ), + ( + item: "factory_game::yellow_science", + amount: 1, + ), + ], + num_units: 1000, + precursors: [ + "factory_game::concrete", + "factory_game::radar", + "factory_game::rocket_fuel", + "factory_game::utility_science_pack", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + // RecipeUnlock("factory_game::rocket_silo"), + RecipeUnlock("factory_game::rocket_part"), + RecipeUnlock("factory_game::rocket_launch"), + // TODO: + RecipeUnlock("factory_game::rcu"), + RecipeUnlock("factory_game::speed_module_1"), + ], + ), + ( + name: "factory_game::white_science", + display_name: "Space Science Pack", + cost_of_single_research_unit: [ + ( + item: "factory_game::red_science", + amount: 1, + ), + ( + item: "factory_game::green_science", + amount: 1, + ), + ( + item: "factory_game::blue_science", + amount: 1, + ), + ( + item: "factory_game::purple_science", + amount: 1, + ), + ( + item: "factory_game::yellow_science", + amount: 1, + ), + ], + num_units: 2000, + precursors: [ + "factory_game::accumulator", + "factory_game::solar_energy", + "factory_game::rocket_silo", + ], + + pre_unlocked: false, + + infinite: None, + effects: [ + RecipeUnlock("factory_game::rocket_launch_with_satellite"), + RecipeUnlock("factory_game::satellite"), + RecipeUnlock("factory_game::white_science"), + ], + ), + ], + solar_panels: [ + ( + name: "factory_game::infinity_battery", + display_name: "Infinity Battery", + tile_size: (2, 2), + output: (100000000000), + ), + ( + name: "factory_game::solar_panel", + display_name: "Solar Panel", + tile_size: (3, 3), + output: (60000), + ), + ], + accumulators: [], + fluid_tanks: [ + ( + name: "factory_game::pipe", + display_name: "Pipe", + tile_size: (1, 1), + capacity: 100, + connections: [ + ((0, 0), North, Direct), + ((0, 0), South, Direct), + ((0, 0), East, Direct), + ((0, 0), West, Direct), + ], + ), + ( + name: "factory_game::underground_pipe", + display_name: "Underground Pipe", + tile_size: (1, 1), + capacity: 100, + connections: [ + ((0, 0), North, Direct), + ((0, 0), South, Underground ( + max_distance: 10, + underground_allowed_kinds: ["default"] + )), + ], + ), + ( + name: "factory_game::storage_tank", + display_name: "Storage Tank", + tile_size: (3, 3), + capacity: 25000, + connections: [ + ((0, 0), North, Direct), + ((0, 0), East, Direct), + ((2, 2), South, Direct), + ((2, 2), West, Direct), + ], + ) + + ], +) \ No newline at end of file diff --git a/src/data/factorio_1_1.rs b/src/data/factorio_1_1.rs new file mode 100644 index 0000000..e7d6ee7 --- /dev/null +++ b/src/data/factorio_1_1.rs @@ -0,0 +1,544 @@ +use crate::power::Watt; + +use super::{ + AllowedIn, RawAssemblingMachine, RawBeacon, RawChest, RawDataStore, RawEntity, RawItem, + RawItemStack, RawLab, RawModule, RawPowerPole, RawRecipeData, RawSolarPanel, RawTechnology, + RawTechnologyEffect, +}; + +const RAW_DATA_STR: &'static str = include_str!("factorio_1_1.fgmod"); + +#[must_use] +pub fn get_raw_data_test() -> RawDataStore { + ron::from_str(RAW_DATA_STR).expect("RAW_DATA_STR invalid") + // get_raw_data_fn() +} + +#[must_use] +pub fn get_raw_data_fn() -> RawDataStore { + RawDataStore { + recipes: vec![ + RawRecipeData { + name: "factory_game::iron_ore_generation".to_string(), + display_name: "Generate Iron Ore from nothing".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![].into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::iron_ore".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 60, + is_intermediate: false, + }, + RawRecipeData { + name: "factory_game::copper_ore_generation".to_string(), + display_name: "Generate Copper Ore from nothing".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![].into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::copper_ore".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 60, + is_intermediate: false, + }, + RawRecipeData { + name: "factory_game::iron_smelting".to_string(), + display_name: "Smelt Iron Ore into Iron Plates".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![RawItemStack { + item: "factory_game::iron_ore".to_string(), + amount: 1, + }] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::iron_plate".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 192, + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::copper_smelting".to_string(), + display_name: "Smelt Copper Ore into Copper Plates".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![RawItemStack { + item: "factory_game::copper_ore".to_string(), + amount: 1, + }] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::copper_plate".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 192, + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::gears".to_string(), + display_name: "Gears".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![RawItemStack { + item: "factory_game::iron_plate".to_string(), + amount: 2, + }] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::gear".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 30, + + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::red_science".to_string(), + display_name: "Automation Science".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![ + RawItemStack { + item: "factory_game::gear".to_string(), + amount: 1, + }, + RawItemStack { + item: "factory_game::copper_plate".to_string(), + amount: 1, + }, + ] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::red_science".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 300, + + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::copper_wire".to_string(), + display_name: "Copper Wire".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![RawItemStack { + item: "factory_game::copper_plate".to_string(), + amount: 1, + }] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::copper_wire".to_string(), + amount: 2, + }] + .into_boxed_slice(), + time_to_craft: 30, + + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::green_chip".to_string(), + display_name: "Green Chip".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![ + RawItemStack { + item: "factory_game::copper_wire".to_string(), + amount: 3, + }, + RawItemStack { + item: "factory_game::iron_plate".to_string(), + amount: 1, + }, + ] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::green_chip".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 30, + + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::yellow_belt".to_string(), + display_name: "Yellow Belt".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![ + RawItemStack { + item: "factory_game::gear".to_string(), + amount: 1, + }, + RawItemStack { + item: "factory_game::iron_plate".to_string(), + amount: 1, + }, + ] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::yellow_belt".to_string(), + amount: 2, + }] + .into_boxed_slice(), + time_to_craft: 30, + + is_intermediate: false, + }, + RawRecipeData { + name: "factory_game::inserter".to_string(), + display_name: "Inserter".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![ + RawItemStack { + item: "factory_game::green_chip".to_string(), + amount: 1, + }, + RawItemStack { + item: "factory_game::gear".to_string(), + amount: 1, + }, + RawItemStack { + item: "factory_game::iron_plate".to_string(), + amount: 1, + }, + ] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::inserter".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 30, + + is_intermediate: false, + }, + RawRecipeData { + name: "factory_game::green_science".to_string(), + display_name: "Green Science".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![ + RawItemStack { + item: "factory_game::yellow_belt".to_string(), + amount: 1, + }, + RawItemStack { + item: "factory_game::inserter".to_string(), + amount: 1, + }, + ] + .into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::green_science".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 360, + + is_intermediate: true, + }, + RawRecipeData { + name: "factory_game::water_generation".to_string(), + display_name: "Generate Water from nothing".to_string(), + possible_machines: vec!["factory_game::assembler".to_string()].into_boxed_slice(), + ings: vec![].into_boxed_slice(), + output: vec![RawItemStack { + item: "factory_game::water".to_string(), + amount: 1, + }] + .into_boxed_slice(), + time_to_craft: 5, + is_intermediate: true, + }, + ], + items: vec![ + RawItem { + name: "factory_game::iron_ore".to_string(), + display_name: "Iron Ore".to_string(), + stack_size: 100, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::copper_ore".to_string(), + display_name: "Copper Ore".to_string(), + stack_size: 100, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::iron_plate".to_string(), + display_name: "Iron Plate".to_string(), + stack_size: 100, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::copper_plate".to_string(), + display_name: "Copper Plate".to_string(), + stack_size: 100, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::gear".to_string(), + display_name: "Gear".to_string(), + stack_size: 50, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::red_science".to_string(), + display_name: "Automation Science Pack".to_string(), + stack_size: 200, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: Some(()), + is_fluid: false, + }, + RawItem { + name: "factory_game::copper_wire".to_string(), + display_name: "Copper Wire".to_string(), + stack_size: 200, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::green_chip".to_string(), + display_name: "Green Chip".to_string(), + stack_size: 200, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::yellow_belt".to_string(), + display_name: "Yellow Belt".to_string(), + stack_size: 100, + placed_as: Some(RawEntity::Belt(())), + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::inserter".to_string(), + display_name: "Inserter".to_string(), + stack_size: 50, + placed_as: Some(RawEntity::Inserter("inserter".to_string())), + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: false, + }, + RawItem { + name: "factory_game::green_science".to_string(), + display_name: "Green Science".to_string(), + stack_size: 200, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: Some(()), + is_fluid: false, + }, + RawItem { + name: "factory_game::water".to_string(), + display_name: "Water".to_string(), + stack_size: 1, + placed_as: None, + burnable_in: vec![].into_boxed_slice(), + science_data: None, + is_fluid: true, + }, + ], + machines: vec![ + RawAssemblingMachine { + name: "factory_game::assembler1".to_string(), + display_name: "Assembling Machine".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(150_000), + fluid_connection_offsets: vec![], + fluid_connection_flowthrough: vec![], + base_bonus_prod: 0, + base_speed: 10, + num_module_slots: 0, + }, + RawAssemblingMachine { + name: "factory_game::assembler2".to_string(), + display_name: "Assembling Machine 2".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(75_000), + fluid_connection_offsets: vec![], + fluid_connection_flowthrough: vec![], + base_bonus_prod: 0, + base_speed: 15, + num_module_slots: 2, + }, + RawAssemblingMachine { + name: "factory_game::assembler3".to_string(), + display_name: "Assembling Machine 3".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(375_000), + fluid_connection_offsets: vec![], + fluid_connection_flowthrough: vec![], + base_bonus_prod: 0, + base_speed: 25, + num_module_slots: 4, + }, + RawAssemblingMachine { + name: "factory_game::electric_furnace".to_string(), + display_name: "Electric Furnace".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(180000), + fluid_connection_offsets: vec![], + fluid_connection_flowthrough: vec![], + num_module_slots: 2, + base_bonus_prod: 0, + base_speed: 40, + }, + ], + labs: vec![RawLab { + name: "factory_game::lab".to_string(), + display_name: "Lab".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(60_000), + fluid_connection_offsets: vec![], + fluid_connection_flowthrough: vec![], + num_module_slots: 2, + base_bonus_prod: 0, + base_speed: 1, + }], + beacons: vec![RawBeacon { + name: "factory_game::beacon".to_string(), + display_name: "Beacon".to_string(), + tile_size: (3, 3), + working_power_draw: Watt(480_000), + num_module_slots: 2, + effectiveness: (1, 2), + effect_size: (9, 9), + }], + miners: vec![], + power_poles: vec![ + RawPowerPole { + name: "factory_game::small_power_pole".to_string(), + display_name: "Small Power Pole".to_string(), + tile_size: (1, 1), + power_range: 2, + // TODO: + connection_range: 7, + }, + RawPowerPole { + name: "factory_game::medium_power_pole".to_string(), + display_name: "Medium Power Pole".to_string(), + tile_size: (1, 1), + power_range: 3, + // TODO: + connection_range: 9, + }, + RawPowerPole { + name: "factory_game::large_power_pole".to_string(), + display_name: "Large Power Pole".to_string(), + tile_size: (2, 2), + power_range: 1, + // TODO: + connection_range: 32, + }, + RawPowerPole { + name: "factory_game::substation".to_string(), + display_name: "Substation".to_string(), + tile_size: (2, 2), + power_range: 8, + // TODO: + connection_range: 16, + }, + ], + modules: vec![ + RawModule { + name: "factory_game::speed_mod".to_string(), + display_name: "Speed Module".to_string(), + item: "factory_game::speed_mod".to_string(), + productivity_effect: 0, + speed_effect: 10, + power_effect: 14, + allowed_in: AllowedIn::AllIncludingBeacons, + }, + RawModule { + name: "factory_game::prod_mod".to_string(), + display_name: "Productivity Module".to_string(), + item: "factory_game::prod_mod".to_string(), + productivity_effect: 10, + speed_effect: -3, + power_effect: 16, + allowed_in: AllowedIn::OnlyIntermediate, + }, + ], + + chests: vec![ + RawChest { + name: "factory_game::wooden_chest".to_string(), + display_name: "Wooden Chest".to_string(), + tile_size: (1, 1), + number_of_slots: 16, + }, + RawChest { + name: "factory_game::iron_chest".to_string(), + display_name: "Iron Chest".to_string(), + tile_size: (1, 1), + number_of_slots: 32, + }, + RawChest { + name: "factory_game::steel_chest".to_string(), + display_name: "Steel Chest".to_string(), + tile_size: (1, 1), + number_of_slots: 48, + }, + ], + technologies: vec![RawTechnology { + name: "factory_game::automation".to_string(), + display_name: "Automation".to_string(), + cost_of_single_research_unit: vec![RawItemStack { + item: "factory_game::red_science".to_string(), + amount: 1, + }], + num_units: 10, + precursors: vec![], + + pre_unlocked: false, + + infinite: None, + effects: vec![ + RawTechnologyEffect::RecipeUnlock("factory_game::assembling_machine".to_string()), + RawTechnologyEffect::RecipeUnlock("factory_game::red_inserter".to_string()), + ], + }], + solar_panels: vec![ + RawSolarPanel { + name: "factory_game::infinity_battery".to_string(), + display_name: "Infinity Battery".to_string(), + tile_size: (2, 2), + // 1 Terrawatt should be enough for now + output: Watt(1_000_000_000_000), + }, + RawSolarPanel { + name: "factory_game::solar_panel".to_string(), + display_name: "Solar Panel".to_string(), + tile_size: (3, 3), + // TODO: Non constant output + output: Watt(60_000), + }, + ], + accumulators: vec![], + fluid_tanks: vec![], + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..4f13d05 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,1617 @@ +use std::{ + array, + cmp::max, + collections::{BTreeSet, HashMap}, + iter, +}; + +use eframe::egui::Color32; +use itertools::Itertools; +use log::{error, warn}; +use petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}; +use rand::random; +use sha2::{Digest, Sha256}; +use strum::IntoEnumIterator; + +pub mod factorio_1_1; + +use crate::{ + assembler::TIMERTYPE, + frontend::world::tile::Dir, + inserter::StaticID, + item::{ITEMCOUNTTYPE, IdxTrait, Item, Recipe, WeakIdxTrait}, + power::{Joule, Watt}, +}; + +type ItemString = String; +type RecipeString = String; +type AssemblingMachineString = String; +type InserterString = String; +type EngineString = String; +type TechnologyString = String; +type OreString = String; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct RawRecipeData { + /// The fully qualified name of the recipe + name: String, + display_name: String, + possible_machines: Box<[AssemblingMachineString]>, + ings: Box<[RawItemStack]>, + output: Box<[RawItemStack]>, + time_to_craft: TIMERTYPE, + is_intermediate: bool, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawItemStack { + item: ItemString, + amount: ITEMCOUNTTYPE, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawChest { + name: String, + display_name: String, + tile_size: (u8, u8), + number_of_slots: u8, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawAssemblingMachine { + name: String, + display_name: String, + tile_size: (u8, u8), + working_power_draw: Watt, + fluid_connection_offsets: Vec, + fluid_connection_flowthrough: Vec, + + num_module_slots: u8, + + /// Base bonus productivity in % + base_bonus_prod: u8, + /// Speed multiplier compared to "baseline" in 5% + base_speed: u8, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawLab { + name: String, + display_name: String, + tile_size: (u8, u8), + working_power_draw: Watt, + fluid_connection_offsets: Vec, + fluid_connection_flowthrough: Vec, + + num_module_slots: u8, + + /// Base bonus productivity in % + base_bonus_prod: u8, + /// Speed multiplier compared to "baseline" in 5% + base_speed: u8, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawBeacon { + name: String, + display_name: String, + tile_size: (u8, u8), + working_power_draw: Watt, + + num_module_slots: u8, + /// Numerator and denominator + effectiveness: (u8, u8), + effect_size: (u8, u8), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawSolarPanel { + name: String, + display_name: String, + tile_size: (u8, u8), + + // TODO: + output: Watt, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawAccumulator { + name: String, + display_name: String, + tile_size: (u8, u8), + + charge: Joule, + max_charge_rate: Watt, + max_discharge_rate: Watt, +} + +#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum AllowedFluidDirection { + Single(ItemRecipeDir), + Both { preferred: ItemRecipeDir }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawFluidConnection { + allowed_fluid_directions: AllowedFluidDirection, + offset: (u8, u8), + pipe_connection_direction: Dir, + connection_type: RawPipeConnectionType, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum RawPipeConnectionType { + Direct, + Underground { + max_distance: u8, + underground_allowed_kinds: Vec, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawFluidFlowthrough { + fluid_dir: AllowedFluidDirection, + connections: Vec<((u8, u8), Dir, RawPipeConnectionType)>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawInserter { + name: String, + display_name: String, + time_per_trip: TIMERTYPE, + handsize: ITEMCOUNTTYPE, + pickup_offs: (i8, i8), + dropoff_offs: (i8, i8), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct RawDataStore { + recipes: Vec, + items: Vec, + machines: Vec, + labs: Vec, + beacons: Vec, + miners: Vec, + power_poles: Vec, + modules: Vec, + chests: Vec, + technologies: Vec, + solar_panels: Vec, + accumulators: Vec, + fluid_tanks: Vec, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawFluidTank { + name: String, + display_name: String, + capacity: u32, + tile_size: (u8, u8), + connections: Vec<((u8, u8), Dir, RawPipeConnectionType)>, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawTechnology { + name: TechnologyString, + display_name: String, + cost_of_single_research_unit: Vec, + num_units: u64, + precursors: Vec, + + /// Whether or not the technology is unlocked at the start of the game + pre_unlocked: bool, + + infinite: Option, + + effects: Vec, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct InfiniteTechnologyInfo { + scaling: InfiniteCostScaling, + display_level_offset: u32, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum InfiniteCostScaling { + /// final_cost = base_cost + level * units_per_level + Linear { units_per_level: u64 }, + /// final_cost = base_cost + (level * level) * units_per_level + Quadradic { units_per_level: u64 }, + /// final_cost = base_cost + exponential_base ^ level * units_per_level + Exponential { + units_per_level: u64, + exponential_base: f64, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum RawTechnologyEffect { + RecipeUnlock(RecipeString), + RecipeModifier { + recipe: RecipeString, + modifier: RecipeModifier, + }, + MiningDrillModifier { + /// Allows specifying only some ores to be affected + filter: Option, + modifier: RecipeModifier, + }, + InserterHandSizeModifier { + filter: Option, + modifier: i8, + }, + // TODO: Robot speed etc +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum RecipeModifier { + Productivity(i8), + Speed(i8), + PowerConsumption(i8), +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawModule { + name: String, + display_name: String, + item: String, + // TODO: Document the units of these modifiers + // TODO: Maybe in percent? + productivity_effect: i8, + speed_effect: i8, + power_effect: i8, + allowed_in: AllowedIn, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum AllowedIn { + AllIncludingBeacons, + AllNoBeacons, + OnlyIntermediate, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawPowerPole { + name: String, + display_name: String, + tile_size: (u8, u8), + power_range: u8, + connection_range: u8, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawMiner { + name: String, + display_name: String, + timer: TIMERTYPE, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct RawItem { + name: String, + display_name: String, + stack_size: ITEMCOUNTTYPE, + placed_as: Option, + burnable_in: Box<[EngineString]>, + science_data: Option<()>, + is_fluid: bool, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum RawEntity { + AssemblingMachine(AssemblingMachineString), + Inserter(InserterString), + Belt(()), + Accumulator(String), + SolarPanel(String), +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct AssemblerInfo { + pub name: String, + pub display_name: String, + + size: (u16, u16), + pub num_module_slots: u8, + + pub fluid_connections: Vec<(FluidConnection, AllowedFluidDirection)>, + + /// Base Speed. All module modifiers apply multiplicatively + pub base_speed: u8, + pub base_prod: u8, + pub base_power_consumption: Watt, +} + +impl AssemblerInfo { + pub fn size(&self, rotation: Dir) -> (u16, u16) { + let base_size = self.size; + let size = match rotation { + Dir::North | Dir::South => base_size, + Dir::East | Dir::West => (base_size.0, base_size.1), + }; + size + } +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct LabInfo { + pub name: String, + pub display_name: String, + + pub size: (u16, u16), + pub num_module_slots: u8, + + /// Base Speed as a numerator and divisor. All module modifiers apply multiplicatively + pub base_speed: u8, + pub base_prod: u8, + pub base_power_consumption: Watt, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct BeaconInfo { + pub name: String, + pub display_name: String, + + pub size: (u16, u16), + pub num_module_slots: u8, + pub effectiveness: (u8, u8), + // TODO: Currently mining range must be centered on the mining drill + pub effect_range: (u16, u16), + pub power_consumption: Watt, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct ModuleInfo { + pub name: String, + pub display_name: String, + + pub speed_mod: i8, + pub prod_mod: i8, + pub power_mod: i8, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct SolarPanelInfo { + pub name: String, + pub display_name: String, + pub size: [u16; 2], + + pub power_output: SolarPanelOutputFunction, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub enum SolarPanelOutputFunction { + Constant(Watt), + Lookup(Vec), +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct AccumulatorInfo { + pub name: String, + pub display_name: String, + pub size: [u16; 2], + + pub max_charge: Joule, + pub max_charge_rate: Watt, + pub max_discharge_rate: Watt, +} + +type UndergroundGroupMask = u64; + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +pub enum PipeConnectionType { + Direct, + Underground { + max_distance: u8, + underground_group_mask: UndergroundGroupMask, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct DataStore { + pub checksum: String, + + pub max_entity_size: (u16, u16), + + pub assembler_info: Vec, + pub lab_info: Vec, + pub beacon_info: Vec, + pub max_beacon_range: (u16, u16), + pub module_info: Vec, + pub solar_panel_info: Vec, + pub accumulator_info: Vec, + + /// In 5% steps + pub min_power_mod: u8, + + pub recipe_names: Vec, + pub recipe_display_names: Vec, + pub recipe_allowed_assembling_machines: Vec>, + + pub recipe_is_intermediate: Vec, + + pub recipe_num_ing_lookup: Vec, + pub recipe_num_out_lookup: Vec, + pub recipe_to_ing_out_combo_idx: Vec, + pub ing_out_num_to_recipe: HashMap<(usize, usize), Vec>>, + + pub recipe_item_store_to_item: Vec>, + + recipe_item_to_storage_list_idx: + HashMap<(Recipe, Item, ItemRecipeDir), u16>, + + pub recipe_to_items_and_amounts: + HashMap, Vec<(ItemRecipeDir, Item, ITEMCOUNTTYPE)>>, + + /// A lookup from recipe to its ing and out idxs + pub recipe_index_lookups: Vec<(usize, usize)>, + /// A lookup from recipe_ing_idx to its ingredient counts + pub recipe_ings: RecipeIngLookups, + /// A lookup from recipe_out_idx to its output counts + pub recipe_outputs: RecipeOutputLookups, + + pub recipe_timers: Box<[TIMERTYPE]>, + + pub recipe_to_items: HashMap, Vec<(ItemRecipeDir, Item)>>, + + pub science_bottle_items: Vec>, + + pub item_to_recipe_count_where_its_ingredient: Vec, ITEMCOUNTTYPE)>>, + pub item_to_recipe_where_its_output: Vec, ITEMCOUNTTYPE)>>, + + pub lazy_power_machine_infos: Vec>, + + pub item_names: Vec, + pub item_display_names: Vec, + + pub item_is_fluid: Vec, + + pub power_pole_data: Vec, + + pub max_power_search_range: u8, + + pub max_inserter_search_range: u8, + + pub num_different_static_containers: usize, + + /// use Item to index, gives how many recipes have this item as an ingredient + pub num_recipes_with_item: Vec, + + pub item_is_science: Vec, + + pub item_stack_sizes: Vec, + pub chest_num_slots: Vec, + pub chest_tile_sizes: Vec<(u16, u16)>, + pub chest_names: Vec, + + pub fluid_tank_infos: Vec, + + pub recipe_to_translated_index: + HashMap<(Recipe, Item), RecipeIdxType>, + + pub item_to_colour: Vec, + + pub technology_tree: StableGraph, (), Directed, u16>, + pub instantly_finished_technologies: Vec>, + + pub technology_costs: Vec<(u64, Box<[ITEMCOUNTTYPE]>)>, + pub belt_infos: Vec, + pub mining_drill_info: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct Technology { + pub name: String, + pub base_cost: (u64, Box<[ITEMCOUNTTYPE]>), + + pub effect: TechnologyEffect, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct TechnologyEffect { + pub unlocked_recipes: Vec>, + // TODO etc. +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct MiningDrillInfo { + pub name: String, + pub size: [u16; 2], + // TODO: Currently mining range must be centered on the mining drill + pub mining_range: [u16; 2], + pub base_speed: u16, + // Fraction + pub resource_drain: (u8, u8), + // TODO: Allowed ore types +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct BeltInfo { + pub name: String, + pub display_name: String, + pub has_underground: Option, + pub has_splitter: Option, + /// Setting how often this kind of belt moves + pub timer_increase: u8, +} + +#[derive(Debug, Clone, Copy, serde::Serialize, serde:: Deserialize)] +pub struct BeltUndergroundInfo { + pub max_distance: u8, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct BeltSplitterInfo { + can_filter_item: bool, + can_prioritise_side: bool, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct LazyPowerMachineInfo { + pub ingredient: Item, + pub power_per_item: Joule, + pub max_power_per_tick: Joule, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct FluidTankData { + pub name: String, + pub display_name: String, + + pub size: [u16; 2], + /// Capacity in fluid units + pub capacity: u32, + + pub fluid_connections: Vec, + pub max_search_range: u16, +} + +/// These offset and directions are based on the entity facing north +#[derive(Debug, Clone, Copy, serde::Serialize, serde:: Deserialize)] +pub struct FluidConnection { + pub offset: [u16; 2], + pub dir: Dir, + pub kind: PipeConnectionType, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct PowerPoleData { + pub name: String, + pub display_name: String, + pub size: (u16, u16), + pub power_range: u8, + pub connection_range: u8, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Deserialize, serde::Serialize)] +pub enum ItemRecipeDir { + Ing, + Out, +} + +#[derive(Debug)] +pub enum DataStoreOptions { + ItemU8RecipeU8(DataStore), + ItemU8RecipeU16(DataStore), + ItemU16RecipeU8(DataStore), + ItemU16RecipeU16(DataStore), +} + +impl DataStoreOptions { + pub fn assume_simple(self) -> DataStore { + match self { + DataStoreOptions::ItemU8RecipeU8(data_store) => data_store, + _ => unreachable!(), + } + } +} + +struct RecipeIndexLookup { + ing: usize, + out: usize, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct RecipeIngLookups { + pub ing0: Vec<[ITEMCOUNTTYPE; 0]>, + pub ing1: Vec<[ITEMCOUNTTYPE; 1]>, + pub ing2: Vec<[ITEMCOUNTTYPE; 2]>, + pub ing3: Vec<[ITEMCOUNTTYPE; 3]>, + pub ing4: Vec<[ITEMCOUNTTYPE; 4]>, + pub ing5: Vec<[ITEMCOUNTTYPE; 5]>, + pub ing6: Vec<[ITEMCOUNTTYPE; 6]>, +} + +#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +pub struct RecipeOutputLookups { + pub out1: Vec<[ITEMCOUNTTYPE; 1]>, + pub out2: Vec<[ITEMCOUNTTYPE; 2]>, + pub out3: Vec<[ITEMCOUNTTYPE; 3]>, + pub out4: Vec<[ITEMCOUNTTYPE; 4]>, +} + +impl RawDataStore { + #[must_use] + pub fn process(self) -> DataStoreOptions { + match (self.items.len(), self.recipes.len()) { + (items, recipes) if items <= u8::MAX.into() && recipes <= u8::MAX.into() => { + DataStoreOptions::ItemU8RecipeU8(self.turn::()) + }, + (items, recipes) if items <= u8::MAX.into() && recipes <= u16::MAX.into() => { + DataStoreOptions::ItemU8RecipeU16(self.turn::()) + }, + (items, recipes) if items <= u16::MAX.into() && recipes <= u8::MAX.into() => { + DataStoreOptions::ItemU16RecipeU8(self.turn::()) + }, + (items, recipes) if items <= u16::MAX.into() && recipes <= u16::MAX.into() => { + DataStoreOptions::ItemU16RecipeU16(self.turn::()) + }, + + _ => unimplemented!( + "Too many items or recipes, u16::MAX is the max allowed amount (currently) (Btw, are you joking? Who are you trying to torture with this many options? xD)" + ), + } + } + + #[allow(clippy::too_many_lines)] + pub fn turn( + mut self, + ) -> DataStore { + let checksum = self.get_checksum(); + warn!("Parsing game data with checksum {}", checksum); + + // Sort the recipes so they are sorted by number of ings/outputs + // This is needed for the presorted assembler storage lists to be correct + self.recipes.sort_by(|a, b| { + (a.ings.len().cmp(&b.ings.len())).then(a.output.len().cmp(&b.output.len())) + }); + + let pipe_connection_groups_map: Vec<_> = self + .machines + .iter() + .flat_map(|machine| { + machine + .fluid_connection_flowthrough + .iter() + .flat_map(|flowthrough| { + flowthrough + .connections + .iter() + .flat_map(|v| match &v.2 { + RawPipeConnectionType::Direct => None, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => Some(underground_allowed_kinds.iter().cloned()), + }) + .flatten() + }) + .chain( + machine + .fluid_connection_offsets + .iter() + .flat_map(|connection| match &connection.connection_type { + RawPipeConnectionType::Direct => None, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => Some(underground_allowed_kinds.iter().cloned()), + }) + .flatten(), + ) + }) + .chain(self.labs.iter().flat_map(|labs| { + labs.fluid_connection_flowthrough + .iter() + .flat_map(|flowthrough| { + flowthrough.connections.iter().flat_map(|v| match &v.2 { + RawPipeConnectionType::Direct => None, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => Some(underground_allowed_kinds.iter().cloned()), + }) + }) + .chain(labs.fluid_connection_offsets.iter().flat_map(|connection| { + match &connection.connection_type { + RawPipeConnectionType::Direct => None, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => Some(underground_allowed_kinds.iter().cloned()), + } + })) + .flatten() + })) + .chain(self.fluid_tanks.iter().flat_map(|tank| { + tank.connections + .iter() + .flat_map(|v| match &v.2 { + RawPipeConnectionType::Direct => None, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => Some(underground_allowed_kinds.iter().cloned()), + }) + .flatten() + })) + // We collect into a BTreeSet first, to ensure the order is deterministic (as opposed to a HashSet) + .collect::>() + .into_iter() + .collect(); + + assert!( + pipe_connection_groups_map.iter().all(|v| !v.is_empty()), + "Empty Underground groups are not allowed" + ); + + assert!( + pipe_connection_groups_map.len() as u32 <= UndergroundGroupMask::BITS, + "Currently a maximum of {} is supported", + UndergroundGroupMask::BITS + ); + + // TODO: Stop cloning the item names + let item_names = self.items.iter().map(|i| i.name.clone()).collect(); + let item_display_names = self.items.iter().map(|i| i.display_name.clone()).collect(); + + let mut ing_out_num_to_recipe: HashMap<(usize, usize), Vec>> = (0 + ..10) + .cartesian_product(1..5) + .map(|(ings, outputs)| ((ings, outputs), vec![])) + .collect(); + let mut recipe_to_ing_out_combo_idx = vec![]; + + let item_lookup: HashMap<&str, ItemIdxType> = self + .items + .iter() + .map(|i| i.name.as_str()) + .enumerate() + .map(|(i, v)| { + ( + v, + i.try_into() + .unwrap_or_else(|_| panic!("item idx did not fit!!!")), + ) + }) + .collect(); + + let reverse_item_lookup: HashMap = + item_lookup.iter().map(|(k, v)| (*v, *k)).collect(); + + let science_bottle_items: Vec> = self + .items + .iter() + .filter(|i| i.science_data.is_some()) + .map(|r| { + item_lookup + .get(r.name.as_str()) + .expect("Science Item not in lookup?!?") + }) + .map(|idx| Item { id: *idx }) + .collect(); + + let item_to_recipe_where_its_ingredient: Vec> = self + .items + .iter() + .map(|item| { + self.recipes + .iter() + .enumerate() + .filter_map(|(recipe_idx, r)| { + r.ings + .iter() + .find(|stack| stack.item == item.name) + .map(|found_stack| { + ( + Recipe { + id: recipe_idx.try_into().unwrap_or_else(|_| todo!()), + }, + found_stack.amount, + ) + }) + }) + .collect() + }) + .collect(); + + let item_to_recipe_where_its_output: Vec> = + self.items + .iter() + .map(|item| { + self.recipes + .iter() + .enumerate() + .filter_map(|(recipe_idx, r)| { + r.output.iter().find(|stack| stack.item == item.name).map( + |found_stack| { + ( + Recipe { + id: recipe_idx.try_into().unwrap_or_else(|_| todo!()), + }, + found_stack.amount, + ) + }, + ) + }) + .collect() + }) + .collect(); + + for (i, raw_recipe) in self.recipes.iter().enumerate() { + let recipe = Recipe { + id: RecipeIdxType::try_from(i) + .unwrap_or_else(|_| panic!("recipe idx did not fit!!!")), + }; + + let prev_list = + ing_out_num_to_recipe.get_mut(&(raw_recipe.ings.len(), raw_recipe.output.len())); + + if let Some(prev) = prev_list { + recipe_to_ing_out_combo_idx.push(prev.len()); + prev.push(recipe); + } else { + recipe_to_ing_out_combo_idx.push(0); + ing_out_num_to_recipe.insert( + (raw_recipe.ings.len(), raw_recipe.output.len()), + vec![recipe], + ); + } + } + + let mut recipe_item_store_to_item = vec![]; + + let mut recipe_item_to_storage_list_idx = HashMap::new(); + + for num_ings in 0..10 { + for num_out in 1..10 { + if let Some(recipes) = ing_out_num_to_recipe.get(&(num_ings, num_out)) { + for recipe in recipes { + let recipe_id: usize = recipe.id.into(); + for ing in &self.recipes[recipe_id].ings { + let item = *item_lookup.get(ing.item.as_str()).unwrap_or_else(|| { + panic!("Item does not exist: {}", ing.item.as_str()) + }); + + recipe_item_to_storage_list_idx.insert( + (*recipe, Item { id: item }, ItemRecipeDir::Ing), + recipe_item_store_to_item + .iter() + .filter(|i| **i == Item { id: item }) + .count() + .try_into() + .unwrap_or_else(|_| panic!("Too many storage type with item")), + ); + recipe_item_store_to_item.push(Item { id: item }); + } + for output in &self.recipes[recipe_id].output { + let item = + *item_lookup.get(output.item.as_str()).unwrap_or_else(|| { + panic!("Item does not exist: {}", output.item.as_str()) + }); + + recipe_item_to_storage_list_idx.insert( + (*recipe, Item { id: item }, ItemRecipeDir::Out), + recipe_item_store_to_item + .iter() + .filter(|i| **i == Item { id: item }) + .count() + .try_into() + .unwrap_or_else(|_| panic!("Too many storage type with item")), + ); + recipe_item_store_to_item.push(Item { id: item }); + } + } + } + } + } + + let recipe_num_ing_lookup = self.recipes.iter().map(|r| r.ings.len()).collect(); + let recipe_num_out_lookup = self.recipes.iter().map(|r| r.output.len()).collect(); + + let mut recipe_index_lookups = vec![]; + + let mut recipe_ings = RecipeIngLookups { + ing0: vec![], + ing1: vec![], + ing2: vec![], + ing3: vec![], + ing4: vec![], + ing5: vec![], + ing6: vec![], + }; + + let mut recipe_outputs = RecipeOutputLookups { + out1: vec![], + out2: vec![], + out3: vec![], + out4: vec![], + }; + + for (i, recipe) in self.recipes.iter().enumerate() { + let ing_idx = match recipe.ings.len() { + 0 => { + recipe_ings + .ing0 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing0.len() + }, + 1 => { + recipe_ings + .ing1 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing1.len() + }, + 2 => { + recipe_ings + .ing2 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing2.len() + }, + 3 => { + recipe_ings + .ing3 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing3.len() + }, + 4 => { + recipe_ings + .ing4 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing4.len() + }, + 5 => { + recipe_ings + .ing5 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing5.len() + }, + 6 => { + recipe_ings + .ing6 + .push(array::from_fn(|i| recipe.ings[i].amount)); + recipe_ings.ing6.len() + }, + n => { + unimplemented!("{n} ingredients in a single recipe are currently unsupported!!") + }, + } - 1; + + let out_idx = match recipe.output.len() { + 0 => unimplemented!("Recipes without outputs are currently not supported!"), + 1 => { + recipe_outputs + .out1 + .push(array::from_fn(|i| recipe.output[i].amount)); + recipe_outputs.out1.len() + }, + 2 => { + recipe_outputs + .out2 + .push(array::from_fn(|i| recipe.output[i].amount)); + recipe_outputs.out2.len() + }, + 3 => { + recipe_outputs + .out3 + .push(array::from_fn(|i| recipe.output[i].amount)); + recipe_outputs.out3.len() + }, + 4 => { + recipe_outputs + .out4 + .push(array::from_fn(|i| recipe.output[i].amount)); + recipe_outputs.out4.len() + }, + n => { + unimplemented!("{n} outputs in a single recipe are currently unsupported!!") + }, + } - 1; + + recipe_index_lookups.push((ing_idx, out_idx)); + } + + let recipe_timers: Box<[u16]> = self.recipes.iter().map(|r| r.time_to_craft).collect(); + + let recipe_to_items = self + .recipes + .iter() + .enumerate() + .map(|(i, r)| { + let mut v = vec![]; + + for ing in &r.ings { + v.push(( + ItemRecipeDir::Ing, + Item::from(*item_lookup.get(ing.item.as_str()).unwrap()), + )); + } + + for out in &r.output { + v.push(( + ItemRecipeDir::Out, + Item::from(*item_lookup.get(out.item.as_str()).unwrap()), + )); + } + + ( + Recipe { + id: RecipeIdxType::try_from(i).unwrap_or_else(|_| panic!()), + }, + v, + ) + }) + .collect(); + + let recipe_to_items_and_amounts = self + .recipes + .iter() + .enumerate() + .map(|(i, r)| { + let mut v = vec![]; + + for ing in &r.ings { + v.push(( + ItemRecipeDir::Ing, + Item::from(*item_lookup.get(ing.item.as_str()).unwrap()), + ing.amount, + )); + } + + for out in &r.output { + v.push(( + ItemRecipeDir::Out, + Item::from(*item_lookup.get(out.item.as_str()).unwrap()), + out.amount, + )); + } + + ( + Recipe { + id: RecipeIdxType::try_from(i).unwrap_or_else(|_| panic!()), + }, + v, + ) + }) + .collect(); + + let power_pole_data = self + .power_poles + .iter() + .map(|p| PowerPoleData { + name: p.name.clone(), + display_name: p.display_name.clone(), + size: (u16::from(p.tile_size.0), u16::from(p.tile_size.1)), + power_range: p.power_range, + connection_range: p.connection_range, + }) + .collect(); + + let num_recipes_with_item = item_to_recipe_where_its_ingredient + .iter() + .map(Vec::len) + .zip(item_to_recipe_where_its_output.iter().map(Vec::len)) + .map(|(ing, out)| ing + out) + .collect(); + + assert!( + self.technologies + .iter() + .flat_map(|tech| tech.cost_of_single_research_unit.iter()) + .all(|stack| science_bottle_items.contains(&Item { + id: *item_lookup + .get(stack.item.as_str()) + .expect("Could not find item") + })), + "Some research item in a technology is not designated as science item" + ); + + let technology_costs = self + .technologies + .iter() + .map(|tech| { + ( + tech.num_units, + science_bottle_items + .iter() + .map(|item| reverse_item_lookup[&item.id]) + .map(|raw_item_name| { + tech.cost_of_single_research_unit + .iter() + .find_map(|stack| { + (stack.item == raw_item_name).then_some(stack.amount) + }) + .unwrap_or(0) + }) + .collect(), + ) + }) + .collect(); + + let mut instantly_finished_technologies = vec![]; + + let mut tech_tree = StableGraph::<_, (), Directed, u16>::default(); + let mut name_to_node_index = HashMap::new(); + + for (i, raw_tech) in self.technologies.iter().enumerate() { + let index = tech_tree.add_node(Technology { + name: raw_tech.display_name.clone(), + base_cost: ( + raw_tech.num_units, + science_bottle_items + .iter() + .map(|item| reverse_item_lookup[&item.id]) + .map(|raw_item_name| { + raw_tech + .cost_of_single_research_unit + .iter() + .find_map(|stack| { + (stack.item == raw_item_name).then_some(stack.amount) + }) + .unwrap_or(0) + }) + .collect(), + ), + + effect: TechnologyEffect { + unlocked_recipes: raw_tech + .effects + .iter() + .filter_map(|effect| match effect { + RawTechnologyEffect::RecipeUnlock(unlocked_recipe) => { + let index = self + .recipes + .iter() + .position(|recipe| recipe.name == *unlocked_recipe) + .expect( + format!( + "Technology tried unlocking nonexistant recipe: {}", + unlocked_recipe + ) + .as_str(), + ); + + Some(Recipe { + id: index.try_into().unwrap(), + }) + }, + _ => None, + }) + .collect(), + }, + }); + + if raw_tech.pre_unlocked { + instantly_finished_technologies.push(index); + } + + assert_eq!( + name_to_node_index.insert(raw_tech.name.clone(), index), + None, + "{}", + &raw_tech.name + ); + } + + for (i, raw_tech) in self.technologies.iter().enumerate() { + for req in &raw_tech.precursors { + let req = name_to_node_index + .get(req) + .expect(format!("Cannot find technology prerequisite {}", req).as_str()); + + tech_tree.add_edge(*req, NodeIndex::new(i), ()); + } + } + + // TODO: This does not take instantly_finished_technologies into account! + assert!( + !petgraph::algo::is_cyclic_directed(&tech_tree), + "The tech tree contains cycles. This means some technologies are unobtainable!" + ); + + for recipe_index in 0..recipe_timers.len() { + let recipe = Recipe { + id: recipe_index.try_into().unwrap(), + }; + + let can_be_unlocked = tech_tree + .node_weights() + .any(|tech| tech.effect.unlocked_recipes.contains(&recipe)); + + if !can_be_unlocked { + error!( + "Recipe {} cannot be unlocked (and is therefore useless)!", + self.recipes[recipe_index].name + ); + } + } + + DataStore { + checksum, + + technology_tree: tech_tree, + instantly_finished_technologies, + + belt_infos: vec![ + BeltInfo { + name: "factory_game::fast_transport_belt".to_string(), + display_name: "Fast Transport Belt".to_string(), + has_underground: Some(BeltUndergroundInfo { max_distance: 9 }), + has_splitter: None, + timer_increase: 15 * 2, + }, + BeltInfo { + name: "factory_game::transport_belt".to_string(), + 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: "Electric Mining Drill".to_string(), + size: [3, 3], + mining_range: [5, 5], + base_speed: 20, + resource_drain: (1, 1), + }], + + recipe_allowed_assembling_machines: self + .recipes + .iter() + .map(|raw| { + raw.possible_machines + .iter() + .map(|machine_string| { + self.machines + .iter() + .position(|machine| machine.name == *machine_string) + .expect(&format!( + "Could not find machine {} for recipe {}!", + machine_string, raw.name + )) + .try_into() + .unwrap() + }) + .collect() + }) + .collect(), + + recipe_is_intermediate: self.recipes.iter().map(|raw| raw.is_intermediate).collect(), + + assembler_info: self + .machines + .iter() + .map(|m| AssemblerInfo { + name: m.name.clone(), + display_name: m.display_name.clone(), + + size: (m.tile_size.0.into(), m.tile_size.1.into()), + num_module_slots: m.num_module_slots, + base_speed: m.base_speed, + base_prod: m.base_bonus_prod, + base_power_consumption: m.working_power_draw, + + fluid_connections: m + .fluid_connection_offsets + .iter() + .map(|raw_conn| { + ( + FluidConnection { + offset: [raw_conn.offset.0.into(), raw_conn.offset.1.into()], + dir: raw_conn.pipe_connection_direction, + kind: match &raw_conn.connection_type { + RawPipeConnectionType::Direct => PipeConnectionType::Direct, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => PipeConnectionType::Underground { + max_distance: *max_distance, + underground_group_mask: pipe_connection_groups_map + .iter() + .chain(iter::repeat(&String::from(""))) + .take(UndergroundGroupMask::BITS as usize) + .enumerate() + .map(|(i, group)| { + if underground_allowed_kinds.contains(group) { + 1 << i + } else { + 0 + } + }) + .sum(), + }, + }, + }, + raw_conn.allowed_fluid_directions, + ) + }) + .collect(), + }) + .collect(), + + lab_info: self + .labs + .iter() + .map(|m| LabInfo { + name: m.name.clone(), + display_name: m.display_name.clone(), + + size: (m.tile_size.0.into(), m.tile_size.1.into()), + num_module_slots: m.num_module_slots, + base_speed: m.base_speed, + base_prod: m.base_bonus_prod, + base_power_consumption: m.working_power_draw, + }) + .collect(), + + solar_panel_info: self + .solar_panels + .iter() + .map(|raw| { + SolarPanelInfo { + name: raw.name.clone(), + display_name: raw.display_name.clone(), + size: [raw.tile_size.0.into(), raw.tile_size.1.into()], + // FIXME: + power_output: SolarPanelOutputFunction::Constant(raw.output), + } + }) + .collect(), + + accumulator_info: self + .accumulators + .iter() + .map(|raw| AccumulatorInfo { + name: raw.name.clone(), + display_name: raw.display_name.clone(), + size: [raw.tile_size.0.into(), raw.tile_size.1.into()], + max_charge: raw.charge, + max_charge_rate: raw.max_charge_rate, + max_discharge_rate: raw.max_discharge_rate, + }) + .collect(), + + beacon_info: self + .beacons + .iter() + .map(|m| BeaconInfo { + name: m.name.clone(), + display_name: m.display_name.clone(), + + size: (m.tile_size.0.into(), m.tile_size.1.into()), + num_module_slots: m.num_module_slots, + power_consumption: m.working_power_draw, + + effectiveness: m.effectiveness, + effect_range: (m.effect_size.0.into(), m.effect_size.1.into()), + }) + .collect(), + + max_beacon_range: self + .beacons + .iter() + .map(|b| (b.effect_size.0.into(), b.effect_size.1.into())) + .reduce(|a, b| (max(a.0, b.0), max(b.1, b.1))) + .unwrap_or((0, 0)), + + module_info: self + .modules + .iter() + .map(|module| ModuleInfo { + name: module.name.clone(), + display_name: module.display_name.clone(), + speed_mod: module.speed_effect, + prod_mod: module.productivity_effect, + power_mod: module.power_effect, + }) + .collect(), + + min_power_mod: 4, + + // TODO: + max_entity_size: (9, 9), + + recipe_names: self.recipes.iter().map(|r| r.name.clone()).collect(), + recipe_display_names: self + .recipes + .iter() + .map(|r| r.display_name.clone()) + .collect(), + + recipe_num_ing_lookup, + recipe_num_out_lookup, + recipe_to_ing_out_combo_idx, + ing_out_num_to_recipe, + recipe_item_store_to_item, + recipe_item_to_storage_list_idx, + recipe_index_lookups, + recipe_ings, + + recipe_outputs, + + recipe_timers, + + recipe_to_items, + recipe_to_items_and_amounts, + + science_bottle_items, + + item_to_recipe_count_where_its_ingredient: item_to_recipe_where_its_ingredient, + item_to_recipe_where_its_output, + + item_names, + item_display_names, + + power_pole_data, + + item_is_fluid: self.items.iter().map(|i| i.is_fluid).collect(), + + lazy_power_machine_infos: vec![], + + max_power_search_range: self + .power_poles + .iter() + .map(|p| p.power_range) + .max() + .expect("At least one type of power pole must exist"), + + // TODO: + max_inserter_search_range: 2, + + item_is_science: self + .items + .iter() + .map(|i| i.science_data.is_some()) + .collect(), + num_different_static_containers: StaticID::iter().count(), + num_recipes_with_item, + + item_stack_sizes: self.items.iter().map(|item| item.stack_size).collect(), + chest_num_slots: self + .chests + .iter() + .map(|chest| chest.number_of_slots) + .collect(), + chest_tile_sizes: self + .chests + .iter() + .map(|chest| (u16::from(chest.tile_size.0), u16::from(chest.tile_size.1))) + .collect(), + chest_names: self.chests.iter().map(|chest| chest.name.clone()).collect(), + + recipe_to_translated_index: (0..self.recipes.len()) + .cartesian_product( + self.items + .iter() + .enumerate() + .map(|(item_id, item_name)| (item_id, &item_name.name)), + ) + .map(|(recipe_id, (item_id, item_name))| { + ( + ( + Recipe { + id: recipe_id.try_into().unwrap(), + }, + Item { + id: item_id.try_into().unwrap(), + }, + ), + self.recipes + .iter() + .take(recipe_id) + .filter(|recipe| { + recipe + .ings + .iter() + .map(|stack| &stack.item) + .any(|item| item == item_name) + | recipe + .output + .iter() + .map(|stack| &stack.item) + .any(|item| item == item_name) + }) + .count() + .try_into() + .unwrap(), + ) + }) + .collect(), + + item_to_colour: self + .items + .iter() + .map(|item| Color32::from_rgb(random(), random(), random())) + .collect(), + + fluid_tank_infos: self + .fluid_tanks + .into_iter() + .map(|tank| { + let max_search_range = tank + .connections + .iter() + .map(|conn| match &conn.2 { + RawPipeConnectionType::Direct => 1, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => *max_distance, + }) + .max() + .expect("A fluid tank without connections is useless") + .into(); + + FluidTankData { + // TODO: Sanity check the fluid connections + name: tank.name, + display_name: tank.display_name, + size: [tank.tile_size.0.into(), tank.tile_size.1.into()], + capacity: tank.capacity, + fluid_connections: tank + .connections + .into_iter() + .map(|conn| FluidConnection { + dir: conn.1, + offset: [conn.0.0.into(), conn.0.1.into()], + kind: match conn.2 { + RawPipeConnectionType::Direct => PipeConnectionType::Direct, + RawPipeConnectionType::Underground { + max_distance, + underground_allowed_kinds, + } => PipeConnectionType::Underground { + max_distance, + underground_group_mask: pipe_connection_groups_map + .iter() + .chain(iter::repeat(&String::from(""))) + .take(UndergroundGroupMask::BITS as usize) + .enumerate() + .map(|(i, group)| { + if underground_allowed_kinds.contains(group) { + 1 << i + } else { + 0 + } + }) + .sum(), + }, + }, + }) + .collect(), + max_search_range, + } + }) + .collect(), + + technology_costs, + } + } + + pub fn get_checksum(&self) -> String { + let mut hasher = Sha256::new(); + + hasher.update(postcard::to_allocvec(self).unwrap()); + + hex::encode_upper(hasher.finalize()) + } +} + +pub fn all_item_iter( + data_store: &DataStore, +) -> impl Iterator> + use { + (0..data_store.item_display_names.len()).map(|id| Item { + id: id.try_into().unwrap(), + }) +} + +pub fn all_recipe_iter( + data_store: &DataStore, +) -> impl Iterator> + use { + (0..data_store.recipe_display_names.len()).map(|id| Recipe { + id: id.try_into().unwrap(), + }) +} diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index b3cbff8..e1b667d 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -1,9 +1,19 @@ -use std::{collections::HashSet, marker::PhantomData, sync::mpsc::Receiver}; +use std::{ + cmp::{max, min}, + collections::HashSet, + marker::PhantomData, + num::NonZero, + sync::mpsc::Receiver, +}; +use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph}; use log::warn; +use petgraph::Directed; use crate::{ - data::DataStore, + belt::splitter::SplitterDistributionMode, + blueprint::Blueprint, + data::{self, DataStore}, frontend::{ action::{ place_entity::{EntityPlaceOptions, PlaceEntityInfo}, @@ -12,14 +22,17 @@ use crate::{ }, input::{Input, Key}, world::{ - tile::{Dir, Entity, FloorTile, PlaceEntityType, World}, Position, + tile::{AssemblerInfo, Dir, Entity, FloorTile, PlaceEntityType, UndergroundDir, World}, }, }, - item::{IdxTrait, Recipe, WeakIdxTrait}, + item::{ITEMCOUNTTYPE, IdxTrait, Item, Recipe, WeakIdxTrait}, + rendering::render_world::SWITCH_TO_MAPVIEW_ZOOM_LEVEL, }; -use super::{place_tile::PositionInfo, ActionType, PLAYERID}; +use super::{ActionType, PLAYERID, place_tile::PositionInfo}; + +const MAP_VIEW_PAN_SPEED: f32 = 0.05; const MIN_ZOOM_WIDTH: f32 = 1.0; pub const WIDTH_PER_LEVEL: usize = 16; @@ -30,34 +43,76 @@ pub struct ActionStateMachine, + (), + Directed, + u16, + DefaultNodeShape, + DefaultEdgeShape, + >, + >, pub statistics_panel: StatisticsPanel, + pub production_filters: Vec, + pub consumption_filters: Vec, - current_mouse_pos: (f32, f32), + pub current_mouse_pos: (f32, f32), current_held_keys: HashSet, pub state: ActionStateMachineState, + pub escape_menu_open: bool, + pub zoom_level: f32, + pub map_view_info: Option<(f32, f32)>, + + copy_info: Option>, + + pub show_graph_dot_output: bool, recipe: PhantomData, } #[derive(Debug)] +pub enum CopyInfo { + Recipe { + recipe: Recipe, + }, + ChestLimit { + num_slots: u8, + }, + SplitterSetting { + distribution_mode: SplitterDistributionMode, + }, + InserterSettings { + user_movetime: Option>, + max_stack_size: Option, + filter: Option>, + }, +} + +#[derive(Debug, PartialEq)] pub enum StatisticsPanel { - Production(usize), + Items(usize), + Fluids(usize), } impl Default for StatisticsPanel { fn default() -> Self { - Self::Production(1) + Self::Items(2) } } #[derive(Debug)] pub enum ActionStateMachineState { Idle, - Decontructing(Position, u32), + Deconstructing(Position, u32), Holding(HeldObject), Viewing(Position), + + CtrlCPressed, + CopyDragInProgress { start_pos: Position }, } #[derive(Debug)] @@ -65,25 +120,43 @@ pub enum HeldObject { Tile(FloorTile), // TODO: PlaceEntityType is not quite right for this case Entity(PlaceEntityType), + + Blueprint(Blueprint), } impl ActionStateMachine { #[must_use] - pub fn new(my_player_id: PLAYERID, local_player_pos: (f32, f32)) -> Self { + pub fn new( + my_player_id: PLAYERID, + local_player_pos: (f32, f32), + data_store: &DataStore, + ) -> Self { Self { my_player_id, local_player_pos, + tech_tree_render: None, + statistics_panel_open: false, + technology_panel_open: true, statistics_panel: StatisticsPanel::default(), + 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, + + escape_menu_open: false, + + copy_info: None, + + show_graph_dot_output: false, recipe: PhantomData, } @@ -95,96 +168,249 @@ impl input: &'b Receiver, world: &'c World, data_store: &'d DataStore, - ) -> impl IntoIterator> - + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { + ) -> impl Iterator> + + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { input.try_iter().map(|input| { + if self.escape_menu_open && input != Input::KeyPress(Key::Esc) { + match input { + Input::KeyPress(key) => { + self.current_held_keys.insert(key); + }, + Input::KeyRelease(key) => { + self.current_held_keys.remove(&key); + }, + _ => {} + } + + return vec![]; + } let actions = match input { - Input::LeftClickPressed => { - match &self.state { - ActionStateMachineState::Idle => { - // TODO: Check if we are hovering over something that can be opened + Input::Copy => { + self.state = ActionStateMachineState::CtrlCPressed; + vec![] + }, + Input::LeftClickPressed { shift } => { + if shift { + if let Some(copy_info) = &self.copy_info { let pos = Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ); + match world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { Some(e) => { + match (e, copy_info) { + (Entity::Assembler { .. }, CopyInfo::Recipe { recipe }) => { + vec![ActionType::SetRecipe(SetRecipeInfo { pos, recipe: *recipe })] + }, + (Entity::Chest { .. }, CopyInfo::ChestLimit { num_slots }) => { + vec![ActionType::SetChestSlotLimit { pos, num_slots: *num_slots }] + } + (Entity::Inserter { .. }, CopyInfo::InserterSettings { user_movetime, max_stack_size, filter }) => { + // TODO: Add the rest + vec![ActionType::OverrideInserterMovetime { pos, new_movetime: *user_movetime }] + } + (Entity::Splitter { .. }, CopyInfo::SplitterSetting { distribution_mode }) => todo!(), + (_, _) => { + vec![] + } + } + } _ => { + vec![] + }} + } else { + vec![] + } + } else { + match &mut self.state { + ActionStateMachineState::Idle => { + // TODO: Check if we are hovering over something that can be opened + let pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); - if let Some(e) = world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { - self.state = ActionStateMachineState::Viewing(e.get_pos()); - } + if let Some(e) = world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { + self.state = ActionStateMachineState::Viewing(e.get_pos()); + } - vec![] - }, - ActionStateMachineState::Holding(held_object) => { - // TODO: Check if what we are trying to place would collide - - match held_object { - HeldObject::Tile(floor_tile) => { - vec![ActionType::PlaceFloorTile(PlaceFloorTileByHandInfo { - ghost_info: PlaceFloorTileGhostInfo { - tile: *floor_tile, - position: PositionInfo::Single { - pos: Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_pos, - ), + vec![] + }, + + ActionStateMachineState::CtrlCPressed => { + self.state = ActionStateMachineState::CopyDragInProgress { + start_pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ) + }; + + vec![] + }, + + ActionStateMachineState::CopyDragInProgress { start_pos: _ } => { + warn!("Tried starting CopyDraw again!"); + vec![] + }, + + ActionStateMachineState::Holding(held_object) => { + // TODO: Check if what we are trying to place would collide + + match held_object { + HeldObject::Blueprint(bp) => { + bp.get_reusable(data_store).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, + )).collect() + }, + + HeldObject::Tile(floor_tile) => { + vec![ActionType::PlaceFloorTile(PlaceFloorTileByHandInfo { + ghost_info: PlaceFloorTileGhostInfo { + tile: *floor_tile, + position: PositionInfo::Single { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + }, }, - }, - player: (), - })] - }, - HeldObject::Entity(place_entity_type) => { - vec![ActionType::PlaceEntity(PlaceEntityInfo { - entities: EntityPlaceOptions::Single(*place_entity_type), - })] - }, - } - }, - ActionStateMachineState::Viewing(Position { x, y }) => { - let pos = Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_pos, - ); + player: (), + })] + }, + HeldObject::Entity(place_entity_type) => { + let ret = vec![ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(*place_entity_type), + })]; - if let Some(e) = world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { - self.state = ActionStateMachineState::Viewing(e.get_pos()); - } + if let PlaceEntityType::Underground { underground_dir, .. } = place_entity_type { + match *underground_dir { + UndergroundDir::Entrance => *underground_dir = UndergroundDir::Exit, + UndergroundDir::Exit => *underground_dir = UndergroundDir::Entrance, + } - vec![] - }, - ActionStateMachineState::Decontructing(_, _) => { - self.state = ActionStateMachineState::Idle; - vec![] + } + + ret + }, + } + }, + ActionStateMachineState::Viewing(Position { x, y }) => { + let pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + + if let Some(e) = world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { + self.state = ActionStateMachineState::Viewing(e.get_pos()); + } + + vec![] + }, + ActionStateMachineState::Deconstructing(_, _) => { + self.state = ActionStateMachineState::Idle; + vec![] + } } } }, - Input::RightClickPressed => match &self.state { - ActionStateMachineState::Idle => { + Input::RightClickPressed { shift} => { + + + if shift { let pos = Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ); - if world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next().is_some() { - self.state = ActionStateMachineState::Decontructing(pos, 100); + if let Some(e) = world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next() { + match e { + Entity::Assembler { ty, pos, modules, info, rotation } => match info { + AssemblerInfo::UnpoweredNoRecipe => {}, + AssemblerInfo::Unpowered(recipe) => self.copy_info = Some(CopyInfo::Recipe { recipe: *recipe }), + AssemblerInfo::PoweredNoRecipe(position) => {}, + AssemblerInfo::Powered { id, pole_position, weak_index } => self.copy_info = Some(CopyInfo::Recipe { recipe: id.recipe }), + }, + Entity::PowerPole { ty, pos, connected_power_poles } => {}, + Entity::Belt { pos, direction, ty, id, belt_pos } => {}, + Entity::Underground { pos, underground_dir, ty, direction, id, belt_pos } => {}, + Entity::Splitter { pos, direction, id } => todo!(), + Entity::Inserter { ty, user_movetime, type_movetime, pos, direction, filter, info } => { + self.copy_info = Some(CopyInfo::InserterSettings { max_stack_size: None, filter: *filter, user_movetime: *user_movetime }); + }, + Entity::Chest { ty, pos, item, slot_limit } => { + self.copy_info = Some(CopyInfo::ChestLimit { num_slots: *slot_limit }); + }, + Entity::Roboport { ty, pos, power_grid, network, id } => todo!(), + Entity::SolarPanel { pos, ty, pole_position } => {}, + Entity::Lab { pos, ty, modules, pole_position } => {}, + Entity::Beacon { ty, pos, modules, pole_position } => {}, + Entity::FluidTank { ty, pos, rotation } => {}, + } } vec![] - }, - ActionStateMachineState::Holding(_held_object) => { - self.state = ActionStateMachineState::Idle; - vec![] - }, - ActionStateMachineState::Viewing(_) => { - self.state = ActionStateMachineState::Idle; - vec![] - }, - ActionStateMachineState::Decontructing(_, _) => vec![], + } else { + match &self.state { + ActionStateMachineState::Idle => { + let pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + if world.get_entities_colliding_with(pos, (1,1), data_store).into_iter().next().is_some() { + self.state = ActionStateMachineState::Deconstructing(pos, 100); + } + vec![] + }, + ActionStateMachineState::Holding(_held_object) => { + self.state = ActionStateMachineState::Idle; + vec![] + }, + ActionStateMachineState::Viewing(_) => { + self.state = ActionStateMachineState::Idle; + vec![] + }, + ActionStateMachineState::Deconstructing(_, _) => vec![], + ActionStateMachineState::CtrlCPressed | ActionStateMachineState::CopyDragInProgress { start_pos: _ } => { + self.state = ActionStateMachineState::Idle; + vec![] + } + } + } + + } + + + , + Input::LeftClickReleased => { + match self.state { + ActionStateMachineState::Idle => {}, + ActionStateMachineState::Deconstructing(_, _) => {}, + ActionStateMachineState::Holding(_) => {}, + ActionStateMachineState::Viewing(_) => {}, + ActionStateMachineState::CtrlCPressed => {}, + ActionStateMachineState::CopyDragInProgress { start_pos } => { + let end_pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + + 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, [x_range, y_range], data_store))) + }, + } + vec![] }, Input::RightClickReleased => match &self.state { - ActionStateMachineState::Decontructing(_, _) => { + ActionStateMachineState::Deconstructing(_, _) => { self.state = ActionStateMachineState::Idle; vec![] }, @@ -194,66 +420,93 @@ impl self.current_mouse_pos = (x, y); match &mut self.state { + ActionStateMachineState::CtrlCPressed | ActionStateMachineState::CopyDragInProgress { 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: _ - } => { - *position = Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_pos, - ); - }, + 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: _ } => { - *pos = Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_pos, - ); - }, - PlaceEntityType::Belt { pos, direction: _ } => { - *pos = Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_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::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.local_player_pos, - self.current_mouse_pos, - ); - }, - PlaceEntityType::Splitter { pos, direction: _, in_mode: _, out_mode: _ } => { - *pos = Self::player_mouse_to_tile( - self.zoom_level, - self.local_player_pos, - self.current_mouse_pos, - ); - }, - PlaceEntityType::Chest { pos } => {*pos = Self::player_mouse_to_tile( + *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::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.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, );}, - PlaceEntityType::SolarPanel { pos, ty: _ } => {*pos = Self::player_mouse_to_tile( + PlaceEntityType::FluidTank { ty: _, pos, rotation: _ } => {*pos = Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, );}, - PlaceEntityType::Lab { pos, ty: _ } => {*pos = Self::player_mouse_to_tile( + PlaceEntityType::MiningDrill { ty: _, pos, rotation: _ } => {*pos = Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, );}, }, }, - ActionStateMachineState::Decontructing(position, timer) =>{ + ActionStateMachineState::Deconstructing(position, timer) =>{ //todo!("Check if we are still over the same thing") }, } @@ -281,14 +534,30 @@ impl }, Input::MouseScoll(delta) => { + let old_width = WIDTH_PER_LEVEL as f32 * 1.5f32.powf(self.zoom_level); + 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 { winit::event::MouseScrollDelta::LineDelta(_, y) => { - self.zoom_level -= y / 10.0; + self.zoom_level -= y / 10.0; }, winit::event::MouseScrollDelta::PixelDelta(physical_position) => { self.zoom_level -= physical_position.y as f32 / 10.0; }, } + if let Some(view_center) = &mut self.map_view_info { + let new_width = WIDTH_PER_LEVEL as f32 * 1.5f32.powf(self.zoom_level); + + let new_center_x = ((old_width - new_width) / old_width) * (mouse_x as f32 - view_center.0) + view_center.0; + let new_center_y = ((old_width - new_width) / old_width) * (mouse_y as f32 - view_center.1) + view_center.1; + + *view_center = (new_center_x, new_center_y); + } else { + self.zoom_level = if self.zoom_level < *SWITCH_TO_MAPVIEW_ZOOM_LEVEL { + self.zoom_level + } else { + *SWITCH_TO_MAPVIEW_ZOOM_LEVEL + }; + } if self.zoom_level < MIN_ZOOM_WIDTH { self.zoom_level = MIN_ZOOM_WIDTH; } @@ -298,11 +567,6 @@ impl Input::UnknownInput(..) => { vec![] }, - - i @ (Input::LeftClickReleased) => { - dbg!(i); - vec![] - }, }; actions @@ -314,11 +578,205 @@ impl key: Key, world: &World, data_store: &DataStore, - ) -> impl IntoIterator> { - match (&self.state, key) { + ) -> impl Iterator> + use + { + let ret = match (&mut self.state, key) { + (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Q) => { + match world + .get_entities_colliding_with( + Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + (1, 1), + data_store, + ) + .into_iter() + .next() + { + Some(Entity::Assembler { ty, rotation, .. }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Assembler { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + rotation: *rotation, + }, + )); + }, + Some(Entity::Beacon { ty, .. }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Beacon { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + }, + )); + }, + Some(Entity::Belt { direction, ty, .. }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Belt { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + direction: *direction, + }, + )); + }, + Some(Entity::Chest { ty, .. }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Chest { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + }, + )); + }, + Some(Entity::FluidTank { ty, pos, rotation }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::FluidTank { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + rotation: *rotation, + }, + )); + }, + Some(Entity::Inserter { + direction, filter, .. + }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Inserter { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + dir: *direction, + filter: *filter, + }, + )); + }, + Some(Entity::Lab { + pos, + ty, + modules, + pole_position, + }) => todo!(), + Some(Entity::PowerPole { ty, .. }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::PowerPole { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + }, + )); + }, + Some(Entity::Roboport { + ty, + pos, + power_grid, + network, + id, + }) => todo!(), + Some(Entity::SolarPanel { + pos, + ty, + pole_position, + }) => todo!(), + Some(Entity::Splitter { pos, direction, id }) => todo!(), + Some(Entity::Underground { + underground_dir, + direction, + ty, + .. + }) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + ty: *ty, + direction: *direction, + underground_dir: *underground_dir, + }, + )); + }, + None => {}, + } + + vec![] + }, + (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key1) => { - self.state = - ActionStateMachineState::Holding(HeldObject::Tile(FloorTile::Concrete)); + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Splitter { + ty: 0, + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + direction: Dir::North, + in_mode: None, + out_mode: None, + }, + )); + vec![] + }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Assembler { + pos, + ty, + rotation, + })), + Key::R, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Assembler { + pos: *pos, + ty: *ty, + rotation: rotation.turn_right(), + }, + )); + vec![] + }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Assembler { + pos, + ty, + rotation, + })), + Key::Key2, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Assembler { + pos: *pos, + ty: (*ty + 1) % data_store.assembler_info.len() as u8, + rotation: *rotation, + }, + )); vec![] }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key2) => { @@ -326,11 +784,12 @@ impl PlaceEntityType::Assembler { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), // TODO: ty: 0, + rotation: Dir::North, }, )); vec![] @@ -340,10 +799,11 @@ impl ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Belt { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), direction: Dir::North, + ty: 0, })); vec![] }, @@ -351,6 +811,7 @@ impl ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Belt { pos, direction, + ty, })), Key::R, ) => { @@ -358,6 +819,7 @@ impl ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Belt { pos: *pos, direction: direction.turn_right(), + ty: *ty, })); vec![] }, @@ -365,6 +827,8 @@ impl ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Splitter { pos, direction, + ty, + in_mode, out_mode, })), @@ -374,18 +838,40 @@ impl PlaceEntityType::Splitter { pos: *pos, direction: direction.turn_right(), + ty: *ty, in_mode: *in_mode, out_mode: *out_mode, }, )); vec![] }, + ( + ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + }, + )), + Key::R, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { + pos: *pos, + direction: direction.turn_right(), + ty: *ty, + underground_dir: *underground_dir, + }, + )); + vec![] + }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key4) => { self.state = ActionStateMachineState::Holding(HeldObject::Entity( PlaceEntityType::Inserter { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), dir: Dir::North, @@ -395,12 +881,27 @@ impl )); vec![] }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::PowerPole { + pos, + ty, + })), + Key::Key5, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::PowerPole { + pos: *pos, + ty: (*ty + 1) % data_store.power_pole_data.len() as u8, + }, + )); + vec![] + }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key5) => { self.state = ActionStateMachineState::Holding(HeldObject::Entity( PlaceEntityType::PowerPole { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), ty: 0, @@ -408,30 +909,93 @@ impl )); vec![] }, + // (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key6) => { + // self.state = ActionStateMachineState::Holding(HeldObject::Entity( + // PlaceEntityType::FluidTank { + // ty: 0, + // pos: Self::player_mouse_to_tile( + // self.zoom_level, + // self.map_view_info.unwrap_or(self.local_player_pos), + // self.current_mouse_pos, + // ), + // rotation: Dir::North, + // }, + // )); + // vec![] + // }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key6) => { - self.state = ActionStateMachineState::Holding(HeldObject::Entity( - PlaceEntityType::Splitter { + self.state = + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Chest { + ty: 0, pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), - direction: Dir::North, - in_mode: None, - out_mode: None, + })); + vec![] + }, + ( + ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + }, + )), + Key::Key7, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { + pos: *pos, + direction: *direction, + ty: dbg!((*ty + 1) % u8::try_from(data_store.belt_infos.len()).unwrap()), + underground_dir: *underground_dir, }, )); vec![] }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key7) => { - self.state = - ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Chest { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::Underground { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), - })); + ty: 0, + direction: Dir::North, + underground_dir: UndergroundDir::Entrance, + }, + )); + vec![] + }, + // (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key7) => { + // self.state = + // ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Chest { + // pos: Self::player_mouse_to_tile( + // self.zoom_level, + // self.map_view_info.unwrap_or(self.local_player_pos), + // self.current_mouse_pos, + // ), + // ty: 0, + // })); + // vec![] + // }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::SolarPanel { + pos, + ty, + })), + Key::Key8, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::SolarPanel { + pos: *pos, + ty: (*ty + 1) % data_store.solar_panel_info.len() as u8, + }, + )); vec![] }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key8) => { @@ -439,7 +1003,7 @@ impl PlaceEntityType::SolarPanel { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), ty: 0, @@ -447,12 +1011,43 @@ impl )); vec![] }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::FluidTank { + pos, + ty, + rotation, + })), + Key::Key9, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::FluidTank { + pos: *pos, + ty: (*ty + 1) % data_store.fluid_tank_infos.len() as u8, + rotation: *rotation, + }, + )); + vec![] + }, (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key9) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::FluidTank { + ty: 0, + pos: Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ), + rotation: Dir::North, + }, + )); + vec![] + }, + (ActionStateMachineState::Idle | ActionStateMachineState::Holding(_), Key::Key0) => { self.state = - ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Lab { + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::Beacon { pos: Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), ty: 0, @@ -476,6 +1071,23 @@ impl )); vec![] }, + ( + ActionStateMachineState::Holding(HeldObject::Entity(PlaceEntityType::FluidTank { + pos, + ty, + rotation, + })), + Key::R, + ) => { + self.state = ActionStateMachineState::Holding(HeldObject::Entity( + PlaceEntityType::FluidTank { + ty: *ty, + pos: *pos, + rotation: rotation.turn_right(), + }, + )); + vec![] + }, (ActionStateMachineState::Viewing(pos), Key::Key1) => { let chunk = world @@ -499,34 +1111,74 @@ impl vec![] }, + (_, Key::T) => { + self.technology_panel_open = !self.technology_panel_open; + vec![] + }, + + (_, Key::M) => { + if self.map_view_info.is_some() { + self.map_view_info = None; + } else { + self.map_view_info = Some(self.local_player_pos); + } + vec![] + }, + + (ActionStateMachineState::Holding(HeldObject::Blueprint(bp)), Key::V) => { + bp.flip_vertical(data_store); + vec![] + }, + (ActionStateMachineState::Holding(HeldObject::Blueprint(bp)), Key::H) => { + bp.flip_horizontal(data_store); + vec![] + }, + (ActionStateMachineState::Holding(HeldObject::Blueprint(bp)), Key::R) => { + bp.turn_right(data_store); + vec![] + }, + + (_, Key::Esc) => { + self.escape_menu_open = !self.escape_menu_open; + vec![] + }, + (_, _) => vec![], + }; + + // Do not send any actions if we are in the escape menu + if self.escape_menu_open { + vec![].into_iter() + } else { + ret.into_iter() } } fn handle_stop_pressing_key( &mut self, key: Key, - ) -> impl IntoIterator> { + ) -> impl Iterator> + use + { match (&self.state, key) { - (_, _) => vec![], + (_, _) => vec![].into_iter(), } } - fn player_mouse_to_tile( + pub fn player_mouse_to_tile( zoom_level: f32, - player_pos: (f32, f32), + camera_pos: (f32, f32), mouse_pos: (f32, f32), ) -> Position { let mouse_pos = ( - ((mouse_pos.0 - 0.5) * (WIDTH_PER_LEVEL as f32)) - .mul_add(zoom_level * zoom_level, player_pos.0), - ((mouse_pos.1 - 0.5) * (WIDTH_PER_LEVEL as f32)) - .mul_add(zoom_level * zoom_level, player_pos.1), + ((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), ); Position { - x: mouse_pos.0 as usize, - y: mouse_pos.1 as usize, + x: mouse_pos.0 as i32, + y: mouse_pos.1 as i32, } } @@ -535,10 +1187,11 @@ impl &mut self, world: &World, data_store: &DataStore, - ) -> impl IntoIterator> { + ) -> impl Iterator> + use + { let mut actions = Vec::new(); - if let ActionStateMachineState::Decontructing(pos, timer) = &mut self.state { + if let ActionStateMachineState::Deconstructing(pos, timer) = &mut self.state { // Check if we are still over the thing we were deconstructing if world .get_entities_colliding_with(*pos, (1, 1), data_store) @@ -548,7 +1201,7 @@ impl .get_entities_colliding_with( Self::player_mouse_to_tile( self.zoom_level, - self.local_player_pos, + self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, ), (1, 1), @@ -583,17 +1236,24 @@ impl move_dir.0 += 1; } - self.local_player_pos.0 += - move_dir.0 as f32 * world.players[usize::from(self.my_player_id)].movement_speed; - self.local_player_pos.1 += - move_dir.1 as f32 * world.players[usize::from(self.my_player_id)].movement_speed; + if let Some(map_view_pos) = &mut self.map_view_info { + map_view_pos.0 += move_dir.0 as f32 * 1.5f32.powf(self.zoom_level) * MAP_VIEW_PAN_SPEED; + map_view_pos.1 += move_dir.1 as f32 * 1.5f32.powf(self.zoom_level) * MAP_VIEW_PAN_SPEED; + } else { + self.local_player_pos.0 += + move_dir.0 as f32 * world.players[usize::from(self.my_player_id)].movement_speed; + self.local_player_pos.1 += + move_dir.1 as f32 * world.players[usize::from(self.my_player_id)].movement_speed; - // TODO: Maybe only send this event if it changed - actions.push(ActionType::Position( - self.my_player_id, - self.local_player_pos, - )); + if move_dir.0 != 0 || move_dir.1 != 0 { + // Only send this event if it changed + actions.push(ActionType::Position( + self.my_player_id, + self.local_player_pos, + )); + } + } - actions + actions.into_iter() } } diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index bc28a0a..4d232c4 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -1,22 +1,25 @@ +use std::iter::successors; + +use itertools::Itertools; use strum::IntoEnumIterator; use crate::{ belt::{ - smart::Side, - splitter::{Splitter, SplitterDistributionMode, SPLITTER_BELT_LEN}, BeltTileId, SplitterInfo, + smart::Side, + splitter::{SPLITTER_BELT_LEN, SplitterDistributionMode, SplitterSide}, }, data::DataStore, frontend::world::{ - tile::{Dir, DirRelative, Entity, UndergroundDir, World, BELT_LEN_PER_TILE}, Position, + tile::{BELT_LEN_PER_TILE, Dir, DirRelative, Entity, UndergroundDir, World}, }, item::IdxTrait, rendering::app_state::{GameState, SimulationState}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BeltState { +pub enum BeltState { Straight, Curved, Sideloading, @@ -27,100 +30,167 @@ pub fn handle_splitter_placement game_state: &mut GameState, splitter_pos: Position, splitter_direction: Dir, + splitter_ty: u8, in_mode: Option, out_mode: Option, data_store: &DataStore, ) { - let self_positions = [splitter_pos, splitter_pos + splitter_direction.turn_right()]; + #[cfg(debug_assertions)] + { + let all_belt_connections = game_state + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); - let belt_connections: [[BeltTileId; 2]; 2] = self_positions.map(|self_pos| { - let front_pos = self_pos + splitter_direction; - handle_belt_breaking(game_state, front_pos, splitter_direction, data_store); + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = game_state + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist) + ); + } + } + } + let (left_pos, right_pos) = match splitter_direction { + Dir::North => (splitter_pos, splitter_pos + Dir::East), + Dir::East => (splitter_pos, splitter_pos + Dir::South), + Dir::South => (splitter_pos + Dir::East, splitter_pos), + Dir::West => (splitter_pos + Dir::South, splitter_pos), + }; + let self_positions = [left_pos, right_pos]; - // Handle front - let (self_front_id, self_front_len) = if let Some(id) = - should_merge(game_state, splitter_direction, front_pos, data_store) - { - debug_assert!( - should_sideload(game_state, splitter_direction, front_pos, data_store).is_none() - ); - let result = lengthen(game_state, id, SPLITTER_BELT_LEN, Side::BACK); - (id, result.new_belt_len) - } else if let Some((id, pos)) = - should_sideload(game_state, splitter_direction, front_pos, data_store) - { - // Add the little belt at the front of the splitter - let new_belt = game_state - .simulation_state - .factory - .belts - .add_empty_belt(SPLITTER_BELT_LEN); - // Add Sideloading inserter - game_state - .simulation_state - .factory - .belts - .add_sideloading_inserter(new_belt, (id, pos)); - (new_belt, SPLITTER_BELT_LEN) - } else { - let id = game_state - .simulation_state - .factory - .belts - .add_empty_belt(SPLITTER_BELT_LEN); - (id, SPLITTER_BELT_LEN) - }; + let [[left_front, left_back], [right_front, right_back]]: [[BeltTileId; 2]; 2] = + self_positions.map(|self_pos| { + let front_pos = self_pos + splitter_direction; + handle_belt_breaking(game_state, front_pos, splitter_direction, data_store); - let back_pos = self_pos + splitter_direction.reverse(); - let belt_dir = get_belt_out_dir(&game_state.world, back_pos, data_store); - let (self_back_id, self_back_len) = if let Some(belt_dir) = belt_dir { - if belt_dir == splitter_direction { - // The belt at this position is pointing at the back of the splitter - let back_id = match game_state - .world - .get_entities_colliding_with(back_pos, (1, 1), data_store) - .into_iter() - .next() - .unwrap() - { - Entity::Belt { id, .. } => *id, - Entity::Underground { - underground_dir: UndergroundDir::Entrance, - id, - .. - } => *id, - Entity::Splitter { .. } => todo!("get the id from the simstate"), - _ => unreachable!(), - }; + // Handle front + let (self_front_id, self_front_len) = if let Some(id) = + should_merge(game_state, splitter_direction, front_pos, data_store) + { + debug_assert!( + should_sideload(game_state, splitter_direction, front_pos, data_store) + .is_none() + ); + let result = lengthen(game_state, id, SPLITTER_BELT_LEN, Side::BACK); + (id, result.new_belt_len) + } else if let Some((id, pos)) = + should_sideload(game_state, splitter_direction, front_pos, data_store) + { + // Add the little belt at the front of the splitter + let new_belt = game_state + .simulation_state + .factory + .belts + .add_empty_belt(splitter_ty, SPLITTER_BELT_LEN); + // Add Sideloading inserter + game_state + .simulation_state + .factory + .belts + .add_sideloading_inserter(new_belt, (id, pos)); + (new_belt, SPLITTER_BELT_LEN) + } else { + let id = game_state + .simulation_state + .factory + .belts + .add_empty_belt(splitter_ty, SPLITTER_BELT_LEN); + (id, SPLITTER_BELT_LEN) + }; + + let back_pos = self_pos + splitter_direction.reverse(); + dbg!(back_pos); + let belt_dir = get_belt_out_dir(&game_state.world, back_pos, data_store); + let (self_back_id, self_back_len) = if let Some(belt_dir) = belt_dir { + if belt_dir == splitter_direction { + // The belt at this position is pointing at the back of the splitter + let back_id = match game_state + .world + .get_entities_colliding_with(back_pos, (1, 1), data_store) + .into_iter() + .next() + .unwrap() + { + Entity::Belt { id, .. } => *id, + Entity::Underground { + underground_dir: UndergroundDir::Exit, + id, + .. + } => *id, + Entity::Splitter { .. } => todo!("get the id from the simstate"), + e => unreachable!("{:?}", e), + }; - let result = lengthen(game_state, back_id, SPLITTER_BELT_LEN, Side::FRONT); + let result = lengthen(game_state, back_id, SPLITTER_BELT_LEN, Side::FRONT); - (back_id, result.new_belt_len) + (back_id, result.new_belt_len) + } else { + let id = game_state + .simulation_state + .factory + .belts + .add_empty_belt(splitter_ty, SPLITTER_BELT_LEN); + (id, SPLITTER_BELT_LEN) + } } else { let id = game_state .simulation_state .factory .belts - .add_empty_belt(SPLITTER_BELT_LEN); + .add_empty_belt(splitter_ty, SPLITTER_BELT_LEN); (id, SPLITTER_BELT_LEN) - } - } else { - let id = game_state - .simulation_state - .factory - .belts - .add_empty_belt(SPLITTER_BELT_LEN); - (id, SPLITTER_BELT_LEN) - }; + }; - [self_front_id, self_back_id] - }); + [self_front_id, self_back_id] + }); let splitter = SplitterInfo { in_mode: in_mode.unwrap_or_default(), out_mode: out_mode.unwrap_or_default(), - input_belts: [belt_connections[0][0], belt_connections[1][0]], - output_belts: [belt_connections[0][1], belt_connections[1][1]], + input_belts: [left_back, right_back], + output_belts: [left_front, right_front], }; let id = game_state @@ -131,13 +201,76 @@ pub fn handle_splitter_placement game_state.world.add_entity( Entity::Splitter { - pos: splitter_pos, + pos: Position { + x: self_positions.into_iter().map(|pos| pos.x).min().unwrap(), + y: self_positions.into_iter().map(|pos| pos.y).min().unwrap(), + }, direction: splitter_direction, id, }, - &game_state.simulation_state, + &mut game_state.simulation_state, data_store, ); + + #[cfg(debug_assertions)] + { + let all_belt_connections = game_state + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); + + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = game_state + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist), + "{:?}", + belt_pos + ); + } + } + } } #[allow(unreachable_code)] @@ -145,6 +278,7 @@ pub fn handle_belt_placement( game_state: &mut GameState, new_belt_pos: Position, new_belt_direction: Dir, + new_belt_ty: u8, data_store: &DataStore, ) { let front_pos = new_belt_pos + new_belt_direction; @@ -161,10 +295,11 @@ pub fn handle_belt_placement( Entity::Belt { pos: new_belt_pos, direction: new_belt_direction, + ty: new_belt_ty, id, belt_pos: result.belt_pos_of_segment, }, - &game_state.simulation_state, + &mut game_state.simulation_state, data_store, ); (id, result.new_belt_len) @@ -176,7 +311,7 @@ pub fn handle_belt_placement( .simulation_state .factory .belts - .add_empty_belt(BELT_LEN_PER_TILE); + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); // Add Sideloading inserter game_state .simulation_state @@ -188,10 +323,11 @@ pub fn handle_belt_placement( Entity::Belt { pos: new_belt_pos, direction: new_belt_direction, + ty: new_belt_ty, id: new_belt, belt_pos: BELT_LEN_PER_TILE, }, - &game_state.simulation_state, + &mut game_state.simulation_state, data_store, ); (new_belt, BELT_LEN_PER_TILE) @@ -200,15 +336,16 @@ pub fn handle_belt_placement( .simulation_state .factory .belts - .add_empty_belt(BELT_LEN_PER_TILE); + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); game_state.world.add_entity( Entity::Belt { pos: new_belt_pos, direction: new_belt_direction, + ty: new_belt_ty, id, belt_pos: BELT_LEN_PER_TILE, }, - &game_state.simulation_state, + &mut game_state.simulation_state, data_store, ); (id, BELT_LEN_PER_TILE) @@ -243,7 +380,31 @@ pub fn handle_belt_placement( underground_dir: UndergroundDir::Exit, .. } => *id, - Entity::Splitter { .. } => todo!(), + Entity::Splitter { + pos, + id, + direction: splitter_dir, + .. + } => { + let mut side = if potentially_incoming_pos == *pos { + SplitterSide::Left + } else { + SplitterSide::Right + }; + + match splitter_dir { + Dir::North | Dir::East => {}, + Dir::South | Dir::West => side = side.switch(), + } + + let [_, outputs] = game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id); + + outputs[usize::from(bool::from(side))] + }, _ => unreachable!(), }; @@ -260,9 +421,19 @@ pub fn handle_belt_placement( .modify_belt_pos(back_id, self_len.try_into().unwrap()); } if final_id == back_id { - game_state.world.update_belt_id(self_id, final_id); + game_state.world.update_belt_id( + &mut game_state.simulation_state, + self_id, + final_id, + data_store, + ); } else { - game_state.world.update_belt_id(back_id, final_id); + game_state.world.update_belt_id( + &mut game_state.simulation_state, + back_id, + final_id, + data_store, + ); } } else if let Some((id, belt_pos)) = should_sideload(game_state, dir, new_belt_pos, data_store) @@ -298,6 +469,560 @@ pub fn handle_belt_placement( } } } + + #[cfg(debug_assertions)] + { + let all_belt_connections = game_state + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); + + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = game_state + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist) + ); + } + } + } +} + +#[allow(unreachable_code)] +pub fn handle_underground_belt_placement( + game_state: &mut GameState, + new_belt_pos: Position, + new_belt_direction: Dir, + new_belt_ty: u8, + new_underground_dir: UndergroundDir, + data_store: &DataStore, +) { + let max_distance: u8 = data_store.belt_infos[usize::from(new_belt_ty)] + .has_underground + .expect("Tried to place underground of type without underground support") + .max_distance; + + // Break front belt if necessary + match new_underground_dir { + UndergroundDir::Entrance => { + // Find (and break if necessary) the connecting underground + let first_underground = + successors(Some(new_belt_pos), |pos| Some(*pos + new_belt_direction)) + .skip(1) + .take(max_distance.into()) + .find_map(|check_pos| { + let e = game_state + .world + .get_entities_colliding_with(check_pos, (1, 1), data_store) + .into_iter() + .next(); + + if let Some(Entity::Underground { ty, direction, .. }) = e { + if *direction == new_belt_direction { + // Only match with belts of the same type (this allows belt weaving) + if *ty == new_belt_ty { + Some(e.unwrap()) + } else { + None + } + } else { + None + } + } else { + None + } + }); + + let (self_id, self_len) = if let Some(e) = first_underground { + let Entity::Underground { + pos, + underground_dir, + direction, + ty, + id, + belt_pos, + } = e + else { + unreachable!() + }; + + match underground_dir { + UndergroundDir::Entrance => { + // The first underground we found was another Entrance, so we do not need to connect, nor break anything + + let id = game_state + .simulation_state + .factory + .belts + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + direction: new_belt_direction, + ty: new_belt_ty, + underground_dir: new_underground_dir, + id, + belt_pos: BELT_LEN_PER_TILE, + }, + &mut game_state.simulation_state, + data_store, + ); + (id, BELT_LEN_PER_TILE) + }, + UndergroundDir::Exit => { + if *belt_pos == game_state.simulation_state.factory.belts.get_len(*id) { + // This is the start of the belt + // no breaking necessary + } else { + todo!("Break its connection"); + } + + let exit_id: BeltTileId = *id; + + // Add the "underground" belt locs + let res = lengthen( + game_state, + *id, + // Underground + BELT_LEN_PER_TILE + * u16::try_from( + new_belt_pos.x.abs_diff(pos.x) + new_belt_pos.y.abs_diff(pos.y), + ) + .unwrap() + + // The newly placed tile itself + BELT_LEN_PER_TILE, + Side::BACK, + ); + + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + direction: new_belt_direction, + ty: new_belt_ty, + underground_dir: new_underground_dir, + id: exit_id, + belt_pos: res.new_belt_len, + }, + &mut game_state.simulation_state, + data_store, + ); + + (exit_id, res.new_belt_len) + }, + } + } else { + // No underground found to connect to + let id = game_state + .simulation_state + .factory + .belts + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + direction: new_belt_direction, + ty: new_belt_ty, + underground_dir: new_underground_dir, + id, + belt_pos: BELT_LEN_PER_TILE, + }, + &mut game_state.simulation_state, + data_store, + ); + (id, BELT_LEN_PER_TILE) + }; + + // An underground Entrance cannot break any belt + // should_merge does not work if the entity at "front_pos" is not in the world yet + if let Some(id) = should_merge(game_state, new_belt_direction, new_belt_pos, data_store) + { + assert_eq!(self_id, id); + debug_assert!( + should_sideload(game_state, new_belt_direction, new_belt_pos, data_store) + .is_none() + ); + + let potentially_incoming_pos = new_belt_pos + new_belt_direction.reverse(); + + let back_id = match game_state + .world + .get_entities_colliding_with(potentially_incoming_pos, (1, 1), data_store) + .into_iter() + .next() + { + Some(Entity::Belt { id, .. }) + | Some(Entity::Underground { + id, + underground_dir: UndergroundDir::Exit, + .. + }) => Some(*id), + Some(Entity::Splitter { .. }) => todo!(), + Some(_) => None, + None => None, + }; + + if let Some(back_id) = back_id { + game_state + .world + .modify_belt_pos(back_id, self_len.try_into().unwrap()); + let (final_id, final_len) = merge_belts( + &mut game_state.simulation_state, + self_id, + back_id, + data_store, + ); + if final_id == self_id { + game_state.world.update_belt_id( + &mut game_state.simulation_state, + back_id, + final_id, + data_store, + ); + } else { + game_state.world.update_belt_id( + &mut game_state.simulation_state, + self_id, + final_id, + data_store, + ); + } + (final_id, final_len) + } else { + (self_id, self_len) + } + } else if let Some((id, pos)) = + should_sideload(game_state, new_belt_direction, new_belt_pos, data_store) + { + unreachable!() + } else { + (self_id, self_len) + }; + }, + UndergroundDir::Exit => { + let front_pos = new_belt_pos + new_belt_direction; + + handle_belt_breaking(game_state, front_pos, new_belt_direction, data_store); + + // Handle front tile + let (self_id, self_len) = if let Some(id) = + should_merge(game_state, new_belt_direction, front_pos, data_store) + { + debug_assert!( + should_sideload(game_state, new_belt_direction, front_pos, data_store) + .is_none() + ); + let result = lengthen(game_state, id, BELT_LEN_PER_TILE, Side::BACK); + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + underground_dir: new_underground_dir, + direction: new_belt_direction, + ty: new_belt_ty, + id, + belt_pos: result.belt_pos_of_segment, + }, + &mut game_state.simulation_state, + data_store, + ); + (id, result.new_belt_len) + } else if let Some((id, pos)) = + should_sideload(game_state, new_belt_direction, front_pos, data_store) + { + // Add new belt + let new_belt = game_state + .simulation_state + .factory + .belts + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); + // Add Sideloading inserter + game_state + .simulation_state + .factory + .belts + .add_sideloading_inserter(new_belt, (id, pos)); + + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + underground_dir: new_underground_dir, + direction: new_belt_direction, + ty: new_belt_ty, + id, + belt_pos: BELT_LEN_PER_TILE, + }, + &mut game_state.simulation_state, + data_store, + ); + (new_belt, BELT_LEN_PER_TILE) + } else { + let id = game_state + .simulation_state + .factory + .belts + .add_empty_belt(new_belt_ty, BELT_LEN_PER_TILE); + game_state.world.add_entity( + Entity::Underground { + pos: new_belt_pos, + direction: new_belt_direction, + ty: new_belt_ty, + underground_dir: new_underground_dir, + id, + belt_pos: BELT_LEN_PER_TILE, + }, + &mut game_state.simulation_state, + data_store, + ); + (id, BELT_LEN_PER_TILE) + }; + + // Handle side tiles + for dir in Dir::iter() { + if dir == new_belt_direction { + // This is the front tile, which we already handled + continue; + } + if dir == new_belt_direction.reverse() { + // This is the back tile, which we do not care about for sideloading + continue; + } + + let potentially_incoming_pos = new_belt_pos + dir; + + debug_assert!( + should_merge(game_state, dir.reverse(), new_belt_pos, data_store).is_none() + ); + + if let Some((id, belt_pos)) = + should_sideload(game_state, dir.reverse(), new_belt_pos, data_store) + { + debug_assert_eq!(id, self_id); + + let (back_id, back_pos) = match game_state + .world + .get_entities_colliding_with(potentially_incoming_pos, (1, 1), data_store) + .into_iter() + .next() + .unwrap() + { + Entity::Belt { id, belt_pos, .. } + | Entity::Underground { + id, + belt_pos, + underground_dir: UndergroundDir::Exit, + .. + } => (*id, *belt_pos), + Entity::Splitter { .. } => todo!(), + e => unreachable!("{:?}", e), + }; + + // Add Sideloading inserter + game_state + .simulation_state + .factory + .belts + .add_sideloading_inserter(back_id, (id, belt_pos)); + } + } + + let first_underground = successors(Some(new_belt_pos), |pos| { + Some(*pos + new_belt_direction.reverse()) + }) + .skip(1) + .take(max_distance.into()) + .find_map(|check_pos| { + let e = game_state + .world + .get_entities_colliding_with(check_pos, (1, 1), data_store) + .into_iter() + .next(); + + if let Some(Entity::Underground { ty, direction, .. }) = e { + if *direction == new_belt_direction { + // Only match with belts of the same type (this allows belt weaving) + if *ty == new_belt_ty { + Some(e.unwrap()) + } else { + None + } + } else { + None + } + } else { + None + } + }); + + if let Some(e) = first_underground { + let Entity::Underground { + pos, + underground_dir, + direction, + ty, + id, + belt_pos, + } = e + else { + unreachable!() + }; + + match underground_dir { + UndergroundDir::Entrance => { + if *belt_pos == BELT_LEN_PER_TILE { + // This is the end of the belt + // no breaking necessary + } else { + todo!("Break its connection"); + } + + let entrance_id: BeltTileId = *id; + + // Add the "underground" belt locs + let res = lengthen( + game_state, + self_id, + BELT_LEN_PER_TILE + * u16::try_from( + new_belt_pos.x.abs_diff(pos.x) + new_belt_pos.y.abs_diff(pos.y), + ) + .unwrap(), + Side::BACK, + ); + + // Connect them together + let (new_id, new_len) = merge_belts( + &mut game_state.simulation_state, + self_id, + entrance_id, + data_store, + ); + + if self_id != entrance_id { + game_state + .world + .modify_belt_pos(entrance_id, res.new_belt_len.try_into().unwrap()); + } + if new_id == self_id { + game_state.world.update_belt_id( + &mut game_state.simulation_state, + entrance_id, + new_id, + data_store, + ); + } else { + game_state.world.update_belt_id( + &mut game_state.simulation_state, + self_id, + new_id, + data_store, + ); + } + }, + UndergroundDir::Exit => { + // The first underground we found was another Exit, so we do not need to connect, nor break anything + }, + } + } + }, + } + + #[cfg(debug_assertions)] + { + let all_belt_connections = game_state + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); + + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = game_state + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist) + ); + } + } + } } enum BeltChange { @@ -339,10 +1064,16 @@ fn handle_belt_breaking( (BeltState::Straight, BeltState::DoubleSideloading) => unreachable!("Should be impossible"), (BeltState::Curved, BeltState::Straight) => unreachable!("Should be impossible"), (BeltState::Curved, BeltState::Curved) => {}, - (BeltState::Curved, BeltState::Sideloading) | (BeltState::Curved, BeltState::DoubleSideloading) => { - let entity = game_state.world.get_entities_colliding_with(pos_which_might_break, (1,1), data_store).into_iter().next().unwrap(); + (BeltState::Curved, BeltState::Sideloading) + | (BeltState::Curved, BeltState::DoubleSideloading) => { + let entity = game_state + .world + .get_entities_colliding_with(pos_which_might_break, (1, 1), data_store) + .into_iter() + .next() + .unwrap(); - let (id,belt_pos_to_break_at) = match entity { + let (id, belt_pos_to_break_at) = match entity { Entity::Belt { belt_pos, id, .. } => (*id, *belt_pos), Entity::Underground { .. } => { // Undergrounds cannot be curved, so this is not a problem @@ -352,19 +1083,34 @@ fn handle_belt_breaking( // Splitters cannot be curved, so this is not a problem return; }, - e => unreachable!("{e:?} does not have a belt_pos") + e => unreachable!("{e:?} does not have a belt_pos"), }; - let res = game_state.simulation_state.factory.belts.break_belt_at(id, belt_pos_to_break_at); + let res = game_state + .simulation_state + .factory + .belts + .break_belt_at(id, belt_pos_to_break_at); match res.new_belt { Some((new_id, new_belt_side)) => { match new_belt_side { - Side::FRONT => unimplemented!("In the currerent implementation we will always keep the Front."), + Side::FRONT => unimplemented!( + "In the currerent implementation we will always keep the Front." + ), Side::BACK => { // FIXME: Understand this + 1 - game_state.world.update_belt_id_after(res.kept_id, new_id, belt_pos_to_break_at + 1); - game_state.world.modify_belt_pos(new_id, -i16::try_from(belt_pos_to_break_at).unwrap()); + game_state.world.update_belt_id_after( + &mut game_state.simulation_state, + res.kept_id, + new_id, + belt_pos_to_break_at + 1, + data_store, + ); + game_state.world.modify_belt_pos( + new_id, + -i16::try_from(belt_pos_to_break_at).unwrap(), + ); let new_len = game_state.simulation_state.factory.belts.get_len(new_id); @@ -372,7 +1118,10 @@ fn handle_belt_breaking( .simulation_state .factory .belts - .add_sideloading_inserter(new_id, (res.kept_id, belt_pos_to_break_at - 1)); + .add_sideloading_inserter( + new_id, + (res.kept_id, belt_pos_to_break_at - 1), + ); }, } }, @@ -383,7 +1132,67 @@ fn handle_belt_breaking( (BeltState::Sideloading, BeltState::Curved) => unreachable!("Should be impossible"), (BeltState::Sideloading, BeltState::Sideloading) => {}, (BeltState::Sideloading, BeltState::DoubleSideloading) => {}, - (BeltState::DoubleSideloading, _) => unreachable!("For the belt to be DoubleSideloading before, there would have to have been a belt here before"), + (BeltState::DoubleSideloading, _) => unreachable!( + "For the belt to be DoubleSideloading before, there would have to have been a belt here before" + ), + } + + #[cfg(debug_assertions)] + { + let all_belt_connections = game_state + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); + + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = game_state + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist) + ); + } + } } } @@ -411,13 +1220,19 @@ fn attach_to_back_of_belt( ); if final_belt_id == front_belt_id { - game_state - .world - .update_belt_id(back_belt_id_in_sim, final_belt_id); + game_state.world.update_belt_id( + &mut game_state.simulation_state, + back_belt_id_in_sim, + final_belt_id, + data_store, + ); } else { - game_state - .world - .update_belt_id(final_belt_id, back_belt_id_in_sim); + game_state.world.update_belt_id( + &mut game_state.simulation_state, + final_belt_id, + back_belt_id_in_sim, + data_store, + ); } (final_belt_id, final_belt_len) @@ -511,6 +1326,7 @@ fn get_belt_out_dir( .flatten() } +#[derive(Debug, Clone, Copy)] struct LengthenResult { belt_pos_of_segment: u16, new_belt_len: u16, @@ -538,8 +1354,8 @@ fn lengthen( .expect("TODO: This could fail even if the belt is not too long!"), ); LengthenResult { - belt_pos_of_segment: new_len, - new_belt_len: amount, + belt_pos_of_segment: amount, + new_belt_len: new_len, } }, Side::BACK => LengthenResult { @@ -591,7 +1407,32 @@ fn should_merge( underground_dir: UndergroundDir::Exit, .. } => None, - Entity::Splitter { .. } => Some(todo!("get the id from the simstate")), + Entity::Splitter { + pos, + id, + direction: splitter_dir, + .. + } => { + let mut side = if front_pos == *pos { + SplitterSide::Left + } else { + SplitterSide::Right + }; + + match splitter_dir { + Dir::North | Dir::East => {}, + Dir::South | Dir::West => side = side.switch(), + } + + // TODO: Only calculate what we need + let [inputs, _] = game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id); + + Some(inputs[usize::from(bool::from(side))]) + }, _ => unreachable!(), } } else { @@ -679,7 +1520,7 @@ fn should_sideload( } } -fn expected_belt_state(belt_dir: Dir, gets_input_from: impl FnMut(&Dir) -> bool) -> BeltState { +pub fn expected_belt_state(belt_dir: Dir, gets_input_from: impl FnMut(&Dir) -> bool) -> BeltState { let input_dirs: Vec = Dir::iter().filter(gets_input_from).collect(); // Output dirs are unused for determining this, interesting! // let output_dirs: Vec = Dir::iter().filter(|dir| dir_info(*dir) == Some(BeltDir::Ouput)).collect(); @@ -736,20 +1577,21 @@ mod test { use proptest::prelude::{Just, Strategy}; use proptest::{prop_assert, prop_assume, proptest}; + use crate::DATA_STORE; use crate::blueprint::Blueprint; - use crate::frontend::action::set_recipe::SetRecipeInfo; use crate::frontend::action::ActionType; - use crate::frontend::world::tile::{AssemblerInfo, Entity, InserterInfo, PlaceEntityType}; + use crate::frontend::action::set_recipe::SetRecipeInfo; use crate::frontend::world::Position; + use crate::frontend::world::tile::{AssemblerInfo, Dir, Entity, InserterInfo, PlaceEntityType}; use crate::item::Recipe; use crate::rendering::app_state::GameState; - use crate::DATA_STORE; fn chest_onto_belt() -> 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 }, @@ -763,11 +1605,20 @@ mod test { 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, + }), ]) } @@ -776,18 +1627,22 @@ mod test { 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, }), ]) } @@ -809,6 +1664,31 @@ mod test { #[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(Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + + let ent = state.world.get_entities_colliding_with(Position { x: 1600, y: 1603 }, (1, 1), &DATA_STORE).into_iter().next().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_entities_colliding_with(Position { x: 1602, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next().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 }; @@ -827,7 +1707,7 @@ mod test { let ent = state.world.get_entities_colliding_with(Position { x: 1602, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next().unwrap(); - let inserter_attached = matches!(ent, Entity::Inserter { pos, direction, info: InserterInfo::Attached { .. } }); + let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); prop_assert!(inserter_attached, "{:?}", ent); } @@ -852,7 +1732,7 @@ mod test { #[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 mut state = GameState::new( &DATA_STORE); let bp = Blueprint { actions }; @@ -866,7 +1746,7 @@ mod test { let ent = state.world.get_entities_colliding_with(Position { x: 1602, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next().unwrap(); - let inserter_attached = matches!(ent, Entity::Inserter { pos, direction, info: InserterInfo::Attached { .. } }); + let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); prop_assume!(inserter_attached, "{:?}", ent); @@ -874,7 +1754,7 @@ mod test { state.update(&DATA_STORE); } - let Some(Entity::Belt { pos, direction, id, belt_pos }) = state.world.get_entities_colliding_with(Position { x: 1602, y: 1601 }, (1, 1), &DATA_STORE).into_iter().next() else { + let Some(Entity::Belt { id, .. }) = state.world.get_entities_colliding_with(Position { x: 1602, y: 1601 }, (1, 1), &DATA_STORE).into_iter().next() else { unreachable!() }; @@ -893,23 +1773,35 @@ mod test { bp.apply(Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - let inserter_attached = matches!(state.world.get_entities_colliding_with(Position { x: 1602, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next().unwrap(), Entity::Inserter { pos, direction, info: InserterInfo::Attached { .. } }); + let ent = state.world.get_entities_colliding_with(Position { x: 1600, y: 1603 }, (1, 1), &DATA_STORE).into_iter().next().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_entities_colliding_with(Position { x: 1602, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next().unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); prop_assume!(inserter_attached); - for _ in 0..200 { + for _ in 0..2000 { state.update(&DATA_STORE); } - let Some(Entity::Belt { pos, direction, id: id_going_right, belt_pos }) = state.world.get_entities_colliding_with(Position { x: 1602, y: 1601 }, (1, 1), &DATA_STORE).into_iter().next() else { + let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entities_colliding_with(Position { x: 1602, y: 1601 }, (1, 1), &DATA_STORE).into_iter().next() else { unreachable!() }; - let Some(Entity::Belt { pos, direction, id: id_going_down, belt_pos }) = state.world.get_entities_colliding_with(Position { x: 1604, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next() else { + let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entities_colliding_with(Position { x: 1604, y: 1602 }, (1, 1), &DATA_STORE).into_iter().next() else { unreachable!() }; - prop_assume!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); + 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::>()); } diff --git a/src/frontend/action/mod.rs b/src/frontend/action/mod.rs index eb61fc7..82f081b 100644 --- a/src/frontend/action/mod.rs +++ b/src/frontend/action/mod.rs @@ -1,10 +1,16 @@ +use std::num::NonZero; + use place_entity::PlaceEntityInfo; use place_tile::PlaceFloorTileByHandInfo; use set_recipe::SetRecipeInfo; -use crate::item::WeakIdxTrait; +use crate::{ + data::DataStore, + item::{IdxTrait, WeakIdxTrait}, + research::Technology, +}; -use super::world::Position; +use super::world::{Position, tile::PlaceEntityType}; pub mod action_state_machine; pub mod belt_placement; @@ -22,12 +28,163 @@ pub enum ActionType { SetRecipe(SetRecipeInfo), + OverrideInserterMovetime { + pos: Position, + new_movetime: Option>, + }, + Position(PLAYERID, (f32, f32)), - AddModules { pos: Position, modules: Vec }, - RemoveModules { pos: Position, indices: Vec }, + AddModules { + pos: Position, + modules: Vec, + }, + RemoveModules { + pos: Position, + indices: Vec, + }, + + SetChestSlotLimit { + pos: Position, + num_slots: u8, + }, Remove(Position), + SetActiveResearch { + tech: Option, + }, + + CheatUnlockTechnology { + tech: Technology, + }, + Ping(Position), } + +impl ActionType { + pub fn get_pos(&self) -> Option { + match self { + ActionType::PlaceFloorTile(place_floor_tile_by_hand_info) => todo!(), + ActionType::PlaceEntity(place_entity_info) => match &place_entity_info.entities { + place_entity::EntityPlaceOptions::Single(place_entity_type) => { + match place_entity_type { + super::world::tile::PlaceEntityType::Assembler { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Inserter { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Belt { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Underground { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::PowerPole { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Splitter { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Chest { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::SolarPanel { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Lab { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::Beacon { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::FluidTank { pos, .. } => Some(*pos), + super::world::tile::PlaceEntityType::MiningDrill { pos, .. } => Some(*pos), + } + }, + place_entity::EntityPlaceOptions::Multiple(place_entity_types) => todo!(), + }, + ActionType::SetRecipe(set_recipe_info) => Some(set_recipe_info.pos), + ActionType::OverrideInserterMovetime { pos, .. } => Some(*pos), + ActionType::Position(_, _) => todo!(), + ActionType::AddModules { pos, .. } => Some(*pos), + ActionType::RemoveModules { pos, .. } => Some(*pos), + ActionType::SetChestSlotLimit { pos, .. } => Some(*pos), + ActionType::Remove(position) => Some(*position), + ActionType::SetActiveResearch { .. } => None, + ActionType::CheatUnlockTechnology { .. } => None, + ActionType::Ping(position) => Some(*position), + } + } + + pub fn get_building_size( + &self, + data_store: &DataStore, + ) -> Option<[u16; 2]> { + match self { + ActionType::PlaceFloorTile(place_floor_tile_by_hand_info) => todo!(), + ActionType::PlaceEntity(place_entity_info) => match &place_entity_info.entities { + place_entity::EntityPlaceOptions::Single(place_entity_type) => { + match place_entity_type { + PlaceEntityType::Assembler { ty, rotation, .. } => Some( + data_store.assembler_info[usize::from(*ty)] + .size(*rotation) + .into(), + ), + // FIXME: ty + PlaceEntityType::Inserter { + pos: _, + dir: _, + filter: _, + } => Some([1, 1]), + PlaceEntityType::Belt { .. } => Some([1, 1]), + PlaceEntityType::Underground { .. } => Some([1, 1]), + PlaceEntityType::PowerPole { ty, .. } => { + Some(data_store.power_pole_data[*ty as usize].size.into()) + }, + PlaceEntityType::Splitter { direction, .. } => { + Some(match direction.compare(super::world::tile::Dir::North) { + super::world::tile::DirRelative::SameDir + | super::world::tile::DirRelative::Opposite => [2, 1], + super::world::tile::DirRelative::Turned => [1, 2], + }) + }, + PlaceEntityType::Chest { ty, .. } => { + Some(data_store.chest_tile_sizes[*ty as usize].into()) + }, + PlaceEntityType::SolarPanel { ty, .. } => { + Some(data_store.solar_panel_info[*ty as usize].size) + }, + PlaceEntityType::Lab { ty, .. } => { + Some(data_store.lab_info[*ty as usize].size.into()) + }, + PlaceEntityType::Beacon { ty, .. } => { + Some(data_store.beacon_info[*ty as usize].size.into()) + }, + PlaceEntityType::FluidTank { ty, .. } => { + Some(data_store.fluid_tank_infos[*ty as usize].size.into()) + }, + PlaceEntityType::MiningDrill { ty, .. } => { + Some(data_store.mining_drill_info[*ty as usize].size) + }, + } + }, + place_entity::EntityPlaceOptions::Multiple(place_entity_types) => todo!(), + }, + ActionType::SetRecipe(_) => None, + ActionType::OverrideInserterMovetime { .. } => None, + ActionType::Position(_, _) => None, + ActionType::AddModules { .. } => None, + ActionType::RemoveModules { .. } => None, + ActionType::SetChestSlotLimit { .. } => None, + ActionType::Remove(_) => None, + ActionType::SetActiveResearch { .. } => None, + ActionType::CheatUnlockTechnology { .. } => None, + ActionType::Ping(_) => None, + } + } + + pub fn get_effect_size( + &self, + data_store: &DataStore, + ) -> Option<[u16; 2]> { + self.get_building_size(data_store).or(match self { + ActionType::PlaceFloorTile(place_floor_tile_by_hand_info) => todo!(), + ActionType::PlaceEntity(place_entity_info) => match &place_entity_info.entities { + place_entity::EntityPlaceOptions::Single(place_entity_type) => None, + place_entity::EntityPlaceOptions::Multiple(place_entity_types) => None, + }, + ActionType::SetRecipe(set_recipe_info) => Some([1, 1]), + ActionType::OverrideInserterMovetime { .. } => Some([1, 1]), + ActionType::Position(_, _) => None, + ActionType::AddModules { .. } => Some([1, 1]), + ActionType::RemoveModules { .. } => Some([1, 1]), + ActionType::SetChestSlotLimit { .. } => Some([1, 1]), + ActionType::Remove(position) => Some([1, 1]), + ActionType::SetActiveResearch { .. } => None, + ActionType::CheatUnlockTechnology { .. } => None, + ActionType::Ping(position) => None, + }) + } +} diff --git a/src/frontend/action/place_tile.rs b/src/frontend/action/place_tile.rs index 4619112..2e63aac 100644 --- a/src/frontend/action/place_tile.rs +++ b/src/frontend/action/place_tile.rs @@ -1,10 +1,4 @@ -use crate::{ - frontend::world::{ - tile::{FloorTile, World}, - Position, - }, - item::IdxTrait, -}; +use crate::frontend::world::{Position, tile::FloorTile}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PlaceFloorTileByHandInfo { @@ -19,13 +13,12 @@ pub struct PlaceFloorTileGhostInfo { pub position: PositionInfo, } -// TODO: Do not use usize for anything that might go to another machine, where it could be different size! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum PositionInfo { Rect { pos: Position, - width: usize, - height: usize, + width: u32, + height: u32, }, Single { pos: Position, @@ -34,42 +27,3 @@ pub enum PositionInfo { positions: Vec, }, } - -impl PlaceFloorTileByHandInfo { - pub(super) fn still_valid( - &self, - world: &World, - ) -> bool { - if !self.ghost_info.still_valid(world) { - return false; - } - - // Check the inventory requirementsa when placing from hand - match self.ghost_info.tile { - FloorTile::Empty => true, - FloorTile::Concrete => { - todo!() - }, - FloorTile::Water => true, - } - } -} - -impl PlaceFloorTileGhostInfo { - fn still_valid( - &self, - world: &World, - ) -> bool { - match self.tile { - FloorTile::Empty => { - unreachable!("Invalid action, trying to place \"Empty Tile\": {:?}", self) - }, - FloorTile::Concrete => { - todo!() - }, - FloorTile::Water => { - unreachable!("Invalid action, trying to place \"Water\": {:?}", self) - }, - } - } -} diff --git a/src/frontend/input.rs b/src/frontend/input.rs index 27e716b..3eefca3 100644 --- a/src/frontend/input.rs +++ b/src/frontend/input.rs @@ -1,22 +1,24 @@ use eframe::egui; use winit::{dpi::PhysicalPosition, event::MouseScrollDelta, keyboard::KeyCode}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum Input { - LeftClickPressed, + LeftClickPressed { shift: bool }, LeftClickReleased, - RightClickPressed, + RightClickPressed { shift: bool }, RightClickReleased, MouseMove(f32, f32), KeyPress(Key), KeyRelease(Key), + Copy, + MouseScoll(MouseScrollDelta), UnknownInput(UnknownInput), } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy)] enum UnknownInput { UnknownKeyInput(winit::keyboard::NativeKeyCode), } @@ -48,11 +50,17 @@ impl TryFrom for Input { let ret = match event { eframe::egui::Event::Key { key, - physical_key, + physical_key: _, pressed, - repeat, + repeat: _, modifiers, } => { + let key = EguiInputState { + key, + shift: modifiers.shift, + ctrl: modifiers.ctrl, + }; + if pressed { Ok(Input::KeyPress(key.try_into()?)) } else { @@ -61,13 +69,17 @@ impl TryFrom for Input { }, eframe::egui::Event::PointerMoved(pos2) => Ok(Input::MouseMove(pos2.x, pos2.y)), eframe::egui::Event::PointerButton { - pos, + pos: _, button, pressed, modifiers, } => match (pressed, button) { - (true, eframe::egui::PointerButton::Primary) => Ok(Input::LeftClickPressed), - (true, eframe::egui::PointerButton::Secondary) => Ok(Input::RightClickPressed), + (true, eframe::egui::PointerButton::Primary) => Ok(Input::LeftClickPressed { + shift: modifiers.shift, + }), + (true, eframe::egui::PointerButton::Secondary) => Ok(Input::RightClickPressed { + shift: modifiers.shift, + }), (true, eframe::egui::PointerButton::Middle) => Err(()), (true, eframe::egui::PointerButton::Extra1) => Err(()), (true, eframe::egui::PointerButton::Extra2) => Err(()), @@ -93,6 +105,7 @@ impl TryFrom for Input { )), eframe::egui::MouseWheelUnit::Page => Err(()), }, + egui::Event::Copy => Ok(Self::Copy), _ => Err(()), }; @@ -103,13 +116,17 @@ impl TryFrom for Input { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Key { + Shift, W, A, S, D, Q, - R, P, + T, + M, + V, + H, Key0, Key1, Key2, @@ -120,6 +137,9 @@ pub enum Key { Key7, Key8, Key9, + R, + ShiftR, + Esc, } impl TryFrom for Key { @@ -134,6 +154,7 @@ impl TryFrom for Key { KeyCode::KeyQ => Key::Q, KeyCode::KeyR => Key::R, KeyCode::KeyP => Key::P, + KeyCode::KeyT => Key::T, KeyCode::Digit0 => Key::Key0, KeyCode::Digit1 => Key::Key1, KeyCode::Digit2 => Key::Key2, @@ -144,6 +165,7 @@ impl TryFrom for Key { KeyCode::Digit7 => Key::Key7, KeyCode::Digit8 => Key::Key8, KeyCode::Digit9 => Key::Key9, + KeyCode::ShiftLeft | KeyCode::ShiftRight => Key::Shift, _ => return Err(()), }; @@ -152,28 +174,40 @@ impl TryFrom for Key { } } -impl TryFrom for Key { +struct EguiInputState { + key: egui::Key, + shift: bool, + ctrl: bool, +} + +impl TryFrom for Key { type Error = (); - fn try_from(value: egui::Key) -> Result { - let ret = match value { - egui::Key::W => Key::W, - egui::Key::A => Key::A, - egui::Key::S => Key::S, - egui::Key::D => Key::D, - egui::Key::Q => Key::Q, - egui::Key::R => Key::R, - egui::Key::P => Key::P, - egui::Key::Num0 => Key::Key0, - egui::Key::Num1 => Key::Key1, - egui::Key::Num2 => Key::Key2, - egui::Key::Num3 => Key::Key3, - egui::Key::Num4 => Key::Key4, - egui::Key::Num5 => Key::Key5, - egui::Key::Num6 => Key::Key6, - egui::Key::Num7 => Key::Key7, - egui::Key::Num8 => Key::Key8, - egui::Key::Num9 => Key::Key9, + fn try_from(value: EguiInputState) -> Result { + let ret = match (value.key, value.shift, value.ctrl) { + (egui::Key::W, _, false) => Key::W, + (egui::Key::A, _, false) => Key::A, + (egui::Key::S, _, false) => Key::S, + (egui::Key::D, _, false) => Key::D, + (egui::Key::Q, _, false) => Key::Q, + (egui::Key::R, false, false) => Key::R, + (egui::Key::R, true, false) => Key::ShiftR, + (egui::Key::T, _, false) => Key::T, + (egui::Key::P, _, false) => Key::P, + (egui::Key::M, _, false) => Key::M, + (egui::Key::V, _, false) => Key::V, + (egui::Key::H, _, false) => Key::H, + (egui::Key::Num0, _, false) => Key::Key0, + (egui::Key::Num1, _, false) => Key::Key1, + (egui::Key::Num2, _, false) => Key::Key2, + (egui::Key::Num3, _, false) => Key::Key3, + (egui::Key::Num4, _, false) => Key::Key4, + (egui::Key::Num5, _, false) => Key::Key5, + (egui::Key::Num6, _, false) => Key::Key6, + (egui::Key::Num7, _, false) => Key::Key7, + (egui::Key::Num8, _, false) => Key::Key8, + (egui::Key::Num9, _, false) => Key::Key9, + (egui::Key::Escape, _, false) => Key::Esc, _ => return Err(()), }; diff --git a/src/frontend/world/mod.rs b/src/frontend/world/mod.rs index 9ea5207..6e79b0c 100644 --- a/src/frontend/world/mod.rs +++ b/src/frontend/world/mod.rs @@ -6,23 +6,23 @@ pub mod tile; Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, )] pub struct Position { - pub x: usize, - pub y: usize, + pub x: i32, + pub y: i32, } impl Position { pub fn contained_in(self, other: Position, size: (u16, u16)) -> bool { self.x >= other.x && self.y >= other.y - && self.x < other.x + usize::from(size.0) - && self.y < other.y + usize::from(size.1) + && self.x < other.x + i32::from(size.0) + && self.y < other.y + i32::from(size.1) } pub fn overlap(self, self_size: (u16, u16), other: Position, size: (u16, u16)) -> bool { - !((self.x + usize::from(self_size.0)) <= other.x - || (self.y + usize::from(self_size.1)) <= other.y - || (self.x) >= (other.x + usize::from(size.0)) - || (self.y) >= (other.y + usize::from(size.1))) + !((self.x + i32::from(self_size.0)) <= other.x + || (self.y + i32::from(self_size.1)) <= other.y + || (self.x) >= (other.x + i32::from(size.0)) + || (self.y) >= (other.y + i32::from(size.1))) } } @@ -30,9 +30,8 @@ impl Position { mod test { use std::cmp::max; - use crate::{blueprint::random_position, frontend::world::Position}; + use crate::blueprint::random_position; use itertools::Itertools; - use proptest::prelude::ProptestConfig; use proptest::{prop_assert, prop_assert_eq, prop_assume, proptest}; proptest! { @@ -46,15 +45,15 @@ mod test { fn position_contained_in(position in random_position(), test_position in random_position(), size in (1u16..10, 1u16..10)) { prop_assume!(test_position.x >= position.x); prop_assume!(test_position.y >= position.y); - prop_assume!(test_position.x < position.x + size.0 as usize); - prop_assume!(test_position.y < position.y + size.1 as usize); + prop_assume!(test_position.x < position.x + size.0 as i32); + prop_assume!(test_position.y < position.y + size.1 as i32); prop_assert!(test_position.contained_in(position, size)); } #[test] fn position_not_contained_in(position in random_position(), test_position in random_position(), size in (1u16..10, 1u16..10)) { - prop_assume!(test_position.x < position.x || test_position.y < position.y || test_position.x >= position.x + size.0 as usize || test_position.y >= position.y + size.1 as usize); + prop_assume!(test_position.x < position.x || test_position.y < position.y || test_position.x >= position.x + size.0 as i32 || test_position.y >= position.y + size.1 as i32); prop_assert!(!test_position.contained_in(position, size)); } @@ -66,10 +65,10 @@ mod test { #[test] fn position_contained_in_sized(position in random_position(), test_position in random_position(), self_size in (1u16..100, 1u16..100), other_size in (1u16..100, 1u16..100)) { - prop_assume!(test_position.x + self_size.0 as usize >= position.x); - prop_assume!(test_position.y + self_size.1 as usize >= position.y); - prop_assume!(test_position.x < position.x + other_size.0 as usize); - prop_assume!(test_position.y < position.y + other_size.1 as usize); + prop_assume!(test_position.x + self_size.0 as i32 >= position.x); + prop_assume!(test_position.y + self_size.1 as i32 >= position.y); + prop_assume!(test_position.x < position.x + other_size.0 as i32); + prop_assume!(test_position.y < position.y + other_size.1 as i32); prop_assert!(position.overlap(self_size, position, other_size)); } @@ -77,35 +76,39 @@ mod test { #[test] fn super_stupid_test_unsized(position in random_position(), test_position in random_position(), size in (1u16..10, 1u16..10)) { - let grid_size = max(position.x + size.0 as usize, max(test_position.x, max(position.y + size.1 as usize, test_position.y))) + 1; + prop_assume!(position.x >= 0 && position.y >= 0); - let mut grid = vec![vec![false; grid_size]; grid_size]; + let grid_size = max(position.x + size.0 as i32, max(test_position.x, max(position.y + size.1 as i32, test_position.y))) + 1; + + let mut grid = vec![vec![false; grid_size.try_into().unwrap()]; grid_size.try_into().unwrap()]; for x_offs in 0..size.0 { for y_offs in 0..size.1 { - grid[position.x + x_offs as usize][position.y + y_offs as usize] = true; + grid[(position.x + x_offs as i32) as usize][(position.y + y_offs as i32) as usize] = true; } } - prop_assert_eq!(grid[test_position.x][test_position.y], test_position.contained_in(position, size)) + prop_assert_eq!(grid[test_position.x as usize][test_position.y as usize], test_position.contained_in(position, size)) } #[test] fn super_stupid_test_unsized_sized(position in random_position(), test_position in random_position(), size in (1u16..3, 1u16..3), test_size in (1u16..3, 1u16..3)) { - let grid_size = max(position.x + size.0 as usize, max(test_position.x + test_size.0 as usize, max(position.y + size.1 as usize, test_position.y + test_size.1 as usize))) + 1; + prop_assume!(position.x >= 0 && position.y >= 0); + + let grid_size = max(position.x as usize + size.0 as usize, max(test_position.x as usize + test_size.0 as usize, max(position.y as usize + size.1 as usize, test_position.y as usize + test_size.1 as usize))) + 1; let mut grid = vec![vec![false; grid_size]; grid_size]; for x_offs in 0..size.0 { for y_offs in 0..size.1 { - grid[position.x + x_offs as usize][position.y + y_offs as usize] = true; + grid[position.x as usize + x_offs as usize][position.y as usize + y_offs as usize] = true; } } let mut hit = false; for x_offs in 0..test_size.0 { for y_offs in 0..test_size.1 { - hit |= grid[test_position.x + x_offs as usize][test_position.y + y_offs as usize]; + hit |= grid[test_position.x as usize + x_offs as usize][test_position.y as usize + y_offs as usize]; } } prop_assert_eq!(hit, test_position.overlap(test_size, position, size), "grid_size: {:?}, grid: {:?}", grid_size, grid[1600..].iter().map(|v| &v[1600..]).collect_vec()); diff --git a/src/frontend/world/sparse_grid.rs b/src/frontend/world/sparse_grid.rs index 0df18a5..f924394 100644 --- a/src/frontend/world/sparse_grid.rs +++ b/src/frontend/world/sparse_grid.rs @@ -1,99 +1,41 @@ use std::collections::HashMap; +use std::hash::Hash; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct SparseGrid { - width: usize, - height: usize, - values: HashMap<(usize, usize), T>, +pub struct SparseGrid { + values: HashMap<(I, I), T>, } -impl SparseGrid { - pub fn new(width: usize, height: usize) -> Self { +impl SparseGrid { + pub fn new() -> Self { Self { - width, - height, values: HashMap::new(), } } - pub fn get_default(&mut self, x: usize, y: usize) -> &T + pub fn get_default(&mut self, x: I, y: I) -> &T where T: Default, { - assert!( - x < self.width, - "index {x} out of bounds for width {}", - self.width - ); - assert!( - y < self.height, - "index {y} out of bounds for height {}", - self.height - ); - self.values.entry((x, y)).or_default() } - pub fn get(&self, x: usize, y: usize) -> Option<&T> { - assert!( - x < self.width, - "index {x} out of bounds for width {}", - self.width - ); - assert!( - y < self.height, - "index {y} out of bounds for height {}", - self.height - ); - + pub fn get(&self, x: I, y: I) -> Option<&T> { self.values.get(&(x, y)) } - pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut T> { - assert!( - x < self.width, - "index {x} out of bounds for width {}", - self.width - ); - assert!( - y < self.height, - "index {y} out of bounds for height {}", - self.height - ); - + 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: usize, y: usize, value: T) -> Option { - assert!( - x < self.width, - "index {x} out of bounds for width {}", - self.width - ); - assert!( - y < self.height, - "index {y} out of bounds for height {}", - self.height - ); - + pub fn insert(&mut self, x: I, y: I, value: T) -> Option { self.values.insert((x, y), value) } - pub fn insert_deduplicate(&mut self, x: usize, y: usize, value: T) -> Option + pub fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option where T: PartialEq + Default, { - assert!( - x < self.width, - "index {x} out of bounds for width {}", - self.width - ); - assert!( - y < self.height, - "index {y} out of bounds for height {}", - self.height - ); - if value == T::default() { self.values.remove(&(x, y)) } else { @@ -101,11 +43,11 @@ impl SparseGrid { } } - pub fn occupied_entries(&self) -> impl Iterator { + pub fn occupied_entries(&self) -> impl Iterator { self.values.iter().map(|(a, b)| (*a, b)) } - pub fn occupied_entries_mut(&mut self) -> impl Iterator { + pub fn occupied_entries_mut(&mut self) -> impl Iterator { self.values.iter_mut().map(|(a, b)| (*a, b)) } } diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 3400b1b..699509b 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1,46 +1,106 @@ +use egui::Color32; +use log::error; use std::{ + cmp::min, collections::{BTreeMap, BTreeSet, HashMap}, marker::PhantomData, + mem, + num::NonZero, ops::{Add, ControlFlow}, }; use enum_map::{Enum, EnumMap}; +use log::{info, warn}; use strum::EnumIter; use itertools::Itertools; +use noise::{NoiseFn, Simplex}; + use crate::{ - belt::{splitter::SplitterDistributionMode, BeltTileId, SplitterTileId}, - data::DataStore, - frontend::world, - inserter::Storage, - item::{usize_from, IdxTrait, Item, Recipe, WeakIdxTrait}, - network_graph::WeakIndex, - power::power_grid::{PowerGridEntity, PowerGridIdentifier}, - rendering::app_state::{calculate_inserter_positions, SimulationState}, TICKS_PER_SECOND_LOGIC, + belt::{ + BeltBeltInserterAdditionInfo, BeltTileId, SplitterTileId, + splitter::{SPLITTER_BELT_LEN, SplitterDistributionMode, SplitterSide}, + }, + data::{AllowedFluidDirection, DataStore, ItemRecipeDir}, + inserter::{ + HAND_SIZE, MOVETIME, StaticID, Storage, storage_storage_with_buckets::InserterIdentifier, + }, + item::{IdxTrait, Item, Recipe, WeakIdxTrait, usize_from}, + liquid::{ + CannotMixFluidsError, FluidConnectionDir, + connection_logic::can_fluid_tanks_connect_to_single_connection, + }, + network_graph::WeakIndex, + power::power_grid::{BeaconAffectedEntity, PowerGridEntity, PowerGridIdentifier}, + rendering::app_state::{ + InstantiateInserterError, SimulationState, calculate_inserter_positions, + }, }; +use crate::{inserter::FakeUnionStorage, item::Indexable}; +use static_assertions::const_assert; + +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; -use super::{sparse_grid::SparseGrid, Position}; +use serde::Deserializer; +use serde::Serializer; + +use std::iter; + +use noise::Seedable; +use petgraph::prelude::Bfs; + +use super::{Position, sparse_grid::SparseGrid}; +use crate::liquid::FluidSystemId; pub const BELT_LEN_PER_TILE: u16 = 4; -pub const CHUNK_SIZE: usize = 16; +pub const CHUNK_SIZE: u16 = 16; pub const CHUNK_SIZE_FLOAT: f32 = 16.0; -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub enum FloorTile { + #[default] Empty, Concrete, Water, } +pub enum InserterInstantiationNewOptions { + Positions(Vec), + Belts(Vec>), + PositionsAndBelts(Vec, Vec>), + All, +} + +// We rely on this, by storing entity indices as u8 in the chunk +const_assert!(CHUNK_SIZE * CHUNK_SIZE - 1 <= u8::MAX as u16); + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct Chunk { - pub floor_tiles: [[FloorTile; CHUNK_SIZE]; CHUNK_SIZE], + pub floor_tiles: Option>, + chunk_tile_to_entity_into: Option>, entities: Vec>, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum FloorOre { + AllSame { + ore: Item, + amounts: [[u32; CHUNK_SIZE as usize]; CHUNK_SIZE as usize], + }, + Mixed { + // We Box this here to not increase the RAM consumption of chunks which only contain a single type of ore (which are most chunks with ore) + mixed_ores: Box<[[(Item, u32); CHUNK_SIZE as usize]; CHUNK_SIZE as usize]>, + }, +} + +fn is_default(val: &T) -> bool { + *val == T::default() +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PlayerInfo { pub pos: (f32, f32), @@ -55,7 +115,7 @@ impl Default for PlayerInfo { Self { pos: (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), visible: false, - movement_speed: 1.0 / (TICKS_PER_SECOND_LOGIC as f32), + movement_speed: 10.0 / (TICKS_PER_SECOND_LOGIC as f32), inventory: Default::default(), } } @@ -63,23 +123,79 @@ impl Default for PlayerInfo { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct World { + noise: SerializableSimplex, + // TODO: I don´t think I want FP pub players: Vec, - chunks: SparseGrid>, + chunks: SparseGrid>, belt_lookup: BeltIdLookup, belt_recieving_input_directions: HashMap>, power_grid_lookup: PowerGridConnectedDevicesLookup, + + remaining_updates: Vec, + + pub to_instantiate: BTreeSet, + pub to_instantiate_by_belt: HashMap, Vec>, + + #[serde(skip)] + pub map_updates: Option>, +} + +#[derive(Debug, Clone)] +struct SerializableSimplex { + inner: Simplex, +} + +impl Deref for SerializableSimplex { + type Target = Simplex; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for SerializableSimplex { + // type Target = Simplex; + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl serde::Serialize for SerializableSimplex { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u32(self.inner.seed()) + } +} + +impl<'de> serde::Deserialize<'de> for SerializableSimplex { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seed = u32::deserialize(deserializer)?; + Ok(Self { + inner: Simplex::new(seed), + }) + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum WorldUpdate { + EntityNewlyPowered { pos: Position }, + NewEntity { pos: Position }, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct PowerGridConnectedDevicesLookup { - grid_to_chunks: BTreeMap>, + grid_to_chunks: BTreeMap>, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct BeltIdLookup { - belt_id_to_chunks: BTreeMap, BTreeSet<(usize, usize)>>, + belt_id_to_chunks: BTreeMap, BTreeSet<(i32, i32)>>, } #[derive(Debug, Clone, Copy)] @@ -90,235 +206,2260 @@ enum AddEntityError { ChunkMissingError(ChunkMissingError), } -impl World { - #[must_use] - pub fn new() -> Self { - let mut grid = SparseGrid::new(1_000_000, 1_000_000); +struct CascadingUpdate { + update: Box< + dyn FnOnce( + &mut World, + &mut SimulationState, + &mut Vec>, + &DataStore, + ) -> (), + >, +} - for x in 50..400 { - for y in 50..2000 { - grid.insert( - x, - y, - Chunk { - floor_tiles: [[FloorTile::Empty; CHUNK_SIZE]; CHUNK_SIZE], - entities: vec![], +fn try_attaching_fluids( + new_assembler_pos: Position, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("try_attaching_fluids"); + let Some( + e @ Entity::Assembler { + ty: assembler_ty, + pos: assembler_pos, + info: + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + }, + rotation: assembler_rotation, + .. + }, + ) = world + .get_entities_colliding_with(new_assembler_pos, (1, 1), data_store) + .into_iter() + .next() + else { + return; + }; + + assert_eq!(new_assembler_pos, *assembler_pos); + + let e_size: [u16; 2] = e.get_size(data_store).into(); + + world + .get_entities_colliding_with( + Position { + x: assembler_pos.x - 1, + y: assembler_pos.y - 1, + }, + (e_size[0] + 2, e_size[1] + 2), + data_store, + ) + .into_iter() + .for_each(|e| match e { + Entity::FluidTank { + ty, pos, rotation, .. + } => { + let assembler_size = e_size.into(); + + let recipe_fluid_inputs: Vec<_> = data_store.recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| { + (*dir == ItemRecipeDir::Ing + && data_store.item_is_fluid[item.into_usize()]) + .then_some(*item) + }) + .collect(); + let recipe_fluid_outputs: Vec<_> = data_store.recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| { + (*dir == ItemRecipeDir::Out + && data_store.item_is_fluid[item.into_usize()]) + .then_some(*item) + }) + .collect(); + + let fluid_pure_outputs: Vec<_> = data_store.assembler_info + [usize::from(*assembler_ty)] + .fluid_connections + .iter() + .filter(|(_conn, allowed)| { + *allowed == AllowedFluidDirection::Single(ItemRecipeDir::Out) + || matches!(*allowed, AllowedFluidDirection::Both { .. }) + }) + .collect(); + + let fluid_pure_inputs: Vec<_> = data_store.assembler_info + [usize::from(*assembler_ty)] + .fluid_connections + .iter() + .filter(|(_conn, allowed)| { + *allowed == AllowedFluidDirection::Single(ItemRecipeDir::Ing) + || matches!(*allowed, AllowedFluidDirection::Both { .. }) + }) + .collect(); + + // FIXME: FINISH IMPLEMENTING THIS + + let all_connections_with_items = recipe_fluid_inputs + .into_iter() + .cycle() + .zip(fluid_pure_inputs) + .zip(iter::repeat(FluidConnectionDir::Output)) + .chain( + recipe_fluid_outputs + .into_iter() + .cycle() + .zip(fluid_pure_outputs) + .zip(iter::repeat(FluidConnectionDir::Input)), + ); + + let in_out_connections = all_connections_with_items.filter_map( + move |((item, (fluid_conn, _allowed)), fluid_dir)| { + can_fluid_tanks_connect_to_single_connection( + *pos, + *ty, + *rotation, + *assembler_pos, + *fluid_conn, + *assembler_rotation, + assembler_size, + data_store, + ) + .map( + |(dest_conn, dest_conn_dir)| { + ( + fluid_dir, + item, + Storage::Assembler { + grid: id.grid, + index: id.assembler_index, + recipe_idx_with_this_item: data_store + .recipe_to_translated_index[&(id.recipe, item)], + }, + dest_conn, + Box::new(|_weak_index: WeakIndex| {}) + as Box ()>, + ) + }, + ) + }, + ); + for (dir, conn_fluid, conn_storage, conn_pos, cb) in in_out_connections { + let res = match dir { + FluidConnectionDir::Output => { + sim_state.factory.fluid_store.try_add_output( + *pos, + conn_fluid, + conn_storage, + conn_pos, + &mut sim_state.factory.chests, + &mut sim_state.factory.storage_storage_inserters, + data_store, + ) + }, + FluidConnectionDir::Input => { + sim_state.factory.fluid_store.try_add_input( + *pos, + conn_fluid, + conn_storage, + conn_pos, + &mut sim_state.factory.chests, + &mut sim_state.factory.storage_storage_inserters, + data_store, + ) + }, + }; + match res { + Ok(weak_index) => cb(weak_index), + Err(CannotMixFluidsError { items: fluids }) => { + let fluids = fluids.map(|item| { + &data_store.item_display_names[item.into_usize()] + }); + error!("Cannot mix {} and {}", fluids[0], fluids[1]); + }, + } + } + }, + _ => {}, + }); + }), + } +} + +fn try_instantiating_inserters_for_belt_cascade( + belt_id: BeltTileId, +) -> CascadingUpdate { + CascadingUpdate { + 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], + ); + 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()); + }), + } +} + +fn try_instantiating_inserters_for_belt( + belt_id: BeltTileId, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("try_instantiating_inserters_for_belt"); + if !world.to_instantiate_by_belt.contains_key(&belt_id) { + return; + } + + let by_belt = world.to_instantiate_by_belt.get_mut(&belt_id).unwrap(); + + if by_belt.len() > 1_000 { + warn!( + "More than 1_000 inserters waiting to be instantiated on belt {:?}: {}. This will cause lag!", + belt_id, + by_belt.len() + ); + } + + let mut tmp = Vec::default(); + + mem::swap(&mut tmp, &mut *by_belt); + + tmp.retain(|pos| { + match world.try_instantiate_inserter(sim_state, *pos, data_store) { + Ok(newly_instantiated) => { + match newly_instantiated { + InserterInstantiationNewOptions::Positions(positions) => updates + .extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })), + InserterInstantiationNewOptions::Belts(belts) => { + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })) + }, + InserterInstantiationNewOptions::PositionsAndBelts( + positions, + belts, + ) => { + updates.extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })); + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })); + }, + InserterInstantiationNewOptions::All => { + updates.push(try_instantiating_all_inserters_cascade()) + }, + } + + false + }, + Err(InstantiateInserterError::NotUnattachedInserter) => false, + Err( + InstantiateInserterError::ItemConflict { + belts_which_could_help, + } + | InstantiateInserterError::PleaseSpecifyFilter { + belts_which_could_help, + }, + ) => { + for belt in belts_which_could_help { + if belt_id != belt { + let by_belt = world.to_instantiate_by_belt.entry(belt).or_default(); + if !by_belt.contains(pos) { + by_belt.push(*pos); + } + } + } + true + }, + Err( + InstantiateInserterError::SourceMissing + | InstantiateInserterError::DestMissing, + ) => false, + } + }); + + let by_belt = world.to_instantiate_by_belt.get_mut(&belt_id).unwrap(); + + assert!(by_belt.is_empty()); + + mem::swap(&mut tmp, &mut *by_belt); + }), + } +} + +fn try_instantiating_all_inserters_cascade() +-> CascadingUpdate { + CascadingUpdate { + update: Box::new(|world, sim_state, updates, data_store| { + profiling::scope!("try_instantiating_all_inserters_cascade"); + if world.to_instantiate.len() > 1_000 { + warn!( + "More than 1_000 inserters waiting to be instantiated: {}. This will cause lag!", + world.to_instantiate.len() + ); + } + + let mut tmp = BTreeSet::default(); + + mem::swap(&mut tmp, &mut world.to_instantiate); + + tmp.retain(|pos| { + match world.try_instantiate_inserter(sim_state, *pos, data_store) { + Ok(newly_instantiated) => { + match newly_instantiated { + InserterInstantiationNewOptions::Positions(positions) => updates + .extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })), + InserterInstantiationNewOptions::Belts(belts) => { + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })) + }, + InserterInstantiationNewOptions::PositionsAndBelts( + positions, + belts, + ) => { + updates.extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })); + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })); + }, + InserterInstantiationNewOptions::All => { + updates.push(try_instantiating_all_inserters_cascade()) + }, + } + + false + }, + Err(InstantiateInserterError::NotUnattachedInserter) => { + warn!("We seem to have instantiated the same inserter twice?!?"); + false + }, + Err( + InstantiateInserterError::ItemConflict { + belts_which_could_help, + } + | InstantiateInserterError::PleaseSpecifyFilter { + belts_which_could_help, + }, + ) => { + for belt in belts_which_could_help { + let by_belt = world.to_instantiate_by_belt.entry(belt).or_default(); + if !by_belt.contains(pos) { + by_belt.push(*pos); + } + } + true + }, + Err(e) => { + info!("try_instantiate_inserter failed at {:?}, with {e:?}", pos); + false + }, + } + }); + + mem::swap(&mut tmp, &mut world.to_instantiate); + }), + } +} + +fn instantiate_inserter_cascade( + new_instantiate_pos: Position, + data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + match world.try_instantiate_inserter(sim_state, new_instantiate_pos, data_store) { + Ok(newly_instantiated) => { + match newly_instantiated { + InserterInstantiationNewOptions::Positions(positions) => { + updates.extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })) + }, + InserterInstantiationNewOptions::Belts(belts) => { + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })) + }, + InserterInstantiationNewOptions::PositionsAndBelts(positions, belts) => { + updates.extend(positions.into_iter().map(|new_pos| { + // FIXME: Size hardcoded + new_possible_inserter_connection(new_pos, (10, 10)) + })); + updates.extend(belts.into_iter().map(|new_belt| { + try_instantiating_inserters_for_belt_cascade(new_belt) + })); + }, + InserterInstantiationNewOptions::All => { + updates.push(try_instantiating_all_inserters_cascade()) + }, + } + }, + Err(InstantiateInserterError::NotUnattachedInserter) => { + warn!("We seem to have instantiated the same inserter twice?!?"); + }, + Err( + InstantiateInserterError::ItemConflict { + belts_which_could_help, + } + | InstantiateInserterError::PleaseSpecifyFilter { + belts_which_could_help, + }, + ) => { + world.to_instantiate.insert(new_instantiate_pos); + for belt in belts_which_could_help { + let by_belt = world.to_instantiate_by_belt.entry(belt).or_default(); + if !by_belt.contains(&new_instantiate_pos) { + by_belt.push(new_instantiate_pos); + } + } + }, + Err(e) => { + info!( + "try_instantiate_inserter failed at {:?}, with {e:?}", + new_instantiate_pos + ); + world.to_instantiate.insert(new_instantiate_pos); + }, + } + }), + } +} + +fn new_possible_inserter_connection( + pos: Position, + size: (u16, u16), +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, _sim_state, updates, data_store| { + profiling::scope!("new_possible_inserter_connection"); + let inserter_search_start_pos = Position { + x: pos.x - data_store.max_inserter_search_range as i32, + y: pos.y - data_store.max_inserter_search_range as i32, + }; + + let inserter_search_size = ( + 2 * data_store.max_inserter_search_range as u16 + size.0, + 2 * data_store.max_inserter_search_range as u16 + size.1, + ); + + world.mutate_entities_colliding_with( + inserter_search_start_pos, + inserter_search_size, + data_store, + |e| { + match e { + Entity::Inserter { + pos: inserter_pos, + direction: _inserter_dir, + filter: _inserter_filter, + info: InserterInfo::NotAttached { start_pos, end_pos }, + .. + } => { + if start_pos.contained_in(pos, size) || end_pos.contained_in(pos, size) + { + updates + .push(instantiate_inserter_cascade(*inserter_pos, data_store)); + } + }, + _ => {}, + } + ControlFlow::Continue(()) + }, + ); + }), + } +} + +fn new_lab_cascade( + pos: Position, + _data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("new_lab_cascade"); + let Some(Entity::Lab { + pos, + ty, + modules, + pole_position: Some((pole_pos, weak_index, index)), + }) = world + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next() + else { + warn!("Lab missing in new lab cascade"); + return; + }; + + let pos = *pos; + let size = data_store.lab_info[usize::from(*ty)].size; + let pole_pos = *pole_pos; + let index = *index; + + updates.push(new_possible_inserter_connection(pos, size)); + + let beacon_search_start_pos = Position { + x: pos.x - data_store.max_beacon_range.0 as i32, + y: pos.y - data_store.max_beacon_range.1 as i32, + }; + + let beacon_search_size = ( + 2 * data_store.max_beacon_range.0 as u16 + size.0, + 2 * data_store.max_beacon_range.1 as u16 + size.1, + ); + + for entity in world.get_entities_colliding_with( + beacon_search_start_pos, + beacon_search_size, + data_store, + ) { + match entity { + Entity::Beacon { + ty: beacon_ty, + pos: beacon_pos, + modules: beacon_modules, + pole_position: Some((beacon_pole_pos, beacon_weak_idx)), + } => { + let (beacon_range_x, beacon_range_y) = + data_store.beacon_info[usize::from(*beacon_ty)].effect_range; + + let (beacon_offs_x, beacon_offs_y) = + ((beacon_range_x - size.0) / 2, (beacon_range_y - size.1) / 2); + + if pos.overlap( + size, + Position { + x: beacon_pos.x - beacon_offs_x as i32, + y: beacon_pos.y - beacon_offs_y as i32, + }, + (beacon_range_x, beacon_range_y), + ) { + sim_state.factory.power_grids.add_beacon_affected_entity( + *beacon_pole_pos, + *beacon_weak_idx, + BeaconAffectedEntity::Lab { + grid: sim_state.factory.power_grids.pole_pos_to_grid_id + [&pole_pos], + index: index.try_into().unwrap(), + }, + data_store, + ); + } + }, + _ => {}, + } + } + }), + } +} + +fn new_power_pole( + pos: Position, + data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("new_power_pole"); + let pole = world + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next(); + + if let Some(Entity::PowerPole { + ty, pos: pole_pos, .. + }) = pole + { + let pole_pos = *pole_pos; + + let grid_id = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; + + let pole_size = data_store.power_pole_data[usize::from(*ty)].size; + let pole_power_range = data_store.power_pole_data[usize::from(*ty)].power_range; + + world.mutate_entities_colliding_with( + Position { + x: pole_pos.x - pole_power_range as i32, + y: pole_pos.y - pole_power_range as i32, + }, + ( + (pole_power_range as u16) * 2 + pole_size.0, + (pole_power_range as u16) * 2 + pole_size.1, + ), + data_store, + |e| { + match e { + Entity::Assembler { + ty, + pos, + modules, + info: info @ AssemblerInfo::Unpowered(_), + .. + } => { + let AssemblerInfo::Unpowered(recipe) = info else { + unreachable!(); + }; + + let (new_id, weak_index) = + sim_state.factory.power_grids.power_grids[usize::from(grid_id)] + .add_assembler( + *ty, grid_id, *recipe, &modules, pole_pos, *pos, + data_store, + ); + + *info = AssemblerInfo::Powered { + id: new_id, + pole_position: pole_pos, + weak_index, + }; + + updates.push(newly_working_assembler(*pos, data_store)); + }, + Entity::Assembler { + info: info @ AssemblerInfo::UnpoweredNoRecipe, + .. + } => { + *info = AssemblerInfo::PoweredNoRecipe(pole_pos); + }, + Entity::Roboport { + ty, + pos, + power_grid, + network, + id, + } => todo!(), + Entity::SolarPanel { + pos, + ty, + pole_position: pole_position @ None, + } => { + let weak_index = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_solar_panel(*pos, *ty, pole_pos, data_store); + + *pole_position = Some((pole_pos, weak_index)); + }, + Entity::Lab { + pos, + ty, + modules, + pole_position: pole_position @ None, + } => { + let (weak_index, index) = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_lab(*pos, *ty, &modules, pole_pos, data_store); + + *pole_position = Some((pole_pos, weak_index, index)); + + updates.push(new_lab_cascade(*pos, data_store)); + }, + Entity::Beacon { + ty, + pos, + modules, + pole_position: pole_position @ None, + } => { + let weak_index = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_beacon( + *ty, + *pos, + pole_pos, + modules.clone(), + vec![], + data_store, + ); + + *pole_position = Some((pole_pos, weak_index)); + + updates.push(new_powered_beacon_cascade(*pos, data_store)); + }, + e => { + // warn!("Entity {e:?} cannot accept power in start_powering_entity") + }, + } + ControlFlow::Continue(()) }, ); + } else { + warn!("Power pole disappeared, while new_power_pole was in the queue"); + } + }), + } +} + +fn new_chest_cascade( + pos: Position, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("new_chest_cascade"); + let Some(Entity::Chest { + ty, + pos, + item: _, + slot_limit: _, + }) = world + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next() + else { + return; + }; + + let size = data_store.chest_tile_sizes[usize::from(*ty)]; + + updates.push(new_possible_inserter_connection(*pos, size)); + }), + } +} + +fn new_powered_beacon_cascade( + pos: Position, + data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, _updates, data_store| { + profiling::scope!("new_powered_beacon_cascade"); + let Some(Entity::Beacon { + ty, + pos, + modules: _, + pole_position: Some((pole_pos, weak_idx)), + }) = world + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next() + else { + return; + }; + + let size = data_store.beacon_info[usize::from(*ty)].size; + let range = data_store.beacon_info[usize::from(*ty)].effect_range; + + let beacon_search_size = range; + + for affected_entity in world.get_entities_colliding_with( + Position { + x: pos.x - (range.0 as i32 - size.0 as i32) / 2, + y: pos.y - (range.1 as i32 - size.1 as i32) / 2, + }, + beacon_search_size, + data_store, + ) { + match affected_entity { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + pole_position: _, + weak_index: _, + }, + .. + } => { + sim_state.factory.power_grids.add_beacon_affected_entity( + *pole_pos, + *weak_idx, + BeaconAffectedEntity::Assembler { id: *id }, + data_store, + ); + }, + Entity::Lab { + pos: _, + ty: _, + modules: _, + pole_position: Some((lab_pole_pos, _lab_weak_idx, lab_idx)), + } => { + sim_state.factory.power_grids.add_beacon_affected_entity( + *pole_pos, + *weak_idx, + BeaconAffectedEntity::Lab { + grid: sim_state.factory.power_grids.pole_pos_to_grid_id + [lab_pole_pos], + index: (*lab_idx).into(), + }, + data_store, + ); + }, + _ => {}, + } + } + }), + } +} + +fn newly_working_assembler( + pos: Position, + data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("newly_working_assembler"); + let Some( + e @ Entity::Assembler { + ty, + pos, + modules: _, + info: + AssemblerInfo::Powered { + id, + pole_position: _, + weak_index: _, + }, + rotation, + }, + ) = world + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next() + else { + warn!("Assembler missing in new assembler cascade"); + return; + }; + + let pos = *pos; + let size = e.get_size(data_store); + let id = *id; + + updates.push(new_possible_inserter_connection(pos, size)); + updates.push(try_attaching_fluids(pos)); + + let beacon_search_start_pos = Position { + x: pos.x - data_store.max_beacon_range.0 as i32, + y: pos.y - data_store.max_beacon_range.1 as i32, + }; + + let beacon_search_size = ( + 2 * data_store.max_beacon_range.0 as u16 + size.0, + 2 * data_store.max_beacon_range.1 as u16 + size.1, + ); + + for entity in world.get_entities_colliding_with( + beacon_search_start_pos, + beacon_search_size, + data_store, + ) { + match entity { + Entity::Beacon { + ty: beacon_ty, + pos: beacon_pos, + modules: _, + pole_position: Some((beacon_pole_pos, beacon_weak_idx)), + } => { + let (beacon_range_x, beacon_range_y) = + data_store.beacon_info[usize::from(*beacon_ty)].effect_range; + + let (beacon_offs_x, beacon_offs_y) = + ((beacon_range_x - size.0) / 2, (beacon_range_y - size.1) / 2); + + if pos.overlap( + size, + Position { + x: beacon_pos.x - beacon_offs_x as i32, + y: beacon_pos.y - beacon_offs_y as i32, + }, + (beacon_range_x, beacon_range_y), + ) { + sim_state.factory.power_grids.add_beacon_affected_entity( + *beacon_pole_pos, + *beacon_weak_idx, + BeaconAffectedEntity::Assembler { id }, + data_store, + ); + } + }, + _ => {}, + } + } + }), + } +} + +fn removal_of_possible_inserter_connection( + pos: Position, + size: (u16, u16), + data_store: &DataStore, +) -> CascadingUpdate { + CascadingUpdate { + update: Box::new(move |world, sim_state, updates, data_store| { + profiling::scope!("removal_of_possible_inserter_connection"); + let inserter_search_start_pos = Position { + x: pos.x - data_store.max_inserter_search_range as i32, + y: pos.y - data_store.max_inserter_search_range as i32, + }; + + let inserter_search_size = ( + 2 * data_store.max_inserter_search_range as u16 + size.0, + 2 * data_store.max_inserter_search_range as u16 + size.1, + ); + + world.mutate_entities_colliding_with( + inserter_search_start_pos, + inserter_search_size, + data_store, + |e| { + match e { + Entity::Inserter { + ty, + user_movetime, + type_movetime, + + pos: inserter_pos, + direction: _inserter_dir, + filter: _inserter_filter, + info, + } => { + if let InserterInfo::Attached { + start_pos, + end_pos, + info: attached_inserter, + } = info + { + if start_pos.contained_in(pos, size) + || end_pos.contained_in(pos, size) + { + match attached_inserter { + AttachedInserter::BeltStorage { id, belt_pos } => { + sim_state.factory.belts.remove_inserter(*id, *belt_pos); + + *info = InserterInfo::NotAttached { + start_pos: *start_pos, + end_pos: *end_pos, + }; + }, + AttachedInserter::BeltBelt { item, inserter } => { + todo!("Remove BeltBelt inserter"); + }, + AttachedInserter::StorageStorage { item, inserter } => { + let movetime = user_movetime + .map(|v| v.into()) + .unwrap_or(*type_movetime); + + // This might return something at some point, and this will be a compiler error + let () = sim_state + .factory + .storage_storage_inserters + .remove_ins(*item, movetime, *inserter); + + *info = InserterInfo::NotAttached { + start_pos: *start_pos, + end_pos: *end_pos, + }; + }, + } + } + } + }, + _ => {}, + } + ControlFlow::Continue(()) + }, + ); + }), + } +} + +const ORE_THRESHHOLD: f64 = 0.95; +const ORE_DISTANCE_MULT: f64 = 1.0 / 80.0; + +impl World { + #[must_use] + pub fn new() -> Self { + let mut grid = SparseGrid::new(); + #[cfg(debug_assertions)] + const WORLDSIZE_CHUNKS: i32 = 200; + #[cfg(not(debug_assertions))] + const WORLDSIZE_CHUNKS: i32 = 4000; + + let noise = Simplex::new(1); + + for x in 50..WORLDSIZE_CHUNKS { + for y in 50..WORLDSIZE_CHUNKS { + grid.insert( + x, + y, + Chunk { + floor_tiles: None, + chunk_tile_to_entity_into: None, + entities: vec![], + }, + ); + } + } + + Self { + noise: SerializableSimplex { inner: noise }, + chunks: grid, + players: vec![PlayerInfo::default(), PlayerInfo::default()], + + belt_lookup: BeltIdLookup { + belt_id_to_chunks: BTreeMap::new(), + }, + belt_recieving_input_directions: HashMap::new(), + power_grid_lookup: PowerGridConnectedDevicesLookup { + grid_to_chunks: BTreeMap::new(), + }, + + remaining_updates: vec![], + + to_instantiate: BTreeSet::default(), + to_instantiate_by_belt: HashMap::default(), + + map_updates: None, + } + } + + pub fn get_original_ore_at_pos(&self, pos: Position) -> Option<(Item, u32)> { + let v = self.noise.get([ + pos.x as f64 * ORE_DISTANCE_MULT, + pos.y as f64 * ORE_DISTANCE_MULT, + ]); + + (v > ORE_THRESHHOLD).then_some(( + Item { + id: ItemIdxType::try_from(0).unwrap(), + }, + (v * pos.x.abs() as f64 * pos.y.abs() as f64 / 1000.0) as u32, + )) + } + + pub fn get_original_ore_in_area( + &self, + pos: Position, + size: [u16; 2], + ) -> Vec<(Item, u32)> { + (pos.x..(pos.x + size[0] as i32)) + .cartesian_product(pos.y..(pos.y + size[1] as i32)) + .flat_map(|(x, y)| self.get_original_ore_at_pos(Position { x, y })) + .into_group_map() + .into_iter() + // FIXME: This will panic if ore patches are too rich + .flat_map(|(item, amounts)| { + let sum = amounts.iter().sum(); + (sum > 0).then_some((item, sum)) + }) + // CORRECTNESS: We need to sort in some way, since HashMap::into_iter() does not guarantee any order, which is needed for this to be deterministic + .sorted_by_key(|(_, amount)| *amount) + .collect() + } + + pub fn get_belt_possible_inputs(&mut self, pos: Position) -> &EnumMap { + self.belt_recieving_input_directions.entry(pos).or_default() + } + + pub fn get_belt_possible_inputs_no_cache(&self, pos: Position) -> EnumMap { + self.belt_recieving_input_directions + .get(&pos) + .copied() + .unwrap_or_default() + } + + pub fn get_chunks(&self) -> impl Iterator> + Send { + self.chunks.occupied_entries().map(|(_, chunk)| chunk) + } + + pub fn get_chunk(&self, x: i32, y: i32) -> Option<&Chunk> { + self.chunks.get(x, y) + } + + pub fn set_floor_tile(&mut self, pos: Position, floor_tile: FloorTile) -> Result<(), ()> { + if let Some(chunk) = self.get_chunk_for_tile_mut(pos) { + chunk.floor_tiles.get_or_insert_default() + [usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap()] + [usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap()] = floor_tile; + Ok(()) + } else { + Err(()) + } + } + + #[profiling::function] + pub fn change_assembler_recipe( + &mut self, + sim_state: &mut SimulationState, + pos: Position, + new_recipe: Recipe, + data_store: &DataStore, + ) { + let can_accept_prod = data_store.recipe_is_intermediate[new_recipe.into_usize()]; + + // FIXME: Remove Cheated modules + let cheated_modules = if can_accept_prod { Some(1) } else { Some(0) }; + + let mut cascading_updates = vec![]; + + self.mutate_entities_colliding_with(pos, (1, 1), data_store, |e| { + match e { + Entity::Assembler { + ty, + pos, + modules, + info: + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + }, + rotation, + } => { + let assembler_size = data_store.assembler_info[*ty as usize].size(*rotation); + + for module_slot in modules.iter_mut() { + *module_slot = cheated_modules; + } + + if id.recipe == new_recipe { + return ControlFlow::Break(()); + } + + // Change assembler recipe + let (removal_info, new_id) = sim_state.factory.power_grids.power_grids + [usize::from(id.grid)] + .change_assembler_recipe( + *id, + *pole_position, + *weak_index, + new_recipe, + data_store, + ); + + *id = new_id; + + // CORRECTNESS: Since we process updates in a LIFO order, and we push the reconnection first, we will disconnect first + + // Push trying to reconnect all connected inserters (if possible) + cascading_updates.push(new_possible_inserter_connection(*pos, assembler_size)); + + // Push disconnecting all connected inserters + cascading_updates.push(removal_of_possible_inserter_connection( + *pos, + assembler_size, + data_store, + )); + }, + Entity::Assembler { + ty, + pos, + modules, + info: info @ AssemblerInfo::PoweredNoRecipe(_), + rotation, + } => { + for module_slot in modules.iter_mut() { + *module_slot = cheated_modules; + } + + let AssemblerInfo::PoweredNoRecipe(pole_position) = info else { + unreachable!(); + }; + let grid_id = sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position]; + let (new_id, new_weak_idx) = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_assembler( + *ty, + grid_id, + new_recipe, + modules, + *pole_position, + *pos, + data_store, + ); + *info = AssemblerInfo::Powered { + id: new_id, + pole_position: *pole_position, + weak_index: new_weak_idx, + }; + cascading_updates.push(newly_working_assembler(*pos, data_store)); + }, + Entity::Assembler { + modules, + info: AssemblerInfo::Unpowered(recipe), + .. + } => { + for module_slot in modules.iter_mut() { + *module_slot = cheated_modules; + } + *recipe = new_recipe; + }, + Entity::Assembler { + modules, + info: info @ AssemblerInfo::UnpoweredNoRecipe, + .. + } => { + for module_slot in modules.iter_mut() { + *module_slot = cheated_modules; + } + *info = AssemblerInfo::Unpowered(new_recipe); + }, + e => unreachable!("Called change recipe on non assembler: {e:?}"), + } + ControlFlow::Break(()) + }); + + // CORRECTNESS: We rely on the updates being processed in a LIFO order! + while let Some(update) = cascading_updates.pop() { + (update.update)(self, sim_state, &mut cascading_updates, data_store); + } + } + + #[profiling::function] + pub fn add_entity( + &mut self, + entity: Entity, + sim_state: &mut SimulationState, + data_store: &DataStore, + ) -> Result<(), ()> { + if !self.can_fit(entity.get_pos(), entity.get_size(data_store), data_store) { + return Err(()); + } + + profiling::scope!("add_entity {}", entity.get_type_name()); + + let pos = entity.get_pos(); + let size = entity.get_size(data_store); + + let chunk_pos = self.get_chunk_pos_for_tile(pos); + + if self.get_chunk_for_tile_mut(pos).is_none() { + todo!(); + return Err(()); + } + + let mut cascading_updates = vec![]; + + match entity { + Entity::Lab { + pos, + ty, + ref modules, + 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); + } + + cascading_updates.push(new_lab_cascade(pos, data_store)); + }, + Entity::SolarPanel { + pos, + ty, + 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::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, + weak_index, + } => { + 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); + + cascading_updates.push(newly_working_assembler(pos, data_store)); + }, + }, + Entity::PowerPole { + ty, 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)); + }, + Entity::Belt { id, direction, .. } => { + self.belt_lookup + .belt_id_to_chunks + .entry(id) + .or_default() + .insert(chunk_pos); + + self.belt_recieving_input_directions + .entry(pos + direction) + .or_default()[direction.reverse()] = true; + + cascading_updates.push(new_possible_inserter_connection(pos, (1, 1))); + cascading_updates.push(try_instantiating_inserters_for_belt_cascade(id)); + }, + Entity::Underground { + pos, + direction, + id, + underground_dir, + .. + } => { + self.belt_lookup + .belt_id_to_chunks + .entry(id) + .or_default() + .insert(chunk_pos); + + if underground_dir == UndergroundDir::Exit { + self.belt_recieving_input_directions + .entry(pos + direction) + .or_default()[direction.reverse()] = true; + } + + cascading_updates.push(new_possible_inserter_connection(pos, (1, 1))); + cascading_updates.push(try_instantiating_inserters_for_belt_cascade(id)); + }, + Entity::Splitter { + pos, + direction, + id: splitter_id, + } => { + self.belt_recieving_input_directions + .entry(pos + direction) + .or_default()[direction.reverse()] = true; + self.belt_recieving_input_directions + .entry(pos + direction.turn_right() + direction) + .or_default()[direction.reverse()] = true; + + cascading_updates.push(try_instantiating_all_inserters_cascade()); + }, + Entity::Inserter { info, .. } => match info { + InserterInfo::NotAttached { .. } => { + cascading_updates.push(instantiate_inserter_cascade(pos, data_store)); + }, + InserterInfo::Attached { + info: attached_inserter, + .. + } => match attached_inserter { + AttachedInserter::BeltStorage { id, .. } => { + self.belt_lookup + .belt_id_to_chunks + .entry(id) + .or_default() + .insert(chunk_pos); + cascading_updates.push(instantiate_inserter_cascade(pos, data_store)); + }, + AttachedInserter::BeltBelt { item, inserter } => { + todo!("We need to store the position in the belt_id_lookup"); + }, + AttachedInserter::StorageStorage { .. } => todo!(), + }, + }, + Entity::Chest { + ty, + pos, + item: None, + slot_limit: _, + } => { + cascading_updates.push(new_chest_cascade(pos)); + }, + Entity::Chest { + ty, + pos, + item: Some(_), + slot_limit: _, + } => { + cascading_updates.push(new_chest_cascade(pos)); + }, + Entity::Roboport { + ty, + pos, + power_grid, + network, + id, + } => {}, + Entity::Beacon { + ty, + pos, + ref modules, + pole_position: Some((pole_pos, weak_idx)), + } => { + cascading_updates.push(new_powered_beacon_cascade(pos, data_store)); + }, + Entity::Beacon { + ty, + pos, + ref modules, + pole_position: None, + } => {}, + Entity::FluidTank { .. } => {}, + }; + + if let Some(map_updates) = &mut self.map_updates { + for x_offs in 0..entity.get_size(data_store).0 { + for y_offs in 0..entity.get_size(data_store).1 { + let e_pos = entity.get_pos(); + + map_updates.push(Position { + x: e_pos.x + i32::from(x_offs), + y: e_pos.y + i32::from(y_offs), + }); + } + } + } + + let chunk = self + .get_chunk_for_tile_mut(pos) + .expect("Chunk outside the world!"); + + let map = if let Some(map) = &mut chunk.chunk_tile_to_entity_into { + map + } else { + chunk.chunk_tile_to_entity_into = Some(Box::new( + [[u8::MAX; CHUNK_SIZE as usize]; CHUNK_SIZE as usize], + )); + chunk.chunk_tile_to_entity_into.as_mut().unwrap() + }; + + let e_pos: Position = entity.get_pos(); + let e_size = entity.get_size(data_store); + + chunk.entities.push(entity); + + let index = u8::try_from(chunk.entities.len() - 1).expect("Into a chunk of size 16 x 16 we can fit at most 256 entitites. This assumes all entities are at least 1x1"); + + for x_offs in 0..e_size.0 { + for y_offs in 0..e_size.1 { + let x_in_chunk = usize::try_from((e_pos.x).rem_euclid(CHUNK_SIZE as i32)).unwrap(); + let x = min(x_in_chunk + usize::from(x_offs), CHUNK_SIZE as usize - 1); + let y_in_chunk = usize::try_from((e_pos.y).rem_euclid(CHUNK_SIZE as i32)).unwrap(); + let y = min(y_in_chunk + usize::from(y_offs), CHUNK_SIZE as usize - 1); + + map[x][y] = index; + } + } + + #[cfg(debug_assertions)] + for x_offs in 0..e_size.0 { + for y_offs in 0..e_size.1 { + assert!( + self.get_entities_colliding_with( + Position { + x: e_pos.x + i32::from(x_offs), + y: e_pos.y + i32::from(y_offs) + }, + (1, 1), + data_store + ) + .into_iter() + .next() + .is_some() + ); + assert!( + self.get_entities_colliding_with( + Position { + x: e_pos.x + i32::from(x_offs), + y: e_pos.y + i32::from(y_offs) + }, + (1, 1), + data_store + ) + .into_iter() + .next() + .unwrap() + .get_pos() + == e_pos + ); + } + } + + { + profiling::scope!("Cascading updates"); + while let Some(update) = cascading_updates.pop() { + (update.update)(self, sim_state, &mut cascading_updates, data_store); + } + } + + Ok(()) + } + + pub fn try_instantiate_inserter( + &mut self, + simulation_state: &mut SimulationState, + pos: Position, + data_store: &DataStore, + ) -> Result, InstantiateInserterError> + { + enum InserterConnection { + Belt(BeltTileId, u16), + Storage(Static), + } + + enum Static { + Done(Storage), + ToInstantiate, + } + + #[derive(Debug, Clone, PartialEq)] + enum PossibleItem { + All, + List(Vec>), + None, + } + + impl PossibleItem { + fn contains(&self, item: Item) -> bool { + match self { + PossibleItem::All => true, + PossibleItem::List(items) => items.contains(&item), + PossibleItem::None => false, + } + } + } + + struct InserterConnectionPossibility { + conn: InserterConnection, + inserter_item_hint: Option>>, + possible_item_list: PossibleItem, + } + + let Some(Entity::Inserter { + ty, + user_movetime, + type_movetime, + + pos: _pos, + direction, + info: InserterInfo::NotAttached { start_pos, end_pos }, + filter, + }) = self + .get_entities_colliding_with(pos, (1, 1), data_store) + .into_iter() + .next() + else { + return Err(InstantiateInserterError::NotUnattachedInserter); + }; + + let movetime = user_movetime.map(|v| v.into()).unwrap_or(*type_movetime); + + let start_conn: Option> = self + .get_entities_colliding_with(*start_pos, (1, 1), data_store) + .into_iter() + .next() + .map(|e| match e { + Entity::Inserter { .. } | Entity::PowerPole { .. }| Entity::SolarPanel { .. }| Entity::Beacon { .. }| Entity::FluidTank { .. } => None, + + Entity::Roboport { ty, pos, power_grid, network, id } => { + // TODO: + warn!("It is currently not possible to add or remove bots from roboports using inserters"); + None + } + + Entity::Assembler { + ty: _, + pos: _, + info: AssemblerInfo::Powered { + id, + pole_position: _, + weak_index: _ + }, + modules: _, + rotation: _ + // FIXME: Translate the recipe_idx to + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::Done(Storage::Assembler { + grid: id.grid, + recipe_idx_with_this_item: id.recipe.id, + index: id.assembler_index, + })), + inserter_item_hint: None, + possible_item_list: PossibleItem::List( + data_store.recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| (*dir == ItemRecipeDir::Out).then_some(*item)) + .collect(), + ), + }), + Entity::Assembler { .. } => None, + Entity::Belt { + id: BeltTileId::AnyBelt(id, _), + belt_pos, + .. + } + | Entity::Underground { + id: BeltTileId::AnyBelt(id, _), + belt_pos, + .. + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Belt(BeltTileId::AnyBelt(*id, PhantomData), *belt_pos), + inserter_item_hint: match simulation_state + .factory + .belts + .get_pure_item(BeltTileId::AnyBelt(*id, PhantomData)) + { + Some(item) => Some(vec![item]), + None => None, + }, + possible_item_list: match simulation_state + .factory + .belts + .get_pure_item(BeltTileId::AnyBelt(*id, PhantomData)) + { + Some(item) => PossibleItem::List(vec![item]), + None => PossibleItem::All, + }, + }), + Entity::Splitter { pos, id, direction: splitter_dir, .. } => { + let mut side = if *pos == *end_pos { + SplitterSide::Left + } else { + SplitterSide::Right + }; + + match splitter_dir { + Dir::North | Dir::East => {}, + Dir::South | Dir::West => side = side.switch(), + } + + let [_, outputs] = simulation_state + .factory + .belts.get_splitter_belt_ids(*id); + + let id = outputs[usize::from(bool::from(side))]; + + Some(InserterConnectionPossibility { conn: InserterConnection::Belt(id, SPLITTER_BELT_LEN), inserter_item_hint: simulation_state + .factory + .belts + .get_pure_item(id).map(|item| vec![item]), possible_item_list: PossibleItem::All }) + }, + Entity::Chest { + ty: _, + pos: _, + item: Some((item, index)), + slot_limit: _ + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::Done(Storage::Static { + static_id: StaticID::Chest as u16, + index: *index, + })), + inserter_item_hint: None, + possible_item_list: PossibleItem::List(vec![*item]) + }), + Entity::Chest { + ty, + pos, + item: None, + slot_limit: _ + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::ToInstantiate), + inserter_item_hint: None, + possible_item_list: PossibleItem::All + }), + Entity::Lab { pos, ty, pole_position, modules } => { + // No removing items from Labs! + None + } + }) + .flatten(); + + let Some(start_conn) = start_conn else { + return Err(InstantiateInserterError::SourceMissing); + }; + + let dest_conn: Option> = self + .get_entities_colliding_with(*end_pos, (1, 1), data_store) + .into_iter() + .next() + .map(|e| match e { + Entity::Inserter { .. } | Entity::PowerPole { .. }| Entity::SolarPanel { .. }| Entity::Beacon { .. }| Entity::FluidTank { .. } => None, + + Entity::Roboport { ty, pos, power_grid, network, id } => { + // TODO: + warn!("It is currently not possible to add or remove bots from roboports using inserters"); + None + } + + Entity::Assembler { + ty: _, + pos: _, + info: AssemblerInfo::Powered { + id, + pole_position: _, + weak_index: _ + }, + modules: _, + rotation: _, + // FIXME: Translate the recipe_idx to + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::Done(Storage::Assembler { + grid: id.grid, + recipe_idx_with_this_item: id.recipe.id, + index: id.assembler_index, + })), + inserter_item_hint: None, + possible_item_list: PossibleItem::List( + data_store.recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| (*dir == ItemRecipeDir::Ing).then_some(*item)) + .collect(), + ), + }), + Entity::Assembler { .. } => None, + Entity::Belt { + id: BeltTileId::AnyBelt(id, _), + belt_pos, + .. + } + | Entity::Underground { + id: BeltTileId::AnyBelt(id, _), + belt_pos, + .. + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Belt(BeltTileId::AnyBelt(*id, PhantomData), *belt_pos), + inserter_item_hint: simulation_state + .factory + .belts + .get_pure_item(BeltTileId::AnyBelt(*id, PhantomData)).map(|item| vec![item]), + possible_item_list: PossibleItem::All, + }), + Entity::Splitter { pos, id, direction: splitter_dir, .. } => { + let mut side = if *pos == *end_pos { + SplitterSide::Left + } else { + SplitterSide::Right + }; + + match splitter_dir { + Dir::North | Dir::East => {}, + Dir::South | Dir::West => side = side.switch(), + } + + let [inputs, _] = simulation_state + .factory + .belts.get_splitter_belt_ids(*id); + + let id = inputs[usize::from(bool::from(side))]; + + Some(InserterConnectionPossibility { conn: InserterConnection::Belt(id, SPLITTER_BELT_LEN), inserter_item_hint: simulation_state + .factory + .belts + .get_pure_item(id).map(|item| vec![item]), possible_item_list: PossibleItem::All }) + }, + Entity::Chest { + ty, + pos, + item: Some((item, index)), + slot_limit: _ + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::Done(Storage::Static { + static_id: StaticID::Chest as u16, + index: *index, + })), + inserter_item_hint: None, + possible_item_list: PossibleItem::List(vec![*item]) + }), + Entity::Chest { + ty: _, + pos: _, + item: None, + slot_limit: _ + } => Some(InserterConnectionPossibility { + conn: InserterConnection::Storage(Static::ToInstantiate), + inserter_item_hint: None, + possible_item_list: PossibleItem::All + }), + Entity::Lab { pos, ty, pole_position, modules } => { + if let Some((pole_pos, idx, lab_store_index)) = pole_position { + Some(InserterConnectionPossibility { conn: InserterConnection::Storage(Static::Done(Storage::Lab { grid: simulation_state.factory.power_grids.pole_pos_to_grid_id[pole_pos], index: *lab_store_index })), inserter_item_hint: None, possible_item_list: PossibleItem::List(data_store.science_bottle_items.iter().copied().collect()) }) + } else { + None + } + } + }) + .flatten(); + + let Some(dest_conn) = dest_conn else { + return Err(InstantiateInserterError::DestMissing); + }; + + let possible_items: PossibleItem<_> = match ( + &start_conn.possible_item_list, + &dest_conn.possible_item_list, + ) { + (PossibleItem::All, i) | (i, PossibleItem::All) => i.clone(), + (PossibleItem::List(a), PossibleItem::List(b)) => { + PossibleItem::List(a.iter().copied().filter(|v| b.contains(v)).collect()) + }, + (PossibleItem::None, _) => PossibleItem::None, + (_, PossibleItem::None) => PossibleItem::None, + }; + + if possible_items == PossibleItem::None { + match (start_conn.conn, dest_conn.conn) { + ( + InserterConnection::Belt(start_belt_id, _), + InserterConnection::Belt(dest_belt_id, _), + ) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![start_belt_id, dest_belt_id], + }); + }, + (InserterConnection::Belt(start_belt_id, _), InserterConnection::Storage(_)) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![start_belt_id], + }); + }, + (InserterConnection::Storage(_), InserterConnection::Belt(dest_belt_id, _)) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![dest_belt_id], + }); + }, + (InserterConnection::Storage(_), InserterConnection::Storage(_)) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![], + }); + }, } } - Self { - chunks: grid, - players: vec![PlayerInfo::default(), PlayerInfo::default()], - - belt_lookup: BeltIdLookup { - belt_id_to_chunks: BTreeMap::new(), - }, - belt_recieving_input_directions: HashMap::new(), - power_grid_lookup: PowerGridConnectedDevicesLookup { - grid_to_chunks: BTreeMap::new(), + // For determining the filter we use this plan: + // If a filter is specified, use that + // If we can determine a single source item use that, + // If we can determine a single destination item use that + // Else make the user do it + let determined_filter = match filter { + Some(filter) => { + if possible_items.contains(*filter) { + *filter + } else { + match (start_conn.conn, dest_conn.conn) { + ( + InserterConnection::Belt(start_belt_id, _), + InserterConnection::Belt(dest_belt_id, _), + ) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![start_belt_id, dest_belt_id], + }); + }, + ( + InserterConnection::Belt(start_belt_id, _), + InserterConnection::Storage(_), + ) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![start_belt_id], + }); + }, + ( + InserterConnection::Storage(_), + InserterConnection::Belt(dest_belt_id, _), + ) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![dest_belt_id], + }); + }, + (InserterConnection::Storage(_), InserterConnection::Storage(_)) => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: vec![], + }); + }, + } + } }, - } - } + None => { + // The user/game has not specified a filter, try and infer it - pub fn get_belt_possible_inputs(&mut self, pos: Position) -> &EnumMap { - self.belt_recieving_input_directions.entry(pos).or_default() - } + let belts = match (&start_conn.conn, &dest_conn.conn) { + ( + InserterConnection::Belt(start_belt_id, _), + InserterConnection::Belt(dest_belt_id, _), + ) => vec![*start_belt_id, *dest_belt_id], + ( + InserterConnection::Belt(start_belt_id, _), + InserterConnection::Storage(_), + ) => vec![*start_belt_id], + (InserterConnection::Storage(_), InserterConnection::Belt(dest_belt_id, _)) => { + vec![*dest_belt_id] + }, + (InserterConnection::Storage(_), InserterConnection::Storage(_)) => vec![], + }; - pub fn get_chunks(&self) -> impl IntoIterator> { - self.chunks.occupied_entries().map(|(a, b)| b) - } + // TODO: Figure out what is most intuitive here, for now just use the only possible item otherwise error + match possible_items { + PossibleItem::All => { + return Err(InstantiateInserterError::PleaseSpecifyFilter { + belts_which_could_help: belts, + }); + }, + PossibleItem::List(items) => match items.len().cmp(&1) { + std::cmp::Ordering::Less => { + return Err(InstantiateInserterError::ItemConflict { + belts_which_could_help: belts, + }); + }, + std::cmp::Ordering::Equal => items[0], + std::cmp::Ordering::Greater => { + return Err(InstantiateInserterError::PleaseSpecifyFilter { + belts_which_could_help: belts, + }); + }, + }, + PossibleItem::None => unreachable!(), + } + }, + }; - pub fn get_chunk(&self, x: usize, y: usize) -> Option<&Chunk> { - self.chunks.get(x, y) - } + let mut instantiated = InserterInstantiationNewOptions::Positions(vec![]); - pub fn set_floor_tile(&mut self, pos: Position, floor_tile: FloorTile) -> Result<(), ()> { - if let Some(chunk) = self.get_chunk_for_tile_mut(pos) { - chunk.floor_tiles[pos.x % 16][pos.y % 16] = floor_tile; - Ok(()) - } else { - Err(()) - } - } + match (start_conn.conn, dest_conn.conn) { + ( + InserterConnection::Belt(start_belt_id, start_belt_pos), + InserterConnection::Belt(dest_belt_id, dest_belt_pos), + ) => { + let start_pos = *start_pos; + let end_pos = *end_pos; + // FIXME: The movetime should be dependent on the inserter type! + let index = simulation_state.factory.belts.add_belt_belt_inserter( + (start_belt_id, start_belt_pos), + (dest_belt_id, dest_belt_pos), + BeltBeltInserterAdditionInfo { + cooldown: MOVETIME, + filter: determined_filter, + }, + ); + instantiated = + InserterInstantiationNewOptions::Belts(vec![start_belt_id, dest_belt_id]); + + let Entity::Inserter { info, .. } = self + .get_chunk_for_tile_mut(pos) + .unwrap() + .get_entity_at_mut(pos, data_store) + .unwrap() + else { + unreachable!("We already checked it was an unattached inserter before") + }; - pub fn add_entity( - &mut self, - entity: Entity, - sim_state: &SimulationState, - data_store: &DataStore, - ) -> Result<(), ()> { - if !self.can_fit(entity.get_pos(), entity.get_size(data_store), data_store) { - return Err(()); - } + *info = InserterInfo::Attached { + info: AttachedInserter::BeltBelt { + item: determined_filter, + inserter: index, + }, + start_pos, + end_pos, + } + }, + ( + InserterConnection::Belt(start_belt_id, start_belt_pos), + InserterConnection::Storage(dest_storage_untranslated), + ) => { + let start_pos = *start_pos; + let end_pos = *end_pos; + let dest_storage_untranslated = match dest_storage_untranslated { + Static::Done(storage) => storage, + Static::ToInstantiate => { + let mut storage = None; + self.mutate_entities_colliding_with(end_pos, (1, 1), data_store, |e| { + match e { + Entity::Chest { + ty, + pos: chest_pos, + item, + slot_limit, + } => { + let index = simulation_state.factory.chests.stores + [usize_from(determined_filter.id)] + .add_chest(*ty, *slot_limit, data_store); + *item = Some((determined_filter, index)); + storage = Some(Storage::Static { + index, + static_id: StaticID::Chest as u16, + }) + }, + _ => unreachable!(), + } + ControlFlow::Break(()) + }); + storage.unwrap() + }, + }; - let pos = entity.get_pos(); + let dest_storage = + dest_storage_untranslated.translate(determined_filter, data_store); + + match simulation_state.factory.belts.add_belt_storage_inserter( + determined_filter, + start_belt_id, + start_belt_pos - 1, + FakeUnionStorage::from_storage_with_statics_at_zero( + determined_filter, + dest_storage, + data_store, + ), + ) { + Ok(()) => {}, + Err(_) => { + todo!() + }, + }; + instantiated = InserterInstantiationNewOptions::PositionsAndBelts( + vec![end_pos], + vec![start_belt_id], + ); - let chunk_pos = self.get_chunk_pos_for_tile(pos); + let Entity::Inserter { info, .. } = self + .get_chunk_for_tile_mut(pos) + .unwrap() + .get_entity_at_mut(pos, data_store) + .unwrap() + else { + unreachable!("We already checked it was an unattached inserter before") + }; - if self.get_chunk_for_tile_mut(pos).is_none() { - return Err(()); - } + *info = InserterInfo::Attached { + info: AttachedInserter::BeltStorage { + id: start_belt_id, + belt_pos: start_belt_pos - 1, + }, + start_pos, + end_pos, + }; - match entity { - Entity::Lab { - pos, - ty, - 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 { - pos, - ty, - 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::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); - }, - }, - Entity::PowerPole { pos, .. } => { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - }, - Entity::Belt { id, direction, .. } => { self.belt_lookup .belt_id_to_chunks - .entry(id) + .entry(start_belt_id) .or_default() - .insert(chunk_pos); - - self.belt_recieving_input_directions - .entry(pos + direction) - .or_default()[direction.reverse()] = true; + .insert((pos.x / CHUNK_SIZE as i32, pos.y / CHUNK_SIZE as i32)); }, - Entity::Underground { - pos, - direction, - id, - underground_dir, - .. - } => { + ( + InserterConnection::Storage(start_storage_untranslated), + InserterConnection::Belt(dest_belt_id, dest_belt_pos), + ) => { + let start_pos = *start_pos; + let end_pos = *end_pos; + let start_storage_untranslated = match start_storage_untranslated { + Static::Done(storage) => storage, + Static::ToInstantiate => { + let mut storage = None; + self.mutate_entities_colliding_with(start_pos, (1, 1), data_store, |e| { + match e { + Entity::Chest { + ty, + pos: chest_pos, + item, + slot_limit, + } => { + let index = simulation_state.factory.chests.stores + [usize_from(determined_filter.id)] + .add_chest(*ty, *slot_limit, data_store); + *item = Some((determined_filter, index)); + storage = Some(Storage::Static { + index, + static_id: StaticID::Chest as u16, + }) + }, + _ => unreachable!(), + } + ControlFlow::Break(()) + }); + storage.unwrap() + }, + }; + instantiated = InserterInstantiationNewOptions::PositionsAndBelts( + vec![start_pos], + vec![dest_belt_id], + ); + + let start_storage = + start_storage_untranslated.translate(determined_filter, data_store); + + match simulation_state.factory.belts.add_storage_belt_inserter( + determined_filter, + dest_belt_id, + dest_belt_pos - 1, + FakeUnionStorage::from_storage_with_statics_at_zero( + determined_filter, + start_storage, + data_store, + ), + ) { + Ok(()) => {}, + Err(_) => { + todo!() + }, + }; + + let Entity::Inserter { info, .. } = self + .get_chunk_for_tile_mut(pos) + .unwrap() + .get_entity_at_mut(pos, data_store) + .unwrap() + else { + unreachable!("We already checked it was an unattached inserter before") + }; + + *info = InserterInfo::Attached { + info: AttachedInserter::BeltStorage { + id: dest_belt_id, + belt_pos: dest_belt_pos - 1, + }, + start_pos, + end_pos, + }; + self.belt_lookup .belt_id_to_chunks - .entry(id) + .entry(dest_belt_id) .or_default() - .insert(chunk_pos); - - if underground_dir == UndergroundDir::Exit { - self.belt_recieving_input_directions - .entry(pos + direction) - .or_default()[direction.reverse()] = true; - } + .insert((pos.x / CHUNK_SIZE as i32, pos.y / CHUNK_SIZE as i32)); }, - Entity::Splitter { - pos, - direction, - id: splitter_id, - } => { - let ids = sim_state - .factory - .belts - .get_splitter_belt_ids(splitter_id) - .into_iter() - .flatten(); + ( + InserterConnection::Storage(start_storage_untranslated), + InserterConnection::Storage(dest_storage_untranslated), + ) => { + let start_pos = *start_pos; + let end_pos = *end_pos; + + let start_storage_untranslated = match start_storage_untranslated { + Static::Done(storage) => storage, + Static::ToInstantiate => { + let mut storage = None; + self.mutate_entities_colliding_with(start_pos, (1, 1), data_store, |e| { + match e { + Entity::Chest { + ty, + pos: chest_pos, + item, + slot_limit, + } => { + let index = simulation_state.factory.chests.stores + [usize_from(determined_filter.id)] + .add_chest(*ty, *slot_limit, data_store); + *item = Some((determined_filter, index)); + match &mut instantiated { + InserterInstantiationNewOptions::Positions(positions) => { + positions.push(*chest_pos) + }, + _ => todo!(), + } + storage = Some(Storage::Static { + index, + static_id: StaticID::Chest as u16, + }); + }, + _ => unreachable!(), + } + ControlFlow::Break(()) + }); + storage.unwrap() + }, + }; + + let dest_storage_untranslated = match dest_storage_untranslated { + Static::Done(storage) => storage, + Static::ToInstantiate => { + let mut storage = None; + self.mutate_entities_colliding_with(end_pos, (1, 1), data_store, |e| { + match e { + Entity::Chest { + ty, + pos: chest_pos, + item: item @ None, + slot_limit, + } => { + let index = simulation_state.factory.chests.stores + [usize_from(determined_filter.id)] + .add_chest(*ty, *slot_limit, data_store); + *item = Some((determined_filter, index)); + match &mut instantiated { + InserterInstantiationNewOptions::Positions(positions) => { + positions.push(*chest_pos) + }, + _ => todo!(), + } + storage = Some(Storage::Static { + index, + static_id: StaticID::Chest as u16, + }); + }, + _ => unreachable!(), + } + ControlFlow::Break(()) + }); + storage.unwrap() + }, + }; - for id in ids { - let chunk_pos_right = self.get_chunk_pos_for_tile(pos + direction.turn_right()); + let start_storage = + start_storage_untranslated.translate(determined_filter, data_store); + let dest_storage = + dest_storage_untranslated.translate(determined_filter, data_store); + + let index = simulation_state.factory.storage_storage_inserters.add_ins( + determined_filter, + movetime, + start_storage, + dest_storage, + HAND_SIZE, + data_store, + ); - self.belt_lookup - .belt_id_to_chunks - .entry(id) - .or_default() - .extend([chunk_pos, chunk_pos_right]); + let Entity::Inserter { info, .. } = self + .get_chunk_for_tile_mut(pos) + .unwrap() + .get_entity_at_mut(pos, data_store) + .unwrap() + else { + unreachable!("We already checked it was an unattached inserter before") + }; - self.belt_recieving_input_directions - .entry(pos + direction) - .or_default()[direction.reverse()] = true; - self.belt_recieving_input_directions - .entry(pos + direction.turn_right() + direction) - .or_default()[direction.reverse()] = true; - } - }, - Entity::Inserter { info, .. } => match info { - InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached(attached_inserter) => match attached_inserter { - AttachedInserter::BeltStorage { id, .. } => { - self.belt_lookup - .belt_id_to_chunks - .entry(id) - .or_default() - .insert(chunk_pos); - }, - AttachedInserter::BeltBelt { item, inserter } => { - todo!("We need to store the position in the belt_id_lookup") + *info = InserterInfo::Attached { + info: AttachedInserter::StorageStorage { + item: determined_filter, + inserter: index, }, - AttachedInserter::StorageStorage { .. } => todo!(), - }, + start_pos, + end_pos, + }; }, - Entity::Chest { - ty, - pos, - item, - index, - } => {}, - Entity::Roboport { - ty, - pos, - power_grid, - network, - id, - } => {}, } - let chunk = self - .get_chunk_for_tile_mut(pos) - .expect("Chunk outside the world!"); - - chunk.entities.push(entity); - - Ok(()) + Ok(instantiated) } pub fn update_power_grid_id( @@ -362,6 +2503,7 @@ impl World { let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position]; @@ -374,7 +2516,10 @@ impl World {}, Entity::Inserter { info, .. } => match info { InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached(attached_inserter) => match attached_inserter { + InserterInfo::Attached { + info: attached_inserter, + .. + } => match attached_inserter { AttachedInserter::BeltStorage { id, belt_pos } => { // TODO }, @@ -392,7 +2537,9 @@ impl World {}, + | Entity::Chest { .. } + | Entity::Beacon { .. } + | Entity::FluidTank { .. } => {}, } } } @@ -406,10 +2553,22 @@ impl World, old_id: BeltTileId, new_id: BeltTileId, belt_pos_earliest: u16, + data_store: &DataStore, ) { + if belt_pos_earliest != 0 { + if let Some(waiting) = self.to_instantiate_by_belt.get(&old_id) { + let waiting = waiting.clone(); + self.to_instantiate_by_belt + .entry(new_id) + .or_default() + .extend(waiting); + } + } + let old_chunks = self.belt_lookup.belt_id_to_chunks.remove(&old_id); for chunk_pos in old_chunks.iter().flatten() { @@ -420,22 +2579,27 @@ impl World {}, Entity::Lab { .. } => {}, Entity::SolarPanel { .. } => {}, Entity::Assembler { .. } => {}, Entity::PowerPole { .. } => {}, Entity::Chest { .. } => {}, Entity::Roboport { .. } => {}, + Entity::FluidTank { .. } => {}, Entity::Belt { id, belt_pos, .. } | Entity::Underground { id, belt_pos, .. } => { if *id == old_id && belt_pos_earliest <= *belt_pos { *id = new_id; } }, - Entity::Splitter { pos, direction, .. } => todo!(), + Entity::Splitter { .. } => {}, Entity::Inserter { info, .. } => match info { InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached(attached_inserter) => match attached_inserter { + InserterInfo::Attached { + info: attached_inserter, + .. + } => match attached_inserter { AttachedInserter::BeltStorage { id, belt_pos } => { if *id == old_id && belt_pos_earliest <= *belt_pos { *id = new_id; @@ -454,15 +2618,28 @@ impl World, old_id: BeltTileId, new_id: BeltTileId, + data_store: &DataStore, ) { + if let Some(waiting) = self.to_instantiate_by_belt.remove(&old_id) { + self.to_instantiate_by_belt + .entry(new_id) + .or_default() + .extend(waiting); + } // Do it for ALL belt_pos - self.update_belt_id_after(old_id, new_id, 0); + self.update_belt_id_after(sim_state, old_id, new_id, 0, data_store); } pub fn modify_belt_pos(&mut self, id_to_change: BeltTileId, offs: i16) { @@ -476,12 +2653,14 @@ impl World {}, Entity::Lab { .. } => {}, Entity::SolarPanel { .. } => {}, Entity::Assembler { .. } => {}, Entity::PowerPole { .. } => {}, Entity::Chest { .. } => {}, Entity::Roboport { .. } => {}, + Entity::FluidTank { .. } => {}, Entity::Belt { id, belt_pos, .. } | Entity::Underground { id, belt_pos, .. } => { if *id == id_to_change { @@ -490,10 +2669,13 @@ impl World todo!(), + Entity::Splitter { .. } => {}, Entity::Inserter { info, .. } => match info { InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached(attached_inserter) => match attached_inserter { + InserterInfo::Attached { + info: attached_inserter, + .. + } => match attached_inserter { AttachedInserter::BeltStorage { id, belt_pos, .. } => { if *id == id_to_change { *belt_pos = belt_pos @@ -512,23 +2694,25 @@ impl World Option<&Chunk> { - self.chunks.get(pos.x / CHUNK_SIZE, pos.y / CHUNK_SIZE) + self.chunks + .get(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) } - pub fn get_chunk_for_tile_mut( + fn get_chunk_for_tile_mut( &mut self, pos: Position, ) -> Option<&mut Chunk> { - self.chunks.get_mut(pos.x / CHUNK_SIZE, pos.y / CHUNK_SIZE) + self.chunks + .get_mut(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) } - fn get_chunk_pos_for_tile(&self, pos: Position) -> (usize, usize) { - (pos.x / CHUNK_SIZE, pos.y / CHUNK_SIZE) + fn get_chunk_pos_for_tile(&self, pos: Position) -> (i32, i32) { + (pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) } - pub fn get_chunk_mut( + fn get_chunk_mut( &mut self, - chunk: (usize, usize), + chunk: (i32, i32), ) -> Option<&mut Chunk> { self.chunks.get_mut(chunk.0, chunk.1) } @@ -542,8 +2726,8 @@ impl World Option { self.get_entities_colliding_with( Position { - x: entity_pos.x - usize::from(data_store.max_power_search_range as u16), - y: entity_pos.y - usize::from(data_store.max_power_search_range as u16), + x: entity_pos.x - i32::from(data_store.max_power_search_range), + y: entity_pos.y - i32::from(data_store.max_power_search_range), }, ( 2 * data_store.max_power_search_range as u16 + entity_size.0, @@ -571,8 +2755,8 @@ impl World World None, + }) + } + + pub fn get_entity_color( + &self, + pos: Position, + data_store: &DataStore, + ) -> Color32 { + let chunk = self.get_chunk_for_tile(pos); + + chunk + .map(|chunk| { + let index = if let Some(map) = &chunk.chunk_tile_to_entity_into { + map[usize::try_from(pos.x.rem_euclid(CHUNK_SIZE as i32)).unwrap()] + [usize::try_from(pos.y.rem_euclid(CHUNK_SIZE as i32)).unwrap()] + } else { + u8::MAX + }; + + if (index as usize) < chunk.entities.len() { + chunk.entities[index as usize].get_map_color(data_store) + } else { + // This could be part of an entity in a different chunk + let our_chunk_x = pos.x / i32::from(CHUNK_SIZE); + let our_chunk_y = pos.y / i32::from(CHUNK_SIZE); + + let chunk_range_x = ((pos.x - i32::from(data_store.max_entity_size.0)) + / i32::from(CHUNK_SIZE)) + ..=our_chunk_x; + + for chunk_x in chunk_range_x { + let chunk_range_y = ((pos.y - i32::from(data_store.max_entity_size.1)) + / i32::from(CHUNK_SIZE)) + ..=our_chunk_y; + for chunk_y in chunk_range_y { + if chunk_x == our_chunk_x && chunk_y == our_chunk_y { + continue; + } + + if let Some(colliding) = self + .get_chunk(chunk_x, chunk_y) + .iter() + .flat_map(|chunk| chunk.entities.iter()) + .find(|e| { + let e_pos = e.get_pos(); + let e_size = e.get_size(data_store); + + pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) + }) + { + return colliding.get_map_color(data_store); + } + } + } + + if let Some(ore) = self.get_original_ore_at_pos(pos) { + if ore.1 > 0 { + // TODO ORE COLOR + Color32::LIGHT_BLUE + } else { + // TODO: Get floor color + Color32::from_hex("#3f3f3f").unwrap() + } + } else { + // TODO: Get floor color + Color32::from_hex("#3f3f3f").unwrap() + } } - }, - _ => None, - }) + }) + .unwrap_or(Color32::BLACK) } pub fn get_entities_colliding_with<'a, 'b>( @@ -594,23 +2847,64 @@ impl World, ) -> impl IntoIterator, IntoIter: Clone> - + use<'a, 'b, ItemIdxType, RecipeIdxType> { + + use<'a, 'b, ItemIdxType, RecipeIdxType> { let max_size = data_store.max_entity_size; - let bb_top_left = (pos.x - max_size.0, pos.y - max_size.1); + let bb_top_left = (pos.x - i32::from(max_size.0), pos.y - i32::from(max_size.1)); - let bb_bottom_right = (pos.x + usize::from(size.0), pos.y + usize::from(size.1)); + let bb_bottom_right = (pos.x + i32::from(size.0), pos.y + i32::from(size.1)); - let chunk_range_x = (bb_top_left.0 / CHUNK_SIZE)..=(bb_bottom_right.0 / CHUNK_SIZE); - let chunk_range_y = (bb_top_left.1 / CHUNK_SIZE)..=(bb_bottom_right.1 / CHUNK_SIZE); + 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)); debug_assert!(chunk_range_x.clone().count() >= 1); debug_assert!(chunk_range_y.clone().count() >= 1); chunk_range_x .cartesian_product(chunk_range_y) - .filter_map(|(chunk_x, chunk_y)| self.chunks.get(chunk_x, chunk_y)) - .map(|chunk| chunk.entities.iter()) + .filter_map(|(chunk_x, chunk_y)| { + self.chunks + .get(chunk_x, chunk_y) + .map(|c| (chunk_x, chunk_y, c)) + }) + .map(move |(chunk_x, chunk_y, chunk)| { + if size == (1, 1) + && pos.x >= chunk_x * i32::from(CHUNK_SIZE) + && pos.x < (chunk_x + 1) * i32::from(CHUNK_SIZE) + && pos.y >= chunk_y * i32::from(CHUNK_SIZE) + && pos.y < (chunk_y + 1) * i32::from(CHUNK_SIZE) + { + let index = if let Some(map) = &chunk.chunk_tile_to_entity_into { + map[usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap()] + [usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap()] + } else { + u8::MAX + }; + + if index as usize >= chunk.entities.len() { + debug_assert!(chunk.entities.iter().all(|e| { + let e_pos = e.get_pos(); + let e_size = e.get_size(data_store); + + !pos.overlap(size, e_pos, (e_size.0.into(), e_size.1.into())) + })); + [].iter() + } else { + assert!(chunk.entities[(index as usize)..(index as usize + 1)].len() == 1); + { + let e_pos = chunk.entities[index as usize].get_pos(); + let e_size = chunk.entities[index as usize].get_size(data_store); + + assert!(pos.contained_in(e_pos, e_size)) + } + chunk.entities[(index as usize)..(index as usize + 1)].iter() + } + } else { + chunk.entities.iter() + } + }) .flatten() .filter(move |e| { let e_pos = e.get_pos(); @@ -629,12 +2923,14 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); @@ -649,10 +2945,10 @@ impl World= (e_pos.x + usize::from(e_size.0)) - || (pos.y) >= (e_pos.y + usize::from(e_size.1)) + if (pos.x + i32::from(size.0)) <= e_pos.x + || (pos.y + i32::from(size.1)) <= e_pos.y + || (pos.x) >= (e_pos.x + i32::from(e_size.0)) + || (pos.y) >= (e_pos.y + i32::from(e_size.1)) { continue; } @@ -667,6 +2963,7 @@ impl World World { + sim_state.factory.fluid_store.remove_fluid_box( + *pos, + &mut sim_state.factory.chests, + &mut sim_state.factory.storage_storage_inserters, + data_store, + ); + }, + Entity::Beacon { + pos, + ty, + pole_position, + modules, + } => { + if let Some((pole_pos, idx)) = *pole_position { + sim_state + .factory + .power_grids + .remove_beacon(pole_pos, idx, data_store); + } else { + // This was not connected, nothing to do + } + }, Entity::Lab { pos, ty, + modules, pole_position, } => { if let Some((pole_pos, idx, store_idx)) = *pole_position { @@ -705,6 +3017,12 @@ impl World World match info { AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) @@ -737,11 +3056,17 @@ impl World { // TODO: let assembler_removal_info = sim_state.factory.power_grids.power_grids [assembler_id.grid as usize] - .remove_assembler(*assembler_id, data_store); + .remove_assembler( + *assembler_id, + *pole_position, + *weak_index, + data_store, + ); }, }, Entity::PowerPole { @@ -749,6 +3074,8 @@ impl World { + let ty = *ty; + let old_id = sim_state.factory.power_grids.pole_pos_to_grid_id[pos]; let ( @@ -757,151 +3084,175 @@ impl World { - assert_eq!(id.recipe, recipe); - - assert_eq!(id.grid, old_id); - - id.assembler_index = index; - id.grid = index_update.new_grid; - }, - (entity, power_grid_entity) => { - unreachable!( - "Expected {power_grid_entity:?} found {entity:?}" - ) - }, - } - ControlFlow::Break(()) - }, - ); - - // FIXME: HARDCODED! - let assembler_size: (u16, u16) = (3, 3); + { + profiling::scope!("Apply Index updates"); + for index_update in machines_which_changed { + self.mutate_entities_colliding_with( + index_update.position, + (1, 1), + data_store, + |e| { + match (e, index_update.new_pg_entity.clone()) { + ( + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + }, + .. + }, + crate::power::power_grid::PowerGridEntity::Assembler { + ty, + recipe, + index, + }, + ) => { + assert_eq!(id.recipe, recipe); - let inserter_search_area = ( - Position { - x: index_update.position.x - max_inserter_range as usize, - y: index_update.position.y - max_inserter_range as usize, - }, - ( - 2 * max_inserter_range as u16 + assembler_size.0, - 2 * max_inserter_range as u16 + assembler_size.1, - ), - ); + assert_eq!(id.grid, old_id); - let new_storages: Vec<_> = match index_update.new_storage { - PowerGridEntity::Assembler { recipe, index } => data_store - .recipe_to_items[&recipe] - .iter() - .map(|(_dir, item)| { - ( - item, - Storage::Assembler { - grid: index_update.new_grid, - recipe_idx_with_this_item: data_store - .recipe_to_translated_index[&(recipe, *item)], - index, + id.assembler_index = index; + id.grid = index_update.new_grid; }, - ) - }) - .collect(), - PowerGridEntity::Lab { index, ty } => todo!(), - PowerGridEntity::LazyPowerProducer { item, index } => { - todo!("Expand Storage type") - }, - PowerGridEntity::SolarPanel { ty } => { - vec![] - }, - PowerGridEntity::Accumulator { ty } => { - vec![] - }, - }; - - assert!(new_storages - .iter() - .map(|(item, _storage)| item) - .all_unique()); - - self.mutate_entities_colliding_with( - inserter_search_area.0, - inserter_search_area.1, - data_store, - |e| { - match e { - Entity::Inserter { - pos, - direction, - info: InserterInfo::NotAttached { .. }, - } => { - // Nothing to do - }, - Entity::Inserter { - pos, - direction, - info, - } => { - let (start_pos, end_pos) = - calculate_inserter_positions(*pos, *direction); - - if start_pos - .contained_in(index_update.position, assembler_size) - || end_pos - .contained_in(index_update.position, assembler_size) - { - // This Inserter is connected to the entity we are removing! - match info { - InserterInfo::NotAttached { - start_pos, - end_pos, - } => { - unreachable!() - }, - InserterInfo::Attached(attached_inserter) => { - match attached_inserter { - AttachedInserter::BeltStorage { - id, - belt_pos, - } => sim_state - .factory - .belts - .remove_inserter(*id, *belt_pos), - AttachedInserter::BeltBelt { - item, - inserter, - } => todo!(), - AttachedInserter::StorageStorage { - .. - } => { - todo!() - }, - } - todo!(); - }, - } - } - }, - - _ => {}, - } - ControlFlow::Continue(()) - }, - ); + (Entity::Beacon { .. }, PowerGridEntity::Beacon { .. }) => { + // Do nothing. The beacon only stores the pole_position, which has not changed + }, + (entity, power_grid_entity) => { + unreachable!( + "Expected {power_grid_entity:?} found {entity:?}" + ) + }, + } + ControlFlow::Break(()) + }, + ); + + // let assembler_size: (u16, u16) = + // data_store.assembler_info[usize::from(*ty)].size(*rotation); + + // let inserter_search_area = ( + // Position { + // x: index_update.position.x - i32::from(max_inserter_range), + // y: index_update.position.y - i32::from(max_inserter_range), + // }, + // ( + // 2 * max_inserter_range as u16 + assembler_size.0, + // 2 * max_inserter_range as u16 + assembler_size.1, + // ), + // ); + + // let new_storages: Vec<_> = match index_update.new_pg_entity { + // PowerGridEntity::Assembler { ty, recipe, index } => data_store + // .recipe_to_items[&recipe] + // .iter() + // .map(|(_dir, item)| { + // ( + // item, + // Storage::Assembler { + // grid: index_update.new_grid, + // recipe_idx_with_this_item: data_store + // .recipe_to_translated_index[&(recipe, *item)], + // index, + // }, + // ) + // }) + // .collect(), + // PowerGridEntity::Lab { index, ty } => todo!(), + // PowerGridEntity::LazyPowerProducer { item, index } => { + // todo!("Expand Storage type") + // }, + // PowerGridEntity::SolarPanel { .. } => { + // vec![] + // }, + // PowerGridEntity::Accumulator { .. } => { + // vec![] + // }, + // PowerGridEntity::Beacon { .. } => { + // vec![] + // }, + // }; + + // assert!( + // new_storages + // .iter() + // .map(|(item, _storage)| item) + // .all_unique() + // ); + + // self.mutate_entities_colliding_with( + // inserter_search_area.0, + // inserter_search_area.1, + // data_store, + // |e| { + // match e { + // Entity::Inserter { + // pos, + // direction, + // info: InserterInfo::NotAttached { .. }, + // .. + // } => { + // // Nothing to do + // }, + // Entity::Inserter { + // pos, + // direction, + // info, + // .. + // } => { + // let (start_pos, end_pos) = + // calculate_inserter_positions(*pos, *direction); + + // if start_pos + // .contained_in(index_update.position, assembler_size) + // || end_pos.contained_in( + // index_update.position, + // assembler_size, + // ) + // { + // // This Inserter is connected to the entity we are removing! + // match info { + // InserterInfo::NotAttached { + // start_pos, + // end_pos, + // } => { + // unreachable!() + // }, + // InserterInfo::Attached { + // info: attached_inserter, + // .. + // } => { + // match attached_inserter { + // AttachedInserter::BeltStorage { + // id, + // belt_pos, + // } => sim_state + // .factory + // .belts + // .remove_inserter(*id, *belt_pos), + // AttachedInserter::BeltBelt { + // item, + // inserter, + // } => todo!(), + // AttachedInserter::StorageStorage { + // .. + // } => { + // todo!() + // }, + // } + // todo!(); + // }, + // } + // } + // }, + + // _ => {}, + // } + // ControlFlow::Continue(()) + // }, + // ); + } } for unconnected_position in no_longer_connected_entity_positions { @@ -924,17 +3275,24 @@ impl World match info { AssemblerInfo::UnpoweredNoRecipe => unreachable!(), AssemblerInfo::Unpowered(recipe) => unreachable!(), AssemblerInfo::PoweredNoRecipe(position) => unreachable!(), - AssemblerInfo::Powered { id, pole_position } => { - assert!(sim_state - .factory - .power_grids - .pole_pos_to_grid_id - .get(pole_position) - .is_none()); + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + } => { + assert!( + sim_state + .factory + .power_grids + .pole_pos_to_grid_id + .get(pole_position) + .is_none() + ); if let Some(new_pole_pos) = pole_pos { // FIXME: Items are lost here! @@ -943,7 +3301,7 @@ impl World World World { *pole_position = None; }, + Entity::Beacon { + ty, + pos, + modules, + pole_position, + } => { + *pole_position = None; + }, e => unreachable!("Tried to unpower {e:?}"), } @@ -983,6 +3350,7 @@ impl World todo!(), @@ -990,6 +3358,7 @@ impl World todo!(), @@ -999,14 +3368,18 @@ impl World { - if let Some(item) = item { + if let Some((item, index)) = item { let chest_removal_info = sim_state.factory.chests.stores [usize_from(item.id)] - .remove_chest(*index, data_store); - } else { - todo!("What does not having an item mean?") + .remove_chest(*index); + + let chest_size = data_store.chest_tile_sizes[usize::from(*ty)]; + + cascading_updates.push(removal_of_possible_inserter_connection( + *pos, chest_size, data_store, + )); } }, Entity::Roboport { @@ -1021,12 +3394,21 @@ impl World {}, Entity::Inserter { + ty, + user_movetime, + type_movetime, pos, direction, - info: InserterInfo::Attached(attached), - } => match attached { + info: + InserterInfo::Attached { + info: attached_inserter, + .. + }, + .. + } => match attached_inserter { AttachedInserter::BeltStorage { id, belt_pos } => { sim_state.factory.belts.remove_inserter(*id, *belt_pos); }, @@ -1034,101 +3416,62 @@ impl World { - sim_state - .factory - .storage_storage_inserters - .remove_ins(*item, *inserter); + sim_state.factory.storage_storage_inserters.remove_ins( + *item, + user_movetime.map(|v| v.into()).unwrap_or(*type_movetime), + *inserter, + ); }, }, } - // Unattach inserters from self - self.mutate_entities_colliding_with( - inserter_search_area.0, - inserter_search_area.1, - data_store, - |e| { - match e { - Entity::Inserter { - pos, - direction, - info: InserterInfo::NotAttached { .. }, - } => { - // Nothing to do + let chunk = self.get_chunk_for_tile_mut(e_pos).unwrap(); + let old_idx = u8::try_from( + chunk + .entities + .iter() + .position(|e| e.get_pos() == e_pos) + .unwrap(), + ) + .unwrap(); + + for outer in chunk.chunk_tile_to_entity_into.as_mut().unwrap().iter_mut() { + for v in outer.iter_mut() { + match (*v).cmp(&old_idx) { + std::cmp::Ordering::Less => { + // We will not be moved by Vec::remove }, - Entity::Inserter { - pos, - direction, - info, - } => { - let (start_pos, end_pos) = - calculate_inserter_positions(*pos, *direction); - - if start_pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - || end_pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - { - // This Inserter is connected to the entity we are removing! - match info { - InserterInfo::NotAttached { start_pos, end_pos } => { - unreachable!() - }, - InserterInfo::Attached(attached_inserter) => { - match attached_inserter { - AttachedInserter::BeltStorage { id, belt_pos } => { - sim_state - .factory - .belts - .remove_inserter(*id, *belt_pos); - }, - AttachedInserter::BeltBelt { item, inserter } => { - sim_state - .factory - .belts - .remove_belt_belt_inserter(*inserter); - }, - AttachedInserter::StorageStorage { item, inserter } => { - sim_state - .factory - .storage_storage_inserters - .remove_ins(*item, *inserter); - }, - } - }, - } - } + std::cmp::Ordering::Equal => { + // Remove it + *v = u8::MAX; + }, + std::cmp::Ordering::Greater => { + // Vec::remove will move us one step to the left + *v -= 1; }, - - _ => {}, } - ControlFlow::Continue(()) - }, - ); + } + } + + // Actually remove the entity + chunk.entities.remove(old_idx as usize); } else { // Nothing to do } - // TODO: Actually remove the entity! - self.get_chunk_for_tile_mut(pos) - .unwrap() - .entities - .retain(|e| { - !pos.contained_in( - e.get_pos(), - ( - e.get_size(data_store).0.into(), - e.get_size(data_store).1.into(), - ), - ) - }); + while let Some(update) = cascading_updates.pop() { + (update.update)(self, sim_state, &mut cascading_updates, data_store); + } } #[must_use] pub fn can_fit( &self, pos: Position, - size: (u8, u8), + size: (u16, u16), data_store: &DataStore, ) -> bool { + // TODO: Make sure all chunks are generated self.get_entities_colliding_with(pos, (size.0 as u16, size.1 as u16), data_store) .into_iter() .next() @@ -1138,16 +3481,16 @@ impl World( &'a self, pole_pos: Position, - pole_size: (u8, u8), + pole_size: (u16, u16), connection_range: u8, data_store: &'b DataStore, ) -> impl IntoIterator, IntoIter: Clone> - + Clone - + use<'a, 'b, ItemIdxType, RecipeIdxType> { + + Clone + + use<'a, 'b, ItemIdxType, RecipeIdxType> { self.get_entities_colliding_with( Position { - x: pole_pos.x - usize::from(connection_range), - y: pole_pos.y - usize::from(connection_range), + 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, @@ -1184,7 +3527,7 @@ impl World, - ) -> (u8, (u8, u8)) { + ) -> (u8, (u16, u16)) { let Some(Entity::PowerPole { ty, .. }) = self .get_entities_colliding_with(pole_pos, (1, 1), data_store) .into_iter() @@ -1247,81 +3590,16 @@ impl Chunk= (e_pos.x + usize::from(e_size.0)) - || (pos.y) >= (e_pos.y + usize::from(e_size.1)) + (pos.x + i32::from(size.0)) <= e_pos.x + || (pos.y + i32::from(size.1)) <= e_pos.y + || (pos.x) >= (e_pos.x + i32::from(e_size.0)) + || (pos.y) >= (e_pos.y + i32::from(e_size.1)) }) } - // pub fn add_entity(&mut self, entity: Entity) { - // if let Entity::Belt { - // pos, - // direction, - // id, - // belt_pos: _belt_pos, - // } = entity - // { - // let v = self.belt_exit_chunks.entry(id).or_default(); - - // let my_chunk = (pos.x / CHUNK_SIZE, pos.y / CHUNK_SIZE); - - // if (pos.x % CHUNK_SIZE).abs_diff( - // (pos.x.wrapping_add_signed(direction.into_offset().0.into())) % CHUNK_SIZE, - // ) > 1 - // { - // v.insert(( - // my_chunk - // .0 - // .wrapping_add_signed(direction.into_offset().0.into()), - // my_chunk.1, - // )); - // } - // if (pos.y % CHUNK_SIZE).abs_diff( - // (pos.y.wrapping_add_signed(direction.into_offset().1.into())) % CHUNK_SIZE, - // ) > 1 - // { - // v.insert(( - // my_chunk.0, - // my_chunk - // .1 - // .wrapping_add_signed(direction.into_offset().1.into()), - // )); - // } - // if (pos.x % CHUNK_SIZE).abs_diff( - // (pos.x - // .wrapping_add_signed(direction.reverse().into_offset().0.into())) - // % CHUNK_SIZE, - // ) > 1 - // { - // v.insert(( - // my_chunk - // .0 - // .wrapping_add_signed(direction.reverse().into_offset().0.into()), - // my_chunk.1, - // )); - // } - // if (pos.y % CHUNK_SIZE).abs_diff( - // (pos.y - // .wrapping_add_signed(direction.reverse().into_offset().1.into())) - // % CHUNK_SIZE, - // ) > 1 - // { - // v.insert(( - // my_chunk.0, - // my_chunk - // .1 - // .wrapping_add_signed(direction.reverse().into_offset().1.into()), - // )); - // } - // } - - // self.entities.push(entity); - // } - #[must_use] - pub fn get_entities(&self) -> impl IntoIterator> { - &self.entities + pub fn get_entities(&self) -> impl Iterator> { + self.entities.iter() } } @@ -1333,6 +3611,7 @@ pub enum AssemblerInfo { Powered { id: AssemblerID, pole_position: Position, + weak_index: WeakIndex, }, } @@ -1342,7 +3621,22 @@ pub enum InserterInfo { start_pos: Position, end_pos: Position, }, - Attached(AttachedInserter), + Attached { + start_pos: Position, + end_pos: Position, + info: AttachedInserter, + }, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] +pub enum InternalInserterInfo { + NotAttached { + end_pos: Position, + }, + Attached { + end_pos: Position, + info: AttachedInternalInserter, + }, } #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] @@ -1357,24 +3651,47 @@ pub enum AttachedInserter { }, StorageStorage { item: Item, - inserter: usize, + // TODO: Do I want to store this Identifier of calculate it on demand to save RAM? + inserter: InserterIdentifier, + }, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] +pub enum AttachedInternalInserter { + BeltStorage { + id: BeltTileId, + belt_pos: u16, + }, + StorageStorage { + item: Item, + // TODO: Do I want to store this Identifier of calculate it on demand to save RAM? + inserter: InserterIdentifier, }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize, Enum)] pub enum UndergroundDir { Entrance, Exit, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +struct PipeConnection { + pipe_pos: Position, + connection_weak_index: WeakIndex, +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] pub enum Entity { Assembler { ty: u8, pos: Position, - /// List of all the module slots of this inserter + /// List of all the module slots of this assembler modules: Box<[Option]>, info: AssemblerInfo, + + #[serde(default = "Dir::default")] + rotation: Dir, }, PowerPole { // This means at most 256 different types of power poles can exist, should be fine :) @@ -1385,6 +3702,7 @@ pub enum Entity { Belt { pos: Position, direction: Dir, + ty: u8, id: BeltTileId, belt_pos: u16, }, @@ -1392,6 +3710,7 @@ pub enum Entity { pos: Position, underground_dir: UndergroundDir, direction: Dir, + ty: u8, id: BeltTileId, belt_pos: u16, }, @@ -1401,8 +3720,13 @@ pub enum Entity { id: SplitterTileId, }, Inserter { + ty: u8, + user_movetime: Option>, + type_movetime: u16, + pos: Position, direction: Dir, + filter: Option>, info: InserterInfo, }, @@ -1410,8 +3734,8 @@ pub enum Entity { // This means at most 256 different types of Chest can exist, should be fine :) ty: u8, pos: Position, - item: Option>, - index: usize, + item: Option<(Item, u32)>, + slot_limit: u8, }, Roboport { // This means at most 256 different types of Roboports can exist, should be fine @@ -1429,8 +3753,43 @@ pub enum Entity { Lab { pos: Position, ty: u8, - pole_position: Option<(Position, WeakIndex, u16)>, + /// List of all the module slots of this assembler + modules: Box<[Option]>, + pole_position: Option<(Position, WeakIndex, u32)>, + }, + Beacon { + ty: u8, + pos: Position, + /// List of all the module slots of this beacon + modules: Box<[Option]>, + pole_position: Option<(Position, WeakIndex)>, + }, + // Pipes are coded as fluid tanks with connections on all sides + FluidTank { + ty: u8, + pos: Position, + rotation: Dir, }, + // TODO: + // MiningDrill { + // ty: u8, + // pos: Position, + // rotation: Dir, + // drill_id: DrillID, + // internal_inserter: InternalInserterInfo, + // }, +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] +enum DrillID { + OnlySolo(u32), + WithShared(u32), +} + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq)] +pub struct UndergroundPipeConnection { + connected_pipe_pos: Position, + system_id: FluidSystemId, } impl Entity { @@ -1446,14 +3805,18 @@ impl Entity *pos, Self::SolarPanel { pos, .. } => *pos, Self::Lab { pos, .. } => *pos, + Self::Beacon { pos, .. } => *pos, + Self::FluidTank { pos, .. } => *pos, } } - pub fn get_size(&self, data_store: &DataStore) -> (u8, u8) { - // FIXME: Use data_store + pub fn get_size(&self, data_store: &DataStore) -> (u16, u16) { + // FIXME: Use data_store for everything match self { - Self::Assembler { .. } => (3, 3), - Self::PowerPole { ty, .. } => (1, 1), + Self::Assembler { ty, rotation, .. } => { + data_store.assembler_info[usize::from(*ty)].size(*rotation) + }, + Self::PowerPole { ty, .. } => data_store.power_pole_data[usize::from(*ty)].size, Self::Belt { .. } => (1, 1), Self::Inserter { .. } => (1, 1), Self::Underground { .. } => (1, 1), @@ -1463,10 +3826,66 @@ impl Entity (2, 1), Dir::West => (1, 2), }, - Self::Chest { ty, .. } => data_store.chest_tile_sizes[*ty as usize], - Self::Roboport { ty, .. } => (4, 4), - Self::SolarPanel { ty, .. } => (3, 3), - Self::Lab { ty, .. } => (3, 3), + Self::Chest { ty, .. } => data_store.chest_tile_sizes[usize::from(*ty)], + Self::Roboport { .. } => (4, 4), + Self::SolarPanel { ty, .. } => ( + data_store.solar_panel_info[usize::from(*ty)].size[0], + data_store.solar_panel_info[usize::from(*ty)].size[1], + ), + Self::Lab { ty, .. } => data_store.lab_info[usize::from(*ty)].size, + Self::Beacon { ty, .. } => data_store.beacon_info[usize::from(*ty)].size, + Self::FluidTank { ty, .. } => data_store.fluid_tank_infos[usize::from(*ty)].size.into(), + } + } + + pub fn get_map_color(&self, data_store: &DataStore) -> Color32 { + match self { + Self::Assembler { .. } => Color32::from_hex("#0086c9").unwrap(), + Self::PowerPole { .. } => Color32::from_hex("#eeee29").unwrap(), + Self::Belt { .. } => Color32::from_hex("#faba00").unwrap(), + Self::Inserter { .. } => Color32::from_hex("#006192").unwrap(), + Self::Underground { .. } => Color32::from_hex("#faba00").unwrap(), + Self::Splitter { .. } => Color32::from_hex("#faba00").unwrap(), + Self::Chest { .. } => Color32::from_hex("#ccd8cc").unwrap(), + Self::Roboport { .. } => Color32::from_hex("#4888e8").unwrap(), + Self::SolarPanel { .. } => Color32::from_hex("#1f2124").unwrap(), + Self::Lab { .. } => Color32::from_hex("#ff90bd").unwrap(), + Self::Beacon { .. } => Color32::from_hex("#008192").unwrap(), + Self::FluidTank { .. } => Color32::from_hex("#b429ff").unwrap(), + } + } + + pub fn cares_about_power(&self) -> bool { + match self { + Self::Assembler { .. } => true, + Self::PowerPole { .. } => true, + Self::Belt { .. } => false, + Self::Inserter { .. } => false, + Self::Underground { .. } => false, + Self::Splitter { .. } => false, + Self::Chest { .. } => false, + Self::Roboport { .. } => true, + Self::SolarPanel { .. } => true, + Self::Lab { .. } => true, + Self::Beacon { .. } => true, + Self::FluidTank { .. } => false, + } + } + + pub fn get_type_name(&self) -> &'static str { + match self { + Self::Assembler { .. } => "Assembler", + Self::PowerPole { .. } => "PowerPole", + Self::Belt { .. } => "Belt", + Self::Inserter { .. } => "Inserter", + Self::Underground { .. } => "Underground", + Self::Splitter { .. } => "Splitter", + Self::Chest { .. } => "Chest", + Self::Roboport { .. } => "Roboport", + Self::SolarPanel { .. } => "SolarPanel", + Self::Lab { .. } => "Lab", + Self::Beacon { .. } => "Beacon", + Self::FluidTank { .. } => "FluidTank", } } } @@ -1484,6 +3903,9 @@ pub enum PlaceEntityType { Assembler { pos: Position, ty: u8, + + #[serde(default = "Dir::default")] + rotation: Dir, }, Inserter { pos: Position, @@ -1494,6 +3916,13 @@ pub enum PlaceEntityType { Belt { pos: Position, direction: Dir, + ty: u8, + }, + Underground { + pos: Position, + direction: Dir, + ty: u8, + underground_dir: UndergroundDir, }, PowerPole { pos: Position, @@ -1502,11 +3931,14 @@ pub enum PlaceEntityType { Splitter { pos: Position, direction: Dir, + ty: u8, + in_mode: Option, out_mode: Option, }, Chest { pos: Position, + ty: u8, }, SolarPanel { pos: Position, @@ -1516,12 +3948,47 @@ pub enum PlaceEntityType { pos: Position, ty: u8, }, + Beacon { + ty: u8, + pos: Position, + }, + FluidTank { + ty: u8, + pos: Position, + rotation: Dir, + }, + MiningDrill { + ty: u8, + pos: Position, + rotation: Dir, + }, +} + +impl PlaceEntityType { + pub fn cares_about_power(&self) -> bool { + match self { + Self::Assembler { .. } => true, + Self::PowerPole { .. } => true, + Self::Belt { .. } => false, + Self::Underground { .. } => false, + Self::Inserter { .. } => false, + Self::Splitter { .. } => false, + Self::Chest { .. } => false, + // Self::Roboport { .. } => true, + Self::SolarPanel { .. } => true, + Self::Lab { .. } => true, + Self::Beacon { .. } => true, + Self::FluidTank { .. } => false, + Self::MiningDrill { .. } => true, + } + } } #[derive( - Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Enum, EnumIter, + Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, Enum, EnumIter, )] pub enum Dir { + #[default] North, East, South, @@ -1584,7 +4051,7 @@ impl Dir { pub struct AssemblerID { pub recipe: Recipe, pub grid: PowerGridIdentifier, - pub assembler_index: u16, + pub assembler_index: u32, } #[derive( Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, @@ -1615,8 +4082,8 @@ impl Add for Position { fn add(self, rhs: Dir) -> Self::Output { let offs = rhs.into_offset(); Self { - x: self.x.checked_add_signed(offs.0.into()).unwrap(), - y: self.y.checked_add_signed(offs.1.into()).unwrap(), + x: self.x.checked_add(offs.0.into()).unwrap(), + y: self.y.checked_add(offs.1.into()).unwrap(), } } } @@ -1627,14 +4094,14 @@ mod test { use proptest::{prop_assert, prop_assert_eq, proptest}; use crate::{ - blueprint::{random_entity_to_place, random_position, Blueprint}, + DATA_STORE, + blueprint::{Blueprint, random_entity_to_place, random_position}, frontend::{ - action::{place_entity::PlaceEntityInfo, ActionType}, + action::{ActionType, place_entity::PlaceEntityInfo}, world::Position, }, rendering::app_state::GameState, replays::Replay, - DATA_STORE, }; proptest! { @@ -1643,7 +4110,7 @@ mod 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(GameState::new(&DATA_STORE), &*DATA_STORE); + let mut rep = Replay::new(&state, None, &*DATA_STORE); rep.append_actions([ActionType::PlaceEntity(PlaceEntityInfo { entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ent) })]); @@ -1664,8 +4131,8 @@ mod test { 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 usize)) { - for y_pos in e_pos.y..(e_pos.y + (e_size.1 as usize)) { + 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)); } } diff --git a/src/inserter/belt_belt_inserter.rs b/src/inserter/belt_belt_inserter.rs index 999e307..dd0456d 100644 --- a/src/inserter/belt_belt_inserter.rs +++ b/src/inserter/belt_belt_inserter.rs @@ -2,7 +2,9 @@ use crate::item::{IdxTrait, Item, WeakIdxTrait}; use super::{InserterState, SushiInserterState}; -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +use crate::item::ITEMCOUNTTYPE; + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] pub struct BeltBeltInserter { state: InserterState, } @@ -97,15 +99,17 @@ impl BeltBeltInserter { #[must_use] pub const fn new() -> Self { Self { - state: InserterState::Empty, + state: InserterState::WaitingForSourceItems(0), } } + #[profiling::function] pub fn update( &mut self, loc_in: &mut LocType, loc_out: &mut LocType, movetime: u8, + max_hand_size: ITEMCOUNTTYPE, filter: LocType::Item, filter_test: impl Fn(LocType::Item) -> bool, ) { @@ -113,19 +117,28 @@ impl BeltBeltInserter { // Try and find a faster implementation of similar logic match self.state { - InserterState::Empty => { + InserterState::WaitingForSourceItems(count) => { if let Some(item) = loc_in.peek_item() { if filter_test(item) { loc_in.force_take_item(); - self.state = InserterState::FullAndMovingOut(movetime); + + if count + 1 == max_hand_size { + self.state = InserterState::FullAndMovingOut(movetime); + } else { + self.state = InserterState::WaitingForSourceItems(count + 1); + } } } }, - InserterState::FullAndWaitingForSlot => { + InserterState::WaitingForSpaceInDestination(count) => { if !loc_out.has_item() { loc_out.force_put_item(filter); - self.state = InserterState::EmptyAndMovingBack(movetime); + if count == 1 { + self.state = InserterState::EmptyAndMovingBack(movetime); + } else { + self.state = InserterState::WaitingForSpaceInDestination(count - 1); + } } }, InserterState::FullAndMovingOut(time) => { @@ -133,7 +146,7 @@ impl BeltBeltInserter { self.state = InserterState::FullAndMovingOut(time - 1); } else { // TODO: Do I want to try inserting immediately? - self.state = InserterState::FullAndWaitingForSlot; + self.state = InserterState::WaitingForSpaceInDestination(max_hand_size); } }, InserterState::EmptyAndMovingBack(time) => { @@ -141,12 +154,13 @@ impl BeltBeltInserter { self.state = InserterState::EmptyAndMovingBack(time - 1); } else { // TODO: Do I want to try getting a new item immediately? - self.state = InserterState::Empty; + self.state = InserterState::WaitingForSourceItems(0); } }, } } + #[profiling::function] pub fn update_instant(&mut self, loc_in: &mut LocType, loc_out: &mut LocType) { if !loc_out.has_item() { let item = loc_in.try_take_item(); diff --git a/src/inserter/belt_storage_inserter.rs b/src/inserter/belt_storage_inserter.rs index 333b118..9e6be0f 100644 --- a/src/inserter/belt_storage_inserter.rs +++ b/src/inserter/belt_storage_inserter.rs @@ -1,11 +1,11 @@ -use std::marker::ConstParamTy; +use std::{cmp::min, marker::ConstParamTy}; use crate::{ - item::{IdxTrait, WeakIdxTrait}, - storage_list::{index, SingleItemStorages}, + item::ITEMCOUNTTYPE, + storage_list::{SingleItemStorages, index_fake_union}, }; -use super::{InserterState, Storage}; +use super::{FakeUnionStorage, InserterState}; #[derive(Debug, ConstParamTy, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum Dir { @@ -14,8 +14,8 @@ pub enum Dir { } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct BeltStorageInserter { - pub storage_id: Storage, +pub struct BeltStorageInserter { + pub storage_id: FakeUnionStorage, pub state: InserterState, } @@ -27,50 +27,50 @@ pub struct BeltStorageInserter { // Luckily since inserter only have limited range (3 tiles or whatever) there is inherent locality in the accesses, if the MultiStores are somewhat spacially aligned. // Though this could also lead to particularly poor access patterns if the belt/line of inserters is perpendicular to the stride pattern of the Multistore // (maybe some quadtree weirdness could help?) -impl BeltStorageInserter { +impl BeltStorageInserter { #[must_use] - pub const fn new(id: Storage) -> Self { + pub const fn new(id: FakeUnionStorage) -> Self { Self { storage_id: id, - state: InserterState::Empty, + state: InserterState::WaitingForSourceItems(0), } } } -impl BeltStorageInserter { +impl BeltStorageInserter<{ Dir::BeltToStorage }> { pub fn update( &mut self, loc: &mut bool, storages: SingleItemStorages, movetime: u8, - num_grids_total: usize, - num_recipes: usize, + max_hand_size: ITEMCOUNTTYPE, grid_size: usize, ) { // TODO: I just added InserterStates and it is a lot slower (unsurprisingly), // Try and find a faster implementation of similar logic match self.state { - InserterState::Empty => { + InserterState::WaitingForSourceItems(count) => { if *loc { *loc = false; - self.state = InserterState::FullAndMovingOut(movetime); + if count + 1 == max_hand_size { + self.state = InserterState::FullAndMovingOut(movetime); + } else { + self.state = InserterState::WaitingForSourceItems(count + 1); + } } }, - InserterState::FullAndWaitingForSlot => { - let (max_insert, old) = index( - storages, - self.storage_id, - num_grids_total, - num_recipes, - grid_size, - ); - // TODO: - if *old < *max_insert { - // There is space in the machine - *old += 1; + InserterState::WaitingForSpaceInDestination(count) => { + let (max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + let to_insert = min(count, *max_insert - *old); - self.state = InserterState::EmptyAndMovingBack(movetime); + if to_insert > 0 { + *old += to_insert; + if to_insert == count { + self.state = InserterState::EmptyAndMovingBack(movetime); + } else { + self.state = InserterState::WaitingForSpaceInDestination(count - to_insert); + } } }, InserterState::FullAndMovingOut(time) => { @@ -78,7 +78,7 @@ impl BeltStorageInserter { @@ -86,21 +86,20 @@ impl BeltStorageInserter BeltStorageInserter { +impl BeltStorageInserter<{ Dir::StorageToBelt }> { pub fn update( &mut self, loc: &mut bool, storages: SingleItemStorages, movetime: u8, - num_grids_total: usize, - num_recipes: usize, + max_hand_size: ITEMCOUNTTYPE, grid_size: usize, ) { // TODO: I just added InserterStates and it is a lot slower (unsurprisingly), @@ -108,25 +107,31 @@ impl BeltStorageInserter { - let (_max_insert, old) = index( - storages, - self.storage_id, - num_grids_total, - num_recipes, - grid_size, - ); - if *old > 0 { + InserterState::WaitingForSourceItems(count) => { + let (_max_insert, old) = index_fake_union(storages, self.storage_id, grid_size); + + let to_extract = min(max_hand_size - count, *old); + + if to_extract > 0 { // There is an item in the machine - *old -= 1; + *old -= to_extract; - self.state = InserterState::FullAndMovingOut(movetime); + if to_extract + count == max_hand_size { + self.state = InserterState::FullAndMovingOut(movetime); + } else { + self.state = InserterState::WaitingForSourceItems(count + to_extract); + } } }, - InserterState::FullAndWaitingForSlot => { + InserterState::WaitingForSpaceInDestination(count) => { if !*loc { *loc = true; - self.state = InserterState::EmptyAndMovingBack(movetime); + + if count == 1 { + self.state = InserterState::EmptyAndMovingBack(movetime); + } else { + self.state = InserterState::WaitingForSpaceInDestination(count - 1); + } } }, InserterState::FullAndMovingOut(time) => { @@ -134,7 +139,7 @@ impl BeltStorageInserter { @@ -142,7 +147,7 @@ impl BeltStorageInserter { pub phantom: PhantomData, } +// The power grid id u16::MAX is reserved for static inventories +pub const MAX_GRID_COUNT: usize = u16::MAX as usize - 1; +pub const MAX_TIMES_AN_ITEM_CAN_APPEAR_IN_RECIPES: usize = u16::MAX as usize; +pub const MAX_RECIPE_COUNT: usize = u16::MAX as usize; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub struct FakeUnionStorage { + pub index: u32, + pub grid_or_static_flag: u16, + pub recipe_idx_with_this_item: u16, +} + +impl FakeUnionStorage { + #[inline(always)] + pub fn into_inner_and_outer_indices( + self, + num_grids_total: usize, + grid_size: usize, + ) -> (usize, usize) { + debug_assert!( + num_grids_total < u16::MAX as usize, + "If we have u16::MAX power grids, we can no longer differentiate grids in that grid and static inventories independent of power grids" + ); + + let grid_offs = min(num_grids_total, usize::from(self.grid_or_static_flag)); + let recipe_idx_with_this_item_or_single_kind_power_grid_kind = + usize::from(self.recipe_idx_with_this_item); + + ( + grid_offs * grid_size + recipe_idx_with_this_item_or_single_kind_power_grid_kind, + self.index as usize, + ) + } + + #[inline(always)] + pub fn into_inner_and_outer_indices_with_statics_at_zero( + self, + grid_size: usize, + ) -> (usize, usize) { + let grid_offs = usize::from(self.grid_or_static_flag); + let recipe_idx_with_this_item_or_single_kind_power_grid_kind = + usize::from(self.recipe_idx_with_this_item); + + ( + grid_offs * grid_size + recipe_idx_with_this_item_or_single_kind_power_grid_kind, + self.index as usize, + ) + } + + pub fn from_storage( + item: Item, + storage: Storage, + data_store: &DataStore, + ) -> Self { + match storage { + Storage::Assembler { + grid, + recipe_idx_with_this_item, + index, + } => Self { + index: u32::from(index), + grid_or_static_flag: u16::from(grid), + recipe_idx_with_this_item: u16::try_from(Into::::into( + recipe_idx_with_this_item, + )) + .unwrap(), + }, + Storage::Lab { grid, index } => Self { + index: u32::from(index), + grid_or_static_flag: u16::from(grid), + recipe_idx_with_this_item: data_store.num_recipes_with_item[usize_from(item.id)] + as u16, + }, + Storage::Static { index, static_id } => Self { + index: u32::try_from(index).unwrap(), + grid_or_static_flag: u16::MAX, + recipe_idx_with_this_item: static_id as u16, + }, + } + } + + pub fn from_storage_with_statics_at_zero( + item: Item, + storage: Storage, + data_store: &DataStore, + ) -> Self { + let grid_size: usize = grid_size(item, data_store); + let static_size: usize = static_size(item, data_store); + + let grid_offset = static_size.div_ceil(grid_size); + + match storage { + Storage::Assembler { + grid, + recipe_idx_with_this_item, + index, + } => { + assert!( + recipe_idx_with_this_item.into_usize() + < data_store.num_recipes_with_item[item.into_usize()] + ); + 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: u16::try_from(Into::::into( + recipe_idx_with_this_item, + )) + .unwrap(), + } + }, + Storage::Lab { grid, index } => 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 { + index: u32::try_from(index).unwrap(), + grid_or_static_flag: 0, + recipe_idx_with_this_item: static_id, + }, + } + } +} + #[derive( Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, Hash, PartialOrd, Ord, )] @@ -63,16 +203,15 @@ pub enum Storage { Assembler { grid: PowerGridIdentifier, recipe_idx_with_this_item: RecipeIdxType, - // TODO: - index: u16, + index: u32, }, Lab { grid: PowerGridIdentifier, - index: u16, + index: u32, }, Static { - index: usize, - static_id: StaticID, + static_id: u16, + index: u32, }, } @@ -119,6 +258,72 @@ impl Storage { Storage::Static { static_id, index } => Storage::Static { static_id, index }, } } + + 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( + self, + num_recipes: usize, + grid_size: usize, + static_size: usize, + ) -> (usize, usize) { + let grid_offset = static_size.div_ceil(grid_size); + + 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_offset) * grid_size + + Into::::into(recipe_idx_with_this_item); + (outer, index.try_into().unwrap()) + }, + Storage::Lab { grid, index } => { + let outer = (Into::::into(grid) + grid_offset) * 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 = Into::::into(static_id as u8); + (outer, index.try_into().unwrap()) + }, + } + } } #[derive( @@ -138,3 +343,96 @@ impl Storage { pub enum StaticID { Chest = 0, } + +#[cfg(test)] +mod test { + use crate::DATA_STORE; + use crate::blueprint::random_item; + use crate::inserter::{FakeUnionStorage, Storage}; + use crate::item::{Item, usize_from}; + use crate::storage_list::{grid_size, static_size}; + use proptest::prelude::{Just, Strategy}; + use proptest::prop_oneof; + use proptest::{prop_assert_eq, proptest}; + + use crate::power::power_grid::PowerGridIdentifier; + + use super::StaticID; + + fn random_storage(num_grids: u16, num_recipes: u8) -> impl Strategy> { + prop_oneof![ + (0..num_grids, 0..num_recipes, 0..u32::MAX).prop_map( + |(grid, recipe_idx_with_this_item, index)| { + Storage::Assembler { + grid: grid.try_into().unwrap_or(PowerGridIdentifier::MAX), + recipe_idx_with_this_item, + index, + } + } + ), + (0..num_grids, 0..u32::MAX).prop_map(|(grid, index)| { + Storage::Lab { + grid: grid.try_into().unwrap_or(PowerGridIdentifier::MAX), + index, + } + }), + (random_static(), 0..u16::MAX).prop_map(|(static_id, index)| { + Storage::Static { + static_id: static_id as u16, + index: index.into(), + } + }) + ] + } + + fn random_static() -> impl Strategy { + Just(StaticID::Chest) + } + + fn random_grid_count() -> impl Strategy { + 1..u16::MAX + } + + fn union_test_input() -> impl Strategy, u16, Storage)> { + (random_item(&DATA_STORE), random_grid_count()).prop_flat_map(|(item, grid_count)| { + ( + Just(item), + Just(grid_count), + random_storage( + grid_count, + DATA_STORE.num_recipes_with_item[usize_from(item.id)] as u8, + ), + ) + }) + } + + 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); + + 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 storage_indices = storage.into_inner_and_outer_indices(num_grids.into(), DATA_STORE.num_recipes_with_item[usize_from(item.id)], grid_size); + + 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 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)); + + prop_assert_eq!(union_indices, storage_indices); + } + } +} diff --git a/src/inserter/storage_storage_inserter.rs b/src/inserter/storage_storage_inserter.rs index b52ed9b..f4dd84e 100644 --- a/src/inserter/storage_storage_inserter.rs +++ b/src/inserter/storage_storage_inserter.rs @@ -1,19 +1,19 @@ -use std::num::NonZero; +use std::cmp::min; use crate::{ - item::{IdxTrait, WeakIdxTrait, ITEMCOUNTTYPE}, - storage_list::{index, SingleItemStorages}, + item::ITEMCOUNTTYPE, + storage_list::{SingleItemStorages, index_fake_union}, }; -use super::{InserterState, Storage}; +use super::{FakeUnionStorage, InserterState}; // FIXME: the storage_id cannot properly represent an index into multiple slices (which I have here, since // there are multiple lists of storages in the different MultiAssemblerStores (since multiple different recipes take for example Iron Plates)) #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct StorageStorageInserter { - storage_id_in: Storage, - storage_id_out: Storage, - state: InserterState, +pub struct StorageStorageInserter { + pub storage_id_in: FakeUnionStorage, + pub storage_id_out: FakeUnionStorage, + pub state: InserterState, } // This issue is less important then BeltStorage since these inserters are less common @@ -25,13 +25,13 @@ pub struct StorageStorageInserter { // Luckily since inserter only have limited range (3 tiles or whatever) there is inherent locality in the accesses, if the MultiStores are somewhat spacially aligned. // Though this could also lead to particularly poor access patterns if the belt/line of inserters is perpendicular to the stride pattern of the Multistore // (maybe some weird quadtree weirdness could help?) -impl StorageStorageInserter { +impl StorageStorageInserter { #[must_use] - pub const fn new(in_id: Storage, out_id: Storage) -> Self { + pub const fn new(in_id: FakeUnionStorage, out_id: FakeUnionStorage) -> Self { Self { storage_id_in: in_id, storage_id_out: out_id, - state: InserterState::Empty, + state: InserterState::WaitingForSourceItems(0), } } @@ -39,44 +39,41 @@ impl StorageStorageInserter { &mut self, storages: SingleItemStorages, movetime: u8, - num_grids_total: usize, - num_recipes: usize, + max_hand_size: ITEMCOUNTTYPE, grid_size: usize, ) { // TODO: I just added InserterStates and it is a lot slower (unsurprisingly), // Try and find a faster implementation of similar logic match self.state { - InserterState::Empty => { - let (_max_insert, old) = index( - storages, - self.storage_id_in, - num_grids_total, - num_recipes, - grid_size, - ); + InserterState::WaitingForSourceItems(count) => { + let (_max_insert, old) = index_fake_union(storages, self.storage_id_in, grid_size); - if *old > 0 { + let to_extract = min(max_hand_size - count, *old); + + if to_extract > 0 { // There is an item in the machine - *old -= 1; + *old -= to_extract; - self.state = InserterState::FullAndMovingOut(movetime); + if to_extract + count == max_hand_size { + self.state = InserterState::FullAndMovingOut(movetime); + } else { + self.state = InserterState::WaitingForSourceItems(count + to_extract); + } } }, - InserterState::FullAndWaitingForSlot => { - let (max_insert, old) = index( - storages, - self.storage_id_out, - num_grids_total, - num_recipes, - grid_size, - ); + InserterState::WaitingForSpaceInDestination(count) => { + let (max_insert, old) = index_fake_union(storages, self.storage_id_out, grid_size); - if *old < *max_insert { - // There is space in the machine - *old += 1; + let to_insert = min(count, *max_insert - *old); - self.state = InserterState::EmptyAndMovingBack(movetime); + if to_insert > 0 { + *old += to_insert; + if to_insert == count { + self.state = InserterState::EmptyAndMovingBack(movetime); + } else { + self.state = InserterState::WaitingForSpaceInDestination(count - to_insert); + } } }, InserterState::FullAndMovingOut(time) => { @@ -84,7 +81,7 @@ impl StorageStorageInserter { self.state = InserterState::FullAndMovingOut(time - 1); } else { // TODO: Do I want to try inserting immediately? - self.state = InserterState::FullAndWaitingForSlot; + self.state = InserterState::WaitingForSpaceInDestination(max_hand_size); } }, InserterState::EmptyAndMovingBack(time) => { @@ -92,7 +89,7 @@ impl StorageStorageInserter { self.state = InserterState::EmptyAndMovingBack(time - 1); } else { // TODO: Do I want to try getting a new item immediately? - self.state = InserterState::Empty; + self.state = InserterState::WaitingForSourceItems(0); } }, } diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs new file mode 100644 index 0000000..355183f --- /dev/null +++ b/src/inserter/storage_storage_with_buckets.rs @@ -0,0 +1,1365 @@ +use itertools::Itertools; +use std::mem; +use std::{array, collections::HashMap}; + +use super::{FakeUnionStorage, HAND_SIZE}; +use crate::assembler::arrays; +use crate::{ + item::ITEMCOUNTTYPE, + storage_list::{SingleItemStorages, index_fake_union}, +}; +use log::{info, trace, warn}; +use std::{cmp::min, iter}; + +const NUM_BUCKETS: usize = 120; +const MAX_MOVE_TIME: usize = NUM_BUCKETS * u8::MAX as usize; + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +pub struct Inserter { + pub storage_id_in: FakeUnionStorage, + pub storage_id_out: FakeUnionStorage, + pub max_hand_size: ITEMCOUNTTYPE, + pub current_hand: ITEMCOUNTTYPE, + pub remaining_loops: u8, + pub id: InserterId, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub enum LargeInserterState { + WaitingForSourceItems(ITEMCOUNTTYPE), + WaitingForSpaceInDestination(ITEMCOUNTTYPE), + FullAndMovingOut(u16), + EmptyAndMovingBack(u16), +} + +// This means at most u8::MAX inserters connecting any pair of Storages, that seems plenty +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub struct InserterId(u8); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct BucketedStorageStorageInserterStoreFrontend { + lookup: HashMap, + next_tick: NextTick, +} + +#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] +struct NextTick { + time: u32, + waiting_for_item: Vec, + waiting_for_item_result: Vec, + waiting_for_space: Vec, + waiting_for_space_result: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub struct InserterIdentifier { + pub source: FakeUnionStorage, + pub dest: FakeUnionStorage, + pub id: InserterId, +} + +impl BucketedStorageStorageInserterStoreFrontend { + pub fn new() -> Self { + Self { + lookup: HashMap::default(), + next_tick: NextTick { + time: 0, + waiting_for_item: vec![], + waiting_for_item_result: vec![], + waiting_for_space: vec![], + waiting_for_space_result: vec![], + }, + } + } + + fn get_info_naive( + &mut self, + id: InserterIdentifier, + movetime: u16, + current_time: u32, + ) -> LargeInserterState { + // FIXME: + todo!(); + match self.lookup.entry(id) { + std::collections::hash_map::Entry::Occupied(mut occupied_entry) => { + let (old_time, old_state) = occupied_entry.get(); + + let possible_states = get_possible_new_states_after_n_ticks( + (*old_state).into(), + movetime, + HAND_SIZE, + current_time - *old_time, + ); + + if let Ok(possible_states) = possible_states { + if possible_states.len() == 1 { + occupied_entry.insert((current_time, possible_states[0])); + possible_states[0] + } else { + todo!("We need to search in the possible states"); + } + } else { + todo!("We need to search everywhere"); + } + }, + std::collections::hash_map::Entry::Vacant(vacant_entry) => { + todo!("We need to search everywhere") + }, + } + } + + #[profiling::function] + pub fn get_info_batched<'a>( + &mut self, + to_find: impl IntoIterator, + store: &BucketedStorageStorageInserterStore, + do_next_tick_storing: bool, + current_time: u32, + ) -> HashMap { + let mut num_to_find = 0; + + assert!( + self.next_tick.waiting_for_item.len() >= self.next_tick.waiting_for_item_result.len() + ); + assert!( + self.next_tick.waiting_for_space.len() >= self.next_tick.waiting_for_space_result.len() + ); + + info!( + "Frontend requested info for {} Inserters on tick {}", + num_to_find, current_time + ); + + let mut ret = HashMap::new(); + + let mut waiting_for_item = vec![]; + let mut waiting_for_space = vec![]; + let mut moving_out: [_; NUM_BUCKETS] = array::from_fn(|_| vec![]); + let mut moving_in: [_; NUM_BUCKETS] = array::from_fn(|_| vec![]); + + { + profiling::scope!("Build Search Lists"); + for to_find in to_find { + num_to_find += 1; + + match self.lookup.get(&to_find) { + Some((old_time, old_state)) => { + let possible_states = get_possible_new_states_after_n_ticks( + (*old_state).into(), + store.movetime, + HAND_SIZE, + current_time - *old_time, + ); + + if let Ok(possible_states) = possible_states { + if possible_states.len() == 1 { + ret.insert(to_find, possible_states[0]); + } else { + assert!(possible_states.iter().all_unique()); + for possible in possible_states { + match possible { + LargeInserterState::WaitingForSourceItems(_) => { + waiting_for_item.push(to_find) + }, + LargeInserterState::WaitingForSpaceInDestination(_) => { + waiting_for_space.push(to_find) + }, + LargeInserterState::FullAndMovingOut(time) => { + moving_out[usize::from(time) - 1].push(to_find) + }, + LargeInserterState::EmptyAndMovingBack(time) => { + moving_in[usize::from(time) - 1].push(to_find) + }, + } + } + } + } else { + // Search evern - MOVING_IN + + for moving_in in &mut moving_in { + moving_in.push(to_find); + } + } + }, + None => { + // Search everywhere + waiting_for_item.push(to_find); + waiting_for_space.push(to_find); + + for moving_out in &mut moving_out { + moving_out.push(to_find); + } + + for moving_in in &mut moving_in { + moving_in.push(to_find); + } + }, + } + } + } + + info!( + "Did not have to search for {} out of {} requests", + ret.len(), + num_to_find + ); + + let sizes = store.get_list_sizes(); + + if self.next_tick.time == current_time { + // We know only one tick has passed! + + waiting_for_item.retain(|to_find| { + if self.next_tick.waiting_for_item.contains(to_find) { + if let Some(ins) = self.next_tick.waiting_for_item_result.iter().find(|ins| { + ins.storage_id_in == to_find.source + && ins.storage_id_out == to_find.dest + && ins.id == to_find.id + }) { + // TODO: Do I want to remove this inserter from next_tick result list? + ret.insert( + *to_find, + LargeInserterState::WaitingForSourceItems(ins.current_hand), + ); + } else { + let to_remove = moving_out[store.movetime as usize - 1].iter().position(|v| v == to_find).expect("Inserter with last state being WaitingForSourceItems not in moving_out list?"); + moving_out[store.movetime as usize - 1].swap_remove(to_remove); + ret.insert(*to_find, LargeInserterState::FullAndMovingOut(store.movetime)); + } + false + } else { + true + } + }); + + waiting_for_space.retain(|to_find| { + if self.next_tick.waiting_for_space.contains(to_find) { + if let Some(ins) = self.next_tick.waiting_for_space_result.iter().find(|ins| { + ins.storage_id_in == to_find.source + && ins.storage_id_out == to_find.dest + && ins.id == to_find.id + }) { + // TODO: Do I want to remove this inserter from next_tick result list? + ret.insert( + *to_find, + LargeInserterState::WaitingForSpaceInDestination(ins.current_hand), + ); + } else { + let to_remove = moving_in[store.movetime as usize - 1].iter().position(|v| v == to_find).expect("Inserter with last state being WaitingForSpaceInDestination not in moving_in list?"); + moving_in[store.movetime as usize - 1].swap_remove(to_remove); + ret.insert(*to_find, LargeInserterState::EmptyAndMovingBack(store.movetime)); + } + false + } else { + true + } + }); + } + + for (i, _) in sizes.into_iter().enumerate().sorted_by_key(|v| v.0) { + const MOVING_OUT_END: usize = NUM_BUCKETS + 1; + const WATING_FOR_SPACE: usize = MOVING_OUT_END; + const MOVING_IN: usize = MOVING_OUT_END + 1; + const MOVING_IN_END: usize = MOVING_OUT_END + 1 + NUM_BUCKETS; + + let search_list = match i { + 0 => &mut waiting_for_item, + n @ 1..MOVING_OUT_END => &mut moving_out[n - 1], + WATING_FOR_SPACE => &mut waiting_for_space, + n @ MOVING_IN..MOVING_IN_END => &mut moving_in[n - MOVING_IN], + + _ => unreachable!(), + }; + + if search_list.is_empty() { + continue; + } + + let found = store.find_in(search_list, i); + + { + profiling::scope!("Remove already found inserters"); + for n in (i + 1)..MOVING_IN_END { + let search_list = match n { + 0 => &mut waiting_for_item, + n @ 1..MOVING_OUT_END => &mut moving_out[n - 1], + WATING_FOR_SPACE => &mut waiting_for_space, + n @ MOVING_IN..MOVING_IN_END => &mut moving_in[n - MOVING_IN], + + _ => unreachable!(), + }; + search_list.retain(|v| !found.contains_key(v)); + } + } + + ret.extend(found.into_iter().map(|(k, inserter)| { + ( + k, + // TODO: Respect store.current_tick + match i { + 0 => LargeInserterState::WaitingForSourceItems(inserter.current_hand), + n @ 1..MOVING_OUT_END => { + LargeInserterState::FullAndMovingOut((n - 1).try_into().unwrap()) + }, + WATING_FOR_SPACE => { + LargeInserterState::WaitingForSpaceInDestination(inserter.current_hand) + }, + n @ MOVING_IN..MOVING_IN_END => LargeInserterState::EmptyAndMovingBack( + (n - MOVING_IN).try_into().unwrap(), + ), + + _ => unreachable!(), + }, + ) + })); + } + + let mut waiting_for_item = vec![]; + let mut waiting_for_space = vec![]; + + for (k, v) in ret.iter() { + if matches!(*v, LargeInserterState::WaitingForSourceItems(_)) { + waiting_for_item.push(*k); + } else if matches!(*v, LargeInserterState::WaitingForSpaceInDestination(_)) { + waiting_for_space.push(*k); + } + } + + if do_next_tick_storing { + self.next_tick = NextTick { + time: current_time + 1, + waiting_for_item, + waiting_for_item_result: vec![], + waiting_for_space, + waiting_for_space_result: vec![], + }; + } else { + self.next_tick = NextTick { + time: 0, + waiting_for_item: vec![], + waiting_for_item_result: vec![], + waiting_for_space: vec![], + waiting_for_space_result: vec![], + }; + } + + assert_eq!( + ret.len(), + num_to_find, + "Expected to find {num_to_find} Inserters, found {}", + ret.len() + ); + + self.lookup.extend( + ret.iter() + .map(|(to_find, state)| (*to_find, (current_time, *state))), + ); + + ret + } +} + +// TODO: Make this faster +#[profiling::function] +fn get_possible_new_states_after_n_ticks( + starting_state: LargeInserterState, + movetime: u16, + max_hand_size: ITEMCOUNTTYPE, + n: u32, +) -> Result, ()> { + const MAX_SEARCH_SIZE: usize = 100; + + let mut current = vec![starting_state]; + + for _ in 0..n { + let this_tick = std::mem::take(&mut current); + + current.extend( + this_tick + .into_iter() + .flat_map(|state| get_possible_new_states(state, movetime, max_hand_size)) + .unique(), + ); + + if current.len() > MAX_SEARCH_SIZE { + return Err(()); + } + } + + Ok(current) +} + +fn get_possible_new_states( + starting_state: LargeInserterState, + movetime: u16, + max_hand_size: ITEMCOUNTTYPE, +) -> Vec { + match starting_state { + LargeInserterState::WaitingForSourceItems(_) => { + iter::once(LargeInserterState::FullAndMovingOut(movetime)) + .chain(iter::once(LargeInserterState::WaitingForSourceItems(0))) + .collect() + }, + LargeInserterState::WaitingForSpaceInDestination(_) => { + iter::once(LargeInserterState::EmptyAndMovingBack(movetime)) + .chain(iter::once( + LargeInserterState::WaitingForSpaceInDestination(0), + )) + .collect() + }, + LargeInserterState::FullAndMovingOut(time) => { + if time > 0 { + vec![LargeInserterState::FullAndMovingOut(time - 1)] + } else { + vec![LargeInserterState::WaitingForSpaceInDestination(0)] + } + }, + LargeInserterState::EmptyAndMovingBack(time) => { + if time > 0 { + vec![LargeInserterState::EmptyAndMovingBack(time - 1)] + } else { + vec![LargeInserterState::WaitingForSourceItems(0)] + } + }, + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct BucketedStorageStorageInserterStore { + pub movetime: u16, + + id_lookup: HashMap<(FakeUnionStorage, FakeUnionStorage), u8>, + + current_tick: usize, + waiting_for_item: Vec, + waiting_for_space_in_destination: Vec, + #[serde(with = "arrays")] + full_and_moving_out: [Vec; NUM_BUCKETS], + #[serde(with = "arrays")] + empty_and_moving_back: [Vec; NUM_BUCKETS], +} + +impl BucketedStorageStorageInserterStore { + pub fn new(movetime: u16) -> Self { + assert!((movetime as usize) < MAX_MOVE_TIME); + + Self { + movetime, + id_lookup: HashMap::new(), + waiting_for_item: vec![], + full_and_moving_out: array::from_fn(|_| vec![]), + waiting_for_space_in_destination: vec![], + empty_and_moving_back: array::from_fn(|_| vec![]), + current_tick: 0, + } + } + + pub fn add_inserter( + &mut self, + source: FakeUnionStorage, + dest: FakeUnionStorage, + max_hand_size: ITEMCOUNTTYPE, + ) -> InserterId { + let id = self.id_lookup.entry((source, dest)).or_default(); + + let new_id = InserterId(*id); + + self.waiting_for_item.push(Inserter { + storage_id_in: source, + storage_id_out: dest, + max_hand_size, + current_hand: 0, + id: new_id, + remaining_loops: 0, + }); + + match (*id).checked_add(1) { + Some(next_id) => { + *id = next_id; + }, + None => todo!( + "A pair of storages has u8::MAX inserter between them! (Or a lot of ids were lost due to removals)" + ), + // None => { + // // FIXME FIXME FIXME + // }, + } + + new_id + } + + pub fn remove_inserter( + &mut self, + source: FakeUnionStorage, + dest: FakeUnionStorage, + id: InserterId, + ) -> Inserter { + // TODO: We do not decrease the id (since it would invalidate all higher ids), which means we "lose" possible ids. + // This means after a lot of additions and removals it might be impossible to connect a pair of storages + + let old_id = self + .id_lookup + .get_mut(&(source, dest)) + .expect("No ids for (source, dest) were ever given out!"); + + // Only decrease if the id is the highest, so we do not invalidate but a simple place -> remove loop does not cause problems + if *old_id == id.0 + 1 { + *old_id -= 1; + } + + if let Some(idx) = self + .waiting_for_item + .iter() + .position(|i| i.storage_id_in == source && i.storage_id_out == dest && i.id == id) + { + let removed = self.waiting_for_item.swap_remove(idx); + return removed; + } + + if let Some(idx) = self + .waiting_for_space_in_destination + .iter() + .position(|i| i.storage_id_in == source && i.storage_id_out == dest && i.id == id) + { + let removed = self.waiting_for_space_in_destination.swap_remove(idx); + return removed; + } + + for moving_out in &mut self.full_and_moving_out { + if let Some(idx) = moving_out + .iter() + .position(|i| i.storage_id_in == source && i.storage_id_out == dest && i.id == id) + { + let removed = moving_out.swap_remove(idx); + return removed; + } + } + + for moving_in in &mut self.empty_and_moving_back { + if let Some(idx) = moving_in + .iter() + .position(|i| i.storage_id_in == source && i.storage_id_out == dest && i.id == id) + { + let removed = moving_in.swap_remove(idx); + return removed; + } + } + + unreachable!("Tried to remove an inserter that does not exist!"); + } + + #[profiling::function] + pub fn update( + &mut self, + frontend: &mut BucketedStorageStorageInserterStoreFrontend, + storages: SingleItemStorages, + grid_size: usize, + current_tick: u32, + ) { + let old_len: usize = self.get_list_sizes().iter().sum(); + + assert!(self.current_tick < NUM_BUCKETS); + + { + profiling::scope!("Try taking Items from inventories"); + let now_moving = self.waiting_for_item.extract_if(.., |inserter| { + if inserter.remaining_loops > 0 { + inserter.remaining_loops -= 1; + return true; + } + + let (_max_insert, old) = + index_fake_union(storages, inserter.storage_id_in, grid_size); + + let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); + + if to_extract > 0 { + *old -= to_extract; + + inserter.current_hand += to_extract; + } + + let extract = inserter.current_hand == inserter.max_hand_size; + + if !extract + && frontend.next_tick.time == current_tick + && frontend.next_tick.waiting_for_item.iter().any(|v| { + v.source == inserter.storage_id_in + && v.dest == inserter.storage_id_out + && v.id == inserter.id + }) + { + frontend.next_tick.waiting_for_item_result.push(*inserter); + } + + if extract { + inserter.remaining_loops = (self.movetime / NUM_BUCKETS as u16) as u8; + } + + extract + }); + + for ins in now_moving { + // FIXME: I am sure there are some off by one errors in here + if ins.remaining_loops == 0 { + self.full_and_moving_out[(self.current_tick + + (self.movetime as usize % NUM_BUCKETS)) + % NUM_BUCKETS] + .push(ins); + } else { + self.empty_and_moving_back + [(self.current_tick + (NUM_BUCKETS - 1)) % NUM_BUCKETS] + .push(ins); + } + } + } + + { + profiling::scope!("Try putting Items into inventories"); + let now_moving_back = + self.waiting_for_space_in_destination + .extract_if(.., |inserter| { + if inserter.remaining_loops > 0 { + inserter.remaining_loops -= 1; + return true; + } + + let (max_insert, old) = + index_fake_union(storages, inserter.storage_id_out, grid_size); + + let to_insert = min(inserter.current_hand, *max_insert - *old); + + if to_insert > 0 { + *old += to_insert; + + inserter.current_hand -= to_insert; + } + + let extract = inserter.current_hand == 0; + + if !extract + && frontend.next_tick.time == current_tick + && frontend.next_tick.waiting_for_space.iter().any(|v| { + v.source == inserter.storage_id_in + && v.dest == inserter.storage_id_out + && v.id == inserter.id + }) + { + frontend.next_tick.waiting_for_space_result.push(*inserter); + } + + if extract { + inserter.remaining_loops = (self.movetime / NUM_BUCKETS as u16) as u8; + } + + extract + }); + + for ins in now_moving_back { + // FIXME: I am sure there are some off by one errors in here + if ins.remaining_loops == 0 { + self.empty_and_moving_back[(self.current_tick + + (self.movetime as usize % NUM_BUCKETS)) + % NUM_BUCKETS] + .push(ins); + } else { + self.full_and_moving_out[(self.current_tick + (NUM_BUCKETS - 1)) % NUM_BUCKETS] + .push(ins); + } + } + } + + { + profiling::scope!("Advance time moving back"); + if self.empty_and_moving_back[self.current_tick].len() < self.waiting_for_item.len() { + self.waiting_for_item + .extend_from_slice(&self.empty_and_moving_back[self.current_tick]); + self.empty_and_moving_back[self.current_tick].clear(); + } else { + // self.empty_and_moving_back[self.current_tick] + // .splice(0..0, self.waiting_for_item.drain(..)); + self.empty_and_moving_back[self.current_tick] + .extend_from_slice(&self.waiting_for_item); + self.waiting_for_item.clear(); + mem::swap( + &mut self.empty_and_moving_back[self.current_tick], + &mut self.waiting_for_item, + ); + } + } + + { + profiling::scope!("Advance time moving out"); + if self.full_and_moving_out[self.current_tick].len() + < self.waiting_for_space_in_destination.len() + { + self.waiting_for_space_in_destination + .extend_from_slice(&self.full_and_moving_out[self.current_tick]); + self.full_and_moving_out[self.current_tick].clear(); + } else { + // self.full_and_moving_out[self.current_tick].splice(0..0, self.waiting_for_space_in_destination.drain(..)); + self.full_and_moving_out[self.current_tick] + .extend_from_slice(&self.waiting_for_space_in_destination); + self.waiting_for_space_in_destination.clear(); + mem::swap( + &mut self.full_and_moving_out[self.current_tick], + &mut self.waiting_for_space_in_destination, + ); + } + } + + self.current_tick = (self.current_tick + 1) % NUM_BUCKETS; + + assert_eq!(old_len, self.get_list_sizes().iter().sum::()); + } + + fn get_list_sizes(&self) -> Vec { + let mut ret = vec![]; + + ret.push(self.waiting_for_item.len()); + ret.extend(self.full_and_moving_out.iter().map(|v| v.len())); + ret.push(self.waiting_for_space_in_destination.len()); + ret.extend(self.empty_and_moving_back.iter().map(|v| v.len())); + + ret + } + + #[profiling::function] + fn find_in( + &self, + to_find: &mut Vec, + search_in: usize, + ) -> HashMap { + const MOVING_OUT_END: usize = NUM_BUCKETS + 1; + const WATING_FOR_SPACE: usize = MOVING_OUT_END; + const MOVING_IN: usize = MOVING_OUT_END + 1; + const MOVING_IN_END: usize = MOVING_OUT_END + 1 + NUM_BUCKETS; + + let mut ret = HashMap::new(); + + let search_list = match search_in { + 0 => &self.waiting_for_item, + n @ 1..MOVING_OUT_END => { + &self.full_and_moving_out[(n - 1 + self.current_tick) % NUM_BUCKETS] + }, + WATING_FOR_SPACE => &self.waiting_for_space_in_destination, + n @ MOVING_IN..MOVING_IN_END => { + &self.empty_and_moving_back[(n - MOVING_IN + self.current_tick) % NUM_BUCKETS] + }, + + _ => unreachable!(), + }; + + trace!( + "Having to search {} inserters for rendering", + search_list.len() + ); + + for inserter in search_list { + to_find.retain(|to_find| { + if to_find.source == inserter.storage_id_in + && to_find.dest == inserter.storage_id_out + && to_find.id == inserter.id + { + ret.insert(*to_find, *inserter); + // Remove this search value for every future inserter + false + } else { + true + } + }); + + if to_find.is_empty() { + return ret; + } + } + + ret + } + + #[profiling::function] + #[must_use] + pub fn update_inserter_src( + &mut self, + id: InserterIdentifier, + new_src: FakeUnionStorage, + ) -> InserterIdentifier { + if id.source == new_src { + warn!("Tried to update src to the same id!"); + return id; + } + + let old_id = self + .id_lookup + .get_mut(&(id.source, id.dest)) + .expect("No ids for (source, dest) were ever given out!"); + + // Only decrease if the id is the highest, so we do not invalidate but a simple place -> remove loop does not cause problems + if *old_id == id.id.0 + 1 { + *old_id -= 1; + } + + if let Some(idx) = self.waiting_for_item.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + self.waiting_for_item[idx].storage_id_in = new_src; + let new_id = self + .id_lookup + .entry(( + self.waiting_for_item[idx].storage_id_in, + self.waiting_for_item[idx].storage_id_out, + )) + .or_insert(0); + + self.waiting_for_item[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: self.waiting_for_item[idx].storage_id_in, + dest: self.waiting_for_item[idx].storage_id_out, + id: self.waiting_for_item[idx].id, + }; + } + + if let Some(idx) = self.waiting_for_space_in_destination.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + self.waiting_for_space_in_destination[idx].storage_id_in = new_src; + let new_id = self + .id_lookup + .entry(( + self.waiting_for_space_in_destination[idx].storage_id_in, + self.waiting_for_space_in_destination[idx].storage_id_out, + )) + .or_insert(0); + + self.waiting_for_space_in_destination[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: self.waiting_for_space_in_destination[idx].storage_id_in, + dest: self.waiting_for_space_in_destination[idx].storage_id_out, + id: self.waiting_for_space_in_destination[idx].id, + }; + } + + for moving_out in &mut self.full_and_moving_out { + if let Some(idx) = moving_out.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + moving_out[idx].storage_id_in = new_src; + let new_id = self + .id_lookup + .entry(( + moving_out[idx].storage_id_in, + moving_out[idx].storage_id_out, + )) + .or_insert(0); + + moving_out[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: moving_out[idx].storage_id_in, + dest: moving_out[idx].storage_id_out, + id: moving_out[idx].id, + }; + } + } + + for moving_in in &mut self.empty_and_moving_back { + if let Some(idx) = moving_in.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + moving_in[idx].storage_id_in = new_src; + let new_id = self + .id_lookup + .entry((moving_in[idx].storage_id_in, moving_in[idx].storage_id_out)) + .or_insert(0); + + moving_in[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: moving_in[idx].storage_id_in, + dest: moving_in[idx].storage_id_out, + id: moving_in[idx].id, + }; + } + } + + unreachable!("Tried to update_inserter_src an inserter that does not exist!"); + } + + #[profiling::function] + #[must_use] + pub fn update_inserter_dest( + &mut self, + id: InserterIdentifier, + new_dest: FakeUnionStorage, + ) -> InserterIdentifier { + if id.dest == new_dest { + warn!("Tried to update dest to the same id!"); + return id; + } + + let old_id = self + .id_lookup + .get_mut(&(id.source, id.dest)) + .expect("No ids for (source, dest) were ever given out!"); + + // Only decrease if the id is the highest, so we do not invalidate but a simple place -> remove loop does not cause problems + if *old_id == id.id.0 + 1 { + *old_id -= 1; + } + + if let Some(idx) = self.waiting_for_item.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + self.waiting_for_item[idx].storage_id_out = new_dest; + + let new_id = self + .id_lookup + .entry(( + self.waiting_for_item[idx].storage_id_in, + self.waiting_for_item[idx].storage_id_out, + )) + .or_insert(0); + + self.waiting_for_item[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: self.waiting_for_item[idx].storage_id_in, + dest: self.waiting_for_item[idx].storage_id_out, + id: self.waiting_for_item[idx].id, + }; + } + + if let Some(idx) = self.waiting_for_space_in_destination.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + self.waiting_for_space_in_destination[idx].storage_id_out = new_dest; + let new_id = self + .id_lookup + .entry(( + self.waiting_for_space_in_destination[idx].storage_id_in, + self.waiting_for_space_in_destination[idx].storage_id_out, + )) + .or_insert(0); + + self.waiting_for_space_in_destination[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: self.waiting_for_space_in_destination[idx].storage_id_in, + dest: self.waiting_for_space_in_destination[idx].storage_id_out, + id: self.waiting_for_space_in_destination[idx].id, + }; + } + + for moving_out in &mut self.full_and_moving_out { + if let Some(idx) = moving_out.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + moving_out[idx].storage_id_out = new_dest; + let new_id = self + .id_lookup + .entry(( + moving_out[idx].storage_id_in, + moving_out[idx].storage_id_out, + )) + .or_insert(0); + + moving_out[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: moving_out[idx].storage_id_in, + dest: moving_out[idx].storage_id_out, + id: moving_out[idx].id, + }; + } + } + + for moving_in in &mut self.empty_and_moving_back { + if let Some(idx) = moving_in.iter().position(|i| { + i.storage_id_in == id.source && i.storage_id_out == id.dest && i.id == id.id + }) { + moving_in[idx].storage_id_out = new_dest; + let new_id = self + .id_lookup + .entry((moving_in[idx].storage_id_in, moving_in[idx].storage_id_out)) + .or_insert(0); + + moving_in[idx].id = InserterId(*new_id); + + return InserterIdentifier { + source: moving_in[idx].storage_id_in, + dest: moving_in[idx].storage_id_out, + id: moving_in[idx].id, + }; + } + } + + unreachable!( + "Tried to update_inserter_dest an inserter that does not exist! {:?}", + self + ); + } +} + +#[cfg(test)] +mod test { + const NUM_INSERTERS: usize = 2_000_000; + const NUM_ITEMS: usize = 5; + const NUM_VISIBLE: usize = 1000; + + use std::array; + + use itertools::Itertools; + use rand::{random, seq::SliceRandom}; + use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; + use test::Bencher; + + use crate::inserter::{ + FakeUnionStorage, + storage_storage_with_buckets::{ + BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, + }, + }; + + use super::BucketedStorageStorageInserterStore; + + #[bench] + fn bench_update_storage_storage_inserter_store_buckets(b: &mut Bencher) { + let mut store: [_; NUM_ITEMS] = + array::from_fn(|_| BucketedStorageStorageInserterStore::new(120)); + let mut frontend: [_; NUM_ITEMS] = + array::from_fn(|_| BucketedStorageStorageInserterStoreFrontend::new()); + + let max_insert = vec![200u8; NUM_INSERTERS]; + let mut storages_in: [_; NUM_ITEMS] = array::from_fn(|_| vec![200u8; NUM_INSERTERS]); + let mut storages_out: [_; NUM_ITEMS] = array::from_fn(|_| vec![0u8; NUM_INSERTERS]); + + let mut current_tick = 0; + + for item in 0..NUM_ITEMS { + let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); + values.shuffle(&mut rand::thread_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()), + ], + 10, + current_tick, + ); + + if storages_in[item][0] < 20 { + storages_in[item] = vec![200u8; NUM_INSERTERS]; + storages_out[item] = vec![0u8; NUM_INSERTERS]; + } + } + + store[item].add_inserter( + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 0, + }, + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 1, + }, + 1, + ); + } + } + + storages_in = array::from_fn(|_| vec![200u8; NUM_INSERTERS]); + storages_out = array::from_fn(|_| vec![0u8; NUM_INSERTERS]); + + let mut num_iter = 0; + + b.iter(|| { + // for _ in 0..10 { + storages_in + .par_iter_mut() + .zip( + storages_out + .par_iter_mut() + .zip(store.par_iter_mut().zip(frontend.par_iter_mut())), + ) + .for_each(|(storage_in, (storage_out, (store, frontend)))| { + if storage_in[0] < 20 { + *storage_in = vec![200u8; NUM_INSERTERS]; + *storage_out = vec![0u8; NUM_INSERTERS]; + } + store.update( + frontend, + &mut [ + (max_insert.as_slice(), storage_in.as_mut_slice()), + (max_insert.as_slice(), storage_out.as_mut_slice()), + ], + 10, + current_tick, + ); + }); + + // } + num_iter += 1; + current_tick += 1; + }); + + dbg!(&storages_in[0][0..10], &storages_out[0][0..10], num_iter); + } + + #[bench] + fn bench_storage_storage_inserter_store_find_batched_with_next_tick_optimization( + b: &mut Bencher, + ) { + let mut store = BucketedStorageStorageInserterStore::new(120); + + let mut frontend = BucketedStorageStorageInserterStoreFrontend::new(); + + let max_insert = vec![200u8; NUM_INSERTERS]; + let mut storages_in = vec![200u8; NUM_INSERTERS]; + let mut storages_out = vec![0u8; NUM_INSERTERS]; + + let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); + values.shuffle(&mut rand::thread_rng()); + + let mut current_time: u32 = 0; + + for i in values { + if random::() < 10 { + store.update( + &mut frontend, + &mut [ + (max_insert.as_slice(), storages_in.as_mut_slice()), + (max_insert.as_slice(), storages_out.as_mut_slice()), + ], + 10, + current_time, + ); + + if storages_in[0] < 20 { + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + } + } + + let id = store.add_inserter( + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 0, + }, + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 1, + }, + 1, + ); + + assert_eq!(id, InserterId(0)); + } + + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + + let to_find: Vec = (0..(NUM_VISIBLE as u32)).collect(); + + // let to_find = vec![0u16]; + + let to_find: Vec<_> = to_find + .into_iter() + .map(|i| InserterIdentifier { + source: FakeUnionStorage { + index: i.into(), + grid_or_static_flag: 0, + recipe_idx_with_this_item: 0, + }, + dest: FakeUnionStorage { + index: i.into(), + grid_or_static_flag: 0, + recipe_idx_with_this_item: 1, + }, + id: InserterId(0), + }) + .collect(); + + b.iter(|| { + let ret = + frontend.get_info_batched(to_find.iter().copied(), &store, true, current_time); + + if storages_in[0] < 20 { + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + } + store.update( + &mut frontend, + &mut [ + (max_insert.as_slice(), storages_in.as_mut_slice()), + (max_insert.as_slice(), storages_out.as_mut_slice()), + ], + 10, + current_time, + ); + + current_time += 1; + + ret + }); + + dbg!(&storages_in[0..10], &storages_out[0..10], current_time); + } + + #[bench] + fn bench_storage_storage_inserter_store_find_batched_without_next_tick_optimization( + b: &mut Bencher, + ) { + let mut store = BucketedStorageStorageInserterStore::new(120); + + let mut frontend = BucketedStorageStorageInserterStoreFrontend::new(); + + let max_insert = vec![200u8; NUM_INSERTERS]; + let mut storages_in = vec![200u8; NUM_INSERTERS]; + let mut storages_out = vec![0u8; NUM_INSERTERS]; + + let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); + values.shuffle(&mut rand::thread_rng()); + + let mut current_time: u32 = 0; + + for i in values { + if random::() < 10 { + store.update( + &mut frontend, + &mut [ + (max_insert.as_slice(), storages_in.as_mut_slice()), + (max_insert.as_slice(), storages_out.as_mut_slice()), + ], + 10, + current_time, + ); + + if storages_in[0] < 20 { + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + } + } + + let id = store.add_inserter( + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 0, + }, + FakeUnionStorage { + index: i, + grid_or_static_flag: 0, + recipe_idx_with_this_item: 1, + }, + 1, + ); + + assert_eq!(id, InserterId(0)); + } + + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + + let to_find: Vec = (0..(NUM_VISIBLE as u32)).collect(); + + // let to_find = vec![0u16]; + + let to_find: Vec<_> = to_find + .into_iter() + .map(|i| InserterIdentifier { + source: FakeUnionStorage { + index: i.into(), + grid_or_static_flag: 0, + recipe_idx_with_this_item: 0, + }, + dest: FakeUnionStorage { + index: i.into(), + grid_or_static_flag: 0, + recipe_idx_with_this_item: 1, + }, + id: InserterId(0), + }) + .collect(); + + b.iter(|| { + let ret = + frontend.get_info_batched(to_find.iter().copied(), &store, false, current_time); + + if storages_in[0] < 20 { + storages_in = vec![200u8; NUM_INSERTERS]; + storages_out = vec![0u8; NUM_INSERTERS]; + } + store.update( + &mut frontend, + &mut [ + (max_insert.as_slice(), storages_in.as_mut_slice()), + (max_insert.as_slice(), storages_out.as_mut_slice()), + ], + 10, + current_time, + ); + + current_time += 1; + + ret + }); + + dbg!(&storages_in[0..10], &storages_out[0..10], current_time); + } + + const LEN: usize = 10_000_000; + + #[bench] + fn bench_extract_if_low_extraction_rate(b: &mut Bencher) { + let mut source = vec![0; LEN]; + let mut dest = vec![]; + + let mut num_iterations = 0; + b.iter(|| { + let extracted = source.extract_if(.., |_| rand::random::() < 20); + + dest.extend(extracted); + + source.resize(LEN, 0); + // dest.clear(); + + num_iterations += 1; + }); + + dbg!(source.len()); + dbg!(dest.len()); + + dbg!(num_iterations); + } + + #[bench] + fn bench_extract_if_high_extraction_rate(b: &mut Bencher) { + let mut source = vec![0; LEN]; + let mut dest = vec![]; + + let mut num_iterations = 0; + b.iter(|| { + let extracted = source.extract_if(.., |_| rand::random::() >= 20); + + dest.extend(extracted); + + source.resize(LEN, 0); + + num_iterations += 1; + }); + + dbg!(source.len()); + dbg!(dest.len()); + + dbg!(num_iterations); + } +} diff --git a/src/item.rs b/src/item.rs index d1eb9a4..49deade 100644 --- a/src/item.rs +++ b/src/item.rs @@ -4,8 +4,12 @@ use std::hash::Hash; pub type ITEMCOUNTTYPE = u8; +pub trait Indexable { + fn into_usize(self) -> usize; +} + pub trait IdxTrait: - Debug + serde::Serialize + for<'de> serde::Deserialize<'de> + WeakIdxTrait + Debug + Indexable + serde::Serialize + for<'de> serde::Deserialize<'de> + WeakIdxTrait { } @@ -40,6 +44,12 @@ pub struct Item { pub id: ItemIdxType, } +impl Indexable for Item { + fn into_usize(self) -> usize { + self.id.into_usize() + } +} + impl From for Item { fn from(value: ItemIdxType) -> Self { Self { id: value } @@ -59,6 +69,12 @@ pub struct Recipe { pub id: RecipeIdxType, } +impl Indexable for Recipe { + fn into_usize(self) -> usize { + self.id.into_usize() + } +} + impl From for Recipe { fn from(value: RecipeIdxType) -> Self { Self { id: value } diff --git a/src/join_many.rs b/src/join_many.rs new file mode 100644 index 0000000..e95713a --- /dev/null +++ b/src/join_many.rs @@ -0,0 +1,54 @@ +// https://stackoverflow.com/a/67140319/15698509 +macro_rules! join { + // Entry point for `let = fork ;` usage. + ( $( let $lhs:pat = fork $rhs:expr ; )+ ) => { + let join!( @left $( $lhs , )+ ) = join!( @right $( $rhs , )+ ); + }; + + // Entry point for `,` usage. + ( $x:expr $( , $xs:expr )* ) => { + join! { @flat $x $( , $xs )* } + }; + + // Flattening tuples with temporary variables. + ( @flat $( let $lhs:ident = $rhs:expr ; )+ ) => { + { + let join!( @left $( $lhs , )+ ) = join!( @right $( $rhs , )+ ); + ($( $lhs ),+) + } + }; + ( @flat $( let $lhs:ident = $rhs:expr ; )* $x:expr $( , $xs:expr )*) => { + join! { @flat + $( let $lhs = $rhs ; )* + let lhs = $x; + $($xs),* + } + }; + + // Left hand side recursion to nest individual patterns into tuple patterns + // like `(x, (y, (z, ...)))`. + ( @left $x:pat , ) => { + $x + }; + ( @left $x:pat , $( $xs:pat , )+ ) => { + ( $x , join!( @left $( $xs , )+ ) ) + }; + + // Right hand side recursion to nest exprs into rayon fork-joins + // like: + // + // rayon::join( + // x, + // || rayon::join( + // y, + // || rayon::join( + // z, + // || ...))) + ( @right $x:expr , ) => { + ($x)() + }; + ( @right $x:expr , $( $xs:expr , )+ ) => { + ::rayon::join( $x , || join!( @right $( $xs , )+ ) ) + } +} +pub(crate) use join; diff --git a/src/lab.rs b/src/lab.rs index a0c249c..16887d9 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -2,12 +2,11 @@ use crate::{ assembler::TIMERTYPE, data::DataStore, frontend::world::Position, - item::{IdxTrait, Item, ITEMCOUNTTYPE}, + item::{ITEMCOUNTTYPE, IdxTrait, Item}, power::{ - power_grid::{IndexUpdateInfo, PowerGridEntity, PowerGridIdentifier, MAX_POWER_MULT}, Joule, Watt, + power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridEntity, PowerGridIdentifier}, }, - research::Technology, }; // TODO: Add variable power consumption and speed @@ -18,6 +17,29 @@ pub struct MultiLabStore { timer: Vec, holes: Vec, + /// Base Crafting Speed in 5% increments + /// i.e. 28 => 140% Crafting speed + /// Maximum is 1275% Crafting Speed + base_speed: Vec, + + /// Crafting Speed in 5% increments + /// i.e. 28 => 140% Crafting speed + /// Maximum is 1275% Crafting Speed + combined_speed_mod: Vec, + /// Bonus Productivity in % + bonus_productivity: Vec, + /// Power Consumption in 5% increments + /// i.e. 28 => 140% Crafting speed + /// Maximum is 1275% x Base Power Consumption + power_consumption_modifier: Vec, + + raw_speed_mod: Vec, + raw_bonus_productivity: Vec, + raw_power_consumption_modifier: Vec, + + // TODO: This can likely be smaller than full u64 + base_power_consumption: Vec, + // This is not used in normal updates, but only for when the indices change (i.e. when merging power networks) positions: Vec, types: Vec, @@ -30,6 +52,18 @@ impl MultiLabStore { max_insert: vec![Vec::new(); science_bottle_items.len()].into_boxed_slice(), sciences: vec![Vec::new(); science_bottle_items.len()].into_boxed_slice(), timer: vec![], + + base_speed: vec![], + combined_speed_mod: vec![], + bonus_productivity: vec![], + power_consumption_modifier: vec![], + + raw_speed_mod: vec![], + raw_bonus_productivity: vec![], + raw_power_consumption_modifier: vec![], + + base_power_consumption: vec![], + holes: vec![], positions: vec![], types: vec![], @@ -44,7 +78,8 @@ impl MultiLabStore { data_store: &DataStore, ) -> ( Self, - impl IntoIterator>, + impl IntoIterator> + + use, ) { let old_len = self.positions.len(); @@ -81,6 +116,78 @@ impl MultiLabStore { .map(|(_, v)| v), ); + self.base_speed.extend( + other + .base_speed + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.combined_speed_mod.extend( + other + .combined_speed_mod + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.bonus_productivity.extend( + other + .bonus_productivity + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.power_consumption_modifier.extend( + other + .power_consumption_modifier + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.raw_speed_mod.extend( + other + .raw_speed_mod + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.raw_bonus_productivity.extend( + other + .raw_bonus_productivity + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.raw_power_consumption_modifier.extend( + other + .raw_power_consumption_modifier + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + + self.base_power_consumption.extend( + other + .base_power_consumption + .into_iter() + .enumerate() + .filter(|(i, _)| !other.holes.contains(i)) + .map(|(_, v)| v), + ); + self.positions.extend( other .positions @@ -109,7 +216,11 @@ impl MultiLabStore { .enumerate() .map(move |(new_index_offs, (old_index, pos))| IndexUpdateInfo { position: pos, - new_storage: PowerGridEntity::Lab { + old_pg_entity: PowerGridEntity::Lab { + index: old_index.try_into().unwrap(), + ty: other.types[old_index], + }, + new_pg_entity: PowerGridEntity::Lab { index: (old_len + new_index_offs).try_into().unwrap(), ty: other.types[old_index], }, @@ -122,6 +233,16 @@ impl MultiLabStore { timer: self.timer, holes: self.holes, positions: self.positions, + + base_power_consumption: self.base_power_consumption, + base_speed: self.base_speed, + bonus_productivity: self.bonus_productivity, + combined_speed_mod: self.combined_speed_mod, + power_consumption_modifier: self.power_consumption_modifier, + raw_bonus_productivity: self.raw_bonus_productivity, + raw_power_consumption_modifier: self.raw_power_consumption_modifier, + raw_speed_mod: self.raw_speed_mod, + types: self.types, }; @@ -129,20 +250,22 @@ impl MultiLabStore { } // TODO: Ensure good compilation results (i.e. vectorization) - pub fn update( + // FIXME: Use module modifiers + pub fn update( &mut self, power_mult: u8, - current_research: &Option, - data_store: &DataStore, + current_research_costs: Option<&[u8]>, ) -> (Joule, u32, u16) { const POWER_CONSUMPTION: Watt = Watt(600); - const TICKS_PER_SCIENCE: TIMERTYPE = 60; + const TICKS_PER_SCIENCE: TIMERTYPE = 10; - let Some(current_research) = current_research else { + let Some(current_research_costs) = current_research_costs else { // We are not currently researching anything. This means we do not use any items any power or gained any progress return (Joule(0), 0, 0); }; + let needed = current_research_costs; + let mut times_ings_used = 0; let mut running = 0; @@ -151,12 +274,6 @@ impl MultiLabStore { * (TIMERTYPE::MAX / TIMERTYPE::from(MAX_POWER_MULT))) / TICKS_PER_SCIENCE; - let needed: Box<[_]> = data_store.technology_costs[usize::from(current_research.id)] - .1 - .iter() - .map(|needed| *needed) - .collect(); - let mut sciences: Box<[_]> = self .sciences .iter_mut() @@ -167,7 +284,7 @@ impl MultiLabStore { for timer in self.timer.iter_mut() { let science_mul: u16 = sciences .iter_mut() - .zip(&needed) + .zip(needed) .map(|(science_iter, needed)| (science_iter.peek().unwrap(), needed)) .map(|(v, needed)| u16::from(**v >= *needed)) .product(); @@ -190,7 +307,7 @@ impl MultiLabStore { *timer = new_timer; sciences .iter_mut() - .zip(&needed) + .zip(needed) .map(|(science_iter, needed)| (science_iter.next().unwrap(), needed)) .for_each(|(v, needed)| *v -= *needed * did_finish_work) } @@ -206,9 +323,26 @@ impl MultiLabStore { &mut self, ty: u8, position: Position, + modules: &[Option], data_store: &DataStore, - ) -> u16 { - // FIXME: respect ty + ) -> u32 { + let base_speed = data_store.lab_info[usize::from(ty)].base_speed; + let base_prod = data_store.lab_info[usize::from(ty)].base_prod; + let base_power = data_store.lab_info[usize::from(ty)].base_power_consumption; + + let (speed, prod, power) = modules + .iter() + .flatten() + .map(|module| { + ( + data_store.module_info[*module].speed_mod as i16, + data_store.module_info[*module].prod_mod as i16, + data_store.module_info[*module].power_mod as i16, + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + let idx = if let Some(hole_idx) = self.holes.pop() { self.positions[hole_idx] = position; // TODO: @@ -217,6 +351,26 @@ impl MultiLabStore { self.timer[hole_idx] = 0; self.types[hole_idx] = ty; + self.base_power_consumption[hole_idx] = base_power; + + self.base_speed[hole_idx] = base_speed; + self.raw_power_consumption_modifier[hole_idx] = power; + self.raw_bonus_productivity[hole_idx] = i16::from(base_prod) + prod; + self.raw_speed_mod[hole_idx] = speed; + + self.power_consumption_modifier[hole_idx] = (power + 20) + .clamp(data_store.min_power_mod.into(), u8::MAX.into()) + .try_into() + .expect("Value clamped already"); + self.bonus_productivity[hole_idx] = (i16::from(base_prod) + prod) + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Value clamped already"); + self.combined_speed_mod[hole_idx] = ((speed + 20) * i16::from(base_speed) / 20) + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Value clamped already"); + hole_idx } else { self.positions.push(position); @@ -225,13 +379,40 @@ impl MultiLabStore { self.timer.push(0); self.types.push(ty); + self.base_power_consumption.push(base_power); + + self.base_speed.push(base_speed); + self.raw_power_consumption_modifier.push(power); + self.raw_bonus_productivity + .push(i16::from(base_prod) + prod); + self.raw_speed_mod.push(speed); + + self.power_consumption_modifier.push( + (power + 20) + .clamp(data_store.min_power_mod.into(), u8::MAX.into()) + .try_into() + .expect("Value clamped already"), + ); + self.bonus_productivity.push( + (i16::from(base_prod) + prod) + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Value clamped already"), + ); + self.combined_speed_mod.push( + ((speed + 20) * i16::from(base_speed) / 20) + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Value clamped already"), + ); + self.positions.len() - 1 }; - idx.try_into().expect("More than u16::MAX Labs in a grid") + idx.try_into().expect("More than u32::MAX Labs in a grid") } - pub fn remove_lab(&mut self, index: u16) -> Box<[ITEMCOUNTTYPE]> { + pub fn remove_lab(&mut self, index: u32) -> Box<[ITEMCOUNTTYPE]> { let index = index as usize; self.holes.push(index); @@ -245,10 +426,16 @@ impl MultiLabStore { }) .collect(); + self.base_power_consumption[index] = Watt(0); + self.sciences + .iter_mut() + .for_each(|v| v[usize::from(index)] = 0); + ret } - pub fn move_lab(&mut self, index: u16, other: &mut Self) -> u16 { + pub fn move_lab(&mut self, index: u32, other: &mut Self) -> u32 { + todo!(); let index = index as usize; self.holes.push(index); @@ -292,6 +479,46 @@ impl MultiLabStore { }); idx.try_into() - .expect("More than u16::MAX Labs in a single grid") + .expect("More than u32::MAX Labs in a single grid") + } + + pub fn modify_modifiers( + &mut self, + index: u32, + speed: i16, + prod: i16, + power: i16, + data_store: &DataStore, + ) { + let index = index as usize; + self.raw_speed_mod[usize::from(index)] = self.raw_speed_mod[usize::from(index)] + .checked_add(speed) + .expect("Over/Underflowed"); + self.raw_bonus_productivity[usize::from(index)] = self.raw_bonus_productivity + [usize::from(index)] + .checked_add(prod) + .expect("Over/Underflowed"); + self.raw_power_consumption_modifier[usize::from(index)] = self + .raw_power_consumption_modifier[usize::from(index)] + .checked_add(power) + .expect("Over/Underflowed"); + + self.power_consumption_modifier[usize::from(index)] = + (self.raw_power_consumption_modifier[usize::from(index)] + 20) + .clamp(data_store.min_power_mod.into(), u8::MAX.into()) + .try_into() + .expect("Values already clamped"); + self.bonus_productivity[usize::from(index)] = self.raw_bonus_productivity + [usize::from(index)] + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Values already clamped"); + self.combined_speed_mod[usize::from(index)] = ((self.raw_speed_mod[usize::from(index)] + + 20) + * i16::from(self.base_speed[usize::from(index)]) + / 10) + .clamp(0, u8::MAX.into()) + .try_into() + .expect("Values already clamped"); } } diff --git a/src/lib.rs b/src/lib.rs index 2462105..5d3b2c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![feature(adt_const_params)] #![feature(array_try_map)] #![feature(never_type)] +#![feature(mixed_integer_ops_unsigned_sub)] extern crate test; @@ -10,33 +11,38 @@ use std::{ array, borrow::Borrow, env, - net::{IpAddr, Ipv4Addr}, + net::{SocketAddr, TcpStream}, process::exit, simd::cmp::SimdPartialEq, sync::{ - atomic::AtomicU64, - mpsc::{channel, Sender}, - Arc, Mutex, + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, + mpsc::{Sender, channel}, }, thread, }; -use data::{get_raw_data_test, DataStore}; +use parking_lot::Mutex; + +use data::{DataStore, factorio_1_1::get_raw_data_test}; use eframe::NativeOptions; use frontend::{ action::action_state_machine::ActionStateMachine, input::Input, world::tile::CHUNK_SIZE_FLOAT, }; use item::{IdxTrait, WeakIdxTrait}; use multiplayer::{ - connection_reciever::accept_continously, ClientConnectionInfo, Game, GameInitData, ServerInfo, + ClientConnectionInfo, Game, GameInitData, ServerInfo, connection_reciever::accept_continously, }; use rendering::{ - app_state::{AppState, GameState}, + app_state::GameState, eframe_app, - window::{LoadedGame, LoadedGameInfo, LoadedGameSized}, + window::{LoadedGame, LoadedGameSized}, }; -use saving::load; +use saving::{load, load_readable}; use simple_logger::SimpleLogger; +use std::path::PathBuf; + +use crate::item::Indexable; const TICKS_PER_SECOND_LOGIC: u64 = 60; @@ -47,6 +53,7 @@ pub mod belt; pub mod inserter; pub mod item; pub mod lab; +pub mod mining_drill; pub mod power; pub mod research; @@ -61,7 +68,7 @@ pub mod bot_system; mod statistics; -mod replays; +pub mod replays; mod saving; @@ -71,19 +78,33 @@ mod storage_list; pub mod split_arbitrary; +pub mod join_many; + mod chest; pub mod blueprint; mod network_graph; +pub mod liquid; + impl WeakIdxTrait for u8 {} impl WeakIdxTrait for u16 {} impl IdxTrait for u8 {} impl IdxTrait for u16 {} -#[cfg(test)] -static DATA_STORE: std::sync::LazyLock> = +impl Indexable for u8 { + fn into_usize(self) -> usize { + self.into() + } +} +impl Indexable for u16 { + fn into_usize(self) -> usize { + self.into() + } +} + +pub static DATA_STORE: std::sync::LazyLock> = std::sync::LazyLock::new(|| get_raw_data_test().turn::()); pub trait NewWithDataStore { @@ -92,15 +113,17 @@ pub trait NewWithDataStore { ) -> Self; } -impl NewWithDataStore for u32 { +impl NewWithDataStore for T { fn new( _data_store: impl Borrow>, ) -> Self { - 0 + T::default() } } -pub fn main() { +pub fn main() -> Result<(), ()> { + puffin::set_scopes_on(true); + SimpleLogger::new() .with_level(log::LevelFilter::Warn) .env() @@ -109,49 +132,47 @@ pub fn main() { let mode = env::args().nth(1); - dbg!(&mode); - - let (loaded, tick, sender) = if Some("--client") == mode.as_deref() { - run_client(StartGameInfo {}) - } else if Some("--dedicated") == mode.as_deref() { - run_dedicated_server(StartGameInfo {}); + if Some("--dedicated") == mode.as_deref() { + run_dedicated_server(StartGameInfo::Load("".try_into().unwrap())); } else { - run_integrated_server(StartGameInfo {}) - }; - // let mut app = App::new(sender); + eframe::run_native( + "FactoryGame", + NativeOptions { + // depth_buffer: 32, + ..Default::default() + }, + Box::new(|cc| { + let app = eframe_app::App::new(cc); + + Ok(Box::new(app)) + }), + ) + .unwrap(); - // app.currently_loaded_game = Some(LoadedGameInfo { - // state: loaded, - // tick: tick, - // }); - // app.state = AppState::Ingame; - // let event_loop = EventLoop::new().unwrap(); + Ok(()) + } +} - // event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); +enum StartGameInfo { + Load(PathBuf), + LoadReadable(PathBuf), + Create(GameCreationInfo), +} - // let _ = event_loop.run_app(&mut app); +enum GameCreationInfo { + Empty, + RedGreen, + RedGreenBelts, - eframe::run_native( - "FactoryGame", - NativeOptions::default(), - Box::new(|cc| { - let mut app = eframe_app::App::new(cc, sender); + RedWithLabs, - app.state = AppState::Ingame; - app.currently_loaded_game = Some(LoadedGameInfo { - state: loaded, - tick, - }); + Gigabase, - Ok(Box::new(app)) - }), - ) - .unwrap(); + FromBP(PathBuf), } -struct StartGameInfo {} - fn run_integrated_server( + progress: Arc, start_game_info: StartGameInfo, ) -> (LoadedGame, Arc, Sender) { // TODO: Do mod loading here @@ -162,47 +183,100 @@ fn run_integrated_server( let connections: Arc>> = Arc::default(); - accept_continously(connections.clone()).unwrap(); + let local_addr = "127.0.0.1:8080"; + let cancel: Arc = Default::default(); + + accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { - let data_store = Arc::new(data_store); - let (send, recv) = channel(); - let state_machine: Arc>> = Arc::new(Mutex::new( - ActionStateMachine::new(0, (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT)), - )); - - let game_state = Arc::new(Mutex::new( - load().map(|save| save.game_state).unwrap_or_else(|| { - GameState::new_with_bp(&data_store, "test_blueprints/red_sci.bp") - // GameState::new_with_production(&data_store) - }), - )); + let state_machine: Arc>> = + Arc::new(Mutex::new(ActionStateMachine::new( + 0, + (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), + &data_store, + ))); + + let game_state = Arc::new(Mutex::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::Gigabase => { + GameState::new_with_gigabase(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 }, - action_state_machine: state_machine.clone(), - inputs: recv, - ui_actions: ui_recv, - }) + let mut game = Game::new( + GameInitData::IntegratedServer { + game_state: game_state.clone(), + tick_counter: tick_counter.clone(), + info: ServerInfo { connections }, + action_state_machine: state_machine.clone(), + inputs: recv, + ui_actions: ui_recv, + cancel_socket: 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. + // So we just connect to ourselves :) + // See https://stackoverflow.com/questions/56692961/how-do-i-gracefully-exit-tcplistener-incoming + let _ = TcpStream::connect(local_addr); + }), + }, + &data_store, + ) .unwrap(); + let stop = Arc::new(AtomicBool::new(false)); + let m_data_store = data_store.clone(); + let m_stop: Arc = stop.clone(); thread::spawn(move || { - game.run(&m_data_store); + profiling::register_thread!("Game Update Thread"); + 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, @@ -219,26 +293,47 @@ 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(); - accept_continously(connections.clone()).unwrap(); + let local_addr = "127.0.0.1:8080"; + let cancel: Arc = Default::default(); + + accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { - let game_state = load() + 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)); - - let mut game = Game::new(GameInitData::DedicatedServer( - game_state, - ServerInfo { connections }, - )) + .unwrap_or_else(|| { + // GameState::new(&data_store) + GameState::new_with_beacon_production(progress, &data_store) + }); + + let mut game = Game::new( + GameInitData::DedicatedServer( + game_state, + ServerInfo { connections }, + 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. + // So we just connect to ourselves :) + // See https://stackoverflow.com/questions/56692961/how-do-i-gracefully-exit-tcplistener-incoming + let _ = TcpStream::connect(local_addr); + }), + ), + &data_store, + ) .unwrap(); + let stop = Arc::new(AtomicBool::new(false)); + let data_store = Arc::new(data_store); - match game.run(&data_store) { + match game.run(stop, &data_store) { multiplayer::ExitReason::UserQuit => exit(0), multiplayer::ExitReason::ConnectionDropped => exit(1), + multiplayer::ExitReason::LoopStopped => exit(0), } }, data::DataStoreOptions::ItemU8RecipeU16(data_store) => todo!(), @@ -247,7 +342,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { } } -fn run_client(start_game_info: StartGameInfo) -> (LoadedGame, Arc, Sender) { +fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender) { // TODO: Do mod loading here let raw_data = get_raw_data_test(); let data_store = raw_data.process(); @@ -256,45 +351,53 @@ fn run_client(start_game_info: StartGameInfo) -> (LoadedGame, Arc, Se match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { - let data_store = Arc::new(data_store); - 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)), - )); + 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(Mutex::new( - load() + // 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 mut game = Game::new(GameInitData::Client { - game_state: game_state.clone(), - action_state_machine: state_machine.clone(), - inputs: recv, - tick_counter: tick_counter.clone(), - info: ClientConnectionInfo { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - port: 8080, + let mut game = Game::new( + GameInitData::Client { + game_state: game_state.clone(), + action_state_machine: state_machine.clone(), + inputs: recv, + tick_counter: tick_counter.clone(), + info: ClientConnectionInfo { addr: remote_addr }, + ui_actions: ui_recv, }, - ui_actions: ui_recv, - }) - .unwrap(); + &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_data_store); + 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, @@ -306,66 +409,12 @@ fn run_client(start_game_info: StartGameInfo) -> (LoadedGame, Arc, Se } } -// fn main_loop( -// current_tick: Arc, -// input_reciever: Receiver, -// app_state: Arc>>, -// state_machine: Arc>>, -// data_store: Arc>, -// ) -> ! { -// let mut update_interval = -// spin_sleep_util::interval(Duration::from_secs(1) / TICKS_PER_SECOND as u32); - -// loop { -// update_interval.tick(); -// match &mut *app_state.lock().unwrap() { -// AppState::Ingame(game_state) => { -// // TODO: For now I collect the actions here. - -// let actions: Vec> = { -// let mut state_machine = state_machine.lock().unwrap(); -// let mut ret: Vec> = input_reciever -// .try_iter() -// .flat_map(|input| { -// state_machine.handle_input(input, &game_state.world, &data_store) -// }) -// .collect(); - -// ret.extend(state_machine.once_per_update_actions()); - -// ret -// }; - -// let start = Instant::now(); -// game_state.apply_actions(actions, &data_store); - -// info!("Apply Actions Time: {:?}", start.elapsed()); -// let start = Instant::now(); - -// game_state.update(&data_store); -// info!("Update Time: {:?}", start.elapsed()); -// }, -// } -// current_tick.fetch_add(1, std::sync::atomic::Ordering::Relaxed); -// } -// } - -// #[cfg(not(debug_assertions))] - -// Type your code here, or load an example. - use std::simd::Simd; // TODO: Increase if possible type BOOLSIMDTYPE = Simd; type SIMDTYPE = Simd; -// As of Rust 1.75, small functions are automatically -// marked as `#[inline]` so they will not show up in -// the output when compiling with optimisations. Use -// `#[no_mangle]` or `#[inline(never)]` to work around -// this issue. -// See https://github.com/compiler-explorer/compiler-explorer/issues/5939 pub struct InserterInfo { num_items: u8, } @@ -443,69 +492,128 @@ pub fn simd( } } -// #[cfg(test)] -// mod tests { -// use std::{iter, rc::Rc}; +#[cfg(test)] +mod tests { + use std::{fs::File, iter, path::PathBuf, rc::Rc}; + + use rstest::rstest; + use test::{Bencher, black_box}; + + use crate::{ + DATA_STORE, TICKS_PER_SECOND_LOGIC, + data::{DataStore, factorio_1_1::get_raw_data_test}, + frontend::{action::ActionType, world::Position}, + rendering::app_state::GameState, + replays::{Replay, run_till_finished}, + }; + + #[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)); -// use test::{black_box, Bencher}; + b.iter(|| replay.clone()); + } -// use crate::{ -// data::get_raw_data_test, -// frontend::{action::ActionType, world::tile::World}, -// rendering::app_state::{GameState, SimulationState}, -// replays::{run_till_finished, Replay}, -// TICKS_PER_SECOND_LOGIC, -// }; + #[bench] + fn empty_simulation(b: &mut Bencher) { + // 1 hour + const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; -// #[bench] -// fn clone_empty_simulation(b: &mut Bencher) { -// let data_store = get_raw_data_test().process().assume_simple(); + let data_store = get_raw_data_test().process().assume_simple(); -// let game_state = GameState::new(&data_store); + let game_state = GameState::new(&data_store); -// let replay = Replay::new(game_state, Rc::new(data_store)); + let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); -// b.iter(|| replay.clone()); -// } + for _ in 0..NUM_TICKS { + replay.tick(); + } -// #[bench] -// fn empty_simulation(b: &mut Bencher) { -// // 1 hour -// const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; + replay.finish(); -// let data_store = get_raw_data_test().process().assume_simple(); + b.iter(|| black_box(replay.clone().run().with(run_till_finished))); + } -// let game_state = GameState::new(&data_store); + #[bench] + fn noop_actions_simulation(b: &mut Bencher) { + // 1 hour + const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; -// let mut replay = Replay::new(game_state, Rc::new(data_store)); + let data_store = get_raw_data_test().process().assume_simple(); -// for _ in 0..NUM_TICKS { -// replay.tick(); -// } + let game_state = GameState::new(&data_store); -// replay.finish(); + let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); -// b.iter(|| black_box(replay.clone().run().with(run_till_finished))); -// } + for _ in 0..NUM_TICKS { + replay.append_actions( + iter::repeat(ActionType::Ping(Position { x: 100, y: 100 })).take(5), + ); + replay.tick(); + } -// #[bench] -// fn noop_actions_simulation(b: &mut Bencher) { -// // 1 hour -// const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; + replay.finish(); -// let data_store = get_raw_data_test().process().assume_simple(); + b.iter(|| replay.clone().run().with(run_till_finished)); + } -// let game_state = GameState::new(&data_store); + #[rstest] + fn crashing_replays(#[files("crash_replays/*.rep")] path: PathBuf) { + use std::io::Read; -// let mut replay = Replay::new(game_state, Rc::new(data_store)); + // Keep running for 30 seconds + const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; -// for _ in 0..NUM_TICKS { -// replay.append_actions(iter::repeat(ActionType::Ping((100, 100))).take(5)); -// replay.tick(); -// } + let mut file = File::open(&path).unwrap(); -// replay.finish(); + let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); -// b.iter(|| replay.clone().run().with(run_till_finished)); -// } -// } + 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/connection_logic.rs b/src/liquid/connection_logic.rs new file mode 100644 index 0000000..038b617 --- /dev/null +++ b/src/liquid/connection_logic.rs @@ -0,0 +1,205 @@ +use std::cmp::min; + +use crate::data::{FluidConnection, PipeConnectionType}; +use crate::frontend::world::tile::Dir; +use crate::{ + data::DataStore, + frontend::world::{Position, tile::DirRelative}, + item::IdxTrait, +}; + +pub fn can_fluid_tanks_connect( + first_tank_pos: Position, + first_tank_ty: u8, + first_rotation: Dir, + second_tank_pos: Position, + second_tank_ty: u8, + second_rotation: Dir, + data_store: &DataStore, +) -> Option<(Position, Dir)> { + let first_size = data_store.fluid_tank_infos[usize::from(first_tank_ty)].size; + let second_size = data_store.fluid_tank_infos[usize::from(second_tank_ty)].size; + + assert!( + !first_tank_pos.overlap(first_size.into(), second_tank_pos, second_size.into()), + "If these entities overlap, weird thing can happen" + ); + + for first_conn in &data_store.fluid_tank_infos[usize::from(first_tank_ty)].fluid_connections { + for second_conn in + &data_store.fluid_tank_infos[usize::from(second_tank_ty)].fluid_connections + { + if let Some((connection_pos, connection_dir)) = can_two_connections_connect( + first_tank_pos, + *first_conn, + first_rotation, + first_size, + second_tank_pos, + *second_conn, + second_rotation, + second_size, + ) { + return Some((connection_pos, connection_dir)); + } + } + } + + return None; +} + +pub fn can_fluid_tanks_connect_to_single_connection< + ItemIdxType: IdxTrait, + RecipeIdxType: IdxTrait, +>( + first_tank_pos: Position, + first_tank_ty: u8, + first_rotation: Dir, + + second_conn_pos: Position, + second_conn: FluidConnection, + second_conn_rotation: Dir, + second_size: [u16; 2], + data_store: &DataStore, +) -> Option<(Position, Dir)> { + let first_tank_size = data_store.fluid_tank_infos[usize::from(first_tank_ty)].size; + + for first_conn in &data_store.fluid_tank_infos[usize::from(first_tank_ty)].fluid_connections { + if let Some(v) = can_two_connections_connect( + first_tank_pos, + *first_conn, + first_rotation, + first_tank_size, + second_conn_pos, + second_conn, + second_conn_rotation, + second_size, + ) { + return Some(v); + } + } + + return None; +} + +fn can_two_connections_connect( + first_conn_pos: Position, + mut first_conn: FluidConnection, + first_rotation: Dir, + first_size: [u16; 2], + second_conn_pos: Position, + mut second_conn: FluidConnection, + second_rotation: Dir, + second_size: [u16; 2], +) -> Option<(Position, Dir)> { + // FIXME: I effectively just made rotation matrices here xD + match first_rotation { + Dir::North => {}, + Dir::East => { + first_conn.dir = first_conn.dir.turn_right(); + first_conn.offset = [ + first_size[0] - 1 - first_conn.offset[1], + first_conn.offset[0], + ]; + }, + Dir::South => { + first_conn.dir = first_conn.dir.reverse(); + first_conn.offset = [ + first_size[0] - 1 - first_conn.offset[0], + first_size[1] - 1 - first_conn.offset[1], + ]; + }, + Dir::West => { + first_conn.dir = first_conn.dir.turn_right().turn_right().turn_right(); + first_conn.offset = [ + first_conn.offset[1], + first_size[0] - 1 - first_conn.offset[0], + ]; + }, + } + + match second_rotation { + Dir::North => {}, + Dir::East => { + second_conn.dir = second_conn.dir.turn_right(); + second_conn.offset = [ + second_size[0] - 1 - second_conn.offset[1], + second_conn.offset[0], + ]; + }, + Dir::South => { + second_conn.dir = second_conn.dir.reverse(); + second_conn.offset = [ + second_size[0] - 1 - second_conn.offset[0], + second_size[1] - 1 - second_conn.offset[1], + ]; + }, + Dir::West => { + second_conn.dir = second_conn.dir.turn_right().turn_right().turn_right(); + second_conn.offset = [ + second_conn.offset[1], + second_size[0] - 1 - second_conn.offset[0], + ]; + }, + } + + match (first_conn.kind, second_conn.kind) { + (PipeConnectionType::Direct, PipeConnectionType::Direct) => { + // Ok + let second_conn_pos = Position { + x: second_conn_pos.x + i32::from(second_conn.offset[0]), + y: second_conn_pos.y + i32::from(second_conn.offset[1]), + }; + + if (first_conn_pos + first_conn.dir) == second_conn_pos + && (second_conn_pos + second_conn.dir) == first_conn_pos + { + assert!(first_conn.dir.compare(second_conn.dir) == DirRelative::Opposite); + + // We can connect via these connections + return Some((second_conn_pos, second_conn.dir)); + } + }, + (PipeConnectionType::Direct, PipeConnectionType::Underground { .. }) + | (PipeConnectionType::Underground { .. }, PipeConnectionType::Direct) => { + // We can never connect, if the types differ + }, + ( + PipeConnectionType::Underground { + max_distance: first_max_distance, + underground_group_mask: first_underground_group_mask, + }, + PipeConnectionType::Underground { + max_distance: second_max_distance, + underground_group_mask: second_underground_group_mask, + }, + ) => { + if first_underground_group_mask & second_underground_group_mask != 0 { + // The pipes are allowed to connect + let second_conn_pos = Position { + x: second_conn_pos.x + i32::from(second_conn.offset[0]), + y: second_conn_pos.y + i32::from(second_conn.offset[1]), + }; + + let dist_x = second_conn_pos.x - first_conn_pos.x; + let dist_y = second_conn_pos.y - first_conn_pos.y; + + if dist_x == 0 || dist_y == 0 { + // TODO: Make sure this is right + if dist_x + dist_y - 1 + <= i32::from(min(first_max_distance, second_max_distance)) + { + if first_conn.dir.compare(second_conn.dir) == DirRelative::Opposite { + return Some((second_conn_pos, second_conn.dir)); + } + } + } else { + // We do not line up, since the direction is diagonal + } + } else { + // Pipe connection masks do not match + } + }, + } + + None +} diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs new file mode 100644 index 0000000..1b399e2 --- /dev/null +++ b/src/liquid/mod.rs @@ -0,0 +1,1724 @@ +use std::cmp::min; +use std::collections::HashMap; + +use itertools::Itertools; +use log::warn; + +use crate::chest::ChestSize; +use crate::inserter::storage_storage_with_buckets::InserterIdentifier; +use crate::item::Indexable; +use crate::{ + chest::FullChestStore, + data::DataStore, + frontend::world::Position, + inserter::{StaticID, Storage}, + item::{IdxTrait, Item, WeakIdxTrait}, + network_graph::{Network, WeakIndex}, + rendering::app_state::StorageStorageInserterStore, +}; + +pub mod connection_logic; + +const FLUID_INSERTER_MOVETIME: u16 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub struct FluidSystemId { + pub fluid: Option>, + index: usize, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct FluidSystemStore { + fluid_systems_with_fluid: Box<[Vec>>]>, + empty_fluid_systems: Vec>>, + pub fluid_box_pos_to_network_id: HashMap>, +} + +#[derive(Debug, Clone, Copy)] +pub struct CannotMixFluidsError { + pub items: [Item; 2], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub enum FluidConnectionDir { + Output, + Input, +} + +impl FluidSystemStore { + #[must_use] + pub fn new( + data_store: &DataStore, + ) -> Self { + // TODO: We can save some space here by only having fluid systems for items which are actually a fluid + Self { + fluid_systems_with_fluid: vec![vec![]; data_store.item_display_names.len()] + .into_boxed_slice(), + empty_fluid_systems: vec![], + fluid_box_pos_to_network_id: HashMap::new(), + } + } + + /// # Errors: + /// If adding this fluid box would connect fluid systems with different fluids + pub fn try_add_fluid_box( + &mut self, + new_fluid_box_position: Position, + fluid_box_capacity: u32, + + connected_fluid_box_positions: impl IntoIterator, + connected_storages: impl IntoIterator< + Item = ( + FluidConnectionDir, + Item, + Storage, + Position, + Box ()>, + ), + >, + + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + + data_store: &DataStore, + ) -> Result<(), CannotMixFluidsError> { + let connected_boxes: Vec<_> = connected_fluid_box_positions.into_iter().collect(); + let connected_storages: Vec<_> = connected_storages.into_iter().collect(); + + let connected_storages_fluid = match connected_storages + .iter() + .map(|(_dir, fluid, _storage, _pos, _cb)| *fluid) + .all_equal_value() + { + Ok(fluid) => Some(fluid), + Err(Some((a, b))) => return Err(CannotMixFluidsError { items: [a, b] }), + Err(None) => None, + }; + + let id_the_box_ends_up_with = if let Some(first_connection) = connected_boxes.last() { + let network_to_join_into = self.fluid_box_pos_to_network_id[first_connection]; + + let need_to_merge = !connected_boxes + .iter() + .map(|pos| self.fluid_box_pos_to_network_id[pos]) + .all_equal(); + + let final_id = if need_to_merge { + let merge_fluid = match connected_boxes + .iter() + // Remove all fluid networks without a set fluid type since we can just set them to whatever fluid we might decide on + .flat_map(|pos| self.fluid_box_pos_to_network_id[pos].fluid) + .all_equal_value() + { + Ok(fluid) => Some(fluid), + Err(Some((a, b))) => { + return Err(CannotMixFluidsError { items: [a, b] }); + }, + Err(None) => None, + }; + + let network_to_join_into = connected_boxes + .iter() + .map(|pos| self.fluid_box_pos_to_network_id[pos]) + .find(|id| id.fluid == merge_fluid) + .unwrap(); + + assert_eq!(network_to_join_into.fluid, merge_fluid); + + let _final_fluid = match (merge_fluid, connected_storages_fluid) { + (None, None) => None, + (None, Some(fluid)) => Some(fluid), + (Some(fluid), None) => Some(fluid), + (Some(a), Some(b)) => { + if a == b { + Some(a) + } else { + return Err(CannotMixFluidsError { items: [a, b] }); + } + }, + }; + + let removed_ids: Vec<_> = connected_boxes + .iter() + .map(|pos| self.fluid_box_pos_to_network_id[pos]) + .unique() + .collect(); + + assert!(removed_ids.iter().all_unique()); + for removed_id in removed_ids { + self.merge_fluid_system( + network_to_join_into, + removed_id, + chest_store, + inserter_store, + new_fluid_box_position, + fluid_box_capacity, + connected_boxes.iter().copied(), + data_store, + ); + } + + network_to_join_into + } else { + // We have connections, but they are all part of the same fluid network + let network = match network_to_join_into.fluid { + Some(fluid) => { + &mut self.fluid_systems_with_fluid[fluid.into_usize()] + [network_to_join_into.index] + }, + None => &mut self.empty_fluid_systems[network_to_join_into.index], + }; + + let (network, new_id) = match ( + network + .as_ref() + .expect("network_to_join_into points to placeholder fluidsystem!") + .state, + connected_storages_fluid, + ) { + (FluidSystemState::NoFluid, None) => { + // We still do not have any fluid + (network.as_mut().unwrap(), network_to_join_into) + }, + (FluidSystemState::NoFluid, Some(fluid)) => { + let mut network = network.take().unwrap(); + + network.set_fluid(fluid, chest_store); + + let index = self.fluid_systems_with_fluid[fluid.into_usize()] + .iter() + .position(Option::is_none); + + let index = if let Some(hole_idx) = index { + assert!( + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx] + .is_none() + ); + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx] = + Some(network); + hole_idx + } else { + self.fluid_systems_with_fluid[fluid.into_usize()].push(Some(network)); + self.fluid_systems_with_fluid[fluid.into_usize()].len() - 1 + }; + + let new_id = FluidSystemId { + fluid: Some(fluid), + index, + }; + + for rhs in self.fluid_box_pos_to_network_id.values_mut() { + if *rhs == network_to_join_into { + *rhs = new_id; + } + } + + ( + self.fluid_systems_with_fluid[fluid.into_usize()][index] + .as_mut() + .unwrap(), + new_id, + ) + }, + (FluidSystemState::HasFluid { .. }, None) => { + // We just keep the fluid + (network.as_mut().unwrap(), network_to_join_into) + }, + (FluidSystemState::HasFluid { fluid, .. }, Some(connection_fluid)) => { + if fluid == connection_fluid { + // We just keep the fluid + (network.as_mut().unwrap(), network_to_join_into) + } else { + return Err(CannotMixFluidsError { + items: [fluid, connection_fluid], + }); + } + }, + }; + + network.add_fluid_box( + new_fluid_box_position, + fluid_box_capacity, + connected_boxes, + chest_store, + ); + + for (dir, fluid, storage, pos, callback) in connected_storages { + let weak_index = match dir { + FluidConnectionDir::Input => network.add_input( + fluid, + new_fluid_box_position, + storage, + pos, + inserter_store, + data_store, + ), + FluidConnectionDir::Output => network.add_output( + fluid, + new_fluid_box_position, + storage, + pos, + inserter_store, + data_store, + ), + }; + + callback(weak_index); + } + + new_id + }; + + final_id + } else { + // We are not connected to anything, make a new network + let mut new_network = FluidSystem::new_from_single_fluid_box( + connected_storages_fluid, + new_fluid_box_position, + fluid_box_capacity, + chest_store, + ); + + for (dir, fluid, storage, pos, callback) in connected_storages { + let weak_index = match dir { + FluidConnectionDir::Input => new_network.add_input( + fluid, + new_fluid_box_position, + storage, + pos, + inserter_store, + data_store, + ), + FluidConnectionDir::Output => new_network.add_output( + fluid, + new_fluid_box_position, + storage, + pos, + inserter_store, + data_store, + ), + }; + + callback(weak_index); + } + + let index = match connected_storages_fluid { + Some(fluid) => { + let index = self.fluid_systems_with_fluid[fluid.into_usize()] + .iter() + .position(Option::is_none); + + if let Some(hole_idx) = index { + assert!( + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx].is_none() + ); + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx] = + Some(new_network); + hole_idx + } else { + self.fluid_systems_with_fluid[fluid.into_usize()].push(Some(new_network)); + self.fluid_systems_with_fluid[fluid.into_usize()].len() - 1 + } + }, + None => { + let index = self.empty_fluid_systems.iter().position(Option::is_none); + + if let Some(hole_idx) = index { + assert!(self.empty_fluid_systems[hole_idx].is_none()); + self.empty_fluid_systems[hole_idx] = Some(new_network); + hole_idx + } else { + self.empty_fluid_systems.push(Some(new_network)); + self.empty_fluid_systems.len() - 1 + } + }, + }; + + let new_id: FluidSystemId = FluidSystemId { + fluid: connected_storages_fluid, + index, + }; + + match self + .fluid_box_pos_to_network_id + .entry(new_fluid_box_position) + { + std::collections::hash_map::Entry::Occupied(_) => { + unreachable!("Two Fluid Boxes at the same position") + }, + std::collections::hash_map::Entry::Vacant(vacant_entry) => { + vacant_entry.insert(new_id); + }, + } + + new_id + }; + + self.fluid_box_pos_to_network_id + .insert(new_fluid_box_position, id_the_box_ends_up_with); + + #[cfg(debug_assertions)] + match id_the_box_ends_up_with.fluid { + Some(fluid) => self.fluid_systems_with_fluid[fluid.into_usize()] + [id_the_box_ends_up_with.index] + .as_ref() + .unwrap() + .check_consistency(chest_store), + None => self.empty_fluid_systems[id_the_box_ends_up_with.index] + .as_ref() + .unwrap() + .check_consistency(chest_store), + } + + Ok(()) + } + + pub fn remove_fluid_box( + &mut self, + fluid_box_position: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) { + let old_id = self + .fluid_box_pos_to_network_id + .remove(&fluid_box_position) + .unwrap(); + + let (new_systems, delete) = match old_id.fluid { + Some(fluid) => self.fluid_systems_with_fluid[fluid.into_usize()][old_id.index] + .as_mut() + .unwrap() + .remove_fluid_box(fluid_box_position, chest_store, inserter_store, data_store), + None => self.empty_fluid_systems[old_id.index] + .as_mut() + .unwrap() + .remove_fluid_box(fluid_box_position, chest_store, inserter_store, data_store), + }; + + if delete { + match old_id.fluid { + Some(fluid) => { + self.fluid_systems_with_fluid[fluid.into_usize()][old_id.index] = None + }, + None => self.empty_fluid_systems[old_id.index] = None, + } + } + + for new_system in new_systems { + match new_system.state { + FluidSystemState::NoFluid => { + let index = self.empty_fluid_systems.iter().position(Option::is_none); + + let new_index = if let Some(hole_idx) = index { + assert!(self.empty_fluid_systems[hole_idx].is_none()); + self.empty_fluid_systems[hole_idx] = Some(new_system); + hole_idx + } else { + self.empty_fluid_systems.push(Some(new_system)); + self.empty_fluid_systems.len() - 1 + }; + + let new_id = FluidSystemId { + fluid: None, + index: new_index, + }; + + for pos in self.empty_fluid_systems[new_index] + .as_ref() + .unwrap() + .graph + .keys() + { + let old = self.fluid_box_pos_to_network_id.insert(*pos, new_id); + assert_eq!(old, Some(old_id)); + } + }, + FluidSystemState::HasFluid { fluid, chest_id: _ } => { + let index = self.fluid_systems_with_fluid[fluid.into_usize()] + .iter() + .position(Option::is_none); + + let new_index = if let Some(hole_idx) = index { + assert!( + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx].is_none() + ); + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx] = + Some(new_system); + hole_idx + } else { + self.fluid_systems_with_fluid[fluid.into_usize()].push(Some(new_system)); + self.fluid_systems_with_fluid[fluid.into_usize()].len() - 1 + }; + + let new_id = FluidSystemId { + fluid: Some(fluid), + index: new_index, + }; + + for pos in self.fluid_systems_with_fluid[fluid.into_usize()][new_index] + .as_ref() + .unwrap() + .graph + .keys() + { + let old = self.fluid_box_pos_to_network_id.insert(*pos, new_id); + assert_eq!(old, Some(old_id)); + } + }, + } + } + } + + pub fn remove_fluid_box_connection_if_exists( + &mut self, + first_fluid_box_position: Position, + second_fluid_box_position: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) { + let old_id = *self + .fluid_box_pos_to_network_id + .get(&first_fluid_box_position) + .unwrap(); + + let (new_systems, delete) = match old_id.fluid { + Some(fluid) => self.fluid_systems_with_fluid[fluid.into_usize()][old_id.index] + .as_mut() + .unwrap() + .remove_fluid_box_connection( + first_fluid_box_position, + second_fluid_box_position, + chest_store, + inserter_store, + data_store, + ), + None => self.empty_fluid_systems[old_id.index] + .as_mut() + .unwrap() + .remove_fluid_box_connection( + first_fluid_box_position, + second_fluid_box_position, + chest_store, + inserter_store, + data_store, + ), + }; + + if delete { + match old_id.fluid { + Some(fluid) => { + self.fluid_systems_with_fluid[fluid.into_usize()][old_id.index] = None + }, + None => self.empty_fluid_systems[old_id.index] = None, + } + } + + for new_system in new_systems { + match new_system.state { + FluidSystemState::NoFluid => { + let index = self.empty_fluid_systems.iter().position(Option::is_none); + + let new_index = if let Some(hole_idx) = index { + assert!(self.empty_fluid_systems[hole_idx].is_none()); + self.empty_fluid_systems[hole_idx] = Some(new_system); + hole_idx + } else { + self.empty_fluid_systems.push(Some(new_system)); + self.empty_fluid_systems.len() - 1 + }; + + let new_id = FluidSystemId { + fluid: None, + index: new_index, + }; + + for pos in self.empty_fluid_systems[new_index] + .as_ref() + .unwrap() + .graph + .keys() + { + let old = self.fluid_box_pos_to_network_id.insert(*pos, new_id); + assert_eq!(old, Some(old_id)); + } + }, + FluidSystemState::HasFluid { fluid, chest_id: _ } => { + let index = self.fluid_systems_with_fluid[fluid.into_usize()] + .iter() + .position(Option::is_none); + + let new_index = if let Some(hole_idx) = index { + assert!( + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx].is_none() + ); + self.fluid_systems_with_fluid[fluid.into_usize()][hole_idx] = + Some(new_system); + hole_idx + } else { + self.fluid_systems_with_fluid[fluid.into_usize()].push(Some(new_system)); + self.fluid_systems_with_fluid[fluid.into_usize()].len() - 1 + }; + + let new_id = FluidSystemId { + fluid: Some(fluid), + index: new_index, + }; + + for pos in self.fluid_systems_with_fluid[fluid.into_usize()][new_index] + .as_ref() + .unwrap() + .graph + .keys() + { + let old = self.fluid_box_pos_to_network_id.insert(*pos, new_id); + assert_eq!(old, Some(old_id)); + } + }, + } + } + } + + pub fn update_fluid_conn_if_needed( + &mut self, + fluid_box_position: Position, + update_pos: Position, + update_size: [u16; 2], + new_storage: Storage, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) { + let id = self.fluid_box_pos_to_network_id[&fluid_box_position]; + + match id.fluid { + Some(fluid) => { + self.fluid_systems_with_fluid[fluid.into_usize()][id.index] + .as_mut() + .unwrap() + .update_fluid_conn_if_needed( + fluid, + update_pos, + update_size, + new_storage, + inserter_store, + data_store, + ); + }, + None => {}, + } + } + + pub fn try_add_output( + &mut self, + fluid_box_position: Position, + conn_fluid: Item, + dest: Storage, + dest_pos: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> Result> { + let id = self.fluid_box_pos_to_network_id[&fluid_box_position]; + + match id.fluid { + Some(fluid) => { + if fluid != conn_fluid { + return Err(CannotMixFluidsError { + items: [fluid, conn_fluid], + }); + } + let weak_index = self.fluid_systems_with_fluid[fluid.into_usize()][id.index] + .as_mut() + .unwrap() + .add_output( + fluid, + fluid_box_position, + dest, + dest_pos, + inserter_store, + data_store, + ); + Ok(weak_index) + }, + None => { + let mut removed = self.empty_fluid_systems[id.index].take().unwrap(); + let removed_id = id; + removed.set_fluid(conn_fluid, chest_store); + let weak_index = removed.add_output( + conn_fluid, + fluid_box_position, + dest, + dest_pos, + inserter_store, + data_store, + ); + + let new_idx = self.fluid_systems_with_fluid[conn_fluid.into_usize()] + .iter() + .position(Option::is_none); + + let new_idx = if let Some(hole_idx) = new_idx { + self.fluid_systems_with_fluid[conn_fluid.into_usize()][hole_idx] = + Some(removed); + hole_idx + } else { + self.fluid_systems_with_fluid[conn_fluid.into_usize()].push(Some(removed)); + self.fluid_systems_with_fluid[conn_fluid.into_usize()].len() - 1 + }; + + let new_id = FluidSystemId { + fluid: Some(conn_fluid), + index: new_idx, + }; + + for pos_id in self.fluid_box_pos_to_network_id.values_mut() { + if *pos_id == removed_id { + *pos_id = new_id; + } + } + + Ok(weak_index) + }, + } + } + + pub fn try_add_input( + &mut self, + fluid_box_position: Position, + conn_fluid: Item, + source: Storage, + source_pos: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> Result> { + let id = self.fluid_box_pos_to_network_id[&fluid_box_position]; + + match id.fluid { + Some(fluid) => { + if fluid != conn_fluid { + return Err(CannotMixFluidsError { + items: [fluid, conn_fluid], + }); + } + let weak_index = self.fluid_systems_with_fluid[fluid.into_usize()][id.index] + .as_mut() + .unwrap() + .add_input( + fluid, + fluid_box_position, + source, + source_pos, + inserter_store, + data_store, + ); + Ok(weak_index) + }, + None => { + let mut removed = self.empty_fluid_systems[id.index].take().unwrap(); + let removed_id = id; + removed.set_fluid(conn_fluid, chest_store); + let weak_index = removed.add_input( + conn_fluid, + fluid_box_position, + source, + source_pos, + inserter_store, + data_store, + ); + + let new_idx = self.fluid_systems_with_fluid[conn_fluid.into_usize()] + .iter() + .position(Option::is_none); + + let new_idx = if let Some(hole_idx) = new_idx { + self.fluid_systems_with_fluid[conn_fluid.into_usize()][hole_idx] = + Some(removed); + hole_idx + } else { + self.fluid_systems_with_fluid[conn_fluid.into_usize()].push(Some(removed)); + self.fluid_systems_with_fluid[conn_fluid.into_usize()].len() - 1 + }; + + let new_id = FluidSystemId { + fluid: Some(conn_fluid), + index: new_idx, + }; + + for pos_id in self.fluid_box_pos_to_network_id.values_mut() { + if *pos_id == removed_id { + *pos_id = new_id; + } + } + + Ok(weak_index) + }, + } + } + + fn merge_fluid_system( + &mut self, + kept_id: FluidSystemId, + removed_id: FluidSystemId, + + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + + new_fluid_box_position: Position, + fluid_box_capacity: u32, + connected_fluid_box_positions: impl IntoIterator, + + data_store: &DataStore, + ) { + if kept_id == removed_id { + warn!("Tried to merge a fluid system with itself!"); + return; + } + + match (kept_id.fluid, removed_id.fluid) { + (None, None) => (), + (None, Some(_)) => unreachable!(), + (Some(_), None) => (), + (Some(a), Some(b)) => assert_eq!(a, b), + } + + let removed = match removed_id.fluid { + Some(fluid) => self.fluid_systems_with_fluid[fluid.into_usize()][removed_id.index] + .take() + .expect("Tried to merge placeholder"), + None => self.empty_fluid_systems[removed_id.index] + .take() + .expect("Tried to merge placeholder"), + }; + + for box_pos_in_removed in removed.graph.keys() { + assert_eq!( + self.fluid_box_pos_to_network_id[box_pos_in_removed], + removed_id + ); + self.fluid_box_pos_to_network_id + .insert(*box_pos_in_removed, kept_id); + } + + match kept_id.fluid { + Some(fluid) => self.fluid_systems_with_fluid[fluid.into_usize()][kept_id.index] + .as_mut() + .unwrap() + .join( + removed, + new_fluid_box_position, + fluid_box_capacity, + connected_fluid_box_positions, + chest_store, + inserter_store, + data_store, + ), + None => self.empty_fluid_systems[kept_id.index] + .as_mut() + .unwrap() + .join( + removed, + new_fluid_box_position, + fluid_box_capacity, + connected_fluid_box_positions, + chest_store, + inserter_store, + data_store, + ), + } + + for rhs in self.fluid_box_pos_to_network_id.values_mut() { + if *rhs == removed_id { + *rhs = kept_id; + } + } + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum FluidSystemEntity { + OutgoingPump { + inserter_id: InserterIdentifier, + }, + IncomingPump { + inserter_id: InserterIdentifier, + }, + Input { + inserter_id: InserterIdentifier, + connected_entity_position: Position, + }, + Output { + inserter_id: InserterIdentifier, + connected_entity_position: Position, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct FluidBox { + capacity: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +enum FluidSystemState { + NoFluid, + HasFluid { + fluid: Item, + chest_id: u32, + }, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct FluidSystem { + graph: Network, + storage_capacity: u32, + state: FluidSystemState, +} + +impl FluidSystem { + #[must_use] + pub fn new_from_single_fluid_box( + fluid: Option>, + fluid_box_position: Position, + fluid_box_capacity: u32, + chest_store: &mut FullChestStore, + ) -> Self { + let mut ret = Self { + graph: Network::new( + FluidBox { + capacity: fluid_box_capacity, + }, + fluid_box_position, + ), + storage_capacity: fluid_box_capacity, + state: FluidSystemState::NoFluid, + }; + if let Some(fluid) = fluid { + ret.state = FluidSystemState::HasFluid { + fluid, + // TODO: Use fluid atoms to increase max size + chest_id: chest_store.stores[fluid.into_usize()].add_custom_chest( + fluid_box_capacity + .try_into() + .expect("Fluid system too large"), + ), + } + } + + ret + } + + pub fn update_fluid_conn_if_needed( + &mut self, + fluid: Item, + update_pos: Position, + update_size: [u16; 2], + new_storage: Storage, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) { + for e in self.graph.weak_components_mut() { + match e { + FluidSystemEntity::OutgoingPump { .. } => {}, + FluidSystemEntity::IncomingPump { .. } => {}, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } => { + if connected_entity_position.contained_in(update_pos, update_size.into()) { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + new_storage, + data_store, + ); + dbg!(inserter_id); + } + }, + FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } => { + if connected_entity_position.contained_in(update_pos, update_size.into()) { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + new_storage, + data_store, + ); + } + }, + } + } + } + + fn new_from_graph( + graph: Network, + old_fluid: Option>, + fluid_level_to_distribute: &mut ChestSize, + fluid_capacity_to_distribute: &mut u32, + chest_store: &mut FullChestStore, + ) -> Self { + let new_capacity = graph + .nodes() + .into_iter() + .map(|fluid_box| fluid_box.capacity) + .sum::(); + + // TODO: Ensure this does not wrap + let our_share_of_fluid = u16::try_from( + u32::from(*fluid_level_to_distribute) * new_capacity / *fluid_capacity_to_distribute, + ) + .unwrap(); + + // Clamp the fluid in this network to the capacity + let fluid_left_for_us = + ChestSize::try_from(min(our_share_of_fluid.into(), new_capacity)).unwrap(); + *fluid_level_to_distribute -= fluid_left_for_us; + *fluid_capacity_to_distribute -= new_capacity; + + let new_fluid = + if fluid_left_for_us > 0 || graph.weak_components().into_iter().next().is_some() { + old_fluid + } else { + None + }; + + Self { + graph, + storage_capacity: new_capacity, + state: match new_fluid { + Some(fluid) => { + // TODO: Use fluid atoms to increase max size + let chest_id = chest_store.stores[fluid.into_usize()] + .add_custom_chest(new_capacity.try_into().expect("Fluid system too large")); + + chest_store.stores[fluid.into_usize()] + .add_items_to_chest(chest_id, fluid_left_for_us) + .expect( + "Adding the contents of two chests (and their sizes) should always fit", + ); + + FluidSystemState::HasFluid { fluid, chest_id } + }, + None => FluidSystemState::NoFluid, + }, + } + } + + fn set_fluid( + &mut self, + fluid: Item, + chest_store: &mut FullChestStore, + ) { + if let FluidSystemState::HasFluid { + fluid: our_fluid, .. + } = self.state + { + assert_eq!(our_fluid, fluid); + return; + } else { + let chest_id = chest_store.stores[fluid.into_usize()].add_custom_chest( + self.storage_capacity + .try_into() + .expect("Fluid system too large"), + ); + self.state = FluidSystemState::HasFluid { fluid, chest_id }; + } + } + + pub fn get_fluid(&self) -> Option> { + match self.state { + FluidSystemState::NoFluid => None, + FluidSystemState::HasFluid { fluid, .. } => Some(fluid), + } + } + + fn get_chest_id(&self) -> Option { + match self.state { + FluidSystemState::NoFluid => None, + FluidSystemState::HasFluid { chest_id, .. } => Some(chest_id), + } + } + + fn add_output( + &mut self, + fluid: Item, + source_pipe_position: Position, + dest: Storage, + dest_pos: Position, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> WeakIndex { + let inserter_id = inserter_store.add_ins( + fluid, + FLUID_INSERTER_MOVETIME, + crate::inserter::Storage::Static { + static_id: StaticID::Chest as u16, + index: self.get_chest_id().unwrap(), + }, + dest, + 50, + data_store, + ); + + let weak_index = self.graph.add_weak_element( + source_pipe_position, + FluidSystemEntity::Output { + inserter_id, + connected_entity_position: dest_pos, + }, + ); + + weak_index + } + + fn add_input( + &mut self, + fluid: Item, + dest_pipe_position: Position, + source: Storage, + source_pos: Position, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> WeakIndex { + let inserter_id = inserter_store.add_ins( + fluid, + FLUID_INSERTER_MOVETIME, + source, + crate::inserter::Storage::Static { + static_id: StaticID::Chest as u16, + index: self.get_chest_id().unwrap(), + }, + 50, + data_store, + ); + + let weak_index = self.graph.add_weak_element( + dest_pipe_position, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position: source_pos, + }, + ); + + weak_index + } + + fn add_pump( + &mut self, + // FIXME: For now each pump has to have a filter set. This could be avoided by adding another graph like we do to track + // if a belt is sushi. + pump_filter: Item, + source_pipe_position: Position, + dest_fluid_network: &mut Self, + dest_pipe_position: Position, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> (WeakIndex, WeakIndex) { + let inserter_id = inserter_store.add_ins( + self.get_fluid().unwrap(), + FLUID_INSERTER_MOVETIME, + crate::inserter::Storage::Static { + static_id: StaticID::Chest as u16, + index: self.get_chest_id().unwrap(), + }, + crate::inserter::Storage::Static { + static_id: StaticID::Chest as u16, + index: dest_fluid_network.get_chest_id().unwrap(), + }, + 50, + data_store, + ); + + let weak_index_self = self.graph.add_weak_element( + source_pipe_position, + FluidSystemEntity::OutgoingPump { inserter_id }, + ); + + let weak_index_dest = self.graph.add_weak_element( + dest_pipe_position, + FluidSystemEntity::IncomingPump { inserter_id }, + ); + + (weak_index_self, weak_index_dest) + } + + pub fn remove_output( + &mut self, + source_pipe_position: Position, + weak_index: WeakIndex, + inserter_store: &mut StorageStorageInserterStore, + ) { + let FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } = self + .graph + .remove_weak_element(source_pipe_position, weak_index) + else { + unreachable!("Tried to remove output with non output weakindex"); + }; + + let _inserter_removal_info: () = inserter_store.remove_ins( + self.get_fluid() + .expect("Fluid Networks without a set fluid cannot have outputs"), + FLUID_INSERTER_MOVETIME, + inserter_id, + ); + } + + pub fn remove_input( + &mut self, + dest_pipe_position: Position, + weak_index: WeakIndex, + inserter_store: &mut StorageStorageInserterStore, + ) { + let FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } = self + .graph + .remove_weak_element(dest_pipe_position, weak_index) + else { + unreachable!("Tried to remove output with non input weakindex"); + }; + + let _inserter_removal_info: () = inserter_store.remove_ins( + self.get_fluid() + .expect("Fluid Networks without a set fluid cannot have inputs"), + FLUID_INSERTER_MOVETIME, + inserter_id, + ); + } + + fn add_fluid_box( + &mut self, + fluid_box_position: Position, + fluid_box_capacity: u32, + fluid_box_connections: impl IntoIterator, + chest_store: &mut FullChestStore, + ) { + let mut fluid_box_connections = fluid_box_connections.into_iter(); + self.storage_capacity = self + .storage_capacity + .checked_add(fluid_box_capacity) + .expect("TODO: Fluid network size exceeded u32::MAX"); + if let FluidSystemState::HasFluid { fluid, chest_id } = self.state { + let 0 = chest_store.stores[fluid.into_usize()].change_chest_size( + chest_id, + self.storage_capacity + .try_into() + .expect("Fluid system too large"), + ) else { + unreachable!( + "We increase the size of the fluid network. It should not be possible remove items with it" + ); + }; + } + + self.graph.add_node( + FluidBox { + capacity: fluid_box_capacity, + }, + fluid_box_position, + (fluid_box_connections.next().unwrap(), fluid_box_connections), + ); + } + + fn join( + &mut self, + mut other: Self, + new_fluid_box_position: Position, + fluid_box_capacity: u32, + connected_fluid_box_positions: impl IntoIterator, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) { + self.check_consistency(chest_store); + other.check_consistency(chest_store); + + if !self + .graph + .keys() + .into_iter() + .any(|pos| *pos == new_fluid_box_position) + { + self.storage_capacity += fluid_box_capacity; + } + self.storage_capacity = self + .storage_capacity + .checked_add(other.storage_capacity) + .expect("TODO: Fluid network size exceeded u32::MAX"); + + let mut connected_fluid_box_positions = connected_fluid_box_positions.into_iter(); + + match (self.state, other.state) { + (FluidSystemState::NoFluid, FluidSystemState::NoFluid) => { + // We do not have any fluid, so no chest and no inserters + debug_assert!( + self.graph + .weak_components() + .into_iter() + .all(|conn| match conn { + FluidSystemEntity::OutgoingPump { .. } => true, + FluidSystemEntity::IncomingPump { .. } => true, + // Since input/outputs are always a specific fluid, we cannot have any (otherwise we shoudl already have a fluid) + FluidSystemEntity::Input { .. } => false, + FluidSystemEntity::Output { .. } => false, + }) + ); + debug_assert!( + other + .graph + .weak_components() + .into_iter() + .all(|conn| match conn { + FluidSystemEntity::OutgoingPump { .. } => true, + FluidSystemEntity::IncomingPump { .. } => true, + // Since input/outputs are always a specific fluid, we cannot have any (otherwise we shoudl already have a fluid) + FluidSystemEntity::Input { .. } => false, + FluidSystemEntity::Output { .. } => false, + }) + ); + }, + (FluidSystemState::NoFluid, FluidSystemState::HasFluid { fluid, chest_id }) => { + let 0 = chest_store.stores[fluid.into_usize()].change_chest_size( + chest_id, + (self.storage_capacity) + .try_into() + .expect("Fluid system too large"), + ) else { + unreachable!(); + }; + + self.state = FluidSystemState::HasFluid { fluid, chest_id } + }, + (FluidSystemState::HasFluid { fluid, chest_id }, FluidSystemState::NoFluid) => { + let 0 = chest_store.stores[fluid.into_usize()].change_chest_size( + chest_id, + (self.storage_capacity) + .try_into() + .expect("Fluid system too large"), + ) else { + unreachable!(); + }; + }, + ( + FluidSystemState::HasFluid { + fluid: fluid_a, + chest_id: chest_id_a, + }, + FluidSystemState::HasFluid { + fluid: fluid_b, + chest_id: chest_id_b, + }, + ) => { + assert_eq!(fluid_a, fluid_b); + let fluid = fluid_a; + + let removed_fluid = chest_store.stores[fluid.into_usize()].remove_chest(chest_id_b); + + let 0 = chest_store.stores[fluid.into_usize()].change_chest_size( + chest_id_a, + (self.storage_capacity) + .try_into() + .expect("Fluid system too large"), + ) else { + unreachable!(); + }; + + chest_store.stores[fluid.into_usize()] + .add_items_to_chest(chest_id_a, removed_fluid) + .expect( + "Adding the contents of two chests (and their sizes) should always fit", + ); + + for conn in other.graph.weak_components_mut() { + match conn { + FluidSystemEntity::OutgoingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id_a, + }, + data_store, + ); + }, + FluidSystemEntity::IncomingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id_a, + }, + data_store, + ); + }, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id_a, + }, + data_store, + ); + }, + FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id_a, + }, + data_store, + ); + }, + } + } + }, + } + + self.graph.add_node_merging( + FluidBox { + capacity: fluid_box_capacity, + }, + new_fluid_box_position, + ( + connected_fluid_box_positions.next().unwrap(), + connected_fluid_box_positions, + ), + other.graph, + ); + } + + fn remove_fluid_box( + &mut self, + fluid_box_position: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> ( + impl Iterator + use, + bool, + ) { + let old_fluid = self.get_fluid(); + + let (removed_fluid_box, connections_to_remove, new_graphs) = + self.graph.remove_node(fluid_box_position); + + let old_fluid_level = match self.state { + FluidSystemState::NoFluid => 0, + FluidSystemState::HasFluid { fluid, chest_id } => { + let (fluid_level, _max) = + chest_store.stores[fluid.into_usize()].get_chest(chest_id); + fluid_level + }, + }; + let mut fluid_distribution = old_fluid_level; + + for connection_to_remove in connections_to_remove { + let fluid = old_fluid.expect("If we have any connections we MUST have a fluid set"); + match connection_to_remove { + FluidSystemEntity::OutgoingPump { inserter_id } => { + inserter_store.remove_ins(fluid, FLUID_INSERTER_MOVETIME, inserter_id) + }, + FluidSystemEntity::IncomingPump { inserter_id } => { + inserter_store.remove_ins(fluid, FLUID_INSERTER_MOVETIME, inserter_id) + }, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } => inserter_store.remove_ins(fluid, FLUID_INSERTER_MOVETIME, inserter_id), + FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } => inserter_store.remove_ins(fluid, FLUID_INSERTER_MOVETIME, inserter_id), + } + } + + let new_grids: Vec<_> = new_graphs + .into_iter() + .flatten() + .map(|(graph, positions)| { + let mut new_system = Self::new_from_graph( + graph, + old_fluid, + &mut fluid_distribution, + &mut self.storage_capacity, + chest_store, + ); + + match new_system.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + let our_storage = Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id, + }; + + for connection in new_system.graph.weak_components_mut() { + match connection { + FluidSystemEntity::OutgoingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::IncomingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + } + } + }, + } + + new_system + }) + .collect(); + + self.storage_capacity -= removed_fluid_box.capacity; + + let fluid_left_for_us = + ChestSize::try_from(min(fluid_distribution.into(), self.storage_capacity)).unwrap(); + + if self.graph.nodes().into_iter().next().is_none() { + // We no longer exist + assert!(self.storage_capacity == 0); + assert!(fluid_left_for_us == 0); + match self.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + let _ = chest_store.stores[fluid.into_usize()].remove_chest(chest_id); + }, + } + return (new_grids.into_iter(), true); + } + + match self.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + chest_store.stores[fluid.into_usize()] + .remove_items_from_chest(chest_id, old_fluid_level - fluid_left_for_us) + .expect("Not enough items in fluid system chest"); + assert!(fluid_left_for_us <= self.storage_capacity.try_into().unwrap()); + let 0 = chest_store.stores[fluid.into_usize()] + .change_chest_size(chest_id, self.storage_capacity.try_into().unwrap()) + else { + unreachable!(); + }; + }, + } + + (new_grids.into_iter(), false) + } + + fn remove_fluid_box_connection( + &mut self, + first_fluid_box_position: Position, + second_fluid_box_position: Position, + chest_store: &mut FullChestStore, + inserter_store: &mut StorageStorageInserterStore, + data_store: &DataStore, + ) -> ( + impl Iterator + use, + bool, + ) { + let old_fluid = self.get_fluid(); + + let new_graphs = self + .graph + .remove_edge(first_fluid_box_position, second_fluid_box_position); + + let old_fluid_level = match self.state { + FluidSystemState::NoFluid => 0, + FluidSystemState::HasFluid { fluid, chest_id } => { + let (fluid_level, _max) = + chest_store.stores[fluid.into_usize()].get_chest(chest_id); + fluid_level + }, + }; + let mut fluid_distribution = old_fluid_level; + + let new_grids: Vec<_> = new_graphs + .into_iter() + .flatten() + .map(|(graph, positions)| { + let mut new_system = Self::new_from_graph( + graph, + old_fluid, + &mut fluid_distribution, + &mut self.storage_capacity, + chest_store, + ); + + match new_system.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + let our_storage = Storage::Static { + static_id: StaticID::Chest as u16, + index: chest_id, + }; + + for connection in new_system.graph.weak_components_mut() { + match connection { + FluidSystemEntity::OutgoingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::IncomingPump { inserter_id } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::Input { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_dest( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + FluidSystemEntity::Output { + inserter_id, + connected_entity_position, + } => { + *inserter_id = inserter_store.update_inserter_src( + fluid, + FLUID_INSERTER_MOVETIME, + *inserter_id, + our_storage, + data_store, + ); + }, + } + } + }, + } + + new_system + }) + .collect(); + + let fluid_left_for_us = + ChestSize::try_from(min(fluid_distribution.into(), self.storage_capacity)).unwrap(); + + if self.graph.nodes().into_iter().next().is_none() { + // We no longer exist + assert!(self.storage_capacity == 0); + assert!(fluid_left_for_us == 0); + match self.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + let _ = chest_store.stores[fluid.into_usize()].remove_chest(chest_id); + }, + } + return (new_grids.into_iter(), true); + } + + match self.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + chest_store.stores[fluid.into_usize()] + .remove_items_from_chest(chest_id, old_fluid_level - fluid_left_for_us) + .expect("Not enough items in fluid system chest"); + assert!(fluid_left_for_us <= self.storage_capacity.try_into().unwrap()); + let 0 = chest_store.stores[fluid.into_usize()] + .change_chest_size(chest_id, self.storage_capacity.try_into().unwrap()) + else { + unreachable!(); + }; + }, + } + + (new_grids.into_iter(), false) + } + + pub fn check_consistency(&self, chest_store: &FullChestStore) { + let calculated_capacity: u32 = self + .graph + .nodes() + .into_iter() + .map(|node| node.capacity) + .sum(); + + assert_eq!(self.storage_capacity, calculated_capacity); + + match self.state { + FluidSystemState::NoFluid => {}, + FluidSystemState::HasFluid { fluid, chest_id } => { + let (current_fluid_level, max_fluid_level_chest) = + chest_store.stores[fluid.into_usize()].get_chest(chest_id); + + assert_eq!(self.storage_capacity, u32::from(max_fluid_level_chest)); + + assert!(u32::from(current_fluid_level) <= self.storage_capacity); + }, + } + } +} diff --git a/src/main.rs b/src/main.rs index f562be0..a965623 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,195 +1,383 @@ -fn main() { - factory::main(); -} - -#[cfg(test)] -mod test { - use rand::Rng; - use std::{ - arch::x86_64::{ - __m256i, _mm256_add_epi8, _mm256_blendv_epi8, _mm256_cmpeq_epi8, _mm256_loadu_si256, - _mm256_movemask_epi8, _mm256_set1_epi8, _mm256_set_epi8, _mm256_setzero_si256, - _mm256_storeu_si256, - }, - thread, - time::{Duration, Instant}, - }; - - use rayon::iter::{IntoParallelIterator, ParallelIterator}; - - use rand::random; - - // #[test] - fn main_test() { - const LEN: usize = 1_000; - const NUM_ITER: u32 = 100; - const NUM_CORES: usize = 12; - - (0..NUM_CORES).into_par_iter().for_each(|_| { - let mut belt = Vec::new(); - belt.resize_with(LEN * 64, || rand::thread_rng().gen_bool(0.0001)); - - let mut indices = Vec::new(); - indices.resize_with(LEN * 64, || random::()); - - let mut inserter_hands = Vec::new(); - inserter_hands.resize_with(LEN * 64, || random::() % 8); - - let mut inserter_timers = Vec::new(); - inserter_timers.resize_with(LEN * 64, || random::() % 8); - - let mut output = Vec::new(); - output.resize(256, 0); - - let sum = Duration::from_secs(0); - - let size = belt.len() * size_of::() - + indices.len() * size_of::() - + inserter_hands.len() * size_of::(); - - println!("Total size: {}", size); - - let transfers = 2 * belt.len() * size_of::() - + 1 * indices.len() * size_of::() - + 2 * inserter_hands.len() * size_of::(); - - let start = Instant::now(); - for i in 0..NUM_ITER { - test( - &mut belt, - &indices, - &mut inserter_hands, - &mut inserter_timers, - &mut output, - ); - } - let time = start.elapsed(); - - println!("avg {:?}", time / NUM_ITER); - println!( - "gb/s {:?}", - size as f64 / (time / NUM_ITER).as_secs_f64() / 1_000_000_000.0 - ); - }); - - loop { - thread::sleep(Duration::from_secs(10)); - } - // let mut belt = Vec::new(); - // belt.resize(LEN * 64, true); - - // let mut indices = Vec::new(); - // indices.resize_with(LEN * 64, || random::()); - - // let mut inserter_hands = Vec::new(); - // inserter_hands.resize_with(LEN * 64, || random::() % 8); - - // let mut output = Vec::new(); - // output.resize(256, 0); - - // let mut sum = Duration::from_secs(0); - - // let size = belt.len() * size_of::() - // + indices.len() * size_of::() - // + inserter_hands.len() * size_of::(); - - // println!("Total size: {}", size); - - // let start = Instant::now(); - // for i in 0..NUM_ITER { - // test(&mut belt, &indices, &mut inserter_hands, &mut output); - // } - // let time = start.elapsed(); - - // println!("avg {:?}", time / NUM_ITER); - // println!( - // "gb/s {:?}", - // size as f64 / (time / NUM_ITER).as_secs_f64() / 1_000_000_000.0 - // ); - - // dbg!(inserter_hands); - } - - fn test( - belt: &mut [bool], - indices: &[u8], - inserter_hands: &mut [u8], - inserter_timers: &mut [u8], - output: &mut [u16], - ) { - assert!(output.len() >= 256); - - unsafe { - let zeroes = _mm256_setzero_si256(); - let ones = _mm256_set1_epi8(1); // Create a vector with all 1's - let eights = _mm256_set_epi8( - 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, - 8, 8, 8, 8, - ); // Create a vector with all 8's - - // Process 32 items at a time using AVX2 - for i in (0..belt.len()).step_by(32) { - // Prefetch the next indices and output entries - // Caching is not helpful, since output is small enough to always be in L1 - // for j in 0..32 { - // // Prefetch the corresponding output index (machine counter) - // let machine_index = indices.as_ptr().add(i + j); - // _mm_prefetch::<_MM_HINT_T0>(machine_index as *const i8); - // } - - // Load the next 32 items from the belt - let belt_vals = _mm256_loadu_si256(&belt[i] as *const bool as *const __m256i); - - // Create a mask where each byte is 1 if belt[i] == 1, otherwise 0 - let mask = _mm256_cmpeq_epi8(belt_vals, ones); // Compare to 1 - - let current_inserter_timers = - _mm256_loadu_si256(&inserter_timers[i] as *const u8 as *const __m256i); - - let current_inserters_standing = _mm256_cmpeq_epi8(current_inserter_timers, zeroes); - - // let current_inserters_ - - let current_inserter_hands = - _mm256_loadu_si256(&inserter_hands[i] as *const u8 as *const __m256i); - - let inserter_hands_added = _mm256_add_epi8(current_inserter_hands, ones); - - let new_inserter_hands = - _mm256_blendv_epi8(current_inserter_hands, inserter_hands_added, mask); - - let inserter_full_mask = _mm256_cmpeq_epi8(new_inserter_hands, eights); - - let final_inserter = - _mm256_blendv_epi8(new_inserter_hands, zeroes, inserter_full_mask); - - _mm256_storeu_si256( - &mut inserter_hands[i] as *mut u8 as *mut __m256i, - final_inserter, - ); - - // Get the bitmask (integer) from the mask (this will be a 32-bit value) - let bitmask = _mm256_movemask_epi8(inserter_full_mask); // Returns a 32-bit integer with each bit corresponding to a slot on the belt - - // This checks helps if most inserters do not insert on a specific frame (which is probably a good idea) - if bitmask != 0x0 { - // Directly update the machine counters using the bitmask - for j in 0..32 { - // Check if the bit at position j is set in the bitmask - let machine_index = *indices.get_unchecked(i + j); - // Accumulate the count for the corresponding machine index using the bitmask - *output.get_unchecked_mut(usize::from(machine_index)) += - ((bitmask >> j) & 1) as u16; - } - } - } - - // // Scalar fallback for remaining items if belt_size is not a multiple of 32 - // for (; i < belt_size; i++) { - // output[indices[i]] += belt[i]; - // } - - output.iter_mut().for_each(|o| *o *= 8); - } - } -} +// #![feature(generic_const_exprs)] +#![feature(portable_simd)] +#![feature(iter_array_chunks)] + +use std::simd::{Simd, cmp::SimdPartialEq}; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +fn main() { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + factory::main(); +} + +#[cfg(test)] +mod test { + use rand::Rng; + use std::{ + arch::x86_64::{ + __m256i, _mm256_add_epi8, _mm256_blendv_epi8, _mm256_cmpeq_epi8, _mm256_loadu_si256, + _mm256_movemask_epi8, _mm256_set_epi8, _mm256_set1_epi8, _mm256_setzero_si256, + _mm256_storeu_si256, + }, + array, mem, thread, + time::{Duration, Instant}, + }; + + use rayon::iter::{IntoParallelIterator, ParallelIterator}; + + use rand::random; + + // #[test] + fn main_test() { + const LEN: usize = 1_000; + const NUM_ITER: u32 = 100; + const NUM_CORES: usize = 12; + + (0..NUM_CORES).into_par_iter().for_each(|_| { + let mut belt = Vec::new(); + belt.resize_with(LEN * 64, || rand::thread_rng().gen_bool(0.0001)); + + let mut indices = Vec::new(); + indices.resize_with(LEN * 64, || random::()); + + let mut inserter_hands = Vec::new(); + inserter_hands.resize_with(LEN * 64, || random::() % 8); + + let mut inserter_timers = Vec::new(); + inserter_timers.resize_with(LEN * 64, || random::() % 8); + + let mut output = Vec::new(); + output.resize(256, 0); + + let sum = Duration::from_secs(0); + + let size = belt.len() * size_of::() + + indices.len() * size_of::() + + inserter_hands.len() * size_of::(); + + println!("Total size: {}", size); + + let transfers = 2 * belt.len() * size_of::() + + 1 * indices.len() * size_of::() + + 2 * inserter_hands.len() * size_of::(); + + let start = Instant::now(); + for i in 0..NUM_ITER { + test( + &mut belt, + &indices, + &mut inserter_hands, + &mut inserter_timers, + &mut output, + ); + } + let time = start.elapsed(); + + println!("avg {:?}", time / NUM_ITER); + println!( + "gb/s {:?}", + size as f64 / (time / NUM_ITER).as_secs_f64() / 1_000_000_000.0 + ); + }); + + loop { + thread::sleep(Duration::from_secs(10)); + } + // let mut belt = Vec::new(); + // belt.resize(LEN * 64, true); + + // let mut indices = Vec::new(); + // indices.resize_with(LEN * 64, || random::()); + + // let mut inserter_hands = Vec::new(); + // inserter_hands.resize_with(LEN * 64, || random::() % 8); + + // let mut output = Vec::new(); + // output.resize(256, 0); + + // let mut sum = Duration::from_secs(0); + + // let size = belt.len() * size_of::() + // + indices.len() * size_of::() + // + inserter_hands.len() * size_of::(); + + // println!("Total size: {}", size); + + // let start = Instant::now(); + // for i in 0..NUM_ITER { + // test(&mut belt, &indices, &mut inserter_hands, &mut output); + // } + // let time = start.elapsed(); + + // println!("avg {:?}", time / NUM_ITER); + // println!( + // "gb/s {:?}", + // size as f64 / (time / NUM_ITER).as_secs_f64() / 1_000_000_000.0 + // ); + + // dbg!(inserter_hands); + } + + fn test( + belt: &mut [bool], + indices: &[u8], + inserter_hands: &mut [u8], + inserter_timers: &mut [u8], + output: &mut [u16], + ) { + assert!(output.len() >= 256); + + unsafe { + let zeroes = _mm256_setzero_si256(); + let ones = _mm256_set1_epi8(1); // Create a vector with all 1's + let eights = _mm256_set_epi8( + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, + ); // Create a vector with all 8's + + // Process 32 items at a time using AVX2 + for i in (0..belt.len()).step_by(32) { + // Prefetch the next indices and output entries + // Caching is not helpful, since output is small enough to always be in L1 + // for j in 0..32 { + // // Prefetch the corresponding output index (machine counter) + // let machine_index = indices.as_ptr().add(i + j); + // _mm_prefetch::<_MM_HINT_T0>(machine_index as *const i8); + // } + + // Load the next 32 items from the belt + let belt_vals = + _mm256_loadu_si256(belt.as_ptr().add(i) as *const bool as *const __m256i); + + // Create a mask where each byte is 1 if belt[i] == 1, otherwise 0 + let mask = _mm256_cmpeq_epi8(belt_vals, ones); // Compare to 1 + + let current_inserter_timers = _mm256_loadu_si256(inserter_timers.as_ptr().add(i) + as *const u8 + as *const __m256i); + + let current_inserters_standing = _mm256_cmpeq_epi8(current_inserter_timers, zeroes); + + // let current_inserters_ + + let current_inserter_hands = _mm256_loadu_si256(inserter_hands.as_ptr().add(i) + as *const u8 + as *const __m256i); + + let inserter_hands_added = _mm256_add_epi8(current_inserter_hands, ones); + + let new_inserter_hands = + _mm256_blendv_epi8(current_inserter_hands, inserter_hands_added, mask); + + let inserter_full_mask = _mm256_cmpeq_epi8(new_inserter_hands, eights); + + let final_inserter = + _mm256_blendv_epi8(new_inserter_hands, zeroes, inserter_full_mask); + + _mm256_storeu_si256( + inserter_hands.as_mut_ptr().add(i) as *mut u8 as *mut __m256i, + final_inserter, + ); + + // Get the bitmask (integer) from the mask (this will be a 32-bit value) + let bitmask = _mm256_movemask_epi8(inserter_full_mask); // Returns a 32-bit integer with each bit corresponding to a slot on the belt + + // This checks helps if most inserters do not insert on a specific frame (which is probably a good idea) + if bitmask != 0x0 { + // Directly update the machine counters using the bitmask + for j in 0..32 { + // Check if the bit at position j is set in the bitmask + let machine_index = *indices.get_unchecked(i + j); + // Accumulate the count for the corresponding machine index using the bitmask + *output.get_unchecked_mut(usize::from(machine_index)) += + ((bitmask >> j) & 1) as u16; + } + } + } + + // // Scalar fallback for remaining items if belt_size is not a multiple of 32 + // for (; i < belt_size; i++) { + // output[indices[i]] += belt[i]; + // } + + output.iter_mut().for_each(|o| *o *= 8); + } + } + + pub fn test2( + belts: &mut [&mut [bool]], + hand_content: &mut [u8], + belt_id: &[u32], + belt_index: &[u16], + ) { + const ARR_LEN: usize = 1; + + assert_eq!(hand_content.len(), belt_id.len()); + assert_eq!(hand_content.len(), belt_index.len()); + + let iter = hand_content + .iter_mut() + .zip(belt_id.iter().zip(belt_index)) + .array_chunks(); + + unsafe { + for (hands, belt_id, belt_index) in iter.map(|arr: [_; ARR_LEN]| split_array(arr)) { + // FIXME: Hack for type inferance + let mut hands: [_; ARR_LEN] = hands; + let belt_id: [_; ARR_LEN] = belt_id; + let belt_index: [_; ARR_LEN] = belt_index; + + let belt_ptr = belts.as_mut_ptr(); + let belts: [_; ARR_LEN] = belt_id.map(|id| belt_ptr.add(*id as usize)); + let belt_positions: [&mut bool; ARR_LEN] = array::from_fn(|i| { + let belt = (*belts[i]).as_mut_ptr(); + &mut (*belt.add(*belt_index[i] as usize)) + }); + + let to_move: [bool; ARR_LEN] = + array::from_fn(|i| (*hands[i] > 0) && *belt_positions[i]); + + for ((hand, to_move), belt_position) in + hands.iter_mut().zip(to_move).zip(belt_positions) + { + **hand -= u8::from(to_move); + *belt_position = to_move; + } + } + } + } + + type BeltTransmuteType = u32; + pub fn update_non_overlapping_inserters( + belts: &mut [&mut [BeltTransmuteType]], + belt_first_free_sure: &mut [bool], + belt_first_free_pos: &[u16], + hand_content: &mut [u8], + belt_id: &[u32], + belt_index: &[u16], + ) { + assert_eq!(hand_content.len(), belt_id.len()); + assert_eq!(hand_content.len(), belt_index.len()); + + unsafe { + for (hand, (belt_id, belt_index)) in + hand_content.iter_mut().zip(belt_id.iter().zip(belt_index)) + { + let belt = belts.get_unchecked(*belt_id as usize); + let belt_position = { + belt.get_unchecked( + (*belt_index as usize) / size_of::() * size_of::(), + ) + }; + + let belt_items = (*belt_position + >> 8 * (*belt_index as usize % size_of::())) + as u8; + + *hand -= u8::from(belt_items); + } + + for (belt_id, belt_index) in belt_id.iter().zip(belt_index) { + let belt_ptr = belts.as_mut_ptr(); + let belt = belt_ptr.add(*belt_id as usize); + let belt_position = { + let belt = (*belt).as_mut_ptr(); + belt.add( + (*belt_index as usize) / size_of::() * size_of::(), + ) + }; + *belt_position = + u32::from(true) << 8 * (*belt_index as usize % size_of::()); + + *belt_first_free_sure.get_unchecked_mut(*belt_id as usize) = + !(*belt_first_free_pos.get_unchecked(*belt_id as usize) == *belt_index) + && *belt_first_free_sure.get_unchecked(*belt_id as usize); + } + } + } + + pub fn update_non_overlapping_inserters_pre_indexed( + belts: &mut [&mut BeltTransmuteType], + belt_first_free_pos: &[u16], + hand_content: &mut [u8], + belt_index: &[u16], + ) { + assert_eq!(hand_content.len(), belts.len()); + assert_eq!(hand_content.len(), belt_index.len()); + + unsafe { + for (hand, (belt_position, belt_index)) in hand_content + .iter_mut() + .zip(belts.iter_mut().zip(belt_index)) + { + let belt_items = (**belt_position + >> 8 * (*belt_index as usize % size_of::())) + as u8; + + *hand -= u8::from(belt_items); + } + + for (belt_position, belt_index) in belts.iter_mut().zip(belt_index) { + **belt_position = + u32::from(true) << 8 * (*belt_index as usize % size_of::()); + } + } + } + + fn split_array(arr: [(A, (B, C)); N]) -> ([A; N], [B; N], [C; N]) { + unsafe { + let first = array::from_fn(|idx| ((&arr[idx].0) as *const A).read()); + let second = array::from_fn(|idx| ((&arr[idx].1.0) as *const B).read()); + let third = array::from_fn(|idx| ((&arr[idx].1.1) as *const C).read()); + + mem::forget(arr); + (first, second, third) + } + } +} + +pub fn find_first_free_pos(locs: &[u8]) -> usize { + let (_, locs_simd, _) = locs.as_simd(); + + let Some(simd_index) = locs_simd.into_iter().position(|v| any_zero_byte(*v)) else { + return locs.len(); + }; + + for i in (simd_index * 32)..((simd_index + 1) * 32) { + if locs[i] == 0 { + return i; + } + } + + return locs.len(); +} + +fn any_zero_byte(v: Simd) -> bool { + const ZERO: Simd = Simd::splat(0); + + let eq = v.simd_eq(ZERO); + eq.any() +} + +// trait SimdElem: Sized { +// const SIZE: usize = size_of::(); +// } + +// const OPTIMAL_SIMD_SIZE_BYTES: usize = 64; +// struct Simd { +// v: [T; N], +// } + +// impl SimdElem for u32 {} +// impl SimdElem for u16 {} + +// type Test1 = Simd; +// type Test2 = Simd; + +// const ORIG: Test2 = todo!(); +// const TEST: Simd = ORIG; diff --git a/src/mining_drill/mod.rs b/src/mining_drill/mod.rs new file mode 100644 index 0000000..961097a --- /dev/null +++ b/src/mining_drill/mod.rs @@ -0,0 +1,150 @@ +use std::collections::HashMap; + +use crate::{ + data::DataStore, + frontend::world::{Position, tile::World}, + item::{IdxTrait, Item}, +}; + +pub mod only_solo_owned; +pub mod with_shared_ore; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +struct MiningDrillID { + index: u32, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct FullOreStore { + stores: Box<[SingleOreStore]>, +} + +impl FullOreStore { + pub fn new( + data_store: &DataStore, + ) -> Self { + Self { + stores: vec![SingleOreStore::new(); todo!()].into_boxed_slice(), + } + } + + pub fn get_ore_at_position( + &self, + world: &World, + mining_drill_stores: &[MiningDrillStore], + pos: Position, + ) -> Option<(Item, u32)> { + let tracked = self + .stores + .iter() + .zip(mining_drill_stores) + .enumerate() + .find_map(|(i, (store, mining_drill_store))| { + store + .get_ore_at_position::(pos, mining_drill_store) + .map(|count| (todo!("Map ore idx to item") as Item, count)) + }); + + if tracked.is_some() { + debug_assert!( + self.stores + .iter() + .zip(mining_drill_stores) + .filter(|(store, mining_drill_store)| store + .get_ore_at_position::(pos, mining_drill_store) + .is_some()) + .count() + == 1 + ); + return tracked; + } + + let original_ore = world.get_original_ore_at_pos(pos); + + original_ore + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct SingleOreStore { + shared_locations: Vec, + shared_location_holes: Vec, + + ore_lookup: HashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +enum OreLoc { + Shared { + index: usize, + }, + SoloOwned { + owner: MiningDrillID, + index_in_owner: u8, + }, +} + +pub struct MiningDrillStore { + todo: !, +} + +impl SingleOreStore { + pub fn new() -> Self { + Self { + shared_locations: vec![], + shared_location_holes: vec![], + ore_lookup: HashMap::new(), + } + } + + pub fn get_ore_at_position( + &self, + pos: Position, + mining_drill_store: &MiningDrillStore, + ) -> Option { + let ore = self.ore_lookup.get(&pos)?; + + match ore { + OreLoc::Shared { index } => { + debug_assert!(!self.shared_location_holes.contains(index)); + Some(self.shared_locations[*index]) + }, + OreLoc::SoloOwned { + owner, + index_in_owner, + } => { + todo!() + }, + } + } + + pub fn add_drill_mining_positions( + &mut self, + positions: impl IntoIterator, + world: &mut World, + mining_drill_store: &mut MiningDrillStore, + ) { + let mut positions: Vec<_> = positions.into_iter().collect(); + + positions.sort(); + + for pos in &positions { + match self.ore_lookup.get(pos) { + Some(OreLoc::Shared { index }) => { + // already shared + }, + Some(OreLoc::SoloOwned { + owner, + index_in_owner, + }) => todo!("Make shared"), + None => { + let ore_amount = world.get_original_ore_at_pos(*pos); + + todo!() + }, + } + } + + todo!() + } +} diff --git a/src/mining_drill/only_solo_owned.rs b/src/mining_drill/only_solo_owned.rs new file mode 100644 index 0000000..9ac7cc8 --- /dev/null +++ b/src/mining_drill/only_solo_owned.rs @@ -0,0 +1,130 @@ +use itertools::Itertools; + +use crate::WeakIdxTrait; +use crate::data::DataStore; +use crate::item::ITEMCOUNTTYPE; +use crate::item::IdxTrait; +use crate::item::Item; +use crate::power::Joule; + +use std::cmp::min; +use std::mem; + +use crate::storage_list::PANIC_ON_INSERT; +pub struct PureDrillStorageOnlySoloOwned { + item: Item, + holes: Vec, + + solo_owned_ore: Vec, + // Having a full usize for the length here is OVERKILL, but reducing it would require unsafe, so I will leave it for now + solo_owned_count_original_values: Vec>, + + inventory: Vec, +} + +impl PureDrillStorageOnlySoloOwned { + pub fn new( + item: Item, + data_store: &DataStore, + ) -> Self { + Self { + item, + holes: vec![], + solo_owned_ore: vec![], + solo_owned_count_original_values: vec![], + inventory: vec![], + } + } + + pub fn add_drill(&mut self, ore_tiles: Vec) -> u32 { + if let Some(hole_idx) = self.holes.pop() { + self.inventory[hole_idx] = 0; + self.solo_owned_ore[hole_idx] = ore_tiles.iter().sum(); + self.solo_owned_count_original_values[hole_idx] = ore_tiles; + u32::try_from(hole_idx).unwrap() + } else { + self.inventory.push(0); + self.solo_owned_ore.push(ore_tiles.iter().sum()); + self.solo_owned_count_original_values.push(ore_tiles); + u32::try_from(self.inventory.len() - 1).unwrap() + } + } + + pub fn remove_drill(&mut self, index: u32) -> (ITEMCOUNTTYPE, Vec) { + let mut tmp = vec![]; + + mem::swap( + &mut tmp, + &mut self.solo_owned_count_original_values[index as usize], + ); + + let ret = (self.inventory[index as usize], tmp); + + self.inventory[index as usize] = 0; + + ret + } + + pub fn update( + &mut self, + power_mult: u8, + data_store: &DataStore, + ) -> Joule { + for (inventory, solo_items) in self + .inventory + .iter_mut() + .zip(self.solo_owned_ore.iter_mut()) + { + // TODO: Timer and prod and stuff + + // FIXME: Let's just assume we produce an item every tick for now (and 0 productivity) + let mut amount_to_remove = u8::from(*inventory < u8::MAX); + + // We are mining from a source, which we control alone + + // Make sure we never underflow! + amount_to_remove = min( + amount_to_remove as u8, + min(u8::MAX as u32, *solo_items) as u8, + ); + *solo_items -= amount_to_remove as u32; + + // TODO: Productivity + *inventory += amount_to_remove; + } + + Joule(0) + } + + pub fn get_current_tile_values(&self, index: u32) -> Vec { + let mut owned_resource_tile_list: Vec<_> = self.solo_owned_count_original_values + [index as usize] + .iter() + .copied() + .collect(); + + let mut resources_to_remove = + owned_resource_tile_list.iter().sum::() - self.solo_owned_ore[index as usize]; + + let tile_count = owned_resource_tile_list.len(); + for (i, tile) in owned_resource_tile_list + .iter_mut() + .enumerate() + .sorted_by_key(|v| *v.1) + { + let tiles_left = tile_count - i; + let resources_from_this_tile = min(*tile, resources_to_remove / tiles_left as u32); + + *tile -= resources_from_this_tile; + resources_to_remove -= resources_from_this_tile; + } + + assert_eq!(resources_to_remove, 0); + + owned_resource_tile_list + } + + pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + (PANIC_ON_INSERT, self.inventory.as_mut_slice()) + } +} diff --git a/src/mining_drill/with_shared_ore.rs b/src/mining_drill/with_shared_ore.rs new file mode 100644 index 0000000..c3aea4f --- /dev/null +++ b/src/mining_drill/with_shared_ore.rs @@ -0,0 +1,453 @@ +use std::{cmp::min, iter, u8}; + +use crate::{ + data::DataStore, + item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, + power::Joule, + storage_list::PANIC_ON_INSERT, +}; + +struct PureDrillConcept { + item: Item, + // TODO: Maybe u8? + solo_owned_ore_source_count: u16, + solo_owned_ore: u32, + + inventory: ITEMCOUNTTYPE, + + /// "Chest" indices + shared_ore_sources: Vec, + output_inserter: Option, +} + +pub struct PureDrillStorageWithSharedOreTiles { + item: Item, + holes: Vec, + + solo_owned_ore: Vec, + solo_owned_current_threshhold: Vec, + // Having a full usize for the length here is OVERKILL, but reducing it would require unsafe, so I will leave it for now + solo_owned_count_threshholds: Vec>, + + inventory: Vec, + // TODO: This means the maximium allowed mining area for a mining drill entity is 32x32. Enforce that! + current_source: Vec, + + shared_sources: Vec>, +} + +impl PureDrillStorageWithSharedOreTiles { + pub fn new( + item: Item, + data_store: &DataStore, + ) -> Self { + Self { + item, + holes: vec![], + solo_owned_ore: vec![], + solo_owned_current_threshhold: vec![], + solo_owned_count_threshholds: vec![], + inventory: vec![], + current_source: vec![], + shared_sources: vec![], + } + } + + pub fn add_mining_drill(&mut self, info: !) -> usize { + todo!() + } + + pub fn make_ore_shared( + &mut self, + index: usize, + old_owned_sum: u32, + old_owned_by_newly_shared: u32, + shared_ore_patches: &mut Vec, + ) -> usize { + // let ratio = self.solo_owned_ore[index] as f64 / old_owned_sum as f64; + + // let new_owned_by_shared = (old_owned_by_newly_shared as f64 * ratio) as u32; + + let new_owned_by_shared: u32 = u32::try_from( + (u64::from(old_owned_by_newly_shared) * u64::from(self.solo_owned_ore[index])) + / u64::from(old_owned_sum), + ) + .expect( + "This should always be less than old_owned_by_newly_shared which fit into u32 before", + ); + + self.solo_owned_ore[index] -= new_owned_by_shared; + + // FIXME: This is broken, since we modify the threshholds + let steps = iter::once(&0) + .chain(self.solo_owned_count_threshholds[index].iter()) + .zip( + self.solo_owned_count_threshholds[index] + .iter() + .chain(iter::once(&old_owned_sum)), + ) + .map(|(next, prev)| { + assert!(prev >= next); + prev - next + }); + + let steps_per_tile = steps + .enumerate() + .map(|(num_tiles_left, step)| { + assert_eq!(step % (num_tiles_left as u32 + 1), 0); + step / (num_tiles_left as u32 + 1) + }) + .collect::>(); + + let tile_sizes = (0..steps_per_tile.len()).map(|len| steps_per_tile[len..].iter().sum()); + + let to_remove_idx = tile_sizes + .clone() + .position(|v: u32| v == old_owned_by_newly_shared) + .unwrap(); + + let calculated_tiles = tile_sizes + .enumerate() + .filter(|(i, _)| *i != to_remove_idx) + .map(|(_, v)| v) + .collect::>(); + + let new_threshhold = calculate_threshholds(&calculated_tiles); + + self.solo_owned_count_threshholds[index] = new_threshhold; + + shared_ore_patches.push(new_owned_by_shared); + + let shared_idx = shared_ore_patches.len() - 1; + + self.shared_sources[index].push(u32::try_from(shared_idx).unwrap()); + + shared_idx + } + + pub fn update( + &mut self, + power_mult: u8, + shared_ore_patches: &mut [u32], + switch_sources: bool, + data_store: &DataStore, + ) -> Joule { + if switch_sources { + for ((current_source, num_owned_sources), num_shared_sources) in self + .current_source + .iter_mut() + .zip( + self.solo_owned_count_threshholds + .iter() + .zip(self.solo_owned_ore.iter()) + .map(|(threshholds, solo_ore)| { + if threshholds.len() > 0 { + threshholds.len() + 1 + } else { + if *solo_ore > 0 { 1 } else { 0 } + } + }), + ) + .zip( + self.shared_sources + .iter() + .map(|shared_sources| shared_sources.len()), + ) + { + debug_assert!(num_owned_sources < (u8::MAX as usize)); + debug_assert!(num_shared_sources < (u8::MAX as usize)); + + // FIXME: This breaks if total_sources is 256, which then wraps to 0 + let total_sources = num_owned_sources as u8 + num_shared_sources as u8; + + // FIXME: And causes a crash here + *current_source = (*current_source).wrapping_add(1) % total_sources; + } + } + + for ( + ((((inventory, current_source), shared_source_indices), threshholds), solo_items), + current_threshhold, + ) in self + .inventory + .iter_mut() + .zip(self.current_source.iter_mut()) + .zip(self.shared_sources.iter_mut()) + .zip(self.solo_owned_count_threshholds.iter_mut()) + .zip(self.solo_owned_ore.iter_mut()) + .zip(self.solo_owned_current_threshhold.iter_mut()) + { + // TODO: Timer and prod and stuff + + // FIXME: Let's just assume we produce an item every tick for now (and 0 productivity) + let mut amount_to_remove = u8::from(*inventory < u8::MAX); + + if *current_source >= (shared_source_indices.len() as u8) { + // We are mining from a source, which we control alone + + // Make sure we never underflow! + amount_to_remove = min( + amount_to_remove as u8, + min(u8::MAX as u32, *solo_items) as u8, + ); + *solo_items -= amount_to_remove as u32; + + if *solo_items <= *current_threshhold { + if threshholds.pop().is_none() { + // This drill no longer has any solo items + assert_eq!(*solo_items, 0); + assert_eq!(*current_threshhold, 0); + } else { + while *solo_items <= *threshholds.last().unwrap_or(&0) { + if threshholds.pop().is_none() { + // This drill no longer has any solo items + assert_eq!(*solo_items, 0); + } + todo!("What to do with current_source?"); + } + } + + *current_threshhold = threshholds.last().copied().unwrap_or(0); + + todo!("What to do with current_source?"); + } + } else { + let shared_source = shared_source_indices[(*current_source) as usize]; + + let shared_source_storage = &mut shared_ore_patches[shared_source as usize]; + + // Make sure we never underflow! + amount_to_remove = min( + amount_to_remove as u8, + min(u8::MAX as u32, *shared_source_storage) as u8, + ); + *shared_source_storage -= amount_to_remove as u32; + + if *shared_source_storage == 0 { + shared_source_indices.remove((*current_source) as usize); + + todo!("What to do with current_source?"); + } + } + + // TODO: Productivity + *inventory += amount_to_remove; + } + + Joule(0) + } + + pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + (PANIC_ON_INSERT, self.inventory.as_mut_slice()) + } +} + +struct MixedDrill {} + +fn calculate_threshholds(owned_resource_tile_list: &[u32]) -> Vec { + if owned_resource_tile_list.is_empty() { + return vec![]; + } + + assert!(owned_resource_tile_list.iter().all(|v| *v > 0)); + + let mut owned_resource_tile_list: Vec<_> = owned_resource_tile_list.iter().copied().collect(); + + let mut ret = vec![]; + + loop { + if ret.len() == owned_resource_tile_list.len() - 1 { + break; + } + + let amount_to_remove = owned_resource_tile_list + .iter() + .copied() + .filter(|v| *v > 0) + .min() + .unwrap_or(0); + + let num_empty = owned_resource_tile_list.iter().filter(|v| **v == 0).count(); + + let sum = owned_resource_tile_list.iter().sum::(); + + for _ in 0..(owned_resource_tile_list + .iter() + .filter(|v| **v == amount_to_remove) + .count()) + { + let elem = sum - amount_to_remove * (owned_resource_tile_list.len() - num_empty) as u32; + ret.insert(0, elem); + + if elem == 0 { + break; + } + } + + owned_resource_tile_list + .iter_mut() + .for_each(|v| *v = (*v).saturating_sub(amount_to_remove)); + } + + ret +} + +#[cfg(test)] +mod test { + use std::{ + cmp::{max, min}, + collections::HashSet, + iter, + }; + + use itertools::Itertools; + use proptest::{ + array::uniform5, + prelude::{Just, Strategy}, + prop_assert, prop_assert_eq, proptest, + }; + + use crate::mining_drill::with_shared_ore::calculate_threshholds; + + #[test] + fn test_threshhold_empty() { + assert!(calculate_threshholds(&mut []).is_empty()); + } + + #[test] + fn test_threshhold() { + assert_eq!( + calculate_threshholds(&mut [50, 200, 500]).as_slice(), + &[300, 600] + ); + } + + #[test] + fn test_threshhold_same_highest() { + assert_eq!( + calculate_threshholds(&mut [100, 500, 500]).as_slice(), + &[0, 800] + ); + } + + #[test] + fn test_threshhold_same_middle() { + assert_eq!( + calculate_threshholds(&mut [100, 500, 500, 800]).as_slice(), + &[300, 300, 1500] + ); + } + + #[test] + fn test_threshhold_same_smallest() { + assert_eq!( + calculate_threshholds(&mut [500, 500, 800]).as_slice(), + &[300, 300] + ); + } + + fn old_values_and_mined() -> impl Strategy { + uniform5(1u32..1_000).prop_flat_map(|items| { + let sum = items.iter().sum(); + + (Just(items), 0..=sum) + }) + } + + proptest! { + #[test] + fn test_threshhold_one_slot(value in 1u32..1_000) { + prop_assert!(calculate_threshholds(&mut [value]).is_empty()); + } + + #[test] + fn test_threshhold_two_slot(value in 1u32..1_000, value2 in 1u32..1_000) { + prop_assert!(calculate_threshholds(&mut [value, value2])[0] == max(value, value2) - min(value, value2)); + } + + + #[test] + fn test_threshhold_removal_calculation(to_remove in 0usize..5, old_values in uniform5(1u32..1_0)) { + let after_removal: [u32; 4] = old_values.iter().copied().enumerate().filter(|(i, _)| *i != to_remove).map(|(_, v)| v).collect_array().unwrap(); + let goal_threshholds = calculate_threshholds(&after_removal); + + let removed_value = old_values[to_remove]; + let old_sum: u32 = old_values.iter().sum(); + let starting_threshholds = calculate_threshholds(&old_values); + + let steps = iter::once(&0).chain(starting_threshholds.iter()).zip(starting_threshholds.iter().chain(iter::once(&old_sum))).map(|(next, prev)| { + assert!(prev >= next); + prev - next + }).collect_vec(); + + + let steps_per_tile = steps.iter().enumerate().map(|(num_tiles_left, step)| { + assert_eq!(step % (num_tiles_left as u32 + 1), 0); + step / (num_tiles_left as u32 + 1) + }).collect_vec(); + + let tile_sizes = (0..steps_per_tile.len()).map(|len| { + steps_per_tile[len..].iter().sum() + }).collect_vec(); + + prop_assert!(tile_sizes.contains(&removed_value), "{:?}", tile_sizes); + + let to_remove_idx = tile_sizes.iter().position(|v| *v == removed_value).unwrap(); + + let calculated_tiles = tile_sizes.iter().copied().enumerate().filter(|(i, _)| *i != to_remove_idx).map(|(_, v)| v).collect_vec(); + + let hashset: HashSet<_> = calculated_tiles.iter().copied().collect(); + + prop_assert_eq!(hashset, HashSet::from_iter(after_removal.iter().copied())); + + let calculated_new_thrsh = calculate_threshholds(&calculated_tiles); + + prop_assert_eq!(calculated_new_thrsh, goal_threshholds); + } + + + #[test] + fn test_threshhold_removal_calculation_with_mined_items(to_remove in 0usize..5, (old_values, mined_items) in old_values_and_mined()) { + let after_removal: [u32; 4] = old_values.iter().copied().enumerate().filter(|(i, _)| *i != to_remove).map(|(_, v)| v).collect_array().unwrap(); + let goal_threshholds = calculate_threshholds(&after_removal); + + let removed_value = old_values[to_remove]; + let old_sum: u32 = old_values.iter().sum(); + let mut starting_threshholds = calculate_threshholds(&old_values); + + let solo_items = old_sum - mined_items; + + starting_threshholds.retain(|v| *v < solo_items); + + + let steps = iter::once(&0).chain(starting_threshholds.iter()).zip(starting_threshholds.iter().chain(iter::once(&old_sum))).map(|(next, prev)| { + assert!(prev >= next); + prev - next + }).collect_vec(); + + + let steps_per_tile = steps.iter().enumerate().map(|(num_tiles_left, step)| { + assert_eq!(step % (num_tiles_left as u32 + 1), 0); + step / (num_tiles_left as u32 + 1) + }).collect_vec(); + + let tile_sizes = (0..steps_per_tile.len()).map(|len| { + steps_per_tile[len..].iter().sum() + }).collect_vec(); + + prop_assert!(tile_sizes.contains(&removed_value), "{:?}", tile_sizes); + + let to_remove_idx = tile_sizes.iter().position(|v| *v == removed_value).unwrap(); + + let calculated_tiles = tile_sizes.iter().copied().enumerate().filter(|(i, _)| *i != to_remove_idx).map(|(_, v)| v).collect_vec(); + + let hashset: HashSet<_> = calculated_tiles.iter().copied().collect(); + + prop_assert_eq!(hashset, HashSet::from_iter(after_removal.iter().copied())); + + let calculated_new_thrsh = calculate_threshholds(&calculated_tiles); + + prop_assert_eq!(calculated_new_thrsh, goal_threshholds); + } + } +} diff --git a/src/multiplayer/connection_reciever.rs b/src/multiplayer/connection_reciever.rs index b4170a0..409259c 100644 --- a/src/multiplayer/connection_reciever.rs +++ b/src/multiplayer/connection_reciever.rs @@ -1,20 +1,36 @@ use std::{ - net::{TcpListener, TcpStream}, - sync::{Arc, Mutex}, + net::{TcpListener, TcpStream, ToSocketAddrs}, + sync::{Arc, atomic::AtomicBool}, thread, }; +use parking_lot::Mutex; + pub type ConnectionList = Arc>>; -pub fn accept_continously(connections: ConnectionList) -> Result<(), std::io::Error> { - let listener = TcpListener::bind("127.0.0.1:8080")?; +pub fn accept_continously( + local_addr: impl ToSocketAddrs, + connections: ConnectionList, + cancel: Arc, +) -> Result<(), std::io::Error> { + let listener = TcpListener::bind(local_addr)?; thread::spawn(move || { for conn in listener.incoming() { match conn { - Ok(conn) => connections.lock().unwrap().push(conn), - Err(_) => todo!("Handle errors"), + Ok(conn) => { + if cancel.load(std::sync::atomic::Ordering::Relaxed) { + return; + } else { + connections.lock().push(conn); + } + }, + Err(err) => match err.kind() { + std::io::ErrorKind::WouldBlock => todo!(), + _ => todo!(), + }, } } }); + Ok(()) } diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 1707dcd..94aed70 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -1,24 +1,32 @@ use std::{ - net::{IpAddr, TcpStream}, + env, + net::{SocketAddr, TcpStream}, ops::ControlFlow, - sync::{atomic::AtomicU64, mpsc::Receiver, Arc, Mutex}, - time::{Duration, Instant}, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU64}, + mpsc::Receiver, + }, + time::Duration, }; -use log::info; +use parking_lot::Mutex; + use plumbing::{Client, IntegratedServer, Server}; use server::{ActionSource, GameStateUpdateHandler, HandledActionConsumer}; use crate::{ + TICKS_PER_SECOND_RUNSPEED, data::DataStore, frontend::{ - action::{action_state_machine::ActionStateMachine, ActionType}, + action::{ActionType, action_state_machine::ActionStateMachine}, input::Input, world::tile::World, }, item::{IdxTrait, WeakIdxTrait}, rendering::app_state::GameState, - TICKS_PER_SECOND_RUNSPEED, + replays::Replay, + saving::save, }; mod plumbing; @@ -27,7 +35,7 @@ mod server; pub mod connection_reciever; -pub enum Game { +pub(super) enum Game { Client( Arc>>, GameStateUpdateHandler>, @@ -35,23 +43,44 @@ pub enum Game { ), DedicatedServer( GameState, + Replay>, GameStateUpdateHandler>, + Box, ), /// Integrated Server is also how Singleplayer works IntegratedServer( Arc>>, + Replay>, GameStateUpdateHandler< ItemIdxType, RecipeIdxType, IntegratedServer, >, Arc, + Box, ), } +impl Drop for Game { + fn drop(&mut self) { + match self { + Game::Client(mutex, game_state_update_handler, atomic_u64) => {}, + Game::DedicatedServer(game_state, replay, game_state_update_handler, cancel_socket) => { + cancel_socket() + }, + Game::IntegratedServer( + mutex, + replay, + game_state_update_handler, + atomic_u64, + cancel_socket, + ) => cancel_socket(), + } + } +} + pub struct ClientConnectionInfo { - pub ip: IpAddr, - pub port: u16, + pub addr: SocketAddr, } pub struct ServerInfo { @@ -67,7 +96,11 @@ pub enum GameInitData { tick_counter: Arc, info: ClientConnectionInfo, }, - DedicatedServer(GameState, ServerInfo), + DedicatedServer( + GameState, + ServerInfo, + Box, + ), IntegratedServer { game_state: Arc>>, action_state_machine: Arc>>, @@ -75,16 +108,21 @@ pub enum GameInitData { ui_actions: Receiver>, tick_counter: Arc, info: ServerInfo, + cancel_socket: Box, }, } pub enum ExitReason { + LoopStopped, UserQuit, ConnectionDropped, } impl Game { - pub fn new(init: GameInitData) -> Result { + pub fn new( + init: GameInitData, + data_store: &DataStore, + ) -> Result { match init { GameInitData::Client { game_state, @@ -94,7 +132,7 @@ impl Game { - let stream = std::net::TcpStream::connect((info.ip, info.port))?; + let stream = std::net::TcpStream::connect(info.addr)?; Ok(Self::Client( game_state, GameStateUpdateHandler::new(Client { @@ -106,10 +144,18 @@ impl Game Ok(Self::DedicatedServer( - game_state, - GameStateUpdateHandler::new(Server::new(info)), - )), + GameInitData::DedicatedServer(game_state, info, cancel_socket) => { + #[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()); + Ok(Self::DedicatedServer( + game_state, + replay, + GameStateUpdateHandler::new(Server::new(info)), + cancel_socket, + )) + }, GameInitData::IntegratedServer { game_state, tick_counter, @@ -117,35 +163,52 @@ impl Game Ok(Self::IntegratedServer( - game_state, - GameStateUpdateHandler::new(IntegratedServer { - local_actions: action_state_machine, - local_input: inputs, - server: Server::new(info), - ui_actions, - }), - tick_counter, - )), + cancel_socket, + } => { + #[cfg(debug_assertions)] + let replay = Replay::new(&*game_state.lock(), None, data_store.clone()); + #[cfg(not(debug_assertions))] + let replay = Replay::new_dummy(data_store.clone()); + Ok(Self::IntegratedServer( + game_state, + replay, + GameStateUpdateHandler::new(IntegratedServer { + local_actions: action_state_machine, + local_input: inputs, + server: Server::new(info), + ui_actions, + }), + tick_counter, + cancel_socket, + )) + }, } } - pub fn run(&mut self, data_store: &DataStore) -> ExitReason { + pub fn run( + &mut self, + stop: Arc, + data_store: &DataStore, + ) -> ExitReason { let mut update_interval = spin_sleep_util::interval(Duration::from_secs(1) / TICKS_PER_SECOND_RUNSPEED as u32); - loop { - let start = Instant::now(); - update_interval.tick(); + while stop.load(std::sync::atomic::Ordering::Relaxed) == false { + profiling::finish_frame!(); + profiling::scope!("Update Loop"); - info!("Waited for {:?}", start.elapsed()); match self.do_tick(data_store) { ControlFlow::Continue(_) => {}, ControlFlow::Break(e) => return e, } - info!("Full tick time: {:?}", start.elapsed()); + if !env::var("ZOOM").is_ok() { + profiling::scope!("Wait"); + update_interval.tick(); + } } + + ExitReason::LoopStopped } fn do_tick( @@ -154,18 +217,37 @@ impl Game ControlFlow { match self { Game::Client(game_state, game_state_update_handler, tick_counter) => { - game_state_update_handler.update( - &mut game_state.lock().expect("Lock poison for update"), + game_state_update_handler.update::<&DataStore>( + &mut game_state.lock(), + None, data_store, ); tick_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }, - Game::DedicatedServer(game_state, game_state_update_handler) => { - game_state_update_handler.update(game_state, data_store) - }, - Game::IntegratedServer(game_state, game_state_update_handler, tick_counter) => { + Game::DedicatedServer( + game_state, + replay, + game_state_update_handler, + _cancel_socket, + ) => game_state_update_handler.update(game_state, Some(replay), data_store), + Game::IntegratedServer( + game_state, + replay, + game_state_update_handler, + tick_counter, + _cancel_socket, + ) => { + #[cfg(debug_assertions)] + { + profiling::scope!("Crash anticipation save to disk"); + save(&game_state.lock(), data_store); + } game_state_update_handler.update( - &mut game_state.lock().expect("Lock poison for update"), + &mut *{ + profiling::scope!("Wait for GameState Lock"); + game_state.lock() + }, + Some(replay), data_store, ); tick_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); @@ -184,8 +266,8 @@ impl current_tick: u64, _: &World, _: &DataStore, - ) -> impl IntoIterator> - + use<'a, ItemIdxType, RecipeIdxType> { + ) -> impl Iterator> + use<'a, ItemIdxType, RecipeIdxType> + { self.try_iter() } } @@ -195,8 +277,8 @@ impl { fn consume( &mut self, - current_tick: u64, - actions: impl IntoIterator>, + _current_tick: u64, + _actions: impl IntoIterator>, ) { // Do nothing } diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 8fe34b5..29f3626 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -3,17 +3,18 @@ use std::{ marker::PhantomData, mem, net::TcpStream, - sync::{mpsc::Receiver, Arc, Mutex}, + sync::{Arc, mpsc::Receiver}, time::{Duration, Instant}, }; -use eframe::wgpu::hal::auxil::db; +use parking_lot::Mutex; + use log::{error, warn}; use crate::{ data::DataStore, frontend::{ - action::{action_state_machine::ActionStateMachine, ActionType}, + action::{ActionType, action_state_machine::ActionStateMachine}, input::Input, world::tile::World, }, @@ -21,9 +22,9 @@ use crate::{ }; use super::{ + ServerInfo, connection_reciever::ConnectionList, server::{ActionSource, HandledActionConsumer}, - ServerInfo, }; pub(super) struct Client { @@ -58,8 +59,8 @@ impl ActionSource, data_store: &DataStore, - ) -> impl IntoIterator> - + use<'a, ItemIdxType, RecipeIdxType> { + ) -> impl Iterator> + use<'a, ItemIdxType, RecipeIdxType> + { // This will block (?) if we did not yet recieve the actions from the server for this tick // TODO: This could introduce hitches which might be noticeable. // This could be solved either by introducing some fixed delay on all actions (i.e. just running the client a couple ticks in the past compared to the server) @@ -67,7 +68,7 @@ impl ActionSource = state_machine .handle_inputs(&self.local_input, world, data_store) @@ -109,7 +110,7 @@ impl ActionSource, data_store: &DataStore, - ) -> impl IntoIterator> + use + ) -> impl Iterator> + use { let start = Instant::now(); // This is the Server, it will just keep on chugging along and never block @@ -122,7 +123,6 @@ impl ActionSource> = self .client_connections .lock() - .unwrap() .iter() .flat_map(|mut conn| { let start = Instant::now(); @@ -153,7 +153,7 @@ impl ActionSource ) { let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients - for conn in self.client_connections.lock().unwrap().iter() { + for conn in self.client_connections.lock().iter() { postcard::to_io(&actions, conn).expect("tcp send failed"); } } @@ -188,10 +188,10 @@ impl ActionSource, data_store: &'c DataStore, - ) -> impl IntoIterator> - + use<'a, 'b, 'c, ItemIdxType, RecipeIdxType> { + ) -> impl Iterator> + + use<'a, 'b, 'c, ItemIdxType, RecipeIdxType> { let start = Instant::now(); - let mut state_machine = self.local_actions.lock().unwrap(); + let mut state_machine = self.local_actions.lock(); if start.elapsed() > Duration::from_millis(10) { error!("Post lock {:?}", start.elapsed()); } diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index bcedb32..306ea46 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -1,16 +1,17 @@ use std::{ - borrow::BorrowMut, + borrow::{Borrow, BorrowMut}, + fs::File, + io::Write, marker::PhantomData, - time::{Duration, Instant}, + time::Instant, }; -use log::{error, info}; - use crate::{ data::DataStore, frontend::{action::ActionType, world::tile::World}, item::{IdxTrait, WeakIdxTrait}, rendering::app_state::GameState, + replays::Replay, }; trait ActionInterface: @@ -31,8 +32,8 @@ pub(super) trait ActionSource, data_store: &'c DataStore, - ) -> impl IntoIterator> - + use<'a, 'b, 'c, Self, ItemIdxType, RecipeIdxType>; + ) -> impl Iterator> + + use<'a, 'b, 'c, Self, ItemIdxType, RecipeIdxType>; } pub(super) trait HandledActionConsumer { @@ -55,10 +56,10 @@ pub(super) struct GameStateUpdateHandler< } impl< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - ActionInterfaceType: ActionInterface, - > GameStateUpdateHandler + ItemIdxType: IdxTrait, + RecipeIdxType: IdxTrait, + ActionInterfaceType: ActionInterface, +> GameStateUpdateHandler { pub fn new(actions: ActionInterfaceType) -> Self { Self { @@ -68,41 +69,56 @@ impl< } } - pub fn update( + pub fn update> + serde::Serialize>( &mut self, game_state: &mut GameState, + replay: Option<&mut Replay>, data_store: &DataStore, ) { let start = Instant::now(); - let actions_iter = + let actions_iter = { + profiling::scope!("Get Actions"); + self.action_interface - .get(game_state.current_tick, &game_state.world, data_store); - if start.elapsed() > Duration::from_millis(10) { - error!("Got action iter {:?}", start.elapsed()); - } + .get(game_state.current_tick, &game_state.world, data_store) + }; + let actions: Vec<_> = actions_iter.into_iter().collect(); - if start.elapsed() > Duration::from_millis(10) { - error!("Got actions {:?}", start.elapsed()); + + { + profiling::scope!("Update Replay"); + if let Some(replay) = replay { + replay.append_actions(actions.iter().cloned()); + replay.tick(); + + #[cfg(debug_assertions)] + { + 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()) + .expect("Could not write to file"); + } + } } - game_state - .borrow_mut() - .apply_actions(actions.clone(), data_store); - if start.elapsed() > Duration::from_millis(10) { - error!("Actions applied {:?}", start.elapsed()); + { + profiling::scope!("Apply Actions"); + game_state + .borrow_mut() + .apply_actions(actions.clone(), data_store); } - self.action_interface - .consume(game_state.current_tick, actions); - if start.elapsed() > Duration::from_millis(10) { - error!("Actions sent {:?}", start.elapsed()); + { + profiling::scope!("Send Action Confirmations"); + self.action_interface + .consume(game_state.current_tick, actions); } - game_state.borrow_mut().update(data_store); - if start.elapsed() > Duration::from_millis(10) { - error!("Update done {:?}", start.elapsed()); - } else { - info!("Update done {:?}", start.elapsed()); + { + profiling::scope!("GameState Update"); + game_state.borrow_mut().update(data_store); } } } diff --git a/src/network_graph/mod.rs b/src/network_graph/mod.rs index cec2ecc..0c98c21 100644 --- a/src/network_graph/mod.rs +++ b/src/network_graph/mod.rs @@ -1,10 +1,11 @@ -use std::{fmt::Debug, iter::once, usize}; +use std::{cmp::max, collections::HashMap, fmt::Debug, iter::once, usize}; use bimap::BiMap; use log::info; use petgraph::{ algo::tarjan_scc, prelude::StableUnGraph, + stable_graph::{StableGraphEdge, StableGraphNode}, visit::{EdgeRef, IntoNodeReferences}, }; @@ -28,10 +29,6 @@ pub struct WeakIndex { index: usize, } -pub struct NetworkUpdate { - new_network: usize, -} - impl Network { pub fn new(first_node: S, key: NodeKey) -> Self { let mut graph = StableUnGraph::default(); @@ -47,22 +44,26 @@ impl Network { } } - pub fn keys(&self) -> impl IntoIterator { + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + pub fn keys(&self) -> impl Iterator { self.key_map.iter().map(|v| v.0) } - pub fn nodes(&self) -> impl IntoIterator { + pub fn nodes(&self) -> impl Iterator { self.graph.node_weights().map(|n| &n.node_info) } - pub fn weak_components(&self) -> impl IntoIterator { + pub fn weak_components(&self) -> impl Iterator { self.graph .node_weights() .flat_map(|n| &n.connected_weak_components) .flatten() } - pub fn weak_components_mut(&mut self) -> impl IntoIterator { + pub fn weak_components_mut(&mut self) -> impl Iterator { self.graph .node_weights_mut() .flat_map(|n| &mut n.connected_weak_components) @@ -96,6 +97,7 @@ impl Network { } } + #[profiling::function] pub fn remove_node<'a>( &'a mut self, key: NodeKey, @@ -103,7 +105,8 @@ impl Network { S, impl IntoIterator + use<'a, NodeKey, S, W>, Option< - impl IntoIterator)> + use<'a, NodeKey, S, W>, + impl IntoIterator + use)> + + use<'a, NodeKey, S, W>, >, ) { let NetworkNode { @@ -116,7 +119,11 @@ impl Network { self.key_map.remove_by_left(&key); - let mut components = petgraph::algo::tarjan_scc(&self.graph); + // Use kosaraju_scc instead of tarjan_scc since tarjan_scc is recursive and will overflow the stack for huge power grids + let mut components = { + profiling::scope!("Calculate Graph Components"); + petgraph::algo::kosaraju_scc(&self.graph) + }; // Pop the first component, (which will stay in this network) // TODO: It is probably good to have the largest component stay, but testing is required @@ -131,51 +138,58 @@ impl Network { ); } - // All remaining components (if any, will be turned into other networks) + // All remaining components (if any), will be turned into other networks let move_to_another_network = components; - let new_networks = move_to_another_network.into_iter().map(|component| { - let connections: Vec<_> = component - .iter() - .map(|idx| self.graph.edges(*idx)) - .flat_map(|edges| edges) - .map(|edge| (edge.source(), edge.target())) - .collect(); - - let mut new_graph = StableUnGraph::default(); - - dbg!(&component); - - let new_indices: Vec<_> = component - .iter() - .map(|idx: &petgraph::prelude::NodeIndex| { - new_graph.add_node(self.graph.remove_node(*idx).unwrap()) - }) - .collect(); - - for (source, dest) in connections { - let source_idx_old = component.iter().position(|v| *v == source).unwrap(); - let dest_idx_old = component.iter().position(|v| *v == dest).unwrap(); - - new_graph.add_edge(new_indices[source_idx_old], new_indices[dest_idx_old], ()); - } - - let keys_in_this: Vec<_> = component - .iter() - .map(|old_node| self.key_map.get_by_right(old_node).unwrap().clone()) - .collect(); - - ( - Network { - graph: new_graph, - key_map: component - .into_iter() - .map(|old_node| self.key_map.remove_by_right(&old_node).unwrap()) - .collect(), - }, - keys_in_this, - ) - }); + let new_networks = { + profiling::scope!("Build new networks"); + move_to_another_network.into_iter().map(|component| { + let connections: Vec<_> = component + .iter() + .map(|idx| self.graph.edges(*idx)) + .flat_map(|edges| edges) + .map(|edge| (edge.source(), edge.target())) + .collect(); + + let mut new_graph = StableUnGraph::default(); + + let new_indices: Vec<_> = component + .iter() + .map(|idx: &petgraph::prelude::NodeIndex| { + new_graph.add_node(self.graph.remove_node(*idx).unwrap()) + }) + .collect(); + + for (source, dest) in connections { + let source_idx_old = component.iter().position(|v| *v == source).unwrap(); + let dest_idx_old = component.iter().position(|v| *v == dest).unwrap(); + + new_graph.add_edge(new_indices[source_idx_old], new_indices[dest_idx_old], ()); + } + + let keys_in_this: Vec<_> = component + .iter() + .map(|old_node| self.key_map.get_by_right(old_node).unwrap().clone()) + .collect(); + + ( + Network { + graph: new_graph, + key_map: component + .iter() + .copied() + .map(|old_node| { + let (pos, _) = self.key_map.remove_by_right(&old_node).unwrap(); + let idx_old = + component.iter().position(|v| *v == old_node).unwrap(); + (pos, new_indices[idx_old]) + }) + .collect(), + }, + keys_in_this, + ) + }) + }; ( node_info, @@ -184,6 +198,93 @@ impl Network { ) } + #[profiling::function] + pub fn remove_edge<'a>( + &'a mut self, + a: NodeKey, + b: NodeKey, + ) -> Option< + impl IntoIterator + use)> + + use<'a, NodeKey, S, W>, + > { + let Some(edge_index) = self.graph.find_edge( + *self.key_map.get_by_left(&a).unwrap(), + *self.key_map.get_by_left(&b).unwrap(), + ) else { + // There was no connection + return None; + }; + + assert!(self.graph.remove_edge(edge_index).is_some()); + + // Use kosaraju_scc instead of tarjan_scc since tarjan_scc is recursive and will overflow the stack for huge power grids + let mut components = { + profiling::scope!("Calculate Graph Components"); + petgraph::algo::kosaraju_scc(&self.graph) + }; + + // Pop the first component, (which will stay in this network) + // TODO: It is probably good to have the largest component stay, but testing is required + components.sort_by_key(|v| -(v.len() as isize)); + + assert!(!components.is_empty()); + + // All remaining components (if any), will be turned into other networks + let move_to_another_network = components; + + let new_networks = { + profiling::scope!("Build new networks"); + move_to_another_network.into_iter().map(|component| { + let connections: Vec<_> = component + .iter() + .map(|idx| self.graph.edges(*idx)) + .flat_map(|edges| edges) + .map(|edge| (edge.source(), edge.target())) + .collect(); + + let mut new_graph = StableUnGraph::default(); + + let new_indices: Vec<_> = component + .iter() + .map(|idx: &petgraph::prelude::NodeIndex| { + new_graph.add_node(self.graph.remove_node(*idx).unwrap()) + }) + .collect(); + + for (source, dest) in connections { + let source_idx_old = component.iter().position(|v| *v == source).unwrap(); + let dest_idx_old = component.iter().position(|v| *v == dest).unwrap(); + + new_graph.add_edge(new_indices[source_idx_old], new_indices[dest_idx_old], ()); + } + + let keys_in_this: Vec<_> = component + .iter() + .map(|old_node| self.key_map.get_by_right(old_node).unwrap().clone()) + .collect(); + + ( + Network { + graph: new_graph, + key_map: component + .iter() + .copied() + .map(|old_node| { + let (pos, _) = self.key_map.remove_by_right(&old_node).unwrap(); + let idx_old = + component.iter().position(|v| *v == old_node).unwrap(); + (pos, new_indices[idx_old]) + }) + .collect(), + }, + keys_in_this, + ) + }) + }; + + Some(new_networks) + } + pub fn add_weak_element(&mut self, key: NodeKey, value: W) -> WeakIndex { let weak_components = &mut self .graph @@ -215,13 +316,17 @@ impl Network { .unwrap() } + #[profiling::function] pub fn add_node_merging( &mut self, value: S, node_key: NodeKey, connection_points: (NodeKey, impl IntoIterator), other: Self, - ) { + ) where + W: Debug, + S: Debug, + { let index = if let Some(index) = self.key_map.get_by_left(&node_key) { *index } else { @@ -265,10 +370,11 @@ impl Network { } } -fn join_graphs( +#[profiling::function] +fn join_graphs( first: &mut StableUnGraph, first_map: &mut BiMap, - mut second: StableUnGraph, + second: StableUnGraph, mut second_map: BiMap, ) { #[cfg(debug_assertions)] @@ -276,47 +382,53 @@ fn join_graphs( #[cfg(debug_assertions)] let second_components = petgraph::algo::tarjan_scc(&second).len(); + // #[cfg(debug_assertions)] + let first_max_edge_count = first + .node_references() + .map(|n| first.edges(n.0).count()) + .max(); + + // #[cfg(debug_assertions)] + let second_max_edge_count = second + .node_references() + .map(|n| second.edges(n.0).count()) + .max(); + // Do the merging - let old_node_indices: Vec<_> = second.node_references().map(|r| r.0).collect(); - - let edges: Vec> = old_node_indices - .iter() - .map(|index| { - second - .edges(*index) - .map(|er| (er.source(), er.target())) - .collect() - }) - .collect(); - - let new_node_indices: Vec<_> = old_node_indices - .iter() - .map(|index| first.add_node(second.remove_node(*index).unwrap())) - .collect(); - - for (old_id, (new_id, edges)) in old_node_indices - .iter() - .zip(new_node_indices.iter().zip(edges)) + + let (nodes, edges) = second.into_nodes_edges_iters(); + + let old_to_new_map = HashMap::<_, _>::from_iter(nodes.map( + |StableGraphNode { + index: old_idx, + weight, + }| { + let new_idx = first.add_node(weight); + + first_map.insert( + second_map + .remove_by_right(&old_idx) + .expect("Missing value in map") + .0, + new_idx, + ); + + (old_idx, new_idx) + }, + )); + + for StableGraphEdge { + index: _, + source: old_source, + target: old_dest, + weight, + } in edges { - for edge in edges { - debug_assert!(edge.0 == *old_id || edge.1 == *old_id); - - if edge.0 == *old_id { - first.add_edge( - *new_id, - new_node_indices[old_node_indices.iter().position(|i| *i == edge.1).unwrap()], - S::default(), - ); - } else if edge.1 == *old_id { - first.add_edge( - *new_id, - new_node_indices[old_node_indices.iter().position(|i| *i == edge.0).unwrap()], - S::default(), - ); - } else { - unreachable!() - } - } + first.add_edge( + old_to_new_map[&old_source], + old_to_new_map[&old_dest], + weight, + ); } #[cfg(debug_assertions)] @@ -325,10 +437,18 @@ fn join_graphs( assert_eq!(final_components, first_components + second_components); } - first_map.extend( - old_node_indices - .iter() - .zip(new_node_indices) - .map(|(old, new)| (second_map.remove_by_right(old).unwrap().0, new)), - ); + // first_map.extend( + // old_node_indices + // .iter() + // .zip(new_node_indices) + // .map(|(old, new)| (second_map.remove_by_right(old).unwrap().0, new)), + // ); + + assert_eq!( + first + .node_references() + .map(|n| first.edges(n.0).count()) + .max(), + max(first_max_edge_count, second_max_edge_count) + ) } diff --git a/src/power/mod.rs b/src/power/mod.rs index 4cb1923..e52ce4a 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -3,23 +3,35 @@ use std::{ iter::Sum, mem, ops::{Add, Div, Mul, Sub}, + u64, }; use itertools::Itertools; -use log::warn; -use power_grid::{IndexUpdateInfo, PowerGrid, PowerGridIdentifier}; +use log::{error, warn}; +use power_grid::{ + BeaconAffectedEntity, IndexUpdateInfo, MIN_BEACON_POWER_MULT, PowerGrid, PowerGridEntity, + PowerGridIdentifier, +}; + +use std::fmt::Display; use crate::{ + TICKS_PER_SECOND_LOGIC, assembler::AssemblerOnclickInfo, data::DataStore, frontend::world::{ - tile::{AssemblerID, MachineID}, Position, + tile::{AssemblerID, MachineID}, }, item::{IdxTrait, WeakIdxTrait}, - TICKS_PER_SECOND_LOGIC, + network_graph::WeakIndex, + research::{LabTickInfo, ResearchProgress, TechState}, + statistics::recipe::RecipeTickInfo, }; +use rayon::iter::ParallelIterator; +use rayon::iter::{IntoParallelRefMutIterator, ParallelBridge}; + pub mod power_grid; #[derive( @@ -36,6 +48,20 @@ pub mod power_grid; )] pub struct Joule(pub u64); +impl Display for Joule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 > 1_000_000_000 { + write!(f, "{:.1}GJ", self.0 as f64 / 1_000_000_000.0) + } else if self.0 > 1_000_000 { + write!(f, "{:.1}MJ", self.0 as f64 / 1_000_000.0) + } else if self.0 > 1_000 { + write!(f, "{:.1}KJ", self.0 as f64 / 1_000.0) + } else { + write!(f, "{:.1}J", self.0 as f64) + } + } +} + impl Sum for Joule { fn sum>(iter: I) -> Self { Self(iter.map(|mj| mj.0).sum()) @@ -118,6 +144,14 @@ impl Mul for Watt { } } +impl Div for Watt { + type Output = Self; + + fn div(self, rhs: u64) -> Self::Output { + Self(self.0 / rhs) + } +} + impl Watt { #[must_use] pub const fn joules_per_tick(self) -> Joule { @@ -125,6 +159,26 @@ impl Watt { } } +impl Display for Watt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 > 1_000_000_000 { + write!(f, "{:.1}GW", self.0 as f64 / 1_000_000_000.0) + } else if self.0 > 1_000_000 { + write!(f, "{:.1}MW", self.0 as f64 / 1_000_000.0) + } else if self.0 > 1_000 { + write!(f, "{:.1}KW", self.0 as f64 / 1_000.0) + } else { + write!(f, "{:.1}W", self.0 as f64) + } + } +} + +impl Sum for Watt { + fn sum>(iter: I) -> Self { + iter.reduce(|acc, v| acc + v).unwrap_or(Watt(0)) + } +} + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PowerGridStorage { pub power_grids: Vec>, @@ -209,19 +263,47 @@ impl PowerGridStorage>( &mut self, pole_position: Position, - connected_poles: impl IntoIterator, + connected_poles: T, data_store: &DataStore, ) -> Option<( - impl IntoIterator, - impl IntoIterator>, + impl IntoIterator + use, + impl IntoIterator> + + use, )> { + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + assert!(affected_grids_and_potential_match); + } let mut connected_poles: Vec<_> = connected_poles.into_iter().collect(); - if let Some(first_connection) = connected_poles.last() { - let grid = self.pole_pos_to_grid_id[first_connection]; + let ret = if !connected_poles.is_empty() { + // Find the largest grid, and choose it as the base + let grid = connected_poles + .iter() + .map(|pos| self.pole_pos_to_grid_id[pos]) + .max_by_key(|grid_id| { + self.power_grids[usize::from(*grid_id)] + .grid_graph + .node_count() + }) + .unwrap(); let need_to_merge = !connected_poles .iter() @@ -231,23 +313,24 @@ impl PowerGridStorage = connected_poles - .iter() - .copied() - .filter_map(|pos| { - (self.pole_pos_to_grid_id[&pos] != grid) - .then_some(self.pole_pos_to_grid_id[&pos]) - }) - .unique() - .flat_map(|merged_grid| { - assert_ne!(merged_grid, grid); - self.pole_pos_to_grid_id - .iter() - .filter_map(move |(k, v)| (*v == merged_grid).then_some(*k)) - }) - .collect(); - - dbg!(&poles_to_update); + let poles_to_update: Vec<_> = { + profiling::scope!("Find poles_to_update"); + connected_poles + .iter() + .copied() + .filter_map(|pos| { + (self.pole_pos_to_grid_id[&pos] != grid) + .then_some(self.pole_pos_to_grid_id[&pos]) + }) + .unique() + .flat_map(|merged_grid| { + assert_ne!(merged_grid, grid); + self.pole_pos_to_grid_id + .iter() + .filter_map(move |(k, v)| (*v == merged_grid).then_some(*k)) + }) + .collect() + }; let mut storage_update_vec = vec![]; @@ -285,6 +368,13 @@ impl PowerGridStorage PowerGridStorage, ) -> ( - impl IntoIterator, - impl IntoIterator>, - impl IntoIterator, + impl IntoIterator + use, + impl IntoIterator> + + use, + impl IntoIterator + use, ) { let old_id = self.pole_pos_to_grid_id.remove(&pole_position).unwrap(); - let (new_grids, delete_network, no_longer_connected_entity_positions) = + let (new_grids, delete_network, no_longer_connected_entity_positions, beacon_updates) = self.power_grids[old_id as usize].remove_pole(pole_position, data_store); + { + profiling::scope!("Apply Beacon updates"); + for beacon_update in beacon_updates { + match beacon_update.0 { + power_grid::BeaconAffectedEntity::Assembler { id } => { + let grid = &mut self.power_grids[usize::from(id.grid)]; + + grid.change_assembler_module_modifiers(id, beacon_update.1, data_store); + }, + power_grid::BeaconAffectedEntity::Lab { grid, index } => todo!(), + } + } + } + if delete_network { debug_assert!(new_grids.into_iter().count() == 0); @@ -333,10 +459,11 @@ impl PowerGridStorage PowerGridStorage PowerGridStorage, ) -> Option>> { + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + assert!(affected_grids_and_potential_match); + } + if kept_id == removed_id { warn!("Tried to merge a grid with itself!"); return None; @@ -388,6 +535,56 @@ impl PowerGridStorage= self.power_grids[usize::from(removed_id)].last_power_consumption + { + self.power_grids[usize::from(kept_id)].last_power_mult + } else { + self.power_grids[usize::from(removed_id)].last_power_mult + } + }; + + let beacon_updates = match ( + new_power_mult >= MIN_BEACON_POWER_MULT, + self.power_grids[usize::from(removed_id)].last_power_mult >= MIN_BEACON_POWER_MULT, + ) { + (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![], + }; + + { + profiling::scope!("Apply Beacon Updates"); + for update in beacon_updates { + match update.0 { + power_grid::BeaconAffectedEntity::Assembler { id } => { + self.power_grids[usize::from(id.grid)] + .change_assembler_module_modifiers(id, update.1, data_store); + }, + power_grid::BeaconAffectedEntity::Lab { grid, index } => { + // TODO: + error!("Ignoring Beacon affect on lab"); + }, + } + } + } + let mut placeholder = PowerGrid::new_placeholder(data_store); mem::swap( @@ -428,16 +625,181 @@ impl PowerGridStorage = ret + .1 + .into_iter() + .filter_map(|update| { + let key = match update.old_pg_entity { + PowerGridEntity::Assembler { + ty: _, + recipe, + index, + } => Some((Some(recipe), index)), + PowerGridEntity::Lab { ty: _, index } => Some((None, index)), + _ => None, + }?; + Some((key, update)) + }) + .collect(); + + // FIXME: We need to update all parts of the simulation which hold references across power_grid borders: + // This includes: + // - Inserters + // - Beacons mem::swap(&mut ret.0, &mut self.power_grids[usize::from(kept_id)]); + // Update Beacons when merging + { + profiling::scope!("Update Beacons when merging"); + + for pg in self.power_grids.iter_mut() { + if !pg + .potential_beacon_affected_powergrids + .contains(&removed_id) + { + // This Power Grid did not affect the Power Grid which has been removed, no need to check all of its beacons + continue; + } + { + profiling::scope!("Update grid_graph.weak_components"); + pg.grid_graph + .weak_components_mut() + .par_bridge() + .for_each(|(_, pg_entity)| { + if let PowerGridEntity::Beacon { + affected_entities, .. + } = pg_entity + { + for affected_entity in affected_entities { + match affected_entity { + BeaconAffectedEntity::Assembler { id } => { + if id.grid == removed_id { + let old_recipe = id.recipe; + let old_index = id.assembler_index; + + let new_id = updates + .get(&(Some(old_recipe), old_index)) + .expect("Could not find update for assembler"); + + let PowerGridEntity::Assembler { + ty: _, + recipe: _, + index, + } = new_id.new_pg_entity + else { + unreachable!(); + }; + + *id = AssemblerID { + recipe: id.recipe, + grid: new_id.new_grid, + assembler_index: index, + }; + } + }, + BeaconAffectedEntity::Lab { grid, index } => { + if *grid == removed_id { + let old_index = *index; + + let new_id = updates + .get(&(None, old_index)) + .expect("Could not find update for lab"); + + let PowerGridEntity::Lab { + ty: _, + index: new_index, + } = new_id.new_pg_entity + else { + unreachable!(); + }; + + *grid = new_id.new_grid; + *index = new_index.into(); + } + }, + } + } + } + }); + } + + pg.potential_beacon_affected_powergrids.remove(&removed_id); + + let to_change: Vec<_> = { + profiling::scope!("Extract beacon_affected_entities"); + pg.beacon_affected_entities + .extract_if(|affected, _| match affected { + BeaconAffectedEntity::Assembler { id } => id.grid == removed_id, + BeaconAffectedEntity::Lab { grid, index: _ } => *grid == removed_id, + }) + .collect() + }; + + if !to_change.is_empty() { + pg.potential_beacon_affected_powergrids.insert(kept_id); + } + { + profiling::scope!("Insert beacon_affected_entities"); + pg.beacon_affected_entities + .extend(to_change.into_iter().map(|(mut k, v)| { + match &mut k { + BeaconAffectedEntity::Assembler { id } => { + let old_recipe = id.recipe; + let old_index = id.assembler_index; + + let new_id = updates + .get(&(Some(old_recipe), old_index)) + .expect("Could not find update for assembler"); + + let PowerGridEntity::Assembler { + ty: _, + recipe: _, + index, + } = new_id.new_pg_entity + else { + unreachable!(); + }; + + *id = AssemblerID { + recipe: id.recipe, + grid: new_id.new_grid, + assembler_index: index, + }; + }, + BeaconAffectedEntity::Lab { grid, index } => { + let old_index = *index; + + let new_id = updates + .get(&(None, old_index)) + .expect("Could not find update for lab"); + + let PowerGridEntity::Lab { + ty: _, + index: new_index, + } = new_id.new_pg_entity + else { + unreachable!(); + }; + + *grid = new_id.new_grid; + *index = new_index.into(); + }, + } + + (k, v) + })); + } + } + } + #[cfg(debug_assertions)] { - assert!(self - .pole_pos_to_grid_id - .iter() - .all(|idx| !self.power_grids[usize::from(*idx.1)].is_placeholder)); + assert!( + self.pole_pos_to_grid_id + .iter() + .all(|idx| !self.power_grids[usize::from(*idx.1)].is_placeholder) + ); assert_eq!( num_placeholders + 1, @@ -446,8 +808,373 @@ impl PowerGridStorage { + !self.power_grids[usize::from(id.grid)].is_placeholder + }, + BeaconAffectedEntity::Lab { grid, index: _ } => { + !self.power_grids[usize::from(*grid)].is_placeholder + }, + } + }) + ); + + assert!( + self.power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .flat_map(|pg| { pg.grid_graph.weak_components() }) + .all(|(_pos, e)| { + match e { + PowerGridEntity::Beacon { + affected_entities, .. + } => affected_entities.iter().all(|e| match e { + BeaconAffectedEntity::Assembler { id } => { + !self.power_grids[usize::from(id.grid)].is_placeholder + }, + BeaconAffectedEntity::Lab { grid, index: _ } => { + !self.power_grids[usize::from(*grid)].is_placeholder + }, + }), + // TODO: Other things to check? + _ => true, + } + }) + ); + + let affected_grids_and_potential_match = self + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + assert!(affected_grids_and_potential_match); + } + + Some(updates.into_values()) + } + + #[profiling::function] + pub fn update( + &mut self, + tech_state: &TechState, + current_time: u32, + data_store: &DataStore, + ) -> (ResearchProgress, RecipeTickInfo, Option) { + { + profiling::scope!("Trim Power Grids"); + while self + .power_grids + .last() + .map(|grid| grid.is_placeholder) + .unwrap_or(false) + { + let removed_placeholder = self.power_grids.pop(); + assert!(removed_placeholder.unwrap().is_placeholder); + } + } + + let (research_progress, production_info, times_labs_used_science, beacon_updates) = self + .power_grids + .par_iter_mut() + .map(|grid| { + grid.update( + &data_store + .solar_panel_info + .iter() + .map(|info| match &info.power_output { + crate::data::SolarPanelOutputFunction::Constant(output) => *output, + crate::data::SolarPanelOutputFunction::Lookup(output) => { + output[usize::try_from(current_time).unwrap()] + }, + }) + .collect::>(), + 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, + ) + }, + ); + + { + profiling::scope!("Propagate beacon modifier changes"); + for update in beacon_updates { + match update.0 { + power_grid::BeaconAffectedEntity::Assembler { id } => { + self.power_grids[usize::from(id.grid)] + .change_assembler_module_modifiers(id, update.1, data_store); + }, + power_grid::BeaconAffectedEntity::Lab { grid, index } => { + // TODO: + warn!("Currently Beacons do not affect labs!"); + }, + } + } } - Some(updates) + ( + research_progress, + production_info, + tech_state.current_technology.map(|tech| LabTickInfo { + times_labs_used_science, + tech, + }), + ) + } + + pub fn add_beacon( + &mut self, + ty: u8, + beacon_pos: Position, + pole_pos: Position, + modules: Box<[Option]>, + affected_entities: impl IntoIterator>, + data_store: &DataStore, + ) -> WeakIndex { + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + assert!(affected_grids_and_potential_match); + } + + let effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let effect = if self.power_grids[usize::from(self.pole_pos_to_grid_id[&pole_pos])] + .last_power_mult + >= MIN_BEACON_POWER_MULT + { + // Add the full beacon effect since we are powered + ( + 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(); + + for affected_entity in affected_entities.iter() { + match affected_entity { + BeaconAffectedEntity::Assembler { id } => { + self.power_grids[usize::from(id.grid)] + .change_assembler_module_modifiers(*id, effect, data_store); + }, + BeaconAffectedEntity::Lab { grid, index } => { + self.power_grids[usize::from(*grid)].change_lab_module_modifiers( + (*index).try_into().unwrap(), + effect, + data_store, + ); + }, + } + } + // I put this here to ensure we do not use effect after this + let effect: (i16, i16, i16); + + let idx = self.power_grids[usize::from(self.pole_pos_to_grid_id[&pole_pos])].add_beacon( + ty, + beacon_pos, + pole_pos, + modules, + affected_entities, + data_store, + ); + + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + assert!(affected_grids_and_potential_match); + } + + idx + } + + pub fn remove_beacon( + &mut self, + pole_pos: Position, + idx: WeakIndex, + data_store: &DataStore, + ) { + let beacon_updates = self.power_grids[usize::from(self.pole_pos_to_grid_id[&pole_pos])] + .remove_beacon(pole_pos, idx, data_store); + + for (effected_entity, effect_change) in beacon_updates { + match effected_entity { + BeaconAffectedEntity::Assembler { id } => { + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + id, + effect_change, + data_store, + ); + }, + BeaconAffectedEntity::Lab { grid, index } => todo!(), + } + } + } + + pub fn add_beacon_affected_entity( + &mut self, + beacon_pole_pos: Position, + beacon_weak_idx: WeakIndex, + entity: BeaconAffectedEntity, + data_store: &DataStore, + ) { + self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .potential_beacon_affected_powergrids + .insert(entity.get_power_grid()); + let ( + _, + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + }, + ) = self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .grid_graph + .modify_weak_component(beacon_pole_pos, beacon_weak_idx) + else { + unreachable!() + }; + + let ty = *ty; + + if affected_entities.contains(&entity) { + warn!("Tried to insert beacon affected entity {entity:?} again!"); + return; + } + + affected_entities.push(entity); + + let effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let raw_effect = ( + 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, + ); + + let effect = if self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .last_power_mult + >= 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_sum = self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .beacon_affected_entities + .entry(entity) + .or_insert((0, 0, 0)); + + effect_sum.0 += raw_effect.0; + effect_sum.1 += raw_effect.1; + effect_sum.2 += raw_effect.2; + + match entity { + BeaconAffectedEntity::Assembler { id } => { + self.power_grids[usize::from(id.grid)] + .change_assembler_module_modifiers(id, effect, data_store); + }, + BeaconAffectedEntity::Lab { grid, index } => { + self.power_grids[usize::from(grid)].change_lab_module_modifiers( + index.try_into().unwrap(), + effect, + data_store, + ); + }, + } + } + + pub fn remove_beacon_affected_entity(&mut self, pole_pos: Position, weak_idx: WeakIndex) { + todo!() } } diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 27aac28..cc7d3bb 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -1,51 +1,79 @@ use std::{ - cmp::{max, min}, - iter::{self}, + cmp::min, + collections::{HashMap, HashSet}, mem, }; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; +use crate::join_many::join; +use crate::assembler::MultiAssemblerStore; use crate::{ assembler::{AssemblerOnclickInfo, AssemblerRemovalInfo, FullAssemblerStore}, data::{DataStore, LazyPowerMachineInfo}, - frontend::world::{tile::AssemblerID, Position}, - inserter::Storage, - item::{IdxTrait, Item, Recipe, WeakIdxTrait, ITEMCOUNTTYPE}, + frontend::world::{Position, tile::AssemblerID}, + item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Item, Recipe, WeakIdxTrait, usize_from}, lab::MultiLabStore, network_graph::{Network, WeakIndex}, power::Joule, research::{ResearchProgress, TechState}, statistics::{ - recipe::{RecipeTickInfo, RecipeTickInfoParts, SingleRecipeTickInfo}, Timeline, + recipe::{RecipeTickInfo, RecipeTickInfoParts, SingleRecipeTickInfo}, }, }; +use itertools::Itertools; +use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use super::Watt; pub const MAX_POWER_MULT: u8 = 64; +pub const MIN_BEACON_POWER_MULT: u8 = MAX_POWER_MULT / 2; pub const MAX_BURNER_RATE: Watt = Watt(1_800_000); -const MAX_ACCUMULATOR_CHARGE_RATE: Watt = Watt(300_000); -const MAX_ACCUMULATOR_DISCHARGE_RATE: Watt = Watt(300_000); - -const MAX_ACCUMULATOR_CHARGE: Joule = Joule(5_000_000); +pub enum PowerGridEntityFIXME { + Assembler { + ty: u8, + recipe: Recipe, + index: u32, + }, + Lab { + ty: u8, + index: u32, + }, + LazyPowerProducer { + item: Item, + index: u32, + }, + SolarPanel { + ty: u8, + }, + Accumulator { + ty: u8, + }, + Beacon { + ty: u8, + speed: i16, + prod: i16, + power: i16, + pos: Position, + }, +} -#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub enum PowerGridEntity { Assembler { + ty: u8, recipe: Recipe, - index: u16, + index: u32, }, Lab { ty: u8, - index: u16, + index: u32, }, LazyPowerProducer { item: Item, - index: usize, + index: u32, }, SolarPanel { ty: u8, @@ -53,6 +81,31 @@ pub enum PowerGridEntity Accumulator { ty: u8, }, + Beacon { + ty: u8, + modules: Box<[Option]>, + affected_entities: Vec>, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] +pub enum BeaconAffectedEntity { + Assembler { + id: AssemblerID, + }, + Lab { + grid: PowerGridIdentifier, + index: u32, + }, +} + +impl BeaconAffectedEntity { + pub fn get_power_grid(&self) -> PowerGridIdentifier { + match self { + BeaconAffectedEntity::Assembler { id } => id.grid, + BeaconAffectedEntity::Lab { grid, .. } => *grid, + } + } } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] @@ -64,17 +117,34 @@ pub struct PowerGrid { steam_power_producers: SteamPowerProducerStore, // TODO: Currently there can only be a single type of solar panel and accumulator - num_solar_panels: u64, - main_accumulator_count: u64, - main_accumulator_charge: Joule, + pub num_solar_panels_of_type: Box<[u64]>, + pub main_accumulator_count: Box<[u64]>, + pub main_accumulator_charge: Box<[Joule]>, // unique_accumulators: Vec, use_burnable_fuel_to_charge_accumulators: Option, - last_power_consumption: Watt, + pub last_power_consumption: Watt, + pub last_produced_power: Watt, + + pub last_ticks_max_power_production: Watt, + + max_lazy_power: Watt, pub last_power_mult: u8, - pub power_history: Timeline, + pub power_mult_history: Timeline, + // FIXME: Not actually storing where the power consumption/production originates is not very useful :/ + // pub power_consumption_history: Timeline, + // pub power_production_history: Timeline, pub is_placeholder: bool, + + pub num_assemblers_of_type: Box<[usize]>, + pub num_labs_of_type: Box<[usize]>, + pub num_beacons_of_type: Box<[usize]>, + + /// This stores the power_grid_ids which could be affected by this grids beacons. + /// We do not remove values from here, so it will overapproximate + pub potential_beacon_affected_powergrids: HashSet, + pub beacon_affected_entities: HashMap, (i16, i16, i16)>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)] @@ -96,7 +166,8 @@ impl From for bool { #[derive(Debug)] pub struct IndexUpdateInfo { pub position: Position, - pub new_storage: PowerGridEntity, + pub old_pg_entity: PowerGridEntity, + pub new_pg_entity: PowerGridEntity, pub new_grid: PowerGridIdentifier, // IMPORTANTLY the WeakIdx always stays the same, since it is just a measure of how many machine are connected to a single pole, // and we only move poles over to new networks so that stays @@ -107,30 +178,6 @@ pub struct PowerPoleUpdateInfo { pub new_grid_id: PowerGridIdentifier, } -pub fn all_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( - grids: impl IntoIterator>, -) -> impl IntoIterator)> { - // NOTE: This has to be assembled in the same way the lookup is generated in DataStore - // Currently this means assemblers -> labs -> TODO - // TODO: This is very fragile :/ - grids.into_iter().enumerate().map(|(grid_id, grid)| { - ( - grid_id, - grid.stores - .assemblers_0_1 - .iter_mut() - .flat_map(|store| iter::once(store.get_outputs_mut(0))) - // TODO: Chain the other storages here - .chain( - grid.lab_stores - .sciences - .iter_mut() - .map(std::vec::Vec::as_mut_slice), - ), - ) - }) -} - impl PowerGrid { #[must_use] pub fn new( @@ -150,18 +197,30 @@ impl PowerGrid PowerGrid PowerGrid, self_grid: PowerGridIdentifier, @@ -237,25 +309,87 @@ impl PowerGrid { + let old = occupied_entry.get_mut(); + old.0 += affected.1.0; + old.1 += affected.1.1; + old.2 += affected.1.2; + }, + std::collections::hash_map::Entry::Vacant(vacant_entry) => { + vacant_entry.insert(affected.1); + }, + } + } + + for (((self_charge, other_charge), self_count), other_count) in self + .main_accumulator_charge + .iter_mut() + .zip(other.main_accumulator_charge) + .zip(self.main_accumulator_count.iter().copied()) + .zip(other.main_accumulator_count.iter().copied()) + { + if self_count + other_count == 0 { + assert_eq!(*self_charge, Joule(0)); + assert_eq!(other_charge, Joule(0)); + } else { + *self_charge = (*self_charge * self_count + other_charge * other_count) + / (self_count / other_count); + } + } + + for (self_count, other_count) in self + .num_solar_panels_of_type + .iter_mut() + .zip(other.num_solar_panels_of_type) + { + *self_count += other_count; + } + + for (self_count, other_count) in self + .main_accumulator_count + .iter_mut() + .zip(other.main_accumulator_count) + { + *self_count += other_count; + } + + self.potential_beacon_affected_powergrids + .extend(other.potential_beacon_affected_powergrids); + let ret = Self { stores: new_stores, lab_stores: new_labs, grid_graph: new_grid_graph, // TODO: steam_power_producers: self.steam_power_producers, - num_solar_panels: self.num_solar_panels + other.num_solar_panels, - main_accumulator_count: self.main_accumulator_count + other.main_accumulator_count, - main_accumulator_charge: if self.main_accumulator_count + other.main_accumulator_count - > 0 - { - (self.main_accumulator_charge * self.main_accumulator_count - + other.main_accumulator_charge * other.main_accumulator_count) - / (self.main_accumulator_count + other.main_accumulator_count) - } else { - assert_eq!(Joule(0), self.main_accumulator_charge); - assert_eq!(Joule(0), other.main_accumulator_charge); - Joule(0) - }, + num_solar_panels_of_type: self.num_solar_panels_of_type, + main_accumulator_count: self.main_accumulator_count, + main_accumulator_charge: self.main_accumulator_charge, use_burnable_fuel_to_charge_accumulators: match ( self.use_burnable_fuel_to_charge_accumulators, other.use_burnable_fuel_to_charge_accumulators, @@ -266,12 +400,18 @@ impl PowerGrid= other.last_power_consumption { @@ -280,14 +420,21 @@ impl PowerGrid= other.last_power_consumption { - self.power_history + self.power_mult_history } else { - other.power_history + other.power_mult_history } }, is_placeholder: false, + + num_assemblers_of_type: self.num_assemblers_of_type, + num_labs_of_type: self.num_labs_of_type, + num_beacons_of_type: self.num_beacons_of_type, + + potential_beacon_affected_powergrids: self.potential_beacon_affected_powergrids, + beacon_affected_entities: self.beacon_affected_entities, }; (ret, assembler_updates.into_iter().chain(lab_updates)) @@ -310,8 +457,7 @@ impl PowerGrid WeakIndex { assert!(!self.is_placeholder); - // FIXME: Respect the solar panel type - self.num_solar_panels += 1; + self.num_solar_panels_of_type[usize::from(ty)] += 1; self.grid_graph.add_weak_element( pole_connection, @@ -327,36 +473,38 @@ impl PowerGrid], pole_connection: Position, data_store: &DataStore, - ) -> (WeakIndex, u16) { + ) -> (WeakIndex, u32) { assert!(!self.is_placeholder); - let index = self.lab_stores.add_lab(ty, lab_position, data_store); + self.num_labs_of_type[usize::from(ty)] += 1; + + let index = self + .lab_stores + .add_lab(ty, lab_position, modules, data_store); - // FIXME: Respect the lab type let weak_idx = self.grid_graph.add_weak_element( pole_connection, (lab_position, PowerGridEntity::Lab { ty, index }), ); - ( - weak_idx, - index - .try_into() - .expect("More than u16::MAX labs in single grid"), - ) + (weak_idx, index) } pub fn remove_lab( @@ -367,13 +515,15 @@ impl PowerGrid Vec<(Item, ITEMCOUNTTYPE)> { assert!(!self.is_placeholder); - let (_lab_pos, PowerGridEntity::Lab { ty: _ty, index }) = self + let (_lab_pos, PowerGridEntity::Lab { ty, index }) = self .grid_graph .remove_weak_element(pole_connection, weak_idx) else { unreachable!() }; + self.num_labs_of_type[usize::from(ty)] -= 1; + let residual_items = self.lab_stores.remove_lab(index); data_store @@ -399,28 +549,76 @@ impl PowerGrid( &mut self, pole_pos: Position, - data_store: &DataStore, + data_store: &'a DataStore, ) -> ( impl IntoIterator< Item = ( Self, - impl IntoIterator)>, + impl IntoIterator< + Item = ( + Position, + PowerGridEntity, + PowerGridEntity, + ), + > + use, ), - >, + > + use, bool, // This tells the storage to delete us - impl IntoIterator, + impl IntoIterator + use, + impl IntoIterator, (i16, i16, i16))> + + use<'a, ItemIdxType, RecipeIdxType>, ) { let ((), no_longer_connected_entities, new_electric_networks) = self.grid_graph.remove_node(pole_pos); - let no_longer_connected_entities: Vec<_> = - no_longer_connected_entities.into_iter().collect(); + let (no_longer_connected_entities_pos, no_longer_connected_entities): (Vec<_>, Vec<_>) = + no_longer_connected_entities.into_iter().unzip(); // This is needed to make sure both paths have the same type (since closures have different types even if identical) - let closure = |v: (Position, PowerGridEntity)| v.0; + let beacon_mod_closure = |v: PowerGridEntity| match &v { + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + } => { + let effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let effect = ( + 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, + ); + + if effect == (0, 0, 0) { + // No effect, do not emit any updates + vec![] + } else { + affected_entities + .into_iter() + .map(|e| (*e, (-effect.0, -effect.1, -effect.2))) + .collect() + } + }, + _ => vec![], + }; if new_electric_networks.is_none() { // We no longer exist @@ -434,7 +632,10 @@ impl PowerGrid PowerGrid = new_electric_networks .into_iter() .map(|(network, pole_position_in_new_network)| { - dbg!(&network); let mut new_network: PowerGrid = Self::new_from_graph(network, data_store); @@ -456,73 +656,132 @@ impl PowerGrid, + connected_entity: &PowerGridEntity, data_store: &DataStore, ) { match connected_entity { - PowerGridEntity::Assembler { recipe, index } => { - self.remove_assembler( + PowerGridEntity::Assembler { ty, recipe, index } => { + self.remove_assembler_raw( AssemblerID { - recipe, + recipe: *recipe, grid: 0, // Does not matter - assembler_index: index, + assembler_index: *index, }, data_store, ); }, PowerGridEntity::Lab { index, ty: _ty } => { - let residual_items = self.lab_stores.remove_lab(index); + let residual_items = self.lab_stores.remove_lab(*index); }, PowerGridEntity::LazyPowerProducer { item, index } => { todo!("Remove LazyPowerProducer (Steam Engine)") }, PowerGridEntity::SolarPanel { ty } => { - // FIXME: Respect ty - self.num_solar_panels -= 1; + self.num_solar_panels_of_type[usize::from(*ty)] -= 1; }, PowerGridEntity::Accumulator { ty } => { - // FIXME: Respect ty - self.main_accumulator_count -= 1; + self.main_accumulator_count[usize::from(*ty)] -= 1; + }, + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + } => { + let raw_effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let raw_effect = ( + raw_effect.0 * data_store.beacon_info[usize::from(*ty)].effectiveness.0 as i16 + / data_store.beacon_info[usize::from(*ty)].effectiveness.1 as i16, + raw_effect.1 * data_store.beacon_info[usize::from(*ty)].effectiveness.0 as i16 + / data_store.beacon_info[usize::from(*ty)].effectiveness.1 as i16, + raw_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 effect = if self.last_power_mult >= MIN_BEACON_POWER_MULT { + raw_effect + } else { + (0, 0, raw_effect.2) + }; + + if effect.0 > 0 || effect.1 > 0 || effect.2 > 0 { + for effected_entity in affected_entities { + let old = self.beacon_affected_entities[effected_entity]; + + *self.beacon_affected_entities.get_mut(effected_entity).expect("Beacon affected entities list did not include a beacons affected entity") = (old.0 - effect.0, old.1 - effect.1, old.2 - effect.2); + + // The effect on the other grids is already handled in remove_pole + } + } + + self.num_beacons_of_type[usize::from(*ty)] -= 1; }, } } // The caller is responsible that the connected Entity is removed from self's graph + #[profiling::function] fn move_connected_entities<'a, 'b, 'c>( &'a mut self, other: &'b mut Self, data_store: &'c DataStore, - ) -> impl IntoIterator)> - + use<'a, 'b, 'c, ItemIdxType, RecipeIdxType> { + ) -> impl Iterator< + Item = ( + Position, + PowerGridEntity, + PowerGridEntity, + ), + > + use<'a, 'b, 'c, ItemIdxType, RecipeIdxType> { other .grid_graph .weak_components_mut() .into_iter() - .map(|connected_entity| match &mut dbg!(connected_entity.1) { - PowerGridEntity::Assembler { recipe, index } => { + .map(|connected_entity| match &mut connected_entity.1 { + PowerGridEntity::Assembler { ty, recipe, index } => { let new_idx = self.move_assembler(&mut other.stores, *recipe, *index, data_store); + self.num_assemblers_of_type[usize::from(*ty)] -= 1; + other.num_assemblers_of_type[usize::from(*ty)] += 1; + let ret = ( connected_entity.0, PowerGridEntity::Assembler { + ty: *ty, + recipe: *recipe, + index: (*index).try_into().unwrap(), + }, + PowerGridEntity::Assembler { + ty: *ty, recipe: *recipe, index: new_idx.try_into().unwrap(), }, @@ -537,6 +796,10 @@ impl PowerGrid PowerGrid { // FIXME: Respect ty - self.num_solar_panels -= 1; - other.num_solar_panels += 1; + self.num_solar_panels_of_type[usize::from(*ty)] -= 1; + other.num_solar_panels_of_type[usize::from(*ty)] += 1; - *connected_entity + ( + connected_entity.0, + PowerGridEntity::SolarPanel { ty: *ty }, + PowerGridEntity::SolarPanel { ty: *ty }, + ) }, PowerGridEntity::Accumulator { ty } => { // FIXME: Respect ty - self.main_accumulator_count -= 1; - other.main_accumulator_count += 1; + self.main_accumulator_count[usize::from(*ty)] -= 1; + other.main_accumulator_count[usize::from(*ty)] += 1; + + ( + connected_entity.0, + PowerGridEntity::Accumulator { ty: *ty }, + PowerGridEntity::Accumulator { ty: *ty }, + ) + }, + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + } => { + self.num_beacons_of_type[usize::from(*ty)] -= 1; + other.num_beacons_of_type[usize::from(*ty)] += 1; + + let raw_effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let raw_effect = ( + raw_effect.0 + * data_store.beacon_info[usize::from(*ty)].effectiveness.0 as i16 + / data_store.beacon_info[usize::from(*ty)].effectiveness.1 as i16, + raw_effect.1 + * data_store.beacon_info[usize::from(*ty)].effectiveness.0 as i16 + / data_store.beacon_info[usize::from(*ty)].effectiveness.1 as i16, + raw_effect.2 + * data_store.beacon_info[usize::from(*ty)].effectiveness.0 as i16 + / data_store.beacon_info[usize::from(*ty)].effectiveness.1 as i16, + ); + + for affected_entity in affected_entities { + let entry = self + .beacon_affected_entities + .get_mut(&affected_entity) + .unwrap(); + + entry.0 -= raw_effect.0; + entry.1 -= raw_effect.1; + entry.2 -= raw_effect.2; + + let entry = other + .beacon_affected_entities + .entry(*affected_entity).or_insert((0, 0, 0)); + other.potential_beacon_affected_powergrids.insert(affected_entity.get_power_grid()); + + entry.0 += raw_effect.0; + entry.1 += raw_effect.1; + entry.2 += raw_effect.2; + } - *connected_entity + assert_eq!(self.last_power_mult, other.last_power_mult, "If the destination power grid has a different power mult, then we would have to change the modifiers on the assemblers"); + + // FIXME: Cloning here sucks + (connected_entity.0, connected_entity.1.clone(), connected_entity.1.clone()) }, }) } @@ -612,20 +941,89 @@ impl PowerGrid self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + _ => unreachable!(), }; } - pub fn remove_module_to_assembler( + pub fn remove_module_from_assembler( &mut self, id: AssemblerID, module_kind: usize, data_store: &DataStore, ) { // TODO: This will crash if someone uses i8::MIN but oh well - let speed_mod = -data_store.module_info[module_kind].speed_mod; - let prod_mod = -data_store.module_info[module_kind].prod_mod; - let power_mod = -data_store.module_info[module_kind].power_mod; + let speed_mod = data_store.module_info[module_kind] + .speed_mod + .checked_neg() + .expect("Negation failed"); + let prod_mod = data_store.module_info[module_kind] + .prod_mod + .checked_neg() + .expect("Negation failed"); + let power_mod = data_store.module_info[module_kind] + .power_mod + .checked_neg() + .expect("Negation failed"); assert!(!self.is_placeholder); @@ -662,91 +1060,697 @@ impl PowerGrid self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + _ => unreachable!(), }; } - pub fn add_assembler( + pub fn add_module_to_lab( &mut self, - ty: u8, - grid_id: PowerGridIdentifier, - recipe: Recipe, - modules: &[Option], - connected_power_pole_position: Position, - assembler_position: Position, + index: u32, + module_kind: usize, data_store: &DataStore, - ) -> AssemblerID { - assert!(!self.is_placeholder); - - let new_idx = match ( + ) { + let speed_mod = data_store.module_info[module_kind].speed_mod; + let prod_mod = data_store.module_info[module_kind].prod_mod; + let power_mod = data_store.module_info[module_kind].power_mod; + + assert!(!self.is_placeholder); + + self.lab_stores.modify_modifiers( + index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ); + } + + pub fn remove_module_from_lab( + &mut self, + index: u32, + module_kind: usize, + data_store: &DataStore, + ) { + // TODO: This will crash if someone uses i8::MIN but oh well + let speed_mod = data_store.module_info[module_kind] + .speed_mod + .checked_neg() + .expect("Negation failed"); + let prod_mod = data_store.module_info[module_kind] + .prod_mod + .checked_neg() + .expect("Negation failed"); + let power_mod = data_store.module_info[module_kind] + .power_mod + .checked_neg() + .expect("Negation failed"); + + assert!(!self.is_placeholder); + + self.lab_stores.modify_modifiers( + index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ); + } + + pub(super) fn change_assembler_module_modifiers( + &mut self, + id: AssemblerID, + modifiers: (i16, i16, i16), + data_store: &DataStore, + ) { + let speed_mod = modifiers.0; + let prod_mod = modifiers.1; + let power_mod = modifiers.2; + + assert!(!self.is_placeholder); + + match ( + data_store.recipe_num_ing_lookup[id.recipe.id.into()], + data_store.recipe_num_out_lookup[id.recipe.id.into()], + ) { + (0, 1) => self.stores.assemblers_0_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (1, 1) => self.stores.assemblers_1_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + (2, 1) => self.stores.assemblers_2_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[id.recipe.id.into()]] + .modify_modifiers( + id.assembler_index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ), + + _ => unreachable!(), + }; + } + + pub fn change_lab_module_modifiers( + &mut self, + index: u32, + modifiers: (i16, i16, i16), + data_store: &DataStore, + ) { + let speed_mod = modifiers.0; + let prod_mod = modifiers.1; + let power_mod = modifiers.2; + + assert!(!self.is_placeholder); + + self.lab_stores.modify_modifiers( + index, + speed_mod.into(), + prod_mod.into(), + power_mod.into(), + data_store, + ); + } + + pub fn add_assembler( + &mut self, + ty: u8, + grid_id: PowerGridIdentifier, + recipe: Recipe, + modules: &[Option], + connected_power_pole_position: Position, + assembler_position: Position, + data_store: &DataStore, + ) -> (AssemblerID, WeakIndex) { + assert!(!self.is_placeholder); + + self.num_assemblers_of_type[usize::from(ty)] += 1; + + let new_idx = match ( data_store.recipe_num_ing_lookup[recipe.id.into()], data_store.recipe_num_out_lookup[recipe.id.into()], ) { (0, 1) => self.stores.assemblers_0_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] - .add_assembler(ty, modules, assembler_position, data_store), + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing0, + data_store, + ), (1, 1) => self.stores.assemblers_1_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] - .add_assembler(ty, modules, assembler_position, data_store), + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing1, + data_store, + ), (2, 1) => self.stores.assemblers_2_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] - .add_assembler(ty, modules, assembler_position, data_store), + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + data_store, + ), + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + data_store, + ), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + data_store, + ), + + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing3, + data_store, + ), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing4, + data_store, + ), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing5, + data_store, + ), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .add_assembler( + ty, + modules, + assembler_position, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing6, + data_store, + ), _ => unreachable!(), }; - self.grid_graph.add_weak_element( + let weak_index = self.grid_graph.add_weak_element( connected_power_pole_position, ( assembler_position, PowerGridEntity::Assembler { + ty, recipe, index: new_idx.try_into().unwrap(), }, ), ); - AssemblerID { - recipe, - grid: grid_id, - assembler_index: new_idx.try_into().expect("More than u16::MAX assemblers"), - } + ( + AssemblerID { + recipe, + grid: grid_id, + assembler_index: new_idx.try_into().expect("More than u16::MAX assemblers"), + }, + weak_index, + ) + } + + #[must_use] + pub fn change_assembler_recipe( + &mut self, + old_assembler_id: AssemblerID, + pole_pos: Position, + weak_idx: WeakIndex, + new_recipe: Recipe, + data_store: &DataStore, + ) -> (AssemblerRemovalInfo, AssemblerID) { + let ( + _max_insert, + ings, + outputs, + _timer, + _prod_timer, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + ) = match ( + data_store.recipe_num_ing_lookup[old_assembler_id.recipe.id.into()], + data_store.recipe_num_out_lookup[old_assembler_id.recipe.id.into()], + ) { + (0, 1) => self.stores.assemblers_0_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (1, 1) => self.stores.assemblers_1_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (2, 1) => self.stores.assemblers_2_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[old_assembler_id.recipe.id.into()]] + .remove_assembler_data(old_assembler_id.assembler_index), + + _ => unreachable!(), + }; + + let new_id = match ( + data_store.recipe_num_ing_lookup[new_recipe.id.into()], + data_store.recipe_num_out_lookup[new_recipe.id.into()], + ) { + (0, 1) => self.stores.assemblers_0_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 0], + [0; 0], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (1, 1) => self.stores.assemblers_1_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 1], + [0; 1], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (2, 1) => self.stores.assemblers_2_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 2], + [0; 2], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 2], + [0; 2], + [0; 2], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 2], + [0; 2], + [0; 3], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 3], + [0; 3], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 4], + [0; 4], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 5], + [0; 5], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[new_recipe.id.into()]] + .add_assembler_with_data( + [10; 6], + [0; 6], + [0; 1], + 0, + 0, + base_power, + raw_power_mod, + raw_prod_mod, + base_speed, + raw_speed_mod, + ty, + pos, + data_store, + ), + + _ => unreachable!(), + }; + + let ( + _pos, + PowerGridEntity::Assembler { + ty: _, + recipe, + index, + }, + ) = self.grid_graph.modify_weak_component(pole_pos, weak_idx) + else { + unreachable!() + }; + + *recipe = new_recipe; + *index = new_id.try_into().unwrap(); + + ( + AssemblerRemovalInfo { ings, outputs }, + AssemblerID { + recipe: new_recipe, + grid: old_assembler_id.grid, + assembler_index: new_id.try_into().unwrap(), + }, + ) } pub fn remove_assembler( + &mut self, + assembler_id: AssemblerID, + pole_pos: Position, + weak_idx: WeakIndex, + data_store: &DataStore, + ) -> AssemblerRemovalInfo { + let removal_info = self.remove_assembler_raw(assembler_id, data_store); + + let (_pos, PowerGridEntity::Assembler { ty, .. }) = + self.grid_graph.remove_weak_element(pole_pos, weak_idx) + else { + unreachable!() + }; + + self.num_assemblers_of_type[usize::from(ty)] -= 1; + + removal_info + } + + fn remove_assembler_raw( &mut self, assembler_id: AssemblerID, data_store: &DataStore, ) -> AssemblerRemovalInfo { - match ( + let removal_info = match ( data_store.recipe_num_ing_lookup[assembler_id.recipe.id.into()], data_store.recipe_num_out_lookup[assembler_id.recipe.id.into()], ) { (0, 1) => self.stores.assemblers_0_1 [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] - .remove_assembler(assembler_id.assembler_index as usize), + .remove_assembler(assembler_id.assembler_index), (1, 1) => self.stores.assemblers_1_1 [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] - .remove_assembler(assembler_id.assembler_index as usize), + .remove_assembler(assembler_id.assembler_index), (2, 1) => self.stores.assemblers_2_1 [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] - .remove_assembler(assembler_id.assembler_index as usize), + .remove_assembler(assembler_id.assembler_index), + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[assembler_id.recipe.id.into()]] + .remove_assembler(assembler_id.assembler_index), _ => unreachable!(), - } + }; - // FIXME: Remove Weak Elements from the graph + removal_info } fn move_assembler( &mut self, other_stores: &mut FullAssemblerStore, recipe: Recipe, - index: u16, + index: u32, data_store: &DataStore, - ) -> usize { + ) -> u32 { match ( data_store.recipe_num_ing_lookup[recipe.id.into()], data_store.recipe_num_out_lookup[recipe.id.into()], @@ -754,7 +1758,7 @@ impl PowerGrid self.stores.assemblers_0_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] .move_assembler( - index.into(), + index, &mut other_stores.assemblers_0_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], data_store, @@ -762,7 +1766,7 @@ impl PowerGrid self.stores.assemblers_1_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] .move_assembler( - index.into(), + index, &mut other_stores.assemblers_1_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], data_store, @@ -770,11 +1774,59 @@ impl PowerGrid self.stores.assemblers_2_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] .move_assembler( - index.into(), + index, &mut other_stores.assemblers_2_1 [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], data_store, ), + (2, 2) => self.stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_2_2 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), + (2, 3) => self.stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_2_3 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), + (3, 1) => self.stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_3_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), + (4, 1) => self.stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_4_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), + (5, 1) => self.stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_5_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), + (6, 1) => self.stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]] + .move_assembler( + index, + &mut other_stores.assemblers_6_1 + [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]], + data_store, + ), _ => unreachable!(), } @@ -783,44 +1835,66 @@ impl PowerGrid, - ) -> u16 { + ) -> u32 { self.lab_stores.move_lab(index, other_stores) } - // TODO: Currently impossible because of Ing Generics - // fn do_for_assembler( - // &mut self, - // recipe: Recipe, - // data_store: &DataStore, - // f: impl Fn(&mut MultiAssemblerStore) -> T, - // ) -> T { - // match ( - // data_store.recipe_num_ing_lookup[recipe.id.into()], - // data_store.recipe_num_out_lookup[recipe.id.into()], - // ) { - // (0, 1) => f(&mut self.stores.assemblers_0_1 - // [data_store.recipe_to_ing_out_combo_idx[recipe.id.into()]]), - - // _ => unreachable!(), - // } - // } - // FIXME: This is a huge, high branching function. // Make it simpler and more readable, and reduce repetition + #[profiling::function] fn extract_power( &mut self, goal_amount: Joule, - solar_panel_production_amount: Watt, + solar_panel_production_amounts: &[Watt], data_store: &DataStore, - ) -> u8 { - let solar_power = (solar_panel_production_amount * self.num_solar_panels).joules_per_tick(); + ) -> Joule { + assert_eq!( + solar_panel_production_amounts.len(), + self.num_solar_panels_of_type.len() + ); + let solar_power = self + .num_solar_panels_of_type + .iter() + .zip(solar_panel_production_amounts) + .map(|(a, b)| *b * *a) + .sum::() + .joules_per_tick(); + + self.last_ticks_max_power_production = solar_power.watt_from_tick() + self.max_lazy_power; + + if goal_amount == Joule(0) { + return Joule(0); + } + + let max_charge_amount_per: Box<[Joule]> = self + .main_accumulator_count + .iter() + .zip(self.main_accumulator_charge.iter().copied()) + .zip( + data_store + .accumulator_info + .iter() + .map(|info| info.max_charge_rate), + ) + .zip( + data_store + .accumulator_info + .iter() + .map(|info| info.max_charge), + ) + .map( + |(((count, charge), max_charge_rate), max_charge): ( + ((&u64, Joule), Watt), + Joule, + )| { + min(max_charge_rate.joules_per_tick(), max_charge - charge) * *count + }, + ) + .collect(); + let max_charge_amount = max_charge_amount_per.iter().copied().sum(); - let max_charge_amount: Joule = max( - MAX_ACCUMULATOR_CHARGE_RATE.joules_per_tick(), - MAX_ACCUMULATOR_CHARGE - self.main_accumulator_charge, - ) * self.main_accumulator_count; // + self // .unique_accumulators // .iter() @@ -838,7 +1912,7 @@ impl PowerGrid= max_charge_amount { // We already have enough power, without using burnables. - self.charge_by(max_charge_amount); + self.charge_by(max_charge_amount, &max_charge_amount_per); } else if self .use_burnable_fuel_to_charge_accumulators .unwrap_or_default() @@ -853,12 +1927,12 @@ impl PowerGrid PowerGrid return MAX_POWER_MULT, + std::cmp::Ordering::Equal => return goal_amount, std::cmp::Ordering::Greater => { // Use remaining power for charging let charge_amount = goal_amount - actually_extracted; // This will never underflow - self.charge_by(charge_amount); + self.charge_by(charge_amount, &max_charge_amount_per); - return MAX_POWER_MULT; + return goal_amount; }, } } @@ -920,20 +1990,16 @@ impl PowerGrid MAX_POWER_MULT, + std::cmp::Ordering::Equal => goal_amount, std::cmp::Ordering::Greater => { unreachable!( "We extracted more power than needed from burners, while disallowing charging?!" @@ -943,15 +2009,30 @@ impl PowerGrid= amount + max_charge_amount_per.iter().copied().sum::() >= amount, + "Tried to charge the accumulators more than max amount" ); - self.main_accumulator_charge = min( - self.main_accumulator_charge + amount, - MAX_ACCUMULATOR_CHARGE, - ); + debug_assert!(max_charge_amount_per.is_sorted()); + + // Since we sort, we can simply go through the list sequentially + for ((charge, max_rate), num_left) in self + .main_accumulator_charge + .iter_mut() + .zip(max_charge_amount_per) + .zip((1..=max_charge_amount_per.len()).rev()) + .sorted_by_key(|((_charge, max_rate), _num_left)| *max_rate) + { + // FIXME: This is integer division, so we lose some power here. + let charge_here: Joule = min(*max_rate, amount / u64::try_from(num_left).unwrap()); + *charge = *charge + charge_here; + amount = amount - charge_here; + } + + // Due to the integer div, this assert could fail + assert!(amount == Joule(0)); // This is an algorithm for (kindof) handling accumulators with different charge from the "main pack" // while amount > MegaJoule(0) { @@ -998,152 +2079,442 @@ impl PowerGrid Joule { - // only extract at most MAX_ACCUMULATOR_DISCHARGE_RATE - let to_extract = min( - power_needed, - (MAX_ACCUMULATOR_DISCHARGE_RATE * self.main_accumulator_count).joules_per_tick(), - ); - assert!( - (MAX_ACCUMULATOR_DISCHARGE_RATE * self.main_accumulator_count).joules_per_tick() - >= to_extract - ); + fn extract_from_accumulators( + &mut self, + mut power_needed: Joule, + data_store: &DataStore, + ) -> Joule { + let originally_needed = power_needed; + let discharge_amount_per: Box<[Joule]> = self + .main_accumulator_count + .iter() + .zip(self.main_accumulator_charge.iter().copied()) + .zip( + data_store + .accumulator_info + .iter() + .map(|info| info.max_discharge_rate), + ) + .map( + |((count, charge), max_discharge_rate): ((&u64, Joule), Watt)| { + min(max_discharge_rate.joules_per_tick(), charge) * *count + }, + ) + .collect(); - let old = self.main_accumulator_charge; - self.main_accumulator_charge = - Joule(self.main_accumulator_charge.0.saturating_sub(to_extract.0)); + // Since we sort, we can simply go through the list sequentially + for ((charge, max_rate), num_left) in self + .main_accumulator_charge + .iter_mut() + .zip(discharge_amount_per) + .zip((1..=data_store.accumulator_info.len()).rev()) + // FIXME: This seems incorrect? + .sorted_by_key(|((charge, max_rate), num_left)| *max_rate) + { + // FIXME: This is integer division, so we lose some power here. + let discharge_from_here: Joule = + min(max_rate, power_needed / u64::try_from(num_left).unwrap()); + assert!(*charge >= discharge_from_here); + *charge = *charge - discharge_from_here; + power_needed = power_needed - discharge_from_here; + } - min(old, to_extract) + originally_needed - power_needed } + // fn update_multi_assembler_stores(last_power_mult: u8, stores: &mut Box<[MultiAssemblerStore]>, data_store: &DataStore, active_recipes: &[bool]) { + // profiling::scope!(format!("assemblers_{}_{} updates", INGS, OUT)); + // let v = match (INGS, OUT) { + // (0, 1) => { + // stores + // .par_iter_mut().map(|s| { + // profiling::scope!("Assembler Update", format!("Recipe: {}", data_store.recipe_display_names[usize_from(s.recipe.id)]).as_str()); + // if active_recipes[s.recipe.into_usize()] { + // s.update_branchless::( + // last_power_mult, + // &data_store.recipe_index_lookups, + // &data_store.recipe_ings.ing0, + // &data_store.recipe_outputs.out1, + // &data_store.recipe_timers, + // ) + // } else { + // (Watt(0), 0, 0) + // } + // }) + // .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, + // }) + // }) + // .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) + // }) + // } + + // _ => unreachable!() + // }; + // todo!() + // } + + fn reduce_update( + iter: impl ParallelIterator, + ) -> (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, + }, + ) + }) + .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] pub fn update( &mut self, - solar_panel_production_amount: Watt, + solar_panel_production_amounts: &[Watt], tech_state: &TechState, data_store: &DataStore, - ) -> (ResearchProgress, RecipeTickInfo) { + ) -> ( + ResearchProgress, + RecipeTickInfo, + u64, + Vec<(BeaconAffectedEntity, (i16, i16, i16))>, + ) { + if self.is_placeholder { + return (0, RecipeTickInfo::new(data_store), 0, vec![]); + } + + let active_recipes = tech_state.get_active_recipes(); + + let (a, b, c) = join!(|| {}, || {}, || {}); + let ( - ( - (power_used_0_1, infos_0_1), - ((power_used_1_1, infos_1_1), (power_used_2_1, infos_2_1)), - ), + (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), (lab_power_used, times_labs_used_science, tech_progress), - ) = rayon::join( + ) = join!( || { - rayon::join( - || { - self.stores - .assemblers_0_1 - .par_iter_mut() - .map(|s| { - s.update_branchless::( - self.last_power_mult, - &data_store.recipe_index_lookups, - &data_store.recipe_ings.ing0, - &data_store.recipe_outputs.out1, - &data_store.recipe_timers, - ) - }) - .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, - }) - }) - .fold_with((Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.push(info); - - (acc_power + rhs_power, infos) - }).reduce(|| (Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.extend_from_slice(&info); - - (acc_power + rhs_power, infos) - }) - }, - || { - rayon::join( - || { - self.stores - .assemblers_1_1 - .par_iter_mut() - .map(|s| { - s.update_branchless::( - self.last_power_mult, - &data_store.recipe_index_lookups, - &data_store.recipe_ings.ing1, - &data_store.recipe_outputs.out1, - &data_store.recipe_timers, - ) - }) - .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, - }) - }) - .fold_with((Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.push(info); - - (acc_power + rhs_power, infos) - }).reduce(|| (Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.extend_from_slice(&info); - - (acc_power + rhs_power, infos) - }) - }, - || { - self.stores - .assemblers_2_1 - .par_iter_mut() - .map(|s| { - s.update_branchless::( - self.last_power_mult, - &data_store.recipe_index_lookups, - &data_store.recipe_ings.ing2, - &data_store.recipe_outputs.out1, - &data_store.recipe_timers, - ) - }) - .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, - }) - }) - .fold_with((Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.push(info); - - (acc_power + rhs_power, infos) - }).reduce(|| (Joule(0), vec![]), |(acc_power, mut infos), (rhs_power, info)| { - infos.extend_from_slice(&info); - - (acc_power + rhs_power, infos) - }) - }, + profiling::scope!("assemblers_0_1 updates"); + Self::reduce_update(self.stores.assemblers_0_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] ) - }, - ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing0, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_1_1 updates"); + Self::reduce_update(self.stores.assemblers_1_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing1, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_2_1 updates"); + Self::reduce_update(self.stores.assemblers_2_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) }, || { + profiling::scope!("assemblers_2_2 updates"); + Self::reduce_update(self.stores.assemblers_2_2.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + &data_store.recipe_outputs.out2, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_2_3 updates"); + Self::reduce_update(self.stores.assemblers_2_3.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing2, + &data_store.recipe_outputs.out3, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_3_1 updates"); + Self::reduce_update(self.stores.assemblers_3_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing3, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_4_1 updates"); + Self::reduce_update(self.stores.assemblers_4_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing4, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_5_1 updates"); + Self::reduce_update(self.stores.assemblers_5_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing5, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("assemblers_6_1 updates"); + Self::reduce_update(self.stores.assemblers_6_1.par_iter_mut().map(|s| { + profiling::scope!( + "Assembler Update", + format!( + "Recipe: {}", + data_store.recipe_display_names[usize_from(s.recipe.id)] + ) + .as_str() + ); + if active_recipes[s.recipe.into_usize()] { + s.update_branchless::( + self.last_power_mult, + &data_store.recipe_index_lookups, + &data_store.recipe_ings.ing6, + &data_store.recipe_outputs.out1, + &data_store.recipe_timers, + ) + } else { + (Watt(0), 0, 0) + } + })) + }, + || { + profiling::scope!("Lab updates"); self.lab_stores.update( self.last_power_mult, - &tech_state.current_technology, - data_store, + tech_state + .current_technology + .as_ref() + .map(|tech| &*data_store.technology_costs[tech.id as usize].1), ) - }, + } ); - let power_used = power_used_0_1 + power_used_1_1 + power_used_2_1; + let assembler_power_used = power_used_0_1 + + power_used_1_1 + + power_used_2_1 + + power_used_3_1 + + power_used_4_1 + + power_used_5_1 + + power_used_6_1; + + let beacon_power_used: Watt = self + .num_beacons_of_type + .iter() + .zip(&data_store.beacon_info) + .map(|(count, info)| { + info.power_consumption * u64::try_from(*count).expect("More than u64::MAX Beacons") + }) + .sum(); + + let power_used = assembler_power_used.joules_per_tick() + + lab_power_used + + beacon_power_used.joules_per_tick(); self.last_power_consumption = power_used.watt_from_tick(); - let next_power_mult = self.extract_power( - power_used + lab_power_used, - solar_panel_production_amount, - data_store, - ); + let power_extracted = + self.extract_power(power_used, solar_panel_production_amounts, data_store); + + assert!(power_extracted <= power_used); + let next_power_mult = if power_used == Joule(0) { + 0 + } else { + ((power_extracted.0 * MAX_POWER_MULT as u64) / power_used.0) as u8 + }; + assert!(next_power_mult <= MAX_POWER_MULT); + + self.last_power_consumption = power_used.watt_from_tick(); + self.last_produced_power = power_extracted.watt_from_tick(); + + // TODO: Factorio just scales the beacon effect linearly + let beacon_updates: Vec<(BeaconAffectedEntity<_>, (_, _, _))> = 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() + } else { + vec![] + }; - self.power_history + self.power_mult_history .append_single_set_of_samples(next_power_mult.into()); self.last_power_mult = next_power_mult; @@ -1152,9 +2523,192 @@ impl PowerGrid]>, + affected_entities: Vec>, + data_store: &DataStore, + ) -> WeakIndex { + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + self.potential_beacon_affected_powergrids + .contains(&affected_grid) + }); + assert!(affected_grids_and_potential_match); + } + + let effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let effect = ( + 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, + ); + + for affected_entity in &affected_entities { + self.potential_beacon_affected_powergrids + .insert(affected_entity.get_power_grid()); + + let entry = self + .beacon_affected_entities + .entry(*affected_entity) + .or_insert((0, 0, 0)); + + entry.0 += effect.0; + entry.1 += effect.1; + entry.2 += effect.2; + } + + let idx = self.grid_graph.add_weak_element( + pole_pos, + ( + beacon_pos, + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + }, + ), + ); + + self.num_beacons_of_type[usize::from(ty)] += 1; + + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + self.potential_beacon_affected_powergrids + .contains(&affected_grid) + }); + assert!(affected_grids_and_potential_match); + } + + idx + } + + pub fn remove_beacon( + &mut self, + pole_pos: Position, + weak_idx: WeakIndex, + data_store: &DataStore, + ) -> impl Iterator, (i16, i16, i16))> + + use { + let ( + _beacon_pos, + PowerGridEntity::Beacon { + ty, + modules, + affected_entities, + }, + ) = self.grid_graph.remove_weak_element(pole_pos, weak_idx) + else { + unreachable!(); + }; + + self.num_beacons_of_type[usize::from(ty)] -= 1; + + let effect: (i16, i16, i16) = modules + .iter() + .flatten() + .map(|module_ty| { + ( + data_store.module_info[*module_ty].speed_mod.into(), + data_store.module_info[*module_ty].prod_mod.into(), + data_store.module_info[*module_ty].power_mod.into(), + ) + }) + .reduce(|acc, v| (acc.0 + v.0, acc.1 + v.1, acc.2 + v.2)) + .unwrap_or((0, 0, 0)); + + let effect = ( + 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, + ); + + for affected_entity in affected_entities.iter() { + let stored_effect = self + .beacon_affected_entities + .get_mut(affected_entity) + .unwrap(); + + stored_effect.0 -= effect.0; + stored_effect.1 -= effect.1; + stored_effect.2 -= effect.2; + + if *stored_effect == (0, 0, 0) { + let Some((0, 0, 0)) = self.beacon_affected_entities.remove(affected_entity) else { + unreachable!(); + }; + } + } + + let now_removed_effect = if self.last_power_mult >= MIN_BEACON_POWER_MULT { + (-effect.0, -effect.1, -effect.2) + } else { + (-0, -0, -effect.2) + }; + + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + self.potential_beacon_affected_powergrids + .contains(&affected_grid) + }); + assert!(affected_grids_and_potential_match); + } + + affected_entities + .into_iter() + .map(move |entity| (entity, now_removed_effect)) } } @@ -1276,7 +2830,8 @@ impl MultiLazyPowerProducer { data_store: &DataStore, ) -> ( Self, - impl IntoIterator>, + impl IntoIterator> + + use, ) { (todo!(), []) } diff --git a/src/rendering/app_state.rs b/src/rendering/app_state.rs index 8758a18..457bc91 100644 --- a/src/rendering/app_state.rs +++ b/src/rendering/app_state.rs @@ -1,47 +1,76 @@ -use std::{borrow::Borrow, fs::File, marker::PhantomData, ops::ControlFlow}; - +use crate::belt::BeltTileId; +use crate::chest::ChestSize; +use crate::data::AllowedFluidDirection; +use crate::frontend::world::tile::UndergroundDir; +use crate::inserter::storage_storage_with_buckets::InserterIdentifier; +use crate::inserter::storage_storage_with_buckets::LargeInserterState; +use crate::inserter::storage_storage_with_buckets::{ + BucketedStorageStorageInserterStore, BucketedStorageStorageInserterStoreFrontend, InserterId, +}; +use crate::item::ITEMCOUNTTYPE; +use crate::liquid::FluidConnectionDir; +use crate::liquid::connection_logic::can_fluid_tanks_connect_to_single_connection; use crate::{ - belt::{ - belt::Belt, splitter::Splitter, BeltBeltInserterAdditionInfo, BeltBeltInserterInfo, - BeltStore, BeltTileId, MultiBeltStore, - }, + Input, LoadedGame, + belt::{BeltBeltInserterInfo, BeltStore}, blueprint::Blueprint, chest::{FullChestStore, MultiChestStore}, data::{DataStore, ItemRecipeDir}, frontend::{ action::{ - belt_placement::{handle_belt_placement, handle_splitter_placement}, - set_recipe::SetRecipeInfo, ActionType, + belt_placement::{ + handle_belt_placement, handle_splitter_placement, handle_underground_belt_placement, + }, + set_recipe::SetRecipeInfo, }, world::{ - self, + Position, tile::{ - AssemblerID, AssemblerInfo, AttachedInserter, BeltId, Dir, Entity, InserterInfo, - World, + AssemblerID, AssemblerInfo, AttachedInserter, Dir, Entity, InserterInfo, World, }, - Position, }, }, - inserter::{ - belt_belt_inserter::BeltBeltInserter, storage_storage_inserter::StorageStorageInserter, - StaticID, Storage, MOVETIME, + inserter::{FakeUnionStorage, MOVETIME, Storage, belt_belt_inserter::BeltBeltInserter}, + item::{IdxTrait, Item, Recipe, WeakIdxTrait, usize_from}, + liquid::connection_logic::can_fluid_tanks_connect, + network_graph::WeakIndex, + power::{PowerGridStorage, power_grid::PowerGridIdentifier}, + research::TechState, + statistics::{ + GenStatistics, Timeline, consumption::ConsumptionInfo, production::ProductionInfo, }, - item::{usize_from, IdxTrait, Item, Recipe, WeakIdxTrait}, - power::{power_grid::PowerGridIdentifier, PowerGridStorage, Watt}, - research::{ResearchProgress, TechState, Technology}, - statistics::{production::ProductionInfo, recipe::RecipeTickInfo, GenStatistics}, storage_list::{ - full_to_by_item, grid_size, num_recipes, sizes, storages_by_item, FullStorages, - SingleItemStorages, + SingleItemStorages, full_to_by_item, grid_size, num_recipes, sizes, storages_by_item, }, }; +use crate::{ + item::Indexable, + liquid::{CannotMixFluidsError, FluidSystemStore}, +}; use itertools::Itertools; -use log::{error, info, trace, warn}; +use log::{info, trace, warn}; +use petgraph::graph::NodeIndex; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; +use std::collections::{BTreeMap, HashMap}; +use std::iter; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::{ + borrow::Borrow, + fs::File, + ops::ControlFlow, + time::{Duration, Instant}, +}; use crate::frontend::action::place_tile::PositionInfo; +use std::ops::AddAssign; + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GameState { pub current_tick: u64, @@ -50,54 +79,319 @@ pub struct GameState { pub simulation_state: SimulationState, pub statistics: GenStatistics, + + pub update_times: Timeline, + #[serde(skip)] + last_update_time: Option, + + pub settings: GameSettings, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct GameSettings { + pub show_unresearched_recipes: bool, +} + +#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] +pub struct UpdateTime { + pub dur: Duration, +} + +impl<'a> AddAssign<&'a UpdateTime> for UpdateTime { + fn add_assign(&mut self, rhs: &'a UpdateTime) { + self.dur += rhs.dur; + } } impl GameState { + #[must_use] pub fn new(data_store: &DataStore) -> Self { Self { current_tick: 0, world: World::new(), simulation_state: SimulationState::new(data_store), statistics: GenStatistics::new(data_store), + update_times: Timeline::new(false, data_store), + last_update_time: None, + settings: GameSettings { + show_unresearched_recipes: true, + }, } } - pub fn new_with_production(data_store: &DataStore) -> Self { - let mut ret = Self { - current_tick: 0, - world: World::new(), - simulation_state: SimulationState::new(data_store), - statistics: GenStatistics::new(data_store), - }; + #[must_use] + pub fn new_with_production( + progress: Arc, + data_store: &DataStore, + ) -> Self { + let mut ret = GameState::new(data_store); let file = File::open("test_blueprints/red_sci.bp").unwrap(); - let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let bp = bp.get_reusable(data_store); + + puffin::set_scopes_on(false); + let y_range = (1590..30000).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 (1590..30000).step_by(7) { - for x_pos in (1590..3000).step_by(20) { - if rand::random::() < u16::MAX / 10 { + 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; + + if rand::random::() < u16::MAX / 100 { ret.update(data_store); } bp.apply(Position { x: x_pos, y: y_pos }, &mut ret, data_store); } } + puffin::set_scopes_on(true); ret } - pub fn new_with_bp(data_store: &DataStore, bp_path: &str) -> Self { - let mut ret = Self { - current_tick: 0, - world: World::new(), - simulation_state: SimulationState::new(data_store), - statistics: GenStatistics::new(data_store), - }; + #[must_use] + pub fn new_with_gigabase( + progress: Arc, + data_store: &DataStore, + ) -> Self { + let mut ret = GameState::new(data_store); - let file = File::open(bp_path).unwrap(); - let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let file = File::open("test_blueprints/murphy/megabase.bp").unwrap(); + let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let bp = bp.get_reusable(data_store); + + puffin::set_scopes_on(false); + let y_range = (1590..30000).step_by(6000); + let x_range = (1590..30000).step_by(6000); + + 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; + + if rand::random::() < u16::MAX / 100 { + ret.update(data_store); + } + + 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_beacon_production( + progress: Arc, + 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(data_store); + + 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(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) { + while rand::random::() < u16::MAX / 200 { + ret.update(data_store); + } + 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(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(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) { + // if rand::random::() < 1 { + // ret.update(data_store); + // } + + 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( + progress: Arc, + data_store: &DataStore, + ) -> Self { + let mut ret = GameState::new(data_store); + + let file = File::open("test_blueprints/lots_of_belts.bp").unwrap(); + let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let bp = bp.get_reusable(data_store); + + puffin::set_scopes_on(false); + for y_pos in (1600..60_000).step_by(3) { + ret.update(data_store); + bp.apply(Position { x: 1600, y: y_pos }, &mut ret, data_store); + } + puffin::set_scopes_on(true); + + ret + } + + #[must_use] + pub fn new_with_tons_of_solar( + progress: Arc, + data_store: &DataStore, + ) -> Self { + let mut ret = GameState::new(data_store); + + let file = File::open("test_blueprints/solar_farm.bp").unwrap(); + let bp: Blueprint = ron::de::from_reader(file).unwrap(); + let bp = bp.get_reusable(data_store); + + puffin::set_scopes_on(false); + for y_pos in (1600..30_000).step_by(18) { + for x_pos in (1600..30_000).step_by(18) { + bp.apply(Position { x: x_pos, y: y_pos }, &mut ret, data_store); + } + } + puffin::set_scopes_on(true); + + ret + } + + #[must_use] + pub fn new_eight_beacon_factory( + progress: Arc, + data_store: &DataStore, + ) -> Self { + let mut ret = GameState::new(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(); + let red = red.get_reusable(data_store); + + puffin::set_scopes_on(false); + for y_pos in (1600..=30_000).step_by(20) { + red.apply(Position { x: 1600, y: y_pos }, &mut ret, data_store); + } + puffin::set_scopes_on(true); + + ret + } - bp.apply(Position { x: 1590, y: 1590 }, &mut ret, data_store); + pub fn new_with_bp( + data_store: &DataStore, + bp_path: impl AsRef, + ) -> Self { + let mut ret = GameState::new(data_store); + + let file = File::open(bp_path).unwrap(); + let bp: Blueprint = ron::de::from_reader(file).unwrap(); + + // for x in (0..60).map(|p| p * 15) { + bp.apply( + Position { + // x: 1590 + x, + x: 2000, + y: 2000, + }, + &mut ret, + data_store, + ); + // } + // bp.apply(Position { x: 1600, y: 1590 }, &mut ret, data_store); + // bp.apply(Position { x: 1610, y: 1590 }, &mut ret, data_store); + // bp.apply(Position { x: 1620, y: 1590 }, &mut ret, data_store); + // bp.apply(Position { x: 1630, y: 1590 }, &mut ret, data_store); + + // bp.apply(Position { x: 1590, y: 1600 }, &mut ret, data_store); + // bp.apply(Position { x: 1600, y: 1600 }, &mut ret, data_store); + // bp.apply(Position { x: 1610, y: 1600 }, &mut ret, data_store); + // bp.apply(Position { x: 1620, y: 1600 }, &mut ret, data_store); + // bp.apply(Position { x: 1630, y: 1600 }, &mut ret, data_store); + + // bp.apply(Position { x: 1590, y: 1610 }, &mut ret, data_store); + // bp.apply(Position { x: 1600, y: 1610 }, &mut ret, data_store); + // bp.apply(Position { x: 1610, y: 1610 }, &mut ret, data_store); + // bp.apply(Position { x: 1620, y: 1610 }, &mut ret, data_store); + // bp.apply(Position { x: 1630, y: 1610 }, &mut ret, data_store); + + // bp.apply(Position { x: 1590, y: 1620 }, &mut ret, data_store); + // bp.apply(Position { x: 1600, y: 1620 }, &mut ret, data_store); + // bp.apply(Position { x: 1610, y: 1620 }, &mut ret, data_store); + // bp.apply(Position { x: 1620, y: 1620 }, &mut ret, data_store); + // bp.apply(Position { x: 1630, y: 1620 }, &mut ret, data_store); ret } @@ -105,17 +399,16 @@ impl GameState { - tech_state: TechState, + pub tech_state: TechState, pub factory: Factory, // TODO: } impl SimulationState { + #[must_use] pub fn new(data_store: &DataStore) -> Self { Self { - tech_state: TechState { - current_technology: Some(Technology { id: 0 }), - }, + tech_state: TechState::new(data_store), factory: Factory::new(data_store), } } @@ -124,39 +417,72 @@ impl SimulationState { pub power_grids: PowerGridStorage, - pub belts: BeltStore, - pub storage_storage_inserters: StorageStorageInserterStore, + pub belts: BeltStore, + pub storage_storage_inserters: StorageStorageInserterStore, pub chests: FullChestStore, + + pub fluid_store: FluidSystemStore, } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct StorageStorageInserterStore { - inserters: Box<[Vec>]>, - holes: Box<[Vec]>, +pub struct StorageStorageInserterStore { + pub inserters: Box< + [BTreeMap< + u16, + ( + BucketedStorageStorageInserterStoreFrontend, + BucketedStorageStorageInserterStore, + ), + >], + >, } -impl StorageStorageInserterStore { - fn new(data_store: &DataStore) -> Self { +impl StorageStorageInserterStore { + fn new( + data_store: &DataStore, + ) -> Self { Self { - inserters: vec![vec![]; data_store.item_names.len()].into_boxed_slice(), - holes: vec![vec![]; data_store.item_names.len()].into_boxed_slice(), + inserters: vec![BTreeMap::new(); data_store.item_display_names.len()] + .into_boxed_slice(), } } - fn update<'a, 'b, ItemIdxType: IdxTrait>( + #[profiling::function] + pub fn get_info_batched( + &mut self, + item: Item, + movetime: u16, + ids: impl IntoIterator, + current_tick: u32, + ) -> HashMap { + let (front, back) = self.inserters[item.into_usize()] + .get_mut(&movetime) + .unwrap(); + + let info = front.get_info_batched(ids, &back, true, current_tick); + info + } + + #[profiling::function] + fn update<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( &mut self, full_storages: impl IndexedParallelIterator>, num_grids_total: usize, + current_tick: u32, data_store: &DataStore, ) where 'b: 'a, { self.inserters .par_iter_mut() - .zip(self.holes.par_iter_mut()) .zip(full_storages) .enumerate() - .for_each(|(item_id, ((ins, holes), storages))| { + .for_each(|(item_id, (map, storages))| { + profiling::scope!( + "StorageStorage Inserter Update", + format!("Item: {}", data_store.item_display_names[item_id]).as_str() + ); + let item = Item { id: item_id.try_into().unwrap(), }; @@ -164,39 +490,125 @@ impl StorageStorageInserterStore { let grid_size = grid_size(item, data_store); let num_recipes = num_recipes(item, data_store); - ins.iter_mut() - .enumerate() - // FIXME: This is awful! - // Ideally we could replace inserter holes with placeholder that do not do anything, but I don't quite know how those would work. - .filter_map(|(i, v)| (!holes.contains(&i)).then_some(v)) - .for_each(|ins| { - ins.update(storages, MOVETIME, num_grids_total, num_recipes, grid_size) - }); + for (frontend, ins_store) in map.values_mut() { + if item.into_usize() == 1 && ins_store.movetime == 160 { + // dbg!(&ins_store); + } + + ins_store.update(frontend, storages, grid_size, current_tick); + } }); } - pub fn add_ins( + pub fn add_ins( &mut self, item: Item, + movetime: u16, start: Storage, dest: Storage, - ) -> usize { - let idx = if let Some(hole_idx) = self.holes[usize_from(item.id)].pop() { - self.inserters[usize_from(item.id)][hole_idx] = - StorageStorageInserter::new(start, dest); + 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 id: InserterId = self.inserters[item.into_usize()] + .entry(movetime) + .or_insert_with(|| { + ( + BucketedStorageStorageInserterStoreFrontend::new(), + BucketedStorageStorageInserterStore::new(movetime), + ) + }) + .1 + .add_inserter(source, dest, hand_size); - hole_idx - } else { - self.inserters[usize_from(item.id)].push(StorageStorageInserter::new(start, dest)); + InserterIdentifier { source, dest, id } + } - self.inserters[usize_from(item.id)].len() - 1 - }; + pub fn remove_ins( + &mut self, + item: Item, + movetime: u16, + id: InserterIdentifier, + ) { + let inserter = self.inserters[item.into_usize()] + .get_mut(&movetime) + .unwrap() + .1 + .remove_inserter(id.source, id.dest, id.id); + // TODO: Handle what happens with the items + } + + #[must_use] + pub fn change_movetime( + &mut self, + item: Item, + old_movetime: u16, + new_movetime: u16, + id: InserterIdentifier, + ) -> InserterIdentifier { + // FIXME: This does not preserve the inserter state at all! + let inserter = self.inserters[item.into_usize()] + .get_mut(&old_movetime) + .unwrap() + .1 + .remove_inserter(id.source, id.dest, id.id); + + let inner_id: InserterId = self.inserters[item.into_usize()] + .entry(new_movetime) + .or_insert_with(|| { + ( + BucketedStorageStorageInserterStoreFrontend::new(), + BucketedStorageStorageInserterStore::new(new_movetime), + ) + }) + .1 + .add_inserter(id.source, id.dest, inserter.max_hand_size); - idx + InserterIdentifier { + source: id.source, + dest: id.dest, + id: inner_id, + } + } + + #[profiling::function] + pub fn update_inserter_src( + &mut self, + item: Item, + movetime: u16, + id: InserterIdentifier, + new_src: Storage, + data_store: &DataStore, + ) -> InserterIdentifier { + self.inserters[item.into_usize()] + .get_mut(&movetime) + .unwrap() + .1 + .update_inserter_src( + id, + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_src, data_store), + ) } - pub fn remove_ins(&mut self, item: Item, index: usize) { - self.holes[usize_from(item.id)].push(index); + #[profiling::function] + pub fn update_inserter_dest( + &mut self, + item: Item, + movetime: u16, + id: InserterIdentifier, + new_dest: Storage, + data_store: &DataStore, + ) -> InserterIdentifier { + self.inserters[item.into_usize()] + .get_mut(&movetime) + .unwrap() + .1 + .update_inserter_dest( + id, + FakeUnionStorage::from_storage_with_statics_at_zero(item, new_dest, data_store), + ) } } @@ -213,35 +625,46 @@ impl Factory(&mut self, data_store: &DataStore) { + #[profiling::function] + fn belt_update<'a>( + &mut self, + current_tick: u32, + data_store: &DataStore, + ) { let num_grids_total = self.power_grids.power_grids.len(); - let mut all_storages = - storages_by_item(&mut self.power_grids, &mut self.chests, data_store); + let mut all_storages = { + profiling::scope!("Generate all_storages list"); + storages_by_item(&mut self.power_grids, &mut self.chests, data_store) + }; let sizes: Vec<_> = sizes(data_store, num_grids_total).into_iter().collect(); // dbg!(&all_storages); - assert_eq!(sizes.len(), data_store.item_names.len()); - let storages_by_item = full_to_by_item(&mut all_storages, &sizes); - - let mut storages_by_item: Box<[_]> = storages_by_item.into_iter().collect(); + assert_eq!(sizes.len(), data_store.item_display_names.len()); + let mut storages_by_item: Box<[_]> = { + profiling::scope!("Sort storages by item"); + let storages_by_item = full_to_by_item(&mut all_storages, &sizes); + Iterator::collect(storages_by_item.into_iter()) + }; self.storage_storage_inserters.update( storages_by_item.par_iter_mut().map(|v| &mut **v), num_grids_total, + current_tick, data_store, ); self.belts.update( - num_grids_total, storages_by_item.par_iter_mut().map(|v| &mut **v), data_store, ); @@ -249,27 +672,40 @@ impl Factory, + }, Ingame, - Loading, + Loading { + /// WARNING: This is a f64! + progress: Arc, + game_state_receiver: Receiver<(LoadedGame, Arc, Sender)>, + }, } #[derive(Debug, Clone, Copy)] enum InserterUpdateInfo { + AssemblerRecipeChanged { pos: Position, size: (u8, u8) }, NewAssembler { pos: Position, size: (u8, u8) }, NewBelt { pos: Position }, } #[derive(Debug)] -enum InstantiateInserterError { +pub enum InstantiateInserterError { NotUnattachedInserter, SourceMissing, DestMissing, - PleaseSpecifyFilter, - ItemConflict, + PleaseSpecifyFilter { + belts_which_could_help: Vec>, + }, + ItemConflict { + belts_which_could_help: Vec>, + }, } impl GameState { #[allow(clippy::too_many_lines)] + #[profiling::function] pub fn apply_actions( &mut self, actions: impl IntoIterator>>, @@ -288,21 +724,49 @@ impl GameState { + self.simulation_state.tech_state.current_technology = tech; + }, + + ActionType::CheatUnlockTechnology { tech } => { + if self.simulation_state.tech_state.current_technology == Some(tech) { + self.simulation_state.tech_state.current_technology = None; + } + self.simulation_state + .tech_state + .in_progress_technologies + .remove(&tech); + self.simulation_state + .tech_state + .finished_technologies + .insert(tech); + for recipe in &data_store + .technology_tree + .node_weight(NodeIndex::from(tech.id)) + .unwrap() + .effect + .unlocked_recipes + { + self.simulation_state.tech_state.recipe_active[recipe.into_usize()] = true; + } + }, + ActionType::PlaceFloorTile(place_floor_tile_by_hand_info) => { let num_items_needed = match place_floor_tile_by_hand_info.ghost_info.position { PositionInfo::Rect { pos, width, height } => width * height, PositionInfo::Single { pos } => 1, - PositionInfo::List { ref positions } => positions.len(), + PositionInfo::List { ref positions } => positions.len().try_into().unwrap(), }; // TODO: Check player inventory for enough resources match place_floor_tile_by_hand_info.ghost_info.position { PositionInfo::Rect { pos, width, height } => { - for x in pos.x..(pos.x + width) { - for y in pos.y..(pos.y + height) { + for x in pos.x..(pos.x + i32::try_from(width).unwrap()) { + for y in pos.y..(pos.y + i32::try_from(height).unwrap()) { self.world.set_floor_tile( Position { x, y }, place_floor_tile_by_hand_info.ghost_info.tile, @@ -324,14 +788,99 @@ impl GameState self + .world + .mutate_entities_colliding_with(pos, (1, 1), data_store, |e| { + match e { + Entity::Chest { + ty, + pos, + item, + slot_limit, + } => { + if let Some((item, index)) = item { + let removed_items = self.simulation_state.factory.chests.stores + [usize_from(item.id)] + .change_chest_size( + *index, + data_store.item_stack_sizes[usize_from(item.id)] + as ChestSize + * ChestSize::from(num_slots), + ); + } + *slot_limit = num_slots; + }, + _ => { + warn!("Tried to set slot limit on non chest"); + }, + } + ControlFlow::Break(()) + }), + ActionType::OverrideInserterMovetime { pos, new_movetime } => self + .world + .mutate_entities_colliding_with(pos, (1, 1), data_store, |e| { + match e { + Entity::Inserter { + user_movetime, + type_movetime, + info, + .. + } => { + match info { + InserterInfo::NotAttached { start_pos, end_pos } => {}, + InserterInfo::Attached { + start_pos, + end_pos, + info, + } => match info { + AttachedInserter::BeltStorage { id, belt_pos } => todo!(), + AttachedInserter::BeltBelt { item, inserter } => todo!(), + AttachedInserter::StorageStorage { item, inserter } => { + let old_movetime = user_movetime + .map(|v| v.into()) + .unwrap_or(*type_movetime); + + let new_movetime = new_movetime + .map(|v| v.into()) + .unwrap_or(*type_movetime); + + if old_movetime != new_movetime { + let new_id = self + .simulation_state + .factory + .storage_storage_inserters + .change_movetime( + *item, + old_movetime, + new_movetime.into(), + *inserter, + ); + + *inserter = new_id; + } + }, + }, + } + *user_movetime = new_movetime; + }, + _ => { + warn!("Tried to set Inserter Settings on non inserter"); + }, + } + ControlFlow::Break(()) + }), ActionType::PlaceEntity(place_entity_info) => match place_entity_info.entities { crate::frontend::action::place_entity::EntityPlaceOptions::Single( place_entity_type, ) => match place_entity_type { - crate::frontend::world::tile::PlaceEntityType::Assembler { pos, ty } => { + crate::frontend::world::tile::PlaceEntityType::Assembler { + pos, + ty, + rotation, + } => { info!("Trying to place assembler at {pos:?}"); - // TODO: get size dynamically - if !self.world.can_fit(pos, (3, 3), data_store) { + let size = data_store.assembler_info[ty as usize].size(rotation); + if !self.world.can_fit(pos, size, data_store) { warn!("Tried to place assembler where it does not fit"); continue; } @@ -339,7 +888,7 @@ impl GameState GameState GameState GameState { - let ret = self.try_adding_inserter(pos, dir, filter, data_store); + // TODO: Add ty + let ret = self.add_inserter(0, pos, dir, filter, data_store); trace!("{:?}", ret); }, - crate::frontend::world::tile::PlaceEntityType::Belt { pos, direction } => { + crate::frontend::world::tile::PlaceEntityType::Belt { + pos, + direction, + ty, + } => { if !self.world.can_fit(pos, (1, 1), data_store) { warn!("Tried to place belt where it does not fit"); continue; } - handle_belt_placement(self, pos, direction, data_store); + handle_belt_placement(self, pos, direction, ty, data_store); + + self.update_inserters(InserterUpdateInfo::NewBelt { pos }, data_store); + }, + crate::frontend::world::tile::PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + } => { + if !self.world.can_fit(pos, (1, 1), data_store) { + warn!("Tried to place underground_belt where it does not fit"); + continue; + } + + handle_underground_belt_placement( + self, + pos, + direction, + ty, + underground_dir, + data_store, + ); self.update_inserters(InserterUpdateInfo::NewBelt { pos }, data_store); }, @@ -420,142 +998,221 @@ impl GameState { + // Handle Entities that are now part of another power_grid + for pole_position in pole_updates { + let grid = self + .simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[&pole_position]; - self.world - .update_pole_power(pole_position, grid, data_store); - } + assert!( + !self.simulation_state.factory.power_grids.power_grids + [grid as usize] + .is_placeholder + ); - // Handle storage updates - for storage_update in storage_updates { - self.world.mutate_entities_colliding_with(storage_update.position, (1,1), data_store, |e| { - match (e, storage_update.new_storage) { - (Entity::Assembler { ty: _, pos: _, info: AssemblerInfo::Powered { id, pole_position: _ }, modules: _ }, crate::power::power_grid::PowerGridEntity::Assembler { recipe, index }) => { - assert_eq!(id.recipe, recipe); - id.grid = storage_update.new_grid; - id.assembler_index = index; - // FIXME: Store and update the weak_index - }, - (Entity::Lab { pos: _, ty: _, pole_position: Some((_pole_pos, weak_idx, lab_store_index)) }, crate::power::power_grid::PowerGridEntity::Lab { ty: _, index: new_idx }) => { - *lab_store_index = new_idx; - // The weak index stays the same since it it still connected to the same power pole + self.world.update_pole_power( + pole_position, + grid, + data_store, + ); + } + + // Handle storage updates + for storage_update in storage_updates { + let mut entity_size = None; + self.world.mutate_entities_colliding_with(storage_update.position, (1,1), data_store, |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)); + + assert_eq!(id.recipe, recipe); + id.grid = storage_update.new_grid; + id.assembler_index = index; + // FIXME: Store and update the weak_index + }, + (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 + } + + (_, _) => todo!("Handler storage_update {storage_update:?}") + } + ControlFlow::Break(()) + }); + + // FIXME: Rotation + let e_size = entity_size.unwrap(); + + let inserter_range = data_store.max_inserter_search_range; + + self.world.mutate_entities_colliding_with( + Position { + x: storage_update.position.x + - i32::from(inserter_range), + y: storage_update.position.y + - i32::from(inserter_range), + }, + ( + u16::from(inserter_range) * 2 + e_size.0, + u16::from(inserter_range) * 2 + e_size.1, + ), + data_store, + |e| { + match e { + Entity::Inserter { + ty, + user_movetime, + type_movetime, + + pos, + direction, + filter, + info, + } => match info { + InserterInfo::NotAttached { .. } => {}, + InserterInfo::Attached { + start_pos, + end_pos, + info, + } => { + if start_pos.contained_in( + storage_update.position, + e_size, + ) { + match info { + AttachedInserter::BeltStorage { id, belt_pos } => { + 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::SolarPanel { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), + }.translate(self.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store); + self.simulation_state.factory.belts.update_belt_storage_inserter_src(*id, *belt_pos, self.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); + }, + AttachedInserter::BeltBelt { .. } => { + unreachable!("A BeltBelt inserter should not be pointing at a machine") + }, + AttachedInserter::StorageStorage { item, inserter } => { + 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::SolarPanel { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), + }.translate(*item, data_store); + + let movetime = user_movetime.map(|v| v.into()).unwrap_or(*type_movetime); + + let new_id = self.simulation_state.factory.storage_storage_inserters.update_inserter_src(*item, movetime, *inserter, new_storage, data_store); + + *inserter = new_id; + }, + } } - (_, _) => todo!("Handler storage_update {storage_update:?}") + if end_pos.contained_in( + storage_update.position, + e_size, + ) { + match info { + AttachedInserter::BeltStorage { id, belt_pos } => { + 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::SolarPanel { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), + }.translate(self.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), data_store); + self.simulation_state.factory.belts.update_belt_storage_inserter_dest(*id, *belt_pos, self.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos), new_storage, data_store); + }, + AttachedInserter::BeltBelt { item, inserter } => { + unreachable!("A BeltBelt inserter should not be pointing at a machine") + }, + AttachedInserter::StorageStorage { item, inserter } => { + 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::SolarPanel { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), + }.translate(*item, data_store); + let movetime = user_movetime.map(|v| v.into()).unwrap_or(*type_movetime); + + let new_id = self.simulation_state.factory.storage_storage_inserters.update_inserter_dest(*item, movetime, *inserter, new_storage, data_store); + + *inserter = new_id; + }, + } + } + }, + }, + Entity::FluidTank { ty, pos, rotation } => { + let id = self.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; + if let Some(fluid) = id.fluid { + + 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::SolarPanel { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), + crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), + }.translate(fluid, data_store); + self.simulation_state.factory.fluid_store.update_fluid_conn_if_needed(*pos, storage_update.position, e_size.into(), new_storage, &mut self.simulation_state.factory.storage_storage_inserters, data_store); } - ControlFlow::Break(()) - }); - } - } else { - // No updates needed - } + }, - let grid = self - .simulation_state - .factory - .power_grids - .pole_pos_to_grid_id[&pole_pos]; - - // Handle Entities that are newly powered - let power_range = data_store.power_pole_data[ty as usize].power_range; - self.world.mutate_entities_colliding_with( - Position { - x: pole_pos.x - power_range as usize, - y: pole_pos.y - power_range as usize, - }, - ((2 * power_range + 1).into(), (2 * power_range + 1).into()), - data_store, - |e| { - match e { - Entity::Assembler { - ty, - pos, - info, - modules, - } => match info { - AssemblerInfo::UnpoweredNoRecipe => { - *info = AssemblerInfo::PoweredNoRecipe(pole_pos); - }, - AssemblerInfo::Unpowered(recipe) => { - let assembler_id = self - .simulation_state - .factory - .power_grids - .power_grids - [grid as usize] - .add_assembler( - *ty, grid, *recipe, &modules, pole_pos, - *pos, data_store, - ); - *info = AssemblerInfo::Powered { - id: assembler_id, - pole_position: pole_pos, - }; - }, - _ => {}, + _ => {}, + } + ControlFlow::Continue(()) }, - Entity::Roboport { - ty, - pos, - power_grid: power_grid @ None, - network, - id, - } => { - *power_grid = Some(grid); - todo!("Add Roboport to power grid") - }, - Entity::SolarPanel { - pos, - ty, - pole_position: pole_position @ None, - } => { - let idx = self - .simulation_state - .factory - .power_grids - .power_grids[usize::from(grid)] - .add_solar_panel(*pos, *ty, pole_pos, data_store); - - *pole_position = Some((pole_pos, idx)); - }, - Entity::Lab { - pos, - ty, - pole_position: pole_position @ None, - } => { - let idx = self - .simulation_state - .factory - .power_grids - .power_grids[usize::from(grid)] - .add_lab(*pos, *ty, pole_pos, data_store); - - *pole_position = Some((pole_pos, idx.0, idx.1)); - }, - _ => {}, + ); } - ControlFlow::Continue(()) }, - ); + _ => { + // No updates needed + }, + } + + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .simulation_state + .factory + .power_grids + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + if !affected_grids_and_potential_match { + dbg!(action.borrow()); + } + assert!(affected_grids_and_potential_match); + } // Add the powerpole entity to the correct chunk self.world.add_entity( @@ -564,62 +1221,102 @@ impl GameState { + let (left_pos, right_pos) = match splitter_direction { + Dir::North => (splitter_pos, splitter_pos + Dir::East), + Dir::East => (splitter_pos, splitter_pos + Dir::South), + Dir::South => (splitter_pos + Dir::East, splitter_pos), + Dir::West => (splitter_pos + Dir::South, splitter_pos), + }; + let self_positions = [left_pos, right_pos]; + if self_positions + .into_iter() + .any(|pos| !self.world.can_fit(pos, (1, 1), data_store)) + { + warn!("Tried to place splitter where it does not fit"); + continue; + } let splitter = handle_splitter_placement( - self, pos, direction, in_mode, out_mode, data_store, + self, + splitter_pos, + splitter_direction, + ty, + in_mode, + out_mode, + data_store, ); }, - crate::frontend::world::tile::PlaceEntityType::Chest { pos } => { + crate::frontend::world::tile::PlaceEntityType::Chest { pos, ty } => { info!("Trying to place chest at {pos:?}"); - // TODO: get size dynamically - if !self.world.can_fit(pos, (1, 1), data_store) { + if !self.world.can_fit( + pos, + data_store.chest_tile_sizes[usize::from(ty)], + data_store, + ) { warn!("Tried to place chest where it does not fit"); continue; } - // FIXME: Chest item hardcoded - let item = Item { - id: ItemIdxType::from(0), - }; - - let index = self.simulation_state.factory.chests.stores - [usize_from(item.id)] - .add_chest(0, data_store); - self.world.add_entity( - // FIXME: Chest type hardcoded Entity::Chest { - ty: 0, + ty, pos, - item: Some(item), - index, + item: None, + slot_limit: data_store.chest_num_slots[usize::from(ty)], }, - &self.simulation_state, + &mut self.simulation_state, data_store, ); }, crate::frontend::world::tile::PlaceEntityType::SolarPanel { pos, ty } => { info!("Trying to place solar_panel at {pos:?}"); - // TODO: get size dynamically - if !self.world.can_fit(pos, (3, 3), data_store) { + let size = data_store.solar_panel_info[usize::from(ty)].size; + let size = size.into(); + + if !self.world.can_fit(pos, size, data_store) { warn!("Tried to place solar_panel where it does not fit"); continue; } - // FIXME: Hardcoded let powered_by = self.world.is_powered_by( &self.simulation_state, pos, - (3, 3), + size, data_store, ); @@ -647,23 +1344,32 @@ impl GameState { info!("Trying to place lab at {pos:?}"); - // TODO: get size dynamically - if !self.world.can_fit(pos, (3, 3), data_store) { + if !self.world.can_fit( + pos, + data_store.lab_info[usize::from(ty)].size, + data_store, + ) { warn!("Tried to place lab where it does not fit"); continue; } - // FIXME: Hardcoded + let modules = + vec![ + None; + data_store.lab_info[usize::from(ty)].num_module_slots.into() + ] + .into_boxed_slice(); + let powered_by = self.world.is_powered_by( &self.simulation_state, pos, - (3, 3), + data_store.lab_info[usize::from(ty)].size, data_store, ); @@ -678,7 +1384,8 @@ impl GameState GameState { - todo!() - }, - }, - ActionType::Position(id, pos) => { - self.world.players[usize::from(id)].visible = true; - self.world.players[usize::from(id)].pos = pos; - }, - ActionType::Ping(Position { x, y }) => { - // Do nothing for now - info!("Ping at {:?}", (x, y)); - // TODO: - }, - ActionType::SetRecipe(SetRecipeInfo { pos, recipe }) => { - let mut needs_update = false; - let chunk = self.world.get_chunk_for_tile_mut(pos); + crate::frontend::world::tile::PlaceEntityType::Beacon { pos, ty } => { + info!("Trying to place beacon at {pos:?}"); + let size = data_store.beacon_info[usize::from(ty)].size; - if let Some(chunk) = chunk { - let assembler = chunk.get_entity_at_mut(pos, data_store); + if !self.world.can_fit(pos, size, data_store) { + warn!("Tried to place beacon where it does not fit"); + continue; + } - if let Some(mut entity) = assembler { - match &mut entity { - Entity::Assembler { - ty, + let modules = vec![ + // TODO: Do not add modules immediately + Some(0); + data_store.beacon_info[usize::from(ty)] + .num_module_slots + .into() + ] + .into_boxed_slice(); + + let powered_by = self.world.is_powered_by( + &self.simulation_state, + pos, + data_store.beacon_info[usize::from(ty)].size, + data_store, + ); + + let powered_by = if let Some(pole_pos) = powered_by { + let weak_idx = self + .simulation_state + .factory + .power_grids + .add_beacon(ty, pos, pole_pos, modules.clone(), [], data_store); + + Some((pole_pos, weak_idx)) + } else { + None + }; + + self.world.add_entity( + Entity::Beacon { pos, - ref mut info, + ty, modules, - } => match info { - AssemblerInfo::UnpoweredNoRecipe => { - *info = AssemblerInfo::Unpowered(recipe) - }, - AssemblerInfo::Unpowered(_old_recipe) => { - *info = AssemblerInfo::Unpowered(recipe) - }, - AssemblerInfo::PoweredNoRecipe(pole_position) => { - let grid_id = self - .simulation_state - .factory - .power_grids - .pole_pos_to_grid_id[&pole_position]; - - let new_id = Self::add_assembler_to_sim( - &mut self.simulation_state, - *ty, - recipe, - &modules, - grid_id, - *pole_position, - *pos, - data_store, - ); + pole_position: powered_by, + }, + &mut self.simulation_state, + data_store, + ); + }, + crate::frontend::world::tile::PlaceEntityType::FluidTank { + ty, + pos, + rotation, + } => { + let size = data_store.fluid_tank_infos[usize::from(ty)].size; + // FIXME: Stop ignoring rotation + if !self.world.can_fit(pos, size.into(), data_store) { + warn!("Tried to place storage tank where it does not fit"); + continue; + } - *info = AssemblerInfo::Powered { - id: new_id, - pole_position: *pole_position, - }; + let search_range = + data_store.fluid_tank_infos[usize::from(ty)].max_search_range; - needs_update = true; + // Get connecting entities: + let connecting_fluid_box_positions: Vec<_> = self + .world + .get_entities_colliding_with( + Position { + x: pos.x - i32::from(search_range), + y: pos.y - i32::from(search_range), }, - AssemblerInfo::Powered { - id: assembler_id, + (size[0] + 2 * search_range, size[1] + 2 * search_range), + data_store, + ) + .into_iter() + .filter_map(|e| match e { + Entity::Assembler { + ty, + pos, + info, + rotation, + .. + } => { + // FIXME: Implement assembler flowthough + None + }, + Entity::Lab { + pos, + ty, + modules, pole_position, } => { - let old_recipe_id = assembler_id.recipe; - - if old_recipe_id == recipe { - continue; - } + // TODO: Do I want to support fluid science? Would be really easy + None + }, - let old_assembler = - self.simulation_state.factory.power_grids.power_grids - [assembler_id.grid as usize] - .remove_assembler(*assembler_id, data_store); - - let new_id = Self::add_assembler_to_sim( - &mut self.simulation_state, - *ty, - recipe, - &modules, - assembler_id.grid, - *pole_position, - *pos, + Entity::FluidTank { + ty: other_ty, + pos: other_pos, + rotation: other_rotation, + } => { + let we_can_connect = can_fluid_tanks_connect( + pos, + ty, + rotation, + *other_pos, + *other_ty, + *other_rotation, data_store, ); - *assembler_id = new_id; + we_can_connect + }, - needs_update = true; + // TODO: There are some future entities which might need connections like mining drills + _ => None, + }) + .collect(); + + let in_out_connections = self + .world + .get_entities_colliding_with( + Position { + x: pos.x - 1, + y: pos.y - 1, }, + (size[0] + 2, size[1] + 2), + data_store, + ) + .into_iter() + .filter_map(|e| match e { + Entity::Assembler { + ty: assembler_ty, + pos: assembler_pos, + info: + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + }, + rotation: assembler_rotation, + .. + } => { + let assembler_size = data_store.assembler_info + [usize::from(*assembler_ty)] + .size(*assembler_rotation); + let assembler_size = [assembler_size.0, assembler_size.1]; + + let recipe_fluid_inputs: Vec<_> = data_store + .recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| { + (*dir == ItemRecipeDir::Ing + && data_store.item_is_fluid[item.into_usize()]) + .then_some(*item) + }) + .collect(); + let recipe_fluid_outputs: Vec<_> = data_store + .recipe_to_items[&id.recipe] + .iter() + .filter_map(|(dir, item)| { + (*dir == ItemRecipeDir::Out + && data_store.item_is_fluid[item.into_usize()]) + .then_some(*item) + }) + .collect(); + + let fluid_pure_outputs: Vec<_> = data_store.assembler_info + [usize::from(*assembler_ty)] + .fluid_connections + .iter() + .filter(|(_conn, allowed)| { + *allowed + == AllowedFluidDirection::Single(ItemRecipeDir::Out) + || matches!( + *allowed, + AllowedFluidDirection::Both { .. } + ) + }) + .collect(); + + let fluid_pure_inputs: Vec<_> = data_store.assembler_info + [usize::from(*assembler_ty)] + .fluid_connections + .iter() + .filter(|(_conn, allowed)| { + *allowed + == AllowedFluidDirection::Single(ItemRecipeDir::Ing) + || matches!( + *allowed, + AllowedFluidDirection::Both { .. } + ) + }) + .collect(); + + // FIXME: FINISH IMPLEMENTING THIS + + let all_connections_with_items = recipe_fluid_inputs + .into_iter() + .cycle() + .zip(fluid_pure_inputs) + .zip(iter::repeat(FluidConnectionDir::Output)) + .chain( + recipe_fluid_outputs + .into_iter() + .cycle() + .zip(fluid_pure_outputs) + .zip(iter::repeat(FluidConnectionDir::Input)), + ); + + Some(all_connections_with_items.filter_map( + move |((item, (fluid_conn, _allowed)), fluid_dir)| { + can_fluid_tanks_connect_to_single_connection( + pos, + ty, + rotation, + *assembler_pos, + *fluid_conn, + *assembler_rotation, + assembler_size, + data_store, + ) + .map(|(dest_conn, dest_conn_dir)| { + ( + fluid_dir, + item, + Storage::Assembler { + grid: id.grid, + index: id.assembler_index, + recipe_idx_with_this_item: data_store + .recipe_to_translated_index + [&(id.recipe, item)], + }, + dest_conn, + Box::new(|_weak_index: WeakIndex| {}) + as Box ()>, + ) + }) + }, + )) + }, + Entity::Lab { + pos, + ty, + modules, + pole_position, + } => { + // TODO: Do I want to support fluid science? Would be really easy + None + }, + + // TODO: There are some future entities which might need connections like mining drills + _ => None, + }) + .flatten(); + + // TODO: Only keep the closest connection for each connection + // connecting_fluid_box_positions.retain( + // |(dest_pos, conn_dir_of_destination)| { + // let mut current_pos = *dest_pos; + + // loop { + // if current_pos.contained_in(pos, size.into()) { + // return true; + // } + + // if let Some(e) = self + // .world + // .get_entities_colliding_with( + // current_pos, + // (1, 1), + // data_store, + // ) + // .into_iter() + // .next() + // { + // match e { + // Entity::FluidTank { + // ty: found_ty, + // pos: found_pos, + // rotation: found_rotation, + // } => { + // if can_fluid_tanks_connect( + // pos, + // ty, + // rotation, + // *found_pos, + // *found_ty, + // *found_rotation, + // data_store, + // ) + // .is_some() + // { + // // The underground should connect with the found fluid tank instead + // return false; + // } + // }, + // Entity::Assembler { ty: found_ty, pos: found_pos, modules: found_modules, info: found_info } => { + // for conn in data_store.assembler_info[usize::from(*found_ty)].fluid_connections { + // if can_fluid_tanks_connect_to_single_connection(pos, ty, rotation, *found_pos, conn.0, Dir::North, data_store.assembler_info[usize::from(*found_ty)].size.into(), data_store).is_some() { + // // The underground should connect with the found machine instead + // return false; + // } + // } + // } + // _ => {}, + // } + // } + + // current_pos = current_pos + *conn_dir_of_destination; + // } + // }, + // ); + + // TODO: Check if us connecting might break any already existing connections + + let ret = self.simulation_state.factory.fluid_store.try_add_fluid_box( + pos, + data_store.fluid_tank_infos[usize::from(ty)].capacity, + connecting_fluid_box_positions.iter().map(|v| v.0), + in_out_connections, + &mut self.simulation_state.factory.chests, + &mut self.simulation_state.factory.storage_storage_inserters, + data_store, + ); + match ret { + Ok(id) => { + self.world.add_entity( + Entity::FluidTank { pos, ty, rotation }, + &mut self.simulation_state, + data_store, + ); }, - _ => { - warn!("Tried to change assembler recipe on non assembler!"); + Err(CannotMixFluidsError { items: [a, b] }) => { + warn!( + "Cannot connect systems containing {} and {}", + data_store.item_display_names[a.into_usize()], + data_store.item_display_names[b.into_usize()] + ) }, } - } else { - warn!("Tried to change assembler recipe where there was no entity!"); - } - } else { - warn!("Tried to change assembler recipe outside world!"); - } + }, + crate::frontend::world::tile::PlaceEntityType::MiningDrill { + ty, + pos, + rotation, + } => todo!("Place Mining Drill"), + }, + crate::frontend::action::place_entity::EntityPlaceOptions::Multiple(vec) => { + todo!() + }, + }, + ActionType::Position(id, pos) => { + self.world.players[usize::from(id)].visible = true; + self.world.players[usize::from(id)].pos = pos; + }, + ActionType::Ping(Position { x, y }) => { + // Do nothing for now + info!("Ping at {:?}", (x, y)); + // TODO: + }, + ActionType::SetRecipe(SetRecipeInfo { + pos: assembler_pos, + recipe, + }) => { + let Some(Entity::Assembler { .. }) = self + .world + .get_entities_colliding_with(assembler_pos, (1, 1), data_store) + .into_iter() + .next() + else { + warn!("Tried to set recipe on non assembler"); + continue; + }; - if needs_update { - // FIXME: Size hardcoded! - self.update_inserters( - InserterUpdateInfo::NewAssembler { pos, size: (3, 3) }, - data_store, - ); - } + self.world.change_assembler_recipe( + &mut self.simulation_state, + assembler_pos, + recipe, + data_store, + ); }, ActionType::Remove(pos) => { self.world @@ -822,31 +1802,154 @@ impl GameState num_free_module_slots { // Not enough space in the module slots - warn!("Tried to insert modules into non assembler"); + info!( + "Tried to insert more modules than space is available" + ); + } else { + // We are okay! + + modules + .iter_mut() + .filter(|slot| slot.is_none()) + .zip(new_modules.iter().copied()) + .for_each(|(slot, new_module)| { + assert!(slot.is_none()); + *slot = Some(new_module); + }); + + match info { + AssemblerInfo::UnpoweredNoRecipe + | AssemblerInfo::Unpowered(_) + | AssemblerInfo::PoweredNoRecipe(_) => {}, + AssemblerInfo::Powered { + id, + pole_position, + weak_index, + } => { + for module in &new_modules { + self.simulation_state + .factory + .power_grids + .power_grids[usize::from(id.grid)] + .add_module_to_assembler( + *id, *module, data_store, + ); + } + }, + } + } + }, + Entity::Lab { + pos, + ty, + modules, + pole_position, + } => { + let num_free_module_slots = + modules.iter().filter(|slot| slot.is_none()).count(); + + if new_modules.len() > num_free_module_slots { + // Not enough space in the module slots + info!( + "Tried to insert more modules than space is available" + ); } else { // We are okay! - modules + modules + .iter_mut() + .filter(|slot| slot.is_none()) + .zip(new_modules.iter().copied()) + .for_each(|(slot, new_module)| { + assert!(slot.is_none()); + *slot = Some(new_module); + }); + + match pole_position { + None => {}, + Some((pole_pos, weak_index, index)) => { + for module in &new_modules { + self.simulation_state + .factory + .power_grids + .power_grids[usize::from( + self.simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[pole_pos], + )] + .add_module_to_lab(*index, *module, data_store); + } + }, + } + } + }, + Entity::Beacon { + ty, + pos, + modules, + pole_position, + } => { + // TODO: + // todo!(); + }, + _ => { + warn!( + "Tried to insert modules into entity without module slots" + ); + }, + } + ControlFlow::Break(()) + }); + }, + ActionType::RemoveModules { pos, indices } => { + self.world + .mutate_entities_colliding_with(pos, (1, 1), data_store, |e| { + match e { + Entity::Assembler { modules, info, .. } => { + let num_used_module_slots = + modules.iter().filter(|slot| slot.is_some()).count(); + + if indices.len() > num_used_module_slots { + // Not enough space in the module slots + warn!("Tried to remove more modules than exist in machine"); + } else { + // We are okay! + + assert!(indices.iter().all_unique()); + + assert!(indices.iter().all(|v| *v < modules.len())); + + let modules_to_remove = modules .iter_mut() - .filter(|slot| slot.is_none()) - .zip(new_modules.iter().copied()) - .for_each(|(slot, new_module)| { - assert!(slot.is_none()); - *slot = Some(new_module); + .enumerate() + .filter(|(i, _)| indices.contains(i)) + .map(|(_, slot)| { + let Some(module) = slot else { + todo!("How do I want to handle this"); + }; + + let module = *module; + + *slot = None; + + module }); match info { AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) | AssemblerInfo::PoweredNoRecipe(_) => {}, - AssemblerInfo::Powered { id, pole_position } => { - for module in &new_modules { + AssemblerInfo::Powered { id, .. } => { + for removed_module in modules_to_remove { self.simulation_state .factory .power_grids .power_grids[usize::from(id.grid)] - .add_module_to_assembler( - *id, *module, data_store, + .remove_module_from_assembler( + *id, + removed_module, + data_store, ); } }, @@ -860,66 +1963,170 @@ impl GameState todo!(), } + + #[cfg(debug_assertions)] + { + let affected_grids_and_potential_match = self + .simulation_state + .factory + .power_grids + .power_grids + .iter() + .filter(|grid| !grid.is_placeholder) + .all(|pg| { + pg.beacon_affected_entities + .keys() + .map(|e| e.get_power_grid()) + .all(|affected_grid| { + pg.potential_beacon_affected_powergrids + .contains(&affected_grid) + }) + }); + if !affected_grids_and_potential_match { + dbg!(action.borrow()); + } + assert!(affected_grids_and_potential_match); + } + } + + #[cfg(debug_assertions)] + { + assert!( + self.world + .get_chunks() + .into_iter() + .flat_map(|chunk| chunk.get_entities()) + .all(|e| match e { + Entity::Assembler { info, .. } => { + match info { + AssemblerInfo::UnpoweredNoRecipe => true, + AssemblerInfo::Unpowered(_) => true, + AssemblerInfo::PoweredNoRecipe(_) => true, + AssemblerInfo::Powered { id, .. } => { + !self.simulation_state.factory.power_grids.power_grids + [usize::from(id.grid)] + .is_placeholder + }, + } + }, + Entity::Beacon { + pole_position: Some((pole_pos, _)), + .. + } => { + self.simulation_state + .factory + .power_grids + .pole_pos_to_grid_id + .get(pole_pos) + .is_some() + }, + _ => true, + }) + ); } } + #[profiling::function] pub fn update(&mut self, data_store: &DataStore) { - self.simulation_state.factory.chests.update(); - - self.simulation_state.factory.belt_update(data_store); - - // TODO: Do I want this, or just do it in the belt_update - //self.simulation_state - // .factory - // .belt_belt_inserters - // .update(&mut self.simulation_state.factory.belts, data_store); - #[cfg(debug_assertions)] { - let num_placeholders = self - .simulation_state - .factory - .power_grids - .power_grids - .iter() - .filter(|grid| grid.is_placeholder) - .count(); - dbg!( - num_placeholders, - self.simulation_state.factory.power_grids.power_grids.len() - ); + let all_belt_connections = self + .world + .get_chunks() + .flat_map(|chunk| chunk.get_entities()) + .flat_map(|e| match e { + Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id), (*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Entrance, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, direction.reverse(), *id)], + Entity::Underground { + pos, + underground_dir: UndergroundDir::Exit, + direction, + ty, + id, + belt_pos, + } => vec![(*pos, *direction, *id)], + Entity::Splitter { pos, direction, id } => { + // TODO: + vec![] + }, + _ => vec![], + }); + + for (belt_pos, check_dir, id_that_should_exist) in all_belt_connections { + if let Some(Entity::Splitter { pos, direction, id }) = self + .world + .get_entities_colliding_with(belt_pos + check_dir, (1, 1), data_store) + .into_iter() + .next() + { + assert!( + self.simulation_state + .factory + .belts + .get_splitter_belt_ids(*id) + .iter() + .flatten() + .contains(&id_that_should_exist) + ); + } + } } + info!( + "self.world.to_instantiate.len(): {:?}", + self.world.to_instantiate.len() + ); + info!("{:?}", self.world.to_instantiate.first()); - let (tech_progress, recipe_tick_info): (ResearchProgress, RecipeTickInfo) = self - .simulation_state + self.simulation_state .factory - .power_grids - .power_grids - .par_iter_mut() - .map(|grid| grid.update(Watt(600000), &self.simulation_state.tech_state, data_store)) - .reduce( - || (0, RecipeTickInfo::new(data_store)), - |(acc_progress, infos), (rhs_progress, info)| { - (acc_progress + rhs_progress, infos + &info) - }, - ); + // We can downcast here, since this could only cause graphical weirdness for a couple frame every ~2 years of playtime + .belt_update(self.current_tick as u32, data_store); + + let ((), (tech_progress, recipe_tick_info, lab_info)) = rayon::join( + || self.simulation_state.factory.chests.update(data_store), + || { + self.simulation_state.factory.power_grids.update( + &self.simulation_state.tech_state, + 0, + data_store, + ) + }, + ); self.simulation_state .tech_state - .apply_progress(tech_progress); + .apply_progress(tech_progress, data_store); self.statistics.append_single_set_of_samples(( ProductionInfo::from_recipe_info(&recipe_tick_info, data_store), + ConsumptionInfo::from_infos(&recipe_tick_info, &lab_info, data_store), tech_progress, )); - // if self.statistics.production.num_samples_pushed % 60 == 0 { - // File::create("./stats.svg").unwrap().write(self.statistics.get_chart(1, data_store, Some(|_| true)).svg().unwrap().as_bytes()).unwrap(); - // } - self.current_tick += 1; + + let done_updating = Instant::now(); + + if let Some(last_update_time) = self.last_update_time { + self.update_times.append_single_set_of_samples(UpdateTime { + dur: done_updating - last_update_time, + }); + } + self.last_update_time = Some(done_updating); } fn update_inserters( @@ -938,8 +2145,8 @@ impl GameState { self.world.mutate_entities_colliding_with( Position { - x: assembler_pos.x - usize::from(inserter_range), - y: assembler_pos.y - usize::from(inserter_range), + x: assembler_pos.x - i32::from(inserter_range), + y: assembler_pos.y - i32::from(inserter_range), }, ( (2 * inserter_range + size.0).into(), @@ -948,11 +2155,7 @@ impl GameState match info { + Entity::Inserter { pos, info, .. } => match info { InserterInfo::NotAttached { start_pos, end_pos } => { if start_pos .contained_in(assembler_pos, (size.0.into(), size.1.into())) @@ -964,7 +2167,7 @@ impl GameState {}, + InserterInfo::Attached { .. } => {}, }, _ => {}, } @@ -975,8 +2178,8 @@ impl GameState { self.world.mutate_entities_colliding_with( Position { - x: belt_pos.x - usize::from(inserter_range), - y: belt_pos.y - usize::from(inserter_range), + x: belt_pos.x - i32::from(inserter_range), + y: belt_pos.y - i32::from(inserter_range), }, ( (2 * inserter_range + 1).into(), @@ -985,11 +2188,7 @@ impl GameState match info { + Entity::Inserter { pos, info, .. } => match info { InserterInfo::NotAttached { start_pos, end_pos } => { if start_pos.contained_in(belt_pos, (1, 1)) || end_pos.contained_in(belt_pos, (1, 1)) @@ -997,7 +2196,54 @@ impl GameState {}, + InserterInfo::Attached { .. } => {}, + }, + _ => {}, + } + ControlFlow::Continue(()) + }, + ); + }, + InserterUpdateInfo::AssemblerRecipeChanged { + pos: assembler_pos, + size, + } => { + self.world.mutate_entities_colliding_with( + Position { + x: assembler_pos.x - i32::from(inserter_range), + y: assembler_pos.y - i32::from(inserter_range), + }, + ( + (2 * inserter_range + size.0).into(), + (2 * inserter_range + size.1).into(), + ), + data_store, + |e| { + match e { + Entity::Inserter { pos, info, .. } => match info { + InserterInfo::NotAttached { start_pos, end_pos } => { + if start_pos + .contained_in(assembler_pos, (size.0.into(), size.1.into())) + || end_pos.contained_in( + assembler_pos, + (size.0.into(), size.1.into()), + ) + { + inserter_positions.push(*pos); + } + }, + InserterInfo::Attached { + info: AttachedInserter::BeltStorage { id, belt_pos }, + .. + } => todo!(), + InserterInfo::Attached { + info: AttachedInserter::StorageStorage { item, inserter }, + .. + } => todo!(), + InserterInfo::Attached { + info: AttachedInserter::BeltBelt { .. }, + .. + } => {}, }, _ => {}, } @@ -1008,7 +2254,8 @@ impl GameState GameState, - ) -> AssemblerID { + ) -> (AssemblerID, WeakIndex) { sim_state.factory.power_grids.power_grids[power_grid_id as usize].add_assembler( ty, power_grid_id, @@ -1033,349 +2280,13 @@ impl GameState>, - data_store: &DataStore, - ) -> Result<(), InstantiateInserterError> { - enum InserterConnection { - Belt(BeltTileId, u16), - Storage(Storage), - } - - let Some(Entity::Inserter { - pos: _pos, - direction, - info: InserterInfo::NotAttached { start_pos, end_pos }, - }) = self - .world - .get_entities_colliding_with(pos, (1, 1), data_store) - .into_iter() - .next() - else { - return Err(InstantiateInserterError::NotUnattachedInserter); - }; - - let start_conn: Option<( - InserterConnection, - Option>>, - )> = self - .world - .get_entities_colliding_with(*start_pos, (1, 1), data_store) - .into_iter() - .next() - .map(|e| match e { - Entity::Inserter { .. } | Entity::PowerPole { .. }| Entity::SolarPanel { .. } => None, - - Entity::Roboport { ty, pos, power_grid, network, id } => { - // TODO: - warn!("It is currently not possible to add or remove bots from roboports using inserters"); - None - } - - Entity::Assembler { - ty, - pos, - info: AssemblerInfo::Powered { - id, - pole_position - }, - modules - // FIXME: Translate the recipe_idx to - } => Some(( - InserterConnection::Storage(Storage::Assembler { - grid: id.grid, - recipe_idx_with_this_item: id.recipe.id, - index: id.assembler_index, - }), - Some( - data_store.recipe_to_items[&id.recipe] - .iter() - .filter_map(|(dir, item)| (*dir == ItemRecipeDir::Out).then_some(*item)) - .collect(), - ), - )), - Entity::Assembler { .. } => None, - Entity::Belt { - id: BeltTileId::AnyBelt(id, _), - belt_pos, - .. - } - | Entity::Underground { - id: BeltTileId::AnyBelt(id, _), - belt_pos, - .. - } => Some(( - InserterConnection::Belt(BeltTileId::AnyBelt(*id, PhantomData), *belt_pos), - match self - .simulation_state - .factory - .belts - .get_pure_item(BeltTileId::AnyBelt(*id, PhantomData)) - { - Some(item) => Some(vec![item]), - None => None, - }, - )), - Entity::Splitter { pos, direction, id } => todo!("Inserters on splitters"), - Entity::Chest { - ty, - pos, - item, - index, - } => Some(( - InserterConnection::Storage(Storage::Static { - static_id: StaticID::Chest, - index: *index, - }), - Some(item.into_iter().copied().collect()), - )), - Entity::Lab { pos, ty, pole_position } => { - // No removing items from Labs! - None - } - }) - .flatten(); - - let Some(start_conn) = start_conn else { - return Err(InstantiateInserterError::SourceMissing); - }; - - let dest_conn: Option<( - InserterConnection, - Option>>, - )> = self - .world - .get_entities_colliding_with(*end_pos, (1, 1), data_store) - .into_iter() - .next() - .map(|e| match e { - Entity::Inserter { .. } | Entity::PowerPole { .. }| Entity::SolarPanel { .. } => None, - - Entity::Roboport { ty, pos, power_grid, network, id } => { - // TODO: - warn!("It is currently not possible to add or remove bots from roboports using inserters"); - None - } - - Entity::Assembler { - ty, - pos, - info: AssemblerInfo::Powered { - id, - pole_position, - }, - modules - // FIXME: Translate the recipe_idx to - } => Some(( - InserterConnection::Storage(Storage::Assembler { - grid: id.grid, - recipe_idx_with_this_item: id.recipe.id, - index: id.assembler_index, - }), - Some( - data_store.recipe_to_items[&id.recipe] - .iter() - .filter_map(|(dir, item)| (*dir == ItemRecipeDir::Ing).then_some(*item)) - .collect(), - ), - )), - Entity::Assembler { .. } => None, - Entity::Belt { - id: BeltTileId::AnyBelt(id, _), - belt_pos, - .. - } - | Entity::Underground { - id: BeltTileId::AnyBelt(id, _), - belt_pos, - .. - } => Some(( - InserterConnection::Belt(BeltTileId::AnyBelt(*id, PhantomData), *belt_pos), - match self - .simulation_state - .factory - .belts - .get_pure_item(BeltTileId::AnyBelt(*id, PhantomData)) - { - Some(item) => Some(vec![item]), - None => None, - }, - )), - Entity::Splitter { pos, direction, id } => todo!(), - Entity::Chest { - ty, - pos, - item, - index, - } => Some(( - InserterConnection::Storage(Storage::Static { - static_id: StaticID::Chest, - index: *index, - }), - Some(item.into_iter().copied().collect()), - )), - Entity::Lab { pos, ty, pole_position } => { - if let Some((pole_pos, idx, lab_store_index)) = pole_position { - Some((InserterConnection::Storage(Storage::Lab { grid: self.simulation_state.factory.power_grids.pole_pos_to_grid_id[pole_pos], index: *lab_store_index }), Some(data_store.science_bottle_items.iter().copied().collect()))) - } else { - None - } - } - }) - .flatten(); - - let Some(dest_conn) = dest_conn else { - return Err(InstantiateInserterError::DestMissing); - }; - - // For determining the filter we use this plan: - // If a filter is specified, use that - // If we can determine a single source item use that, - // If we can determine a single destination item use that - // Else make the user do it - let filter = filter.unwrap_or( - if let Some(filter) = (match start_conn.1.into_iter().flatten().all_equal_value() { - Ok(filter) => Some(filter), - Err(None) => None, - Err(Some(wrong)) => { - return Err(InstantiateInserterError::PleaseSpecifyFilter); - }, - }) - .or_else( - || match dest_conn.1.into_iter().flatten().all_equal_value() { - Ok(filter) => Some(filter), - Err(None) => None, - Err(Some(wrong)) => None, - }, - ) { - filter - } else { - return Err(InstantiateInserterError::PleaseSpecifyFilter); - }, - ); - - let Entity::Inserter { info, .. } = self - .world - .get_chunk_for_tile_mut(pos) - .unwrap() - .get_entity_at_mut(pos, data_store) - .unwrap() - else { - unreachable!("We already checked it was an unattached inserter before") - }; - - match (start_conn.0, dest_conn.0) { - ( - InserterConnection::Belt(start_belt_id, start_belt_pos), - InserterConnection::Belt(dest_belt_id, dest_belt_pos), - ) => { - // FIXME: The movetime should be dependent on the inserter type! - let index = self.simulation_state.factory.belts.add_belt_belt_inserter( - (start_belt_id, start_belt_pos), - (dest_belt_id, dest_belt_pos), - BeltBeltInserterAdditionInfo { - cooldown: MOVETIME, - filter, - }, - ); - *info = InserterInfo::Attached(AttachedInserter::BeltBelt { - item: filter, - inserter: index, - }) - }, - ( - InserterConnection::Belt(start_belt_id, start_belt_pos), - InserterConnection::Storage(dest_storage_untranslated), - ) => { - let dest_storage = dest_storage_untranslated.translate(filter, data_store); - - match self - .simulation_state - .factory - .belts - .add_belt_storage_inserter( - filter, - start_belt_id, - start_belt_pos - 1, - dest_storage, - ) { - Ok(()) => {}, - Err(_) => todo!(), - }; - - *info = InserterInfo::Attached(AttachedInserter::BeltStorage { - id: start_belt_id, - belt_pos: start_belt_pos - 1, - }); - }, - ( - InserterConnection::Storage(start_storage_untranslated), - InserterConnection::Belt(dest_belt_id, dest_belt_pos), - ) => { - let start_storage = start_storage_untranslated.translate(filter, data_store); - - match self - .simulation_state - .factory - .belts - .add_storage_belt_inserter( - filter, - dest_belt_id, - dest_belt_pos - 1, - start_storage, - ) { - Ok(()) => {}, - Err(_) => todo!(), - }; - *info = InserterInfo::Attached(AttachedInserter::BeltStorage { - id: dest_belt_id, - belt_pos: dest_belt_pos - 1, - }); - }, - ( - InserterConnection::Storage(start_storage_untranslated), - InserterConnection::Storage(dest_storage_untranslated), - ) => { - let start_storage = start_storage_untranslated.translate(filter, data_store); - let dest_storage = dest_storage_untranslated.translate(filter, data_store); - - let index = self - .simulation_state - .factory - .storage_storage_inserters - .add_ins(filter, start_storage, dest_storage); - *info = InserterInfo::Attached(AttachedInserter::StorageStorage { - item: filter, - inserter: index, - }); - }, - } - - Ok(()) - } - - fn try_adding_inserter( + fn add_inserter( &mut self, + ty: u8, pos: Position, dir: Dir, filter: Option>, data_store: &DataStore, - ) -> Result<(), InstantiateInserterError> { - match self.add_unattached_inserter(pos, dir, data_store) { - Ok(_) => {}, - Err(_) => return Err(InstantiateInserterError::NotUnattachedInserter), - }; - - self.try_instantiate_inserter(pos, filter, data_store) - } - - fn add_unattached_inserter( - &mut self, - pos: Position, - dir: Dir, - data_store: &DataStore, ) -> Result<(), ()> { if !self.world.can_fit(pos, (1, 1), data_store) { warn!("Tried to place inserter where it does not fit"); @@ -1386,11 +2297,16 @@ impl GameState (Position, Posit let start_pos = Position { x: pos .x - .checked_add_signed(dir.reverse().into_offset().0.into()) + .checked_add(dir.reverse().into_offset().0.into()) .unwrap(), y: pos .y - .checked_add_signed(dir.reverse().into_offset().1.into()) + .checked_add(dir.reverse().into_offset().1.into()) .unwrap(), }; let end_pos = Position { - x: pos - .x - .checked_add_signed(dir.into_offset().0.into()) - .unwrap(), - y: pos - .y - .checked_add_signed(dir.into_offset().1.into()) - .unwrap(), + x: pos.x.checked_add(dir.into_offset().0.into()).unwrap(), + y: pos.y.checked_add(dir.into_offset().1.into()).unwrap(), }; (start_pos, end_pos) @@ -1426,25 +2336,89 @@ pub fn calculate_inserter_positions(pos: Position, dir: Dir) -> (Position, Posit #[cfg(test)] mod tests { - use std::sync::LazyLock; + use std::fs::File; use crate::{ - blueprint::{random_blueprint_strategy, random_position, Blueprint}, - data::{get_raw_data_test, DataStore}, + DATA_STORE, + blueprint::{Blueprint, random_blueprint_strategy, random_position}, frontend::{ - action::{place_entity::PlaceEntityInfo, ActionType}, - world::Position, + action::{ + ActionType, + place_entity::{EntityPlaceOptions, PlaceEntityInfo}, + set_recipe::SetRecipeInfo, + }, + world::{ + Position, + tile::{AssemblerInfo, Entity, PlaceEntityType}, + }, }, item::Recipe, + power::{Watt, power_grid::MAX_POWER_MULT}, rendering::app_state::GameState, replays::Replay, - DATA_STORE, }; - use proptest::{prelude::ProptestConfig, proptest}; + use proptest::{ + prelude::{Just, Strategy}, + prop_assert, prop_assert_eq, prop_assume, proptest, + }; + use test::Bencher; + fn beacon_test_val() -> impl Strategy>> { + Just(vec![ + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + pos: Position { x: 1600, y: 1600 }, + ty: 2, + }), + }), + ActionType::SetRecipe(SetRecipeInfo { + pos: Position { x: 1600, y: 1600 }, + recipe: Recipe { id: 0 }, + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Beacon { + pos: Position { x: 1603, y: 1600 }, + ty: 0, + }), + }), + ActionType::AddModules { + pos: Position { x: 1603, y: 1600 }, + modules: vec![0, 0], + }, + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::PowerPole { + pos: Position { x: 1603, y: 1599 }, + ty: 0, + }), + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::PowerPole { + pos: Position { x: 1608, y: 1599 }, + ty: 0, + }), + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::PowerPole { + pos: Position { x: 1613, y: 1599 }, + ty: 0, + }), + }), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::SolarPanel { + pos: Position { x: 1600, y: 1597 }, + ty: 0, + }), + }), + ]) + } + + fn full_beacon() -> impl Strategy>> { + Just(ron::de::from_reader(File::open("test_blueprints/full_beacons.bp").unwrap()).unwrap()) + .prop_map(|bp: Blueprint| bp.actions) + } + proptest! { - // #![proptest_config(ProptestConfig::with_cases(1_000))] #[test] fn test_random_blueprint_does_not_crash(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..1_000, &DATA_STORE)) { @@ -1465,13 +2439,90 @@ mod tests { game_state.update(&DATA_STORE) } } + + #[test] + fn test_beacons_always_effect(actions in beacon_test_val().prop_shuffle()) { + prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + .. + }), + }))) < actions.iter().position(|a| matches!(a, ActionType::SetRecipe(_)))); + // prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { + // entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + // .. + // }), + // }))) < actions.iter().position(|a| matches!(a, ActionType::AddModules { + // pos: Position { x: 1600, y: 1600 }, + // .. + // }))); + + let mut game_state = GameState::new(&DATA_STORE); + + Blueprint { actions }.apply(Position { x: 0, y: 0 }, &mut game_state, &DATA_STORE); + + for _ in 0usize..10 { + game_state.update(&DATA_STORE); + } + + let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, .. }) = game_state.world.get_entities_colliding_with(Position { x: 1600, y: 1600 }, (1,1), &DATA_STORE).into_iter().next() else { + unreachable!("{:?}", game_state.world.get_entities_colliding_with(Position { x: 1600, y: 1600 }, (1,1), &DATA_STORE).into_iter().next()); + }; + + prop_assume!(game_state.simulation_state.factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); + + let info = game_state.simulation_state.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); + 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_eq!(info.base_power_consumption, Watt(375_000), "base_power_consumption: {:?}", info.base_power_consumption); + + } + + #[test] + fn test_full_beacons_always_effect(actions in full_beacon().prop_shuffle()) { + prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + .. + }), + }))) < actions.iter().position(|a| matches!(a, ActionType::SetRecipe(_)))); + prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { + .. + }), + }))) < actions.iter().position(|a| matches!(a, ActionType::AddModules { pos: Position { x: 24, y: 21 }, ..}))); + + let mut game_state = GameState::new(&DATA_STORE); + + Blueprint { actions }.apply(Position { x: 1600, y: 1600 }, &mut game_state, &DATA_STORE); + + for _ in 0usize..10 { + game_state.update(&DATA_STORE); + } + + let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, .. }) = game_state.world.get_entities_colliding_with(Position { x: 1624, y: 1621 }, (1,1), &DATA_STORE).into_iter().next() else { + unreachable!("{:?}", game_state.world.get_entities_colliding_with(Position { x: 1624, y: 1621 }, (1,1), &DATA_STORE).into_iter().next()); + }; + + prop_assume!(game_state.simulation_state.factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); + + let info = game_state.simulation_state.factory.power_grids.power_grids[usize::from(id.grid)].get_assembler_info(*id, &DATA_STORE); + + prop_assert_eq!(info.base_power_consumption, Watt(375_000), "base_power_consumption: {:?}", info.base_power_consumption); + prop_assert!((info.base_speed - 1.25).abs() < 1.0e-6, "base_speed: {:?}", info.base_speed); + prop_assert!((info.prod_mod - 0.4).abs() < 1.0e-6, "prod_mod: {:?}", info.prod_mod); + prop_assert!((info.speed_mod - (5.4)).abs() < 1.0e-6, "speed_mod: {:?}", info.speed_mod); + prop_assert!((info.power_consumption_mod - 11.60).abs() < 1.0e-6, "power_consumption_mod: {:?}", info.power_consumption_mod); + + } } #[bench] fn bench_single_inserter(b: &mut Bencher) { let mut game_state = GameState::new(&DATA_STORE); - let mut rep = Replay::new(game_state.clone(), (*DATA_STORE).clone()); + let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( @@ -1512,6 +2563,7 @@ mod tests { crate::frontend::world::tile::PlaceEntityType::Belt { pos: Position { x: 1, y: 4 }, direction: crate::frontend::world::tile::Dir::East, + ty: 0, }, ), })]); @@ -1521,6 +2573,7 @@ mod tests { crate::frontend::world::tile::PlaceEntityType::Belt { pos: Position { x: 2, y: 4 }, direction: crate::frontend::world::tile::Dir::East, + ty: 0, }, ), })]); @@ -1539,13 +2592,15 @@ mod tests { blueprint.apply(Position { x: 1600, y: 1600 }, &mut game_state, &DATA_STORE); - dbg!(&game_state - .world - .get_chunk_for_tile(Position { x: 1600, y: 1600 })); + dbg!( + &game_state + .world + .get_chunk_for_tile(Position { x: 1600, y: 1600 }) + ); dbg!(game_state.current_tick); - for _ in 0..60 { + for _ in 0..600 { game_state.update(&DATA_STORE); } @@ -1555,17 +2610,14 @@ mod tests { dbg!(game_state.current_tick); - // dbg!(&game_state.simulation_state.factory.belts); - assert!( - dbg!( - game_state - .statistics - .production - .total - .unwrap() - .items_produced - )[0] > 0 + game_state + .statistics + .production + .total + .unwrap() + .items_produced[0] + > 0 ); } } diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 8af3de8..b955801 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -1,14 +1,27 @@ use std::{ mem, - sync::{mpsc::Sender, Arc, LazyLock, Mutex}, + net::ToSocketAddrs, + sync::{ + Arc, LazyLock, + atomic::{AtomicU64, Ordering}, + mpsc::{Sender, channel}, + }, + thread, time::Instant, }; +use directories::ProjectDirs; +use parking_lot::Mutex; + +use crate::StartGameInfo; +use crate::{GameCreationInfo, run_client}; +use crate::{rendering::render_world::EscapeMenuOptions, run_integrated_server}; use eframe::{ egui::{CentralPanel, Event, PaintCallbackInfo, Shape}, egui_wgpu::{self, CallbackTrait}, }; -use log::warn; +use egui::{Color32, CursorIcon, Modal, ProgressBar, RichText, TextEdit, Window}; +use log::{error, warn}; use tilelib::types::RawRenderer; use crate::{ @@ -18,11 +31,11 @@ use crate::{ }; use super::{ + TextureAtlas, app_state::{AppState, GameState}, render_world::{render_ui, render_world}, texture_atlas, window::{LoadedGame, LoadedGameInfo}, - TextureAtlas, }; use crate::saving::save; @@ -34,13 +47,14 @@ pub struct App { last_rendered_update: u64, - input_sender: Sender, + pub input_sender: Option>, texture_atlas: Arc, } impl App { - pub fn new(cc: &eframe::CreationContext, input_sender: Sender) -> Self { + #[must_use] + pub fn new(cc: &eframe::CreationContext) -> Self { let render_state = cc.wgpu_render_state.as_ref().unwrap(); let atlas = Arc::new(texture_atlas()); @@ -50,8 +64,8 @@ impl App { &render_state.queue, render_state.target_format, ), - input_sender, - state: AppState::Ingame, + input_sender: None, + state: AppState::MainMenu { in_ip_box: None }, texture_atlas: atlas, currently_loaded_game: None, last_rendered_update: 0, @@ -63,30 +77,355 @@ impl eframe::App for App { fn update(&mut self, ctx: &eframe::egui::Context, frame: &mut eframe::Frame) { ctx.request_repaint(); + match &mut self.state { + AppState::Ingame => {}, + _ => { + self.raw_renderer.reset_runtime_textures(); + }, + } + + match &mut self.state { + AppState::Ingame => { + self.update_ingame(ctx, frame); + }, + + AppState::Loading { + progress, + game_state_receiver, + } => { + let progress = f64::from_bits(progress.load(Ordering::Relaxed)); + + CentralPanel::default().show(ctx, |ui| { + Window::new("Loading") + .default_pos((0.5, 0.5)) + .show(ctx, |ui| { + ui.add(ProgressBar::new(progress as f32).corner_radius(0)); + }); + }); + + if let Ok((new_state, current_tick, input_sender)) = game_state_receiver.try_recv() + { + self.input_sender = Some(input_sender); + self.currently_loaded_game = Some(LoadedGameInfo { + state: new_state, + tick: current_tick, + }); + self.state = AppState::Ingame; + } + }, + + AppState::MainMenu { in_ip_box } => { + if let Some((current_text, error_pupup)) = in_ip_box { + if ctx.input(|input| input.key_pressed(egui::Key::Escape)) { + *in_ip_box = None; + return; + } + + let popup = Modal::new("Ip Box".into()); + + if let Some(ip) = popup + .show(ctx, |ui| { + let text = TextEdit::singleline(current_text) + .char_limit(100) + .hint_text("ip:port"); + + let ret = if (text.show(ui).response.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter))) + || ui.button("Connect").clicked() + { + match current_text.to_socket_addrs() { + Ok(mut addr) => match addr.next() { + Some(addr) => Some(addr), + None => { + *error_pupup = true; + None + }, + }, + Err(_) => { + *error_pupup = true; + None + }, + } + } else { + None + }; + + if *error_pupup { + ui.label( + RichText::new("Invalid IP. Example: 127.0.0.1:8080") + .color(Color32::RED), + ); + } + + ret + }) + .inner + { + let progress = Arc::new(AtomicU64::new(0f64.to_bits())); + let (send, recv) = channel(); + + send.send(run_client(ip)); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + + return; + } + } + + CentralPanel::default().show(ctx, |ui| { + if ui.button("Load").clicked() { + if let Some(path) = rfd::FileDialog::new() + .set_directory( + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir(), + ) + .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::Load(path), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } + } else if ui.button("Load Debug Save").clicked() { + if let Some(path) = rfd::FileDialog::new() + .set_directory( + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir(), + ) + .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::LoadReadable(path), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } + } else if ui.button("Connect over network").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(); + thread::spawn(move || { + send.send(run_integrated_server( + progress_send, + StartGameInfo::Create(GameCreationInfo::Empty), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } else if ui.button("Red Green Chest Insertion").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::RedGreen), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } else if ui.button("Red Green 1 to 1 belts").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::RedGreenBelts), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } else if ui.button("Red with labs").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::RedWithLabs), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } else if ui.button("Gigabase").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::Gigabase), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } else if ui.button("With bp file").clicked() { + 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)), + )); + }); + + self.state = AppState::Loading { + progress, + game_state_receiver: recv, + }; + } + } + }); + }, + } + } + + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + if let Some(state) = &self.currently_loaded_game { + match &state.state { + LoadedGame::ItemU8RecipeU8(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU8RecipeU16(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU16RecipeU8(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU16RecipeU16(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + } + } + } +} + +impl App { + fn update_ingame(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { let size = ctx.available_rect(); CentralPanel::default().show(ctx, |ui| { + if ui.ui_contains_pointer() { + ctx.set_cursor_icon(CursorIcon::Default); + } let painter = ui.painter(); if let Some(game) = &self.currently_loaded_game { - // if !ctx.wants_keyboard_input() && !ctx.wants_pointer_input() { + // Only create game input actions if the ui does not currently want input + let wants_pointer = ctx.wants_pointer_input(); + let wants_keyboard = ctx.wants_keyboard_input(); + 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; + } + Ok(Input::MouseMove( - dest.x / size.width(), - dest.y / size.height(), + pos_normalized[0] - 0.5, + (pos_normalized[1] - 0.5) / ar, )) } else { event.clone().try_into() }; if let Ok(input) = input { - self.input_sender.send(input).expect("Could not send input"); + if self.input_sender.as_mut().unwrap().send(input).is_err() { + #[cfg(not(test))] + panic!("Could not send input"); + error!("Could not send input"); + } } } }); - // } match &game.state { LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { @@ -101,8 +440,9 @@ impl eframe::App for App { size, cb, ))); - let mut game_state = loaded_game_sized.state.lock().unwrap(); - let mut state_machine = loaded_game_sized.state_machine.lock().unwrap(); + let game_state = loaded_game_sized.state.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); @@ -111,27 +451,28 @@ impl eframe::App for App { let mut now = Instant::now(); - mem::swap(&mut now, &mut *LAST_DRAW.lock().unwrap()); - - let time_since_last_update = now.elapsed(); - - // dbg!(tick - self.last_rendered_update); + mem::swap(&mut now, &mut *LAST_DRAW.lock()); self.last_rendered_update = tick; - let render_actions = render_ui( - ctx, - &ui, - &mut state_machine, - &game_state, - &loaded_game_sized.data_store, - ); - - for action in render_actions { - loaded_game_sized - .ui_action_sender - .send(action) - .expect("Ui action channel died"); + match render_ui(ctx, ui, state_machine, game_state, data_store) { + Ok(render_actions) => { + for action in render_actions { + loaded_game_sized + .ui_action_sender + .send(action) + .expect("Ui action channel died"); + } + }, + Err(escape) => match escape { + EscapeMenuOptions::BackToMainMenu => { + 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!(), @@ -143,29 +484,6 @@ impl eframe::App for App { } }); } - - fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { - if let Some(state) = &self.currently_loaded_game { - match &state.state { - LoadedGame::ItemU8RecipeU8(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU8RecipeU16(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU16RecipeU8(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU16RecipeU16(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - } - } - } } #[derive(Debug, Clone)] @@ -174,7 +492,7 @@ struct Callback { texture_atlas: Arc, state_machine: Arc>>, game_state: Arc>>, - data_store: Arc>, + data_store: Arc>>, } impl CallbackTrait @@ -184,20 +502,26 @@ impl CallbackTrait &self, info: PaintCallbackInfo, render_pass: &mut eframe::wgpu::RenderPass<'static>, - callback_resources: &egui_wgpu::CallbackResources, + _callback_resources: &egui_wgpu::CallbackResources, ) { - let mut rend = self.raw_renderer.start_draw(render_pass); + let mut rend = self.raw_renderer.start_draw( + render_pass, + [ + info.viewport_in_pixels().width_px as f32, + info.viewport_in_pixels().height_px as f32, + ], + ); - let gamestate = self.game_state.lock().unwrap(); + let gamestate = self.game_state.lock(); - let state_machine = self.state_machine.lock().unwrap(); + let state_machine = self.state_machine.lock(); render_world( &mut rend, - &gamestate, + gamestate, &self.texture_atlas, - &state_machine, - &self.data_store, + state_machine, + &self.data_store.lock(), ); } } diff --git a/src/rendering/map_view.rs b/src/rendering/map_view.rs new file mode 100644 index 0000000..c23920e --- /dev/null +++ b/src/rendering/map_view.rs @@ -0,0 +1,258 @@ +use std::cmp::min; + +use egui::Color32; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use tilelib::types::{DrawInstance, Layer, RendererTrait}; + +use crate::{ + frontend::world::{Position, tile::World}, + item::IdxTrait, +}; + +pub struct MapViewUpdate { + pub pos: Position, + pub color: Color32, +} + +const NUM_MAP_TILE_SIZES: usize = 4; +// TODO: Figure out a good tilesize. 1024 seems to work fine, but is larger or smaller better? +const TILE_SIZE_PIXELS: [usize; NUM_MAP_TILE_SIZES] = [1024, 1024, 1024, 1024]; +// TODO: Since array::map is not const, we hack it like this +const NUM_TILES_PER_AXIS: [usize; NUM_MAP_TILE_SIZES] = { + let mut b = [0; NUM_MAP_TILE_SIZES]; + let mut i = 0; + while i < NUM_MAP_TILE_SIZES { + b[i] = 2_000_000usize.div_ceil(TILE_SIZE_PIXELS[i]); // map + i += 1; + } + b +}; +const TILE_PIXEL_TO_WORLD_TILE: [usize; NUM_MAP_TILE_SIZES] = [1, 4, 16, 64]; +const MIN_WIDTH: [usize; NUM_MAP_TILE_SIZES] = [0, 5_000, 10_000, 50_000]; + +#[profiling::function] +pub fn create_map_textures_if_needed( + world: &World, + renderer: &mut impl RendererTrait, + camera_pos: Position, + view_width_in_tiles: usize, + view_height_in_tiles: usize, + data_store: &crate::data::DataStore, +) { + let (map_tile_size, pixel_to_tile, num_per_axis, texture_id_offset) = { + let idx = MIN_WIDTH + .iter() + .filter(|min| **min < view_width_in_tiles) + .count() + - 1; + ( + TILE_SIZE_PIXELS[idx], + TILE_PIXEL_TO_WORLD_TILE[idx], + NUM_TILES_PER_AXIS[idx], + NUM_TILES_PER_AXIS[0..idx] + .iter() + .map(|v| v * v) + .sum::(), + ) + }; + + let tile_x_left_edge = (usize::try_from(camera_pos.x + 1_000_000) + .unwrap() + .saturating_sub(view_width_in_tiles.div_ceil(2))) + / pixel_to_tile + / map_tile_size; + let tile_y_left_edge = (usize::try_from(camera_pos.y + 1_000_000) + .unwrap() + .saturating_sub(view_height_in_tiles.div_ceil(2))) + / pixel_to_tile + / map_tile_size; + let tile_x_right_edge = min( + usize::try_from(camera_pos.x + 1_000_000) + .unwrap() + .saturating_add(view_width_in_tiles.div_ceil(2)), + 2_000_000, + ) / pixel_to_tile + / map_tile_size; + let tile_y_right_edge = min( + usize::try_from(camera_pos.y + 1_000_000) + .unwrap() + .saturating_add(view_height_in_tiles.div_ceil(2)), + 2_000_000, + ) / pixel_to_tile + / map_tile_size; + + for tile_x in tile_x_left_edge..=(min(tile_x_right_edge, num_per_axis)) { + for tile_y in tile_y_left_edge..=(min(tile_y_right_edge, num_per_axis)) { + let tile_texture_id = tile_x * num_per_axis + tile_y + texture_id_offset; + + renderer.create_runtime_texture_if_missing(tile_texture_id, [map_tile_size; 2], || { + profiling::scope!("Collect Entity Colors"); + let data: Vec = ((tile_y * map_tile_size)..((tile_y + 1) * map_tile_size)) + .into_par_iter() + .flat_map(|y_pos| { + rayon::iter::repeat(y_pos).zip( + ((tile_x * map_tile_size)..((tile_x + 1) * map_tile_size)) + .into_par_iter(), + ) + }) + .flat_map_iter(|(y_pos, x_pos)| { + let x_pos_world = + (i32::try_from(x_pos * pixel_to_tile).unwrap() - 1_000_000) as i32; + let y_pos_world = + (i32::try_from(y_pos * pixel_to_tile).unwrap() - 1_000_000) as i32; + + let color = world.get_entity_color( + Position { + x: x_pos_world, + y: y_pos_world, + }, + data_store, + ); + + let color = [color.r(), color.g(), color.b(), 255]; + + color + }) + .collect(); + + data + }); + } + } +} + +#[profiling::function] +pub fn apply_updates( + updates: impl IntoIterator, + renderer: &mut impl RendererTrait, +) { + for update in updates { + for tile_idx in 0..NUM_MAP_TILE_SIZES { + let (map_tile_size, pixel_to_tile, num_per_axis, texture_id_offset) = { + ( + TILE_SIZE_PIXELS[tile_idx], + TILE_PIXEL_TO_WORLD_TILE[tile_idx], + NUM_TILES_PER_AXIS[tile_idx], + NUM_TILES_PER_AXIS[0..tile_idx] + .iter() + .map(|v| v * v) + .sum::(), + ) + }; + + if update.pos.x % pixel_to_tile as i32 == 0 && update.pos.y % pixel_to_tile as i32 == 0 + { + let texture_id = (usize::try_from(update.pos.x + 1_000_000).unwrap() + / pixel_to_tile + / map_tile_size) + * num_per_axis + + (usize::try_from(update.pos.y + 1_000_000).unwrap() + / pixel_to_tile + / map_tile_size) + + texture_id_offset; + + let x = (usize::try_from(update.pos.x + 1_000_000).unwrap() / pixel_to_tile) + % map_tile_size; + let y = (usize::try_from(update.pos.y + 1_000_000).unwrap() / pixel_to_tile) + % map_tile_size; + + renderer.do_texture_updates( + texture_id, + [( + x, + y, + [update.color.r(), update.color.g(), update.color.b(), 255], + )], + ); + } + } + } +} + +#[profiling::function] +pub fn render_map_view( + renderer: &mut impl RendererTrait, + camera_pos: Position, + view_width: f32, + view_height: f32, + view_width_in_tiles: usize, + view_height_in_tiles: usize, + tile_size: f32, + aspect_ratio: f32, + player_pos: (f32, f32), +) { + let (map_tile_size, pixel_to_tile, num_per_axis, texture_id_offset) = { + let idx = MIN_WIDTH + .iter() + .filter(|min| **min < view_width_in_tiles) + .count() + - 1; + + ( + TILE_SIZE_PIXELS[idx], + TILE_PIXEL_TO_WORLD_TILE[idx], + NUM_TILES_PER_AXIS[idx], + NUM_TILES_PER_AXIS[0..idx] + .iter() + .map(|v| v * v) + .sum::(), + ) + }; + let tile_x_left_edge = (usize::try_from(camera_pos.x + 1_000_000) + .unwrap() + .saturating_sub(view_width_in_tiles.div_ceil(2))) + / pixel_to_tile + / map_tile_size; + let tile_y_left_edge = (usize::try_from(camera_pos.y + 1_000_000) + .unwrap() + .saturating_sub(view_height_in_tiles.div_ceil(2))) + / pixel_to_tile + / map_tile_size; + let tile_x_right_edge = min( + usize::try_from(camera_pos.x + 1_000_000) + .unwrap() + .saturating_add(view_width_in_tiles.div_ceil(2)), + 2_000_000, + ) / pixel_to_tile + / map_tile_size; + let tile_y_right_edge = min( + usize::try_from(camera_pos.y + 1_000_000) + .unwrap() + .saturating_add(view_height_in_tiles.div_ceil(2)), + 2_000_000, + ) / pixel_to_tile + / map_tile_size; + + let mut map_layer = Layer::square_tile_grid(tile_size, aspect_ratio); + for tile_x in tile_x_left_edge..=(min(tile_x_right_edge, num_per_axis)) { + for tile_y in tile_y_left_edge..=(min(tile_y_right_edge, num_per_axis)) { + let texture_id = tile_x * num_per_axis + tile_y + texture_id_offset; + + let tile_draw_offs = ( + (tile_x * map_tile_size * pixel_to_tile) as f32 - 1_000_000.0 - player_pos.0 + + (0.5 * view_width), + (tile_y * map_tile_size * pixel_to_tile) as f32 - 1_000_000.0 - player_pos.1 + + (0.5 * view_height), + ); + + 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, + }, + ); + // map_layer.draw_sprite( + // &Sprite::new(Texture::default()), + // DrawInstance { + // position: [tile_draw_offs.0, tile_draw_offs.1], + // size: [MEDIUM_SIZE as f32; 2], + // animation_frame: 0, + // }, + // ); + } + } + + renderer.draw(&map_layer); +} diff --git a/src/rendering/mod.rs b/src/rendering/mod.rs index 51151e4..4011f7e 100644 --- a/src/rendering/mod.rs +++ b/src/rendering/mod.rs @@ -1,150 +1,516 @@ use image::GenericImageView; -use tilelib::types::{Sprite, Texture}; +use tilelib::types::{DrawInstance, Layer, Sprite, Texture}; -use crate::frontend::world::tile::Dir; +use crate::frontend::world::tile::{Dir, UndergroundDir}; pub mod app_state; pub mod eframe_app; -mod render_world; +pub mod render_world; pub mod window; +pub mod map_view; + +use enum_map::{Enum, EnumArray}; + +#[derive(Debug, Clone)] +struct EntitySprite { + pub sprite: Sprite, + pub aspect_ratio: f32, + pub offset: (f32, f32), + pub scaling: (f32, f32), +} + +impl EntitySprite { + const fn new_tiling(sprite: Sprite) -> Self { + Self { + sprite, + aspect_ratio: 1.0, + offset: (0.0, 0.0), + scaling: (1.0, 1.0), + } + } + + const fn new_scaled(sprite: Sprite, size: f32) -> Self { + Self { + sprite, + aspect_ratio: 1.0, + offset: (0.0, 0.0), + scaling: (size, size), + } + } + + const fn new_scaled_centered(sprite: Sprite, size: f32) -> Self { + Self { + sprite, + aspect_ratio: 1.0, + offset: (-(size - 1.0) / 2.0, -(size - 1.0) / 2.0), + scaling: (size, size), + } + } + + fn new_tall(sprite: Sprite, aspect_ratio: f32) -> Self { + assert!(aspect_ratio < 1.0); + let height = 1.0 / aspect_ratio; + Self { + sprite, + aspect_ratio, + offset: (0.0, -(height - 1.0)), + scaling: (1.0, height), + } + } + + fn draw(&self, pos: [f32; 2], tile_size: [u16; 2], animation_frame: u32, layer: &mut Layer) { + layer.draw_sprite( + &self.sprite, + DrawInstance { + position: [ + pos[0] + (self.offset.0 * tile_size[0] as f32), + pos[1] + (self.offset.1 * tile_size[1] as f32), + ], + size: [ + f32::from(tile_size[0]) * self.scaling.0, + f32::from(tile_size[1]) * self.scaling.1, + ], + animation_frame, + }, + ); + } + + fn draw_centered_on( + &self, + underlying: &Self, + pos: [f32; 2], + tile_size: [u16; 2], + animation_frame: u32, + layer: &mut Layer, + ) { + let other_pos = [pos[0] + underlying.offset.0, pos[1] + underlying.offset.1]; + let other_size = [ + f32::from(tile_size[0]) * underlying.scaling.0, + f32::from(tile_size[1]) * underlying.scaling.1, + ]; + let self_size = [self.scaling.0, self.scaling.1]; + + let unused_space = [other_size[0] - self_size[0], other_size[1] - self_size[1]]; + let self_pos = [ + other_pos[0] + unused_space[0] / 2.0, + other_pos[1] + unused_space[1] / 2.0, + ]; + + layer.draw_sprite( + &self.sprite, + DrawInstance { + position: self_pos, + size: self_size, + animation_frame, + }, + ); + } +} + +#[derive(Debug, Clone, Copy, Enum)] +enum BeltSide { + Left, + Right, +} + +#[derive(Debug, Clone, Copy)] +struct Corner { + to_dir: Dir, + from_dir: BeltSide, +} + +impl Enum for Corner { + const LENGTH: usize = Dir::LENGTH * BeltSide::LENGTH; + + fn from_usize(value: usize) -> Self { + let (to, from) = (value / BeltSide::LENGTH, value % BeltSide::LENGTH); + + Self { + to_dir: Dir::from_usize(to), + from_dir: BeltSide::from_usize(from), + } + } + + fn into_usize(self) -> usize { + let Self { from_dir, to_dir } = self; + + from_dir.into_usize() + to_dir.into_usize() * BeltSide::LENGTH + } +} + +impl EnumArray for Corner { + type Array = [V; Self::LENGTH]; +} + #[derive(Debug)] pub struct TextureAtlas { outside_world: Sprite, blue: Sprite, - assembler: Sprite, - no_power: Sprite, - not_connected: Sprite, - belt: enum_map::EnumMap, + chest: EntitySprite, + + assembler: EntitySprite, + no_power: EntitySprite, + not_connected: EntitySprite, + belt: enum_map::EnumMap, + belt_corners: enum_map::EnumMap, inserter: enum_map::EnumMap, player: Sprite, items: Box<[Sprite]>, + beacon: EntitySprite, + power_pole: EntitySprite, + + lab: EntitySprite, + + pub dark_square: Sprite, + + underground: enum_map::EnumMap>, + default: Sprite, } -fn texture_atlas() -> TextureAtlas { - let black = include_bytes!("temp_assets/outside_world.png"); - let black = image::load_from_memory(black).unwrap(); +macro_rules! sprite_from_path { + ($path:literal, $number_anim_frames:expr_2021) => {{ + let sprite = include_bytes!($path); + let sprite = image::load_from_memory(sprite).unwrap(); - let black_dimensions = black.dimensions(); - let black = black.to_rgba8().into_vec(); + let sprite_dimensions = sprite.dimensions(); + let sprite = sprite.to_rgba8().into_vec(); - let blue = include_bytes!("temp_assets/blue.png"); - let blue = image::load_from_memory(blue).unwrap(); + Sprite::new(Texture::new($number_anim_frames, sprite, sprite_dimensions)) + }}; +} - let blue_dimensions = blue.dimensions(); - let blue = blue.to_rgba8().into_vec(); +macro_rules! entity_sprite_from_path_scaled { + ($path:literal, $number_anim_frames:expr_2021, $size:expr_2021) => {{ + let sprite = include_bytes!($path); + let sprite = image::load_from_memory(sprite).unwrap(); - let assembler = include_bytes!("temp_assets/assembler.png"); - let assembler = image::load_from_memory(assembler).unwrap(); + let sprite_dimensions = sprite.dimensions(); + let sprite = sprite.to_rgba8().into_vec(); - let assembler_dimensions = assembler.dimensions(); - let assembler = assembler.to_rgba8().into_vec(); + EntitySprite::new_scaled( + Sprite::new(Texture::new($number_anim_frames, sprite, sprite_dimensions)), + $size, + ) + }}; +} - let belt_north = include_bytes!("temp_assets/belt_north.png"); - let belt_north = image::load_from_memory(belt_north).unwrap(); +macro_rules! entity_sprite_from_path_scaled_centered { + ($path:literal, $number_anim_frames:expr_2021, $size:expr_2021) => {{ + let sprite = include_bytes!($path); + let sprite = image::load_from_memory(sprite).unwrap(); - let belt_north_dimensions = belt_north.dimensions(); - let belt_north = belt_north.to_rgba8().into_vec(); + let sprite_dimensions = sprite.dimensions(); + let sprite = sprite.to_rgba8().into_vec(); - let belt_south = include_bytes!("temp_assets/belt_south.png"); - let belt_south = image::load_from_memory(belt_south).unwrap(); + EntitySprite::new_scaled_centered( + Sprite::new(Texture::new($number_anim_frames, sprite, sprite_dimensions)), + $size, + ) + }}; +} - let belt_south_dimensions = belt_south.dimensions(); - let belt_south = belt_south.to_rgba8().into_vec(); +macro_rules! entity_sprite_from_path_tiling { + ($path:literal, $number_anim_frames:expr_2021) => {{ + let sprite = include_bytes!($path); + let sprite = image::load_from_memory(sprite).unwrap(); - let belt_west = include_bytes!("temp_assets/belt_west.png"); - let belt_west = image::load_from_memory(belt_west).unwrap(); + let sprite_dimensions = sprite.dimensions(); + let sprite = sprite.to_rgba8().into_vec(); - let belt_west_dimensions = belt_west.dimensions(); - let belt_west = belt_west.to_rgba8().into_vec(); + EntitySprite::new_tiling(Sprite::new(Texture::new( + $number_anim_frames, + sprite, + sprite_dimensions, + ))) + }}; +} - let belt_east = include_bytes!("temp_assets/belt_east.png"); - let belt_east = image::load_from_memory(belt_east).unwrap(); +macro_rules! entity_sprite_from_path_tall { + ($path:literal, $number_anim_frames:expr_2021, $ar:expr_2021) => {{ + let sprite = include_bytes!($path); + let sprite = image::load_from_memory(sprite).unwrap(); - let belt_east_dimensions = belt_east.dimensions(); - let belt_east = belt_east.to_rgba8().into_vec(); + let sprite_dimensions = sprite.dimensions(); + let sprite = sprite.to_rgba8().into_vec(); - let inserter_north = include_bytes!("temp_assets/inserter_north.png"); - let inserter_north = image::load_from_memory(inserter_north).unwrap(); + EntitySprite::new_tall( + Sprite::new(Texture::new($number_anim_frames, sprite, sprite_dimensions)), + $ar, + ) + }}; +} - let inserter_north_dimensions = inserter_north.dimensions(); - let inserter_north = inserter_north.to_rgba8().into_vec(); +#[cfg(not(feature = "graphics"))] +fn texture_atlas() -> TextureAtlas { + let belts: enum_map::EnumMap = enum_map::EnumMap::from_array([ + entity_sprite_from_path_tiling!("temp_assets/belt_north.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_east.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_south.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_west.png", 1), + ]); - let inserter_south = include_bytes!("temp_assets/inserter_south.png"); - let inserter_south = image::load_from_memory(inserter_south).unwrap(); + let undergrounds = enum_map::EnumMap::from_fn(|dir| { + enum_map::EnumMap::from_array([belts[dir].clone(), belts[dir].clone()]) + }); - let inserter_south_dimensions = inserter_south.dimensions(); - let inserter_south = inserter_south.to_rgba8().into_vec(); + TextureAtlas { + outside_world: sprite_from_path!("temp_assets/outside_world.png", 1), + blue: sprite_from_path!("temp_assets/light_gray.png", 1), - let inserter_west = include_bytes!("temp_assets/inserter_west.png"); - let inserter_west = image::load_from_memory(inserter_west).unwrap(); + not_connected: entity_sprite_from_path_scaled!("temp_assets/not_connected.png", 1, 3.0), - let inserter_west_dimensions = inserter_west.dimensions(); - let inserter_west = inserter_west.to_rgba8().into_vec(); + no_power: entity_sprite_from_path_scaled!("temp_assets/no_power.png", 1, 3.0), - let inserter_east = include_bytes!("temp_assets/inserter_east.png"); - let inserter_east = image::load_from_memory(inserter_east).unwrap(); + assembler: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), + chest: entity_sprite_from_path_tiling!("temp_assets/outside_world.png", 1), - let inserter_east_dimensions = inserter_east.dimensions(); - let inserter_east = inserter_east.to_rgba8().into_vec(); + items: vec![sprite_from_path!("temp_assets/plate.png", 1); 200].into_boxed_slice(), - let plate = include_bytes!("temp_assets/plate.png"); - let plate = image::load_from_memory(plate).unwrap(); + player: sprite_from_path!("temp_assets/player.png", 1), + belt: belts, - let plate_dimensions = plate.dimensions(); - let plate = plate.to_rgba8().into_vec(); + belt_corners: enum_map::EnumMap::from_array([ + entity_sprite_from_path_tiling!("temp_assets/belt_north.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_north.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_east.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_east.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_south.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_south.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_west.png", 1), + entity_sprite_from_path_tiling!("temp_assets/belt_west.png", 1), + ]), - let player = include_bytes!("temp_assets/player.png"); - let player = image::load_from_memory(player).unwrap(); + inserter: enum_map::EnumMap::from_array([ + sprite_from_path!("temp_assets/inserter_north.png", 1), + sprite_from_path!("temp_assets/inserter_east.png", 1), + sprite_from_path!("temp_assets/inserter_south.png", 1), + sprite_from_path!("temp_assets/inserter_west.png", 1), + ]), - let player_dimensions = player.dimensions(); - let player = player.to_rgba8().into_vec(); + beacon: entity_sprite_from_path_tiling!("temp_assets/beacon.png", 1), + power_pole: entity_sprite_from_path_tiling!("temp_assets/assembler.png", 1), - let not_connected = include_bytes!("temp_assets/not_connected.png"); - let not_connected = image::load_from_memory(not_connected).unwrap(); + lab: entity_sprite_from_path_tiling!("temp_assets/belt_north.png", 1), - let not_connected_dimensions = not_connected.dimensions(); - let not_connected = not_connected.to_rgba8().into_vec(); + dark_square: sprite_from_path!("temp_assets/dark_square.png", 1), - let no_power = include_bytes!("temp_assets/no_power.png"); - let no_power = image::load_from_memory(no_power).unwrap(); + underground: undergrounds, - let no_power_dimensions = no_power.dimensions(); - let no_power = no_power.to_rgba8().into_vec(); + default: Sprite::new(Texture::default()), + } +} +#[cfg(feature = "graphics")] +fn texture_atlas() -> TextureAtlas { TextureAtlas { - outside_world: Sprite::new(Texture::new(1, black, black_dimensions)), - assembler: Sprite::new(Texture::new(1, assembler, assembler_dimensions)), - blue: Sprite::new(Texture::new(1, blue, blue_dimensions)), + outside_world: sprite_from_path!("temp_assets/outside_world.png", 1), + blue: sprite_from_path!("temp_assets/light_gray.png", 1), - not_connected: Sprite::new(Texture::new(1, not_connected, not_connected_dimensions)), + not_connected: entity_sprite_from_path_scaled!("temp_assets/not_connected.png", 1, 3.0), - no_power: Sprite::new(Texture::new(1, no_power, no_power_dimensions)), + no_power: entity_sprite_from_path_scaled!("temp_assets/no_power.png", 1, 3.0), + + assembler: entity_sprite_from_path_tiling!("temp_assets/krastorio/furnace.png", 7 * 4), + chest: entity_sprite_from_path_tiling!("temp_assets/krastorio/chest.png", 1), items: vec![ - Sprite::new(Texture::new(1, plate.clone(), plate_dimensions)), - Sprite::new(Texture::new(1, plate, plate_dimensions)), + sprite_from_path!("temp_assets/krastorio/enriched-iron.png", 1), + sprite_from_path!("temp_assets/krastorio/enriched-copper.png", 1), + sprite_from_path!("temp_assets/krastorio/iron-plate.png", 1), + sprite_from_path!("temp_assets/krastorio/copper-plate.png", 1), + sprite_from_path!("temp_assets/krastorio/iron-gear-wheel.png", 1), + sprite_from_path!("temp_assets/krastorio/automation-tech-card.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/krastorio/electronic-circuit.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/krastorio/logistic-tech-card.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/krastorio/chemical-tech-card.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), + sprite_from_path!("temp_assets/plate.png", 1), ] .into_boxed_slice(), - player: Sprite::new(Texture::new(1, player, player_dimensions)), + player: sprite_from_path!("temp_assets/player.png", 1), belt: enum_map::EnumMap::from_array([ - Sprite::new(Texture::new(1, belt_north, belt_north_dimensions)), - Sprite::new(Texture::new(1, belt_east, belt_east_dimensions)), - Sprite::new(Texture::new(1, belt_south, belt_south_dimensions)), - Sprite::new(Texture::new(1, belt_west, belt_west_dimensions)), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/north.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/east.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/south.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/west.png", + 32, + 2.0 + ), + ]), + + belt_corners: enum_map::EnumMap::from_array([ + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/west-north.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/east-north.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/north-east.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/south-east.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/east-south.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/west-south.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/south-west.png", + 32, + 2.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/belt/north-west.png", + 32, + 2.0 + ), ]), inserter: enum_map::EnumMap::from_array([ - Sprite::new(Texture::new(1, inserter_north, inserter_north_dimensions)), - Sprite::new(Texture::new(1, inserter_east, inserter_east_dimensions)), - Sprite::new(Texture::new(1, inserter_south, inserter_south_dimensions)), - Sprite::new(Texture::new(1, inserter_west, inserter_west_dimensions)), + sprite_from_path!("temp_assets/inserter_north.png", 1), + sprite_from_path!("temp_assets/inserter_east.png", 1), + sprite_from_path!("temp_assets/inserter_south.png", 1), + sprite_from_path!("temp_assets/inserter_west.png", 1), + ]), + + beacon: entity_sprite_from_path_tiling!("temp_assets/beacon.png", 1), + power_pole: entity_sprite_from_path_tall!( + "temp_assets/krastorio/PowerPole.png", + 1, + 1.0 / 2.0 + ), + + lab: entity_sprite_from_path_tiling!("temp_assets/krastorio/advanced-lab.png", 1), + + dark_square: sprite_from_path!("temp_assets/dark_square.png", 1), + + underground: enum_map::EnumMap::from_array([ + enum_map::EnumMap::from_array([ + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/north-entrance.png", + 1, + 3.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/north-exit.png", + 1, + 3.0 + ), + ]), + enum_map::EnumMap::from_array([ + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/east-entrance.png", + 1, + 3.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/east-exit.png", + 1, + 3.0 + ), + ]), + enum_map::EnumMap::from_array([ + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/south-entrance.png", + 1, + 3.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/south-exit.png", + 1, + 3.0 + ), + ]), + enum_map::EnumMap::from_array([ + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/west-entrance.png", + 1, + 3.0 + ), + entity_sprite_from_path_scaled_centered!( + "temp_assets/krastorio/underground/west-exit.png", + 1, + 3.0 + ), + ]), ]), default: Sprite::new(Texture::default()), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 6d89c0f..2b1a9e3 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,588 +1,1844 @@ -use std::{ - cmp::{max, min}, - iter::successors, -}; - +use crate::chest::ChestSize; +use crate::frontend::action::belt_placement::{BeltState, expected_belt_state}; +use crate::item::Indexable; +use crate::rendering::Corner; use crate::{ + TICKS_PER_SECOND_LOGIC, assembler::AssemblerOnclickInfo, - belt::{belt::Belt, splitter::SPLITTER_BELT_LEN, BeltTileId}, - blueprint::Blueprint, - data::DataStore, + belt::{BeltTileId, belt::BeltLenType, splitter::SPLITTER_BELT_LEN}, + data::{DataStore, ItemRecipeDir, factorio_1_1::get_raw_data_test}, frontend::{ action::{ + ActionType, action_state_machine::{ - ActionStateMachine, ActionStateMachineState, StatisticsPanel, WIDTH_PER_LEVEL, + ActionStateMachine, ActionStateMachineState, HeldObject, StatisticsPanel, + WIDTH_PER_LEVEL, }, set_recipe::SetRecipeInfo, - ActionType, }, - world::tile::{ - AssemblerID, AssemblerInfo, Dir, Entity, BELT_LEN_PER_TILE, CHUNK_SIZE_FLOAT, + world::{ + Position, + tile::{AssemblerInfo, BELT_LEN_PER_TILE, CHUNK_SIZE, CHUNK_SIZE_FLOAT, Dir, Entity}, }, }, - item::{usize_from, IdxTrait, Item, Recipe}, - power::power_grid::MAX_POWER_MULT, + item::{IdxTrait, Item, Recipe, usize_from}, + power::{Joule, Watt, power_grid::MAX_POWER_MULT}, + rendering::map_view::{self, MapViewUpdate, create_map_textures_if_needed}, statistics::{ NUM_SAMPLES_AT_INTERVALS, NUM_X_AXIS_TICKS, RELATIVE_INTERVAL_MULTS, TIMESCALE_LEGEND, }, }; use eframe::egui::{ - self, Align2, Color32, ComboBox, Context, CornerRadius, ProgressBar, Stroke, Ui, Window, + self, Align2, Color32, ComboBox, Context, CornerRadius, Label, Layout, ProgressBar, Stroke, Ui, + Window, }; +use egui::{Button, Modal, RichText, ScrollArea, Sense}; use egui_extras::{Column, TableBuilder}; use egui_plot::{AxisHints, GridMark, Line, Plot, PlotPoints}; -use log::{info, trace, warn}; +use log::{info, trace}; +use parking_lot::MutexGuard; +use petgraph::dot::Dot; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use std::cmp::max; +use std::fs::File; +use std::sync::LazyLock; +use std::{ + cmp::{Ordering, min}, + mem, + time::Duration, +}; use tilelib::types::{DrawInstance, Layer, RendererTrait}; -use super::{app_state::GameState, TextureAtlas}; +use super::{TextureAtlas, app_state::GameState}; + +const BELT_ANIM_SPEED: f32 = 1.0 / (BELT_LEN_PER_TILE as f32); + +const ALT_MODE_ICON_SIZE: f32 = 0.5; + +pub const SWITCH_TO_MAPVIEW_TILES: f32 = if cfg!(debug_assertions) { 200.0 } else { 500.0 }; +pub const SWITCH_TO_MAPVIEW_ZOOM_LEVEL: LazyLock = + LazyLock::new(|| ((SWITCH_TO_MAPVIEW_TILES - 1.0) / WIDTH_PER_LEVEL as f32).log(1.5)); // TODO: I found a weird performance cliff while zooming out, jumping from ~10ms to 20ms suddenly // Investigate! // => This seems to happen at 992 -> 993 width, i.e. when the for loop range jumps from 31 to 32, very suspicous +struct Layers { + tile_layer: Layer, + entity_layer: Layer, + entity_overlay_layer: Layer, + item_layer: Layer, + warning_layer: Layer, + range_layer: Layer, +} + +impl Layers { + pub fn extend(&mut self, other: Self) { + let Self { + tile_layer, + entity_layer, + entity_overlay_layer, + item_layer, + warning_layer, + range_layer, + } = self; + let Self { + tile_layer: other_tile_layer, + entity_layer: other_entity_layer, + entity_overlay_layer: other_entity_overlay_layer, + item_layer: other_item_layer, + warning_layer: other_warning_layer, + range_layer: other_range_layer, + } = other; + tile_layer.extend(other_tile_layer); + entity_layer.extend(other_entity_layer); + entity_overlay_layer.extend(other_entity_overlay_layer); + item_layer.extend(other_item_layer); + warning_layer.extend(other_warning_layer); + range_layer.extend(other_range_layer); + } +} + +fn layers_tile_grid(tilesize: f32, ar: f32) -> Layers { + Layers { + tile_layer: Layer::square_tile_grid(tilesize, ar), + entity_layer: Layer::square_tile_grid(tilesize, ar), + entity_overlay_layer: Layer::square_tile_grid(tilesize, ar), + item_layer: Layer::square_tile_grid(tilesize, ar), + warning_layer: Layer::square_tile_grid(tilesize, ar), + range_layer: Layer::square_tile_grid(tilesize, ar), + } +} + #[allow(clippy::too_many_lines)] +#[profiling::function] pub fn render_world( renderer: &mut impl RendererTrait, - game_state: &GameState, + mut game_state: MutexGuard>, texture_atlas: &TextureAtlas, - state_machine: &ActionStateMachine, + state_machine: MutexGuard>, data_store: &DataStore, ) { - let num_tiles_across_screen = - WIDTH_PER_LEVEL as f32 * state_machine.zoom_level * state_machine.zoom_level; - let tilesize: f32 = 1.0 / num_tiles_across_screen; + let mut updates = Some(vec![]); - let mut tile_layer = Layer::square_tile_grid(tilesize); - let mut entity_layer = Layer::square_tile_grid(tilesize); + mem::swap(&mut updates, &mut game_state.world.map_updates); - let mut item_layer = Layer::square_tile_grid(tilesize); + { + profiling::scope!("map_view::apply_updates"); + map_view::apply_updates( + updates + .into_iter() + .flat_map(|v| v.into_iter()) + .map(|pos| MapViewUpdate { + pos, + color: game_state.world.get_entity_color(pos, data_store), + }), + renderer, + ); + } - let mut player_layer = Layer::square_tile_grid(tilesize); + let ar = renderer.get_aspect_ratio(); - let mut warning_layer = Layer::square_tile_grid(tilesize); + let num_tiles_across_screen_horizontal = + WIDTH_PER_LEVEL as f32 * 1.5f32.powf(state_machine.zoom_level); + let num_tiles_across_screen_vertical = num_tiles_across_screen_horizontal / ar; + let tilesize: f32 = 1.0 / num_tiles_across_screen_horizontal; - let range_layer = Layer::square_tile_grid(tilesize); + let mut state_machine_layer = Layer::square_tile_grid(tilesize, ar); + let mut entity_overlay_layer = Layer::square_tile_grid(tilesize, ar); + let mut player_layer = Layer::square_tile_grid(tilesize, ar); - let player_pos = state_machine.local_player_pos; + let camera_pos = match &state_machine.map_view_info { + Some(map_view_pos) => *map_view_pos, + None => state_machine.local_player_pos, + }; let player_chunk = ( - (player_pos.0 / CHUNK_SIZE_FLOAT) as usize, - (player_pos.1 / CHUNK_SIZE_FLOAT) as usize, + (camera_pos.0 / CHUNK_SIZE_FLOAT) as i32, + (camera_pos.1 / CHUNK_SIZE_FLOAT) as i32, ); - for x_offs in -((num_tiles_across_screen / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) - ..=((num_tiles_across_screen / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) - { - // TODO: Use different height (aspect ratio!) - for y_offs in -((num_tiles_across_screen / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) - ..=((num_tiles_across_screen / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) + if num_tiles_across_screen_horizontal > SWITCH_TO_MAPVIEW_TILES { + if let ActionStateMachineState::Holding(HeldObject::Blueprint(bp)) = &state_machine.state { + let Position { x, y } = + ActionStateMachine::::player_mouse_to_tile( + state_machine.zoom_level, + camera_pos, + 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, + ); + } + + mem::drop(state_machine); + { - let chunk_draw_offs = ( - x_offs as f32 * CHUNK_SIZE_FLOAT - player_pos.0 % CHUNK_SIZE_FLOAT - + (0.5 * num_tiles_across_screen), - y_offs as f32 * CHUNK_SIZE_FLOAT - player_pos.1 % CHUNK_SIZE_FLOAT - + (0.5 * num_tiles_across_screen), + profiling::scope!("Create Map Textures"); + create_map_textures_if_needed( + &game_state.world, + renderer, + Position { + x: camera_pos.0 as i32, + y: camera_pos.1 as i32, + }, + num_tiles_across_screen_horizontal as usize, + num_tiles_across_screen_vertical as usize, + data_store, ); + } + mem::drop(game_state); - match game_state.world.get_chunk( - player_chunk - .0 - .wrapping_add_signed(x_offs.try_into().unwrap()), - player_chunk - .1 - .wrapping_add_signed(y_offs.try_into().unwrap()), - ) { - Some(chunk) => { - for (x, row) in chunk.floor_tiles.iter().enumerate() { - for (y, tile) in row.iter().enumerate() { - match tile { - crate::frontend::world::tile::FloorTile::Empty => tile_layer - .draw_sprite( - &texture_atlas.blue, - DrawInstance { - position: [ - chunk_draw_offs.0 + x as f32, - chunk_draw_offs.1 + y as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ), - _ => tile_layer.draw_sprite( - &texture_atlas.default, - DrawInstance { - position: [ - chunk_draw_offs.0 + x as f32, - chunk_draw_offs.1 + y as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ), - } - } - } + { + profiling::scope!("Render Map View"); + map_view::render_map_view( + renderer, + Position { + x: camera_pos.0 as i32, + y: camera_pos.1 as i32, + }, + num_tiles_across_screen_horizontal, + num_tiles_across_screen_vertical, + num_tiles_across_screen_horizontal as usize, + num_tiles_across_screen_vertical as usize, + tilesize, + ar, + camera_pos, + ); + } - for entity in chunk.get_entities() { - match entity { - crate::frontend::world::tile::Entity::Assembler { - pos, info, .. - } => { - entity_layer.draw_sprite( - &texture_atlas.assembler, - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [3.0, 3.0], - animation_frame: 0, - }, - ); + renderer.draw(&entity_overlay_layer); + + return; + } + + // let mut storage_storage_inserter_batch = vec![]; + + let x_range = (-((num_tiles_across_screen_horizontal / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) + ..=((num_tiles_across_screen_horizontal / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32)) + .into_par_iter(); + let y_range = (-((num_tiles_across_screen_vertical / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32) + ..=((num_tiles_across_screen_vertical / CHUNK_SIZE_FLOAT / 2.0).ceil() as i32)); + + let pos_iter = x_range.flat_map_iter(|x| y_range.clone().map(move |y| (x, y))); + + let folded_layers = { + profiling::scope!("Render Chunks"); + pos_iter + .map(|(x_offs, y_offs)| { + let chunk_draw_offs = ( + x_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.0 % CHUNK_SIZE_FLOAT + + (0.5 * num_tiles_across_screen_horizontal), + y_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.1 % CHUNK_SIZE_FLOAT + + (0.5 * num_tiles_across_screen_vertical), + ); + + let (chunk_x, chunk_y) = ( + player_chunk + .0 + .checked_add(x_offs.try_into().unwrap()) + .unwrap(), + player_chunk + .1 + .checked_add(y_offs.try_into().unwrap()) + .unwrap(), + ); + + ((chunk_x, chunk_y), chunk_draw_offs) + }) + .fold( + || layers_tile_grid(tilesize, ar), + |mut layers: Layers, ((chunk_x, chunk_y), chunk_draw_offs)| { + let Layers { + tile_layer, + entity_layer, + entity_overlay_layer, + item_layer, + warning_layer, + range_layer, + } = &mut layers; + + profiling::scope!("Rendering Chunk", format!("{:?}", (chunk_x, chunk_y))); + + match game_state.world.get_chunk(chunk_x, chunk_y) { + Some(chunk) => { + for (x, row) in chunk + .floor_tiles + .as_ref() + .unwrap_or(&Box::new(Default::default())) + .iter() + .enumerate() + { + for (y, tile) in row.iter().enumerate() { + match tile { + crate::frontend::world::tile::FloorTile::Empty => { + tile_layer.draw_sprite( + &texture_atlas.blue, + DrawInstance { + position: [ + chunk_draw_offs.0 + x as f32, + chunk_draw_offs.1 + y as f32, + ], + size: [1.0, 1.0], + animation_frame: 0, + }, + ) + }, + _ => { + tile_layer.draw_sprite( + &texture_atlas.default, + DrawInstance { + position: [ + chunk_draw_offs.0 + x as f32, + chunk_draw_offs.1 + y as f32, + ], + size: [1.0, 1.0], + animation_frame: 0, + }, + ); + }, + } - match info { - AssemblerInfo::UnpoweredNoRecipe - | AssemblerInfo::Unpowered(_) => { - warning_layer.draw_sprite( - &texture_atlas.not_connected, + // TODO: Get current ore + if game_state + .world + .get_original_ore_at_pos(Position { + x: chunk_x * CHUNK_SIZE as i32 + x as i32, + y: chunk_y * CHUNK_SIZE as i32 + y as i32, + }) + .is_some() + { + tile_layer.draw_sprite( + &texture_atlas.items[0], DrawInstance { position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, + chunk_draw_offs.0 + x as f32, + chunk_draw_offs.1 + y as f32, ], - size: [3.0, 3.0], + size: [1.0, 1.0], animation_frame: 0, }, ); - }, - AssemblerInfo::PoweredNoRecipe(pole_position) - | AssemblerInfo::Powered { - id: - AssemblerID { - recipe: _, - grid: _, - assembler_index: _, - }, - pole_position, - } => { - let grid = game_state - .simulation_state - .factory - .power_grids - .pole_pos_to_grid_id[pole_position]; - - let last_power = game_state - .simulation_state - .factory - .power_grids - .power_grids[usize::from(grid)] - .last_power_mult; - - if last_power == 0 { - warning_layer.draw_sprite( - &texture_atlas.no_power, + } + } + } + + { + profiling::scope!("Render Entities"); + for entity in chunk.get_entities() { + match entity { + crate::frontend::world::tile::Entity::Assembler { + ty, + pos, + info, + rotation, + .. + } => { + let size: [u16; 2] = data_store.assembler_info + [usize::from(*ty)] + .size(*rotation) + .into(); + + match info { + AssemblerInfo::UnpoweredNoRecipe => { + texture_atlas.not_connected.draw_centered_on( + &texture_atlas.assembler, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + + texture_atlas.assembler.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); + }, + AssemblerInfo::Unpowered(recipe) => { + texture_atlas.not_connected.draw_centered_on( + &texture_atlas.assembler, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + + texture_atlas.assembler.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); + + let item_idx = data_store.recipe_to_items + [recipe] + .iter() + .find(|item| item.0 == ItemRecipeDir::Out) + .map(|item| item.1.into_usize()) + .unwrap_or(0); + + let icon_size: [f32; 2] = [ + size[0] as f32 * ALT_MODE_ICON_SIZE, + size[1] as f32 * ALT_MODE_ICON_SIZE, + ]; + + entity_overlay_layer.draw_sprite( + &texture_atlas.items[item_idx], + DrawInstance { + position: [ + chunk_draw_offs.0 + + (pos.x % 16) as f32 + + (size[0] as f32 + - icon_size[0]) + / 2.0, + chunk_draw_offs.1 + + (pos.y % 16) as f32 + + (size[1] as f32 + - icon_size[1]) + / 2.0, + ], + size: icon_size, + animation_frame: 0, + }, + ); + }, + AssemblerInfo::PoweredNoRecipe(pole_position) => { + let grid = game_state + .simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[pole_position]; + + let last_power = game_state + .simulation_state + .factory + .power_grids + .power_grids[usize::from(grid)] + .last_power_mult; + + if last_power == 0 { + texture_atlas.no_power.draw_centered_on( + &texture_atlas.assembler, + [ + chunk_draw_offs.0 + + (pos.x % 16) as f32, + chunk_draw_offs.1 + + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } + + texture_atlas.assembler.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); + }, + AssemblerInfo::Powered { + id, + pole_position, + .. + } => { + let grid = game_state + .simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[pole_position]; + + let last_power = game_state + .simulation_state + .factory + .power_grids + .power_grids[usize::from(grid)] + .last_power_mult; + + if last_power == 0 { + texture_atlas.no_power.draw_centered_on( + &texture_atlas.assembler, + [ + chunk_draw_offs.0 + + (pos.x % 16) as f32, + chunk_draw_offs.1 + + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } + + let AssemblerOnclickInfo { + inputs: _, + outputs: _, + timer_percentage, + prod_timer_percentage: _, + base_speed: _, + speed_mod: _, + prod_mod: _, + power_consumption_mod: _, + base_power_consumption: _, + } = game_state + .simulation_state + .factory + .power_grids + .get_assembler_info(*id, data_store); + + texture_atlas.assembler.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + (timer_percentage + * (texture_atlas + .assembler + .sprite + .texture + .number_anim_frames + as f32)) + .floor() + as u32 + % texture_atlas + .assembler + .sprite + .texture + .number_anim_frames, + entity_layer, + ); + + let item_idx = data_store.recipe_to_items + [&id.recipe] + .iter() + .find(|item| item.0 == ItemRecipeDir::Out) + .map(|item| item.1.into_usize()) + .unwrap_or(0); + + let icon_size: [f32; 2] = [ + size[0] as f32 * ALT_MODE_ICON_SIZE, + size[1] as f32 * ALT_MODE_ICON_SIZE, + ]; + + entity_overlay_layer.draw_sprite( + &texture_atlas.items[item_idx], + DrawInstance { + position: [ + chunk_draw_offs.0 + + (pos.x % 16) as f32 + + (size[0] as f32 + - icon_size[0]) + / 2.0, + chunk_draw_offs.1 + + (pos.y % 16) as f32 + + (size[1] as f32 + - icon_size[1]) + / 2.0, + ], + size: icon_size, + animation_frame: 0, + }, + ); + }, + } + }, + + crate::frontend::world::tile::Entity::Belt { + pos, + direction, + ty, + id, + belt_pos, + } => { + let inputs = game_state + .world + .get_belt_possible_inputs_no_cache(*pos); + let (sprite, corner) = + match expected_belt_state(*direction, |dir| { + inputs[*dir] + }) { + BeltState::Straight => { + (&texture_atlas.belt[*direction], None) + }, + BeltState::Curved => { + if inputs[direction.turn_right()] { + ( + &texture_atlas.belt_corners[Corner { + to_dir: *direction, + from_dir: + crate::rendering::BeltSide::Right, + }], + Some(crate::rendering::BeltSide::Right), + ) + } else { + ( + &texture_atlas.belt_corners[Corner { + to_dir: *direction, + from_dir: + crate::rendering::BeltSide::Left, + }], + Some(crate::rendering::BeltSide::Left), + ) + } + }, + BeltState::Sideloading => { + (&texture_atlas.belt[*direction], None) + }, + BeltState::DoubleSideloading => { + (&texture_atlas.belt[*direction], None) + }, + }; + sprite.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + [1, 1], + (game_state + .simulation_state + .factory + .belts + .inner + .belt_update_timers_cumulative + [*ty as usize] + as f32 + / 120.0 + * BELT_ANIM_SPEED + * (texture_atlas.belt[*direction] + .sprite + .texture + .number_anim_frames + as f32)) + as u32 + % texture_atlas.belt[*direction] + .sprite + .texture + .number_anim_frames, + entity_layer, + ); + + // Draw Items + // TODO: Draw items on corners not straight + let items_iter = game_state + .simulation_state + .factory + .belts + .get_item_iter(*id) + .into_iter() + .enumerate() + .skip( + (belt_pos + .checked_sub(BELT_LEN_PER_TILE) + .expect("Belt idx wrapped?!?")) + .into(), + ) + .take(BELT_LEN_PER_TILE.into()); + + let offs = direction.into_offset(); + let item_render_offs = ( + -f32::from(offs.0) / f32::from(BELT_LEN_PER_TILE), + -f32::from(offs.1) / f32::from(BELT_LEN_PER_TILE), + ); + + let centered_on_tile = ( + chunk_draw_offs.0 + (pos.x % 16) as f32 + 0.5 + - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), + chunk_draw_offs.1 + (pos.y % 16) as f32 + 0.5 + - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), + ); + + let last_moved: BeltLenType = game_state + .simulation_state + .factory + .belts + .get_last_moved_pos(*id); + + let belt_progress: u8 = game_state + .simulation_state + .factory + .belts + .get_belt_progress(*ty); + + let offset_perc = belt_progress as f32 / 120.0; + + let slow_offset = ( + item_render_offs.0 * (1.0 - offset_perc), + item_render_offs.1 * (1.0 - offset_perc), + ); + + // TODO: This needs to be positions correctly and take rotation into account + let mut item_render_base_pos: (f32, f32) = ( + centered_on_tile.0 + f32::from(offs.0) * 0.5 + - 0.5 + * (f32::from(offs.0) + / f32::from(BELT_LEN_PER_TILE)), + centered_on_tile.1 + f32::from(offs.1) * 0.5 + - 0.5 + * (f32::from(offs.1) + / f32::from(BELT_LEN_PER_TILE)), + ); + + for (belt_pos, item) in items_iter { + if let Some(item) = item { + let draw_pos = if belt_pos < last_moved.into() { + [ + item_render_base_pos.0, + item_render_base_pos.1, + ] + } else { + [ + item_render_base_pos.0 + slow_offset.0, + item_render_base_pos.1 + slow_offset.1, + ] + }; + + item_layer.draw_sprite( + &texture_atlas.items[item.id.into()], + // &texture_atlas.items[0], + DrawInstance { + position: draw_pos, + size: [ + 1.0 / f32::from(BELT_LEN_PER_TILE), + 1.0 / f32::from(BELT_LEN_PER_TILE), + ], + animation_frame: 0, + }, + ); + } + + item_render_base_pos = ( + item_render_base_pos.0 + item_render_offs.0, + item_render_base_pos.1 + item_render_offs.1, + ); + } + }, + + crate::frontend::world::tile::Entity::Underground { + pos, + direction, + ty, + id, + underground_dir, + belt_pos, + } => { + texture_atlas.belt[*direction].draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + [1, 1], + (game_state + .simulation_state + .factory + .belts + .inner + .belt_update_timers_cumulative + [*ty as usize] + as f32 + / 120.0 + * BELT_ANIM_SPEED + * (texture_atlas.belt[*direction] + .sprite + .texture + .number_anim_frames + as f32)) + as u32 + % texture_atlas.belt[*direction] + .sprite + .texture + .number_anim_frames, + entity_layer, + ); + + texture_atlas.underground[*direction][*underground_dir] + .draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + [1, 1], + 0, + entity_overlay_layer, + ); + + // Draw Items + let items_iter = game_state + .simulation_state + .factory + .belts + .get_item_iter(*id) + .into_iter() + .enumerate() + .skip( + (belt_pos + .checked_sub(BELT_LEN_PER_TILE) + .expect("Belt idx wrapped?!?")) + .into(), + ) + .take(BELT_LEN_PER_TILE.into()); + + let offs = direction.into_offset(); + let item_render_offs = ( + -f32::from(offs.0) / f32::from(BELT_LEN_PER_TILE), + -f32::from(offs.1) / f32::from(BELT_LEN_PER_TILE), + ); + + let centered_on_tile = ( + chunk_draw_offs.0 + (pos.x % 16) as f32 + 0.5 + - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), + chunk_draw_offs.1 + (pos.y % 16) as f32 + 0.5 + - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), + ); + + let last_moved: BeltLenType = game_state + .simulation_state + .factory + .belts + .get_last_moved_pos(*id); + + let belt_progress: u8 = game_state + .simulation_state + .factory + .belts + .get_belt_progress(*ty); + + let offset_perc = belt_progress as f32 / 120.0; + + let slow_offset = ( + item_render_offs.0 * (1.0 - offset_perc), + item_render_offs.1 * (1.0 - offset_perc), + ); + + // TODO: This needs to be positions correctly and take rotation into account + let mut item_render_base_pos: (f32, f32) = ( + centered_on_tile.0 + f32::from(offs.0) * 0.5 + - 0.5 + * (f32::from(offs.0) + / f32::from(BELT_LEN_PER_TILE)), + centered_on_tile.1 + f32::from(offs.1) * 0.5 + - 0.5 + * (f32::from(offs.1) + / f32::from(BELT_LEN_PER_TILE)), + ); + + for (belt_pos, item) in items_iter { + if let Some(item) = item { + let draw_pos = if belt_pos < last_moved.into() { + [ + item_render_base_pos.0, + item_render_base_pos.1, + ] + } else { + [ + item_render_base_pos.0 + slow_offset.0, + item_render_base_pos.1 + slow_offset.1, + ] + }; + + item_layer.draw_sprite( + &texture_atlas.items[item.id.into()], + // &texture_atlas.items[0], + DrawInstance { + position: draw_pos, + size: [ + 1.0 / f32::from(BELT_LEN_PER_TILE), + 1.0 / f32::from(BELT_LEN_PER_TILE), + ], + animation_frame: 0, + }, + ); + } + + item_render_base_pos = ( + item_render_base_pos.0 + item_render_offs.0, + item_render_base_pos.1 + item_render_offs.1, + ); + } + }, + + Entity::Inserter { + pos, + direction, + info, + .. + } => { + entity_layer.draw_sprite( + &texture_atlas.inserter[*direction], DrawInstance { position: [ chunk_draw_offs.0 + (pos.x % 16) as f32, chunk_draw_offs.1 + (pos.y % 16) as f32, ], - size: [3.0, 3.0], + size: [1.0, 1.0], animation_frame: 0, }, ); - } + + match info { + crate::frontend::world::tile::InserterInfo::NotAttached { + start_pos, + end_pos, + } => {}, + crate::frontend::world::tile::InserterInfo::Attached { + start_pos, + end_pos, + info, + } => { + // match info { + // crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => todo!(), + // crate::frontend::world::tile::AttachedInserter::BeltBelt { item, inserter } => todo!(), + // crate::frontend::world::tile::AttachedInserter::StorageStorage { item, inserter } => { + // // let info = game_state.simulation_state.factory.storage_storage_inserters + + // }, + // } }, } - }, + }, - crate::frontend::world::tile::Entity::Belt { - pos, - direction, - id, - belt_pos, - } => { - entity_layer.draw_sprite( - &texture_atlas.belt[*direction], - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ); + Entity::PowerPole { + ty, + pos, + connected_power_poles, + } => { + // TODO: + // println!("Pole at {pos:?}, with grid: {grid_id}"); + let size = + data_store.power_pole_data[usize::from(*ty)].size; + let size = [size.0, size.1]; + texture_atlas.power_pole.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); - // Draw Items - let items_iter = game_state - .simulation_state - .factory - .belts - .get_item_iter(*id) - .into_iter() - .skip( - (belt_pos - .checked_sub(BELT_LEN_PER_TILE) - .expect("Belt idx wrapped?!?")) - .into(), - ) - .take(BELT_LEN_PER_TILE.into()); - - let offs = direction.into_offset(); - let item_render_offs = ( - -f32::from(offs.0) / f32::from(BELT_LEN_PER_TILE), - -f32::from(offs.1) / f32::from(BELT_LEN_PER_TILE), - ); - - let centered_on_tile = ( - chunk_draw_offs.0 + (pos.x % 16) as f32 + 0.5 - - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), - chunk_draw_offs.1 + (pos.y % 16) as f32 + 0.5 - - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), - ); - - // TODO: This needs to be positions correctly and take rotation into account - let mut item_render_base_pos: (f32, f32) = ( - centered_on_tile.0 + f32::from(offs.0) * 0.5 - - 0.5 * (f32::from(offs.0) / f32::from(BELT_LEN_PER_TILE)), - centered_on_tile.1 + f32::from(offs.1) * 0.5 - - 0.5 * (f32::from(offs.1) / f32::from(BELT_LEN_PER_TILE)), - ); - - for item in items_iter { - if let Some(item) = item { - item_layer.draw_sprite( - // &texture_atlas.items[item.id.into()], - &texture_atlas.items[0], - DrawInstance { - position: [ - item_render_base_pos.0, - item_render_base_pos.1, + let power_range = data_store.power_pole_data + [usize::from(*ty)] + .power_range; + + if let ActionStateMachineState::Holding( + HeldObject::Entity(e), + ) = state_machine.state + { + if e.cares_about_power() { + range_layer.draw_sprite( + &texture_atlas.dark_square, + DrawInstance { + position: [ + chunk_draw_offs.0 + + (pos.x % 16) as f32 + - power_range as f32, + chunk_draw_offs.1 + + (pos.y % 16) as f32 + - power_range as f32, + ], + size: [ + power_range as f32 * 2.0 + + size[0] as f32, + power_range as f32 * 2.0 + + size[1] as f32, + ], + animation_frame: 0, + }, + ); + } + } + }, + + Entity::Splitter { pos, direction, id } => { + let [inputs, outputs] = game_state + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id); + + let (left_pos, right_pos) = match direction { + Dir::North => (*pos, *pos + Dir::East), + Dir::East => (*pos, *pos + Dir::South), + Dir::South => (*pos + Dir::East, *pos), + Dir::West => (*pos + Dir::South, *pos), + }; + + // FIXME: We currently do not take partial movement (from slow belt speeds) into account, which leads to ugly jumping of the items on the belt + for ((pos, input), output) in [left_pos, right_pos] + .into_iter() + .zip(inputs) + .zip(outputs) + { + texture_atlas.belt[*direction].draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + [1, 1], + 0, + entity_layer, + ); + + let centered_on_tile = ( + chunk_draw_offs.0 + (pos.x % 16) as f32 + 0.5 + - 0.5 + * (1.0 / f32::from(BELT_LEN_PER_TILE)), + chunk_draw_offs.1 + (pos.y % 16) as f32 + 0.5 + - 0.5 + * (1.0 / f32::from(BELT_LEN_PER_TILE)), + ); + let offs = direction.into_offset(); + render_items_straight::( + game_state + .simulation_state + .factory + .belts + .get_item_iter(input), + *direction, + SPLITTER_BELT_LEN, + SPLITTER_BELT_LEN, + ( + centered_on_tile.0 + - f32::from(offs.0) + * (1.0 + / f32::from(BELT_LEN_PER_TILE)), + centered_on_tile.1 + - f32::from(offs.1) + * (0.5 + / f32::from(BELT_LEN_PER_TILE)), + ), + item_layer, + texture_atlas, + ); + let out_belt_len = game_state + .simulation_state + .factory + .belts + .get_len(output); + let out_belt_iter = game_state + .simulation_state + .factory + .belts + .get_item_iter(output); + let offs = direction.into_offset(); + let item_render_base_pos: (f32, f32) = ( + centered_on_tile.0 + f32::from(offs.0) * 0.5 + - 0.5 + * (f32::from(offs.0) + / f32::from(BELT_LEN_PER_TILE)), + centered_on_tile.1 + f32::from(offs.1) * 0.5 + - 0.5 + * (f32::from(offs.1) + / f32::from(BELT_LEN_PER_TILE)), + ); + render_items_straight::( + out_belt_iter, + *direction, + out_belt_len, + SPLITTER_BELT_LEN, + item_render_base_pos, + item_layer, + texture_atlas, + ); + } + // todo!() + }, + Entity::Chest { + ty, + pos, + item, + slot_limit: _, + } => { + let size = + data_store.chest_tile_sizes[usize::from(*ty)]; + let size = [size.0, size.1]; + texture_atlas.chest.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, ], - size: [ - 1.0 / f32::from(BELT_LEN_PER_TILE), - 1.0 / f32::from(BELT_LEN_PER_TILE), + size, + 0, + entity_layer, + ); + }, + Entity::SolarPanel { ty, pos, .. } => { + entity_layer.draw_sprite( + &texture_atlas.default, + DrawInstance { + position: [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size: data_store.solar_panel_info + [usize::from(*ty)] + .size + .map(|v| v as f32), + animation_frame: 0, + }, + ); + }, + // TODO: Render if a lab is working! + Entity::Lab { + ty, + pos, + pole_position, + .. + } => { + let size = data_store.lab_info[usize::from(*ty)].size; + let size = [size.0, size.1]; + + texture_atlas.lab.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, ], - animation_frame: 0, - }, - ); - } + size, + 0, + entity_layer, + ); - item_render_base_pos = ( - item_render_base_pos.0 + item_render_offs.0, - item_render_base_pos.1 + item_render_offs.1, - ); - } - }, + if let Some((pole_pos, _, _)) = pole_position { + let grid = game_state + .simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[pole_pos]; + + let last_power = game_state + .simulation_state + .factory + .power_grids + .power_grids[usize::from(grid)] + .last_power_mult; + + if last_power == 0 { + texture_atlas.no_power.draw_centered_on( + &texture_atlas.lab, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } + } else { + texture_atlas.not_connected.draw_centered_on( + &texture_atlas.lab, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } + }, - Entity::Inserter { pos, direction, .. } => { - entity_layer.draw_sprite( - &texture_atlas.inserter[*direction], - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ); - }, + Entity::Beacon { + ty, + pos, + modules, + pole_position, + } => { + let size = + data_store.beacon_info[usize::from(*ty)].size; + let size = [size.0, size.1]; + + texture_atlas.beacon.draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); - Entity::PowerPole { - ty, - pos, - connected_power_poles, - } => { - // TODO: - // println!("Pole at {pos:?}, with grid: {grid_id}"); - entity_layer.draw_sprite( - &texture_atlas.assembler, - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ); - }, + if let Some((pole_pos, _)) = pole_position { + let grid = game_state + .simulation_state + .factory + .power_grids + .pole_pos_to_grid_id[pole_pos]; + + let last_power = game_state + .simulation_state + .factory + .power_grids + .power_grids[usize::from(grid)] + .last_power_mult; + + if last_power == 0 { + texture_atlas.no_power.draw_centered_on( + &texture_atlas.beacon, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } + } else { + texture_atlas.not_connected.draw_centered_on( + &texture_atlas.beacon, + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + warning_layer, + ); + } - Entity::Splitter { pos, direction, id } => { - let [inputs, outputs] = game_state - .simulation_state - .factory - .belts - .get_splitter_belt_ids(*id); + // TODO: Render modules + }, + Entity::FluidTank { ty, pos, rotation } => { + let size = + data_store.fluid_tank_infos[usize::from(*ty)].size; - let right_dir = direction.turn_right(); + texture_atlas.belt[*rotation].draw( + [ + chunk_draw_offs.0 + (pos.x % 16) as f32, + chunk_draw_offs.1 + (pos.y % 16) as f32, + ], + size, + 0, + entity_layer, + ); + }, - for ((pos, input), output) in - successors(Some(*pos), |pos| Some(*pos + right_dir)) - .zip(inputs) - .zip(outputs) - { - entity_layer.draw_sprite( - &texture_atlas.belt[*direction], + e => todo!("{:?}", e), + } + } + } + }, + None => { + for y in 0..16 { + for x in 0..16 { + tile_layer.draw_sprite( + &texture_atlas.outside_world, DrawInstance { position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, + chunk_draw_offs.0 + x as f32, + chunk_draw_offs.1 + y as f32, ], size: [1.0, 1.0], animation_frame: 0, }, ); - - let centered_on_tile = ( - chunk_draw_offs.0 + (pos.x % 16) as f32 + 0.5 - - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), - chunk_draw_offs.1 + (pos.y % 16) as f32 + 0.5 - - 0.5 * (1.0 / f32::from(BELT_LEN_PER_TILE)), - ); - render_items_straight::( - game_state - .simulation_state - .factory - .belts - .get_item_iter(input), - *direction, - SPLITTER_BELT_LEN, - SPLITTER_BELT_LEN, - centered_on_tile, - &mut item_layer, - texture_atlas, - ); - let out_belt_len = - game_state.simulation_state.factory.belts.get_len(output); - let out_belt_iter = game_state - .simulation_state - .factory - .belts - .get_item_iter(output); - let offs = direction.into_offset(); - let item_render_base_pos: (f32, f32) = ( - centered_on_tile.0 + f32::from(offs.0) * 0.5 - - 0.5 - * (f32::from(offs.0) - / f32::from(BELT_LEN_PER_TILE)), - centered_on_tile.1 + f32::from(offs.1) * 0.5 - - 0.5 - * (f32::from(offs.1) - / f32::from(BELT_LEN_PER_TILE)), - ); - render_items_straight::( - out_belt_iter, - *direction, - out_belt_len, - SPLITTER_BELT_LEN, - item_render_base_pos, - &mut item_layer, - texture_atlas, - ); } - // todo!() - }, - Entity::Chest { - ty, - pos, - item, - index, - } => { - entity_layer.draw_sprite( - &texture_atlas.default, - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ); - }, - Entity::SolarPanel { ty, pos, .. } => { - entity_layer.draw_sprite( - &texture_atlas.default, - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [3.0, 3.0], - animation_frame: 0, - }, - ); - }, - // TODO: Render if a lab is working! - Entity::Lab { ty, pos, .. } => { - entity_layer.draw_sprite( - &texture_atlas.belt[Dir::North], - DrawInstance { - position: [ - chunk_draw_offs.0 + (pos.x % 16) as f32, - chunk_draw_offs.1 + (pos.y % 16) as f32, - ], - size: [3.0, 3.0], - animation_frame: 0, - }, - ); - }, - - e => todo!("{:?}", e), - } + } + }, } + + layers }, - None => { - for y in 0..16 { - for x in 0..16 { - tile_layer.draw_sprite( - &texture_atlas.outside_world, - DrawInstance { - position: [ - chunk_draw_offs.0 + x as f32, - chunk_draw_offs.1 + y as f32, - ], - size: [1.0, 1.0], - animation_frame: 0, - }, - ); - } - } + ) + .reduce( + || layers_tile_grid(tilesize, ar), + |mut a, b| { + a.extend(b); + a }, - } - } + ) + }; + + for (player_id, player) in game_state + .world + .players + .iter() + .enumerate() + .filter(|(_, p)| p.visible) + .filter(|(i, _)| { + *i != state_machine.my_player_id as usize || state_machine.map_view_info.is_some() + }) + { + player_layer.draw_sprite( + &texture_atlas.player, + DrawInstance { + position: [ + player.pos.0 - camera_pos.0 + num_tiles_across_screen_horizontal / 2.0, + player.pos.1 - camera_pos.1 + num_tiles_across_screen_vertical / 2.0, + ], + size: [1.0, 2.0], + animation_frame: 0, + }, + ); + info!( + "Rendering other player {} at {:?}", + player_id, + [player.pos.0 - camera_pos.0, player.pos.1 - camera_pos.1,] + ); } + if state_machine.map_view_info.is_none() { + player_layer.draw_sprite( + &texture_atlas.player, + DrawInstance { + // Always in the middle + position: [ + num_tiles_across_screen_horizontal / 2.0, + num_tiles_across_screen_vertical / 2.0, + ], + size: [1.0, 2.0], + animation_frame: 0, + }, + ); + trace!("Rendering self at {:?}", state_machine.local_player_pos); + } + + mem::drop(game_state); + match &state_machine.state { + ActionStateMachineState::CtrlCPressed => {}, + ActionStateMachineState::CopyDragInProgress { start_pos } => { + let end_pos = ActionStateMachine::::player_mouse_to_tile( + state_machine.zoom_level, + camera_pos, + state_machine.current_mouse_pos, + ); + + let bottom_right = Position { + x: max(start_pos.x, end_pos.x) + 1, + y: max(start_pos.y, end_pos.y) + 1, + }; + + let base_pos = Position { + x: min(start_pos.x, end_pos.x), + y: min(start_pos.y, end_pos.y), + }; + + entity_overlay_layer.draw_sprite( + &texture_atlas.dark_square, + DrawInstance { + position: [ + base_pos.x as f32 - camera_pos.0 + num_tiles_across_screen_horizontal / 2.0, + base_pos.y as f32 - camera_pos.1 + num_tiles_across_screen_vertical / 2.0, + ], + size: [ + (bottom_right.x - base_pos.x) as f32, + (bottom_right.y - base_pos.y) as f32, + ], + animation_frame: 0, + }, + ); + }, + crate::frontend::action::action_state_machine::ActionStateMachineState::Idle => {}, crate::frontend::action::action_state_machine::ActionStateMachineState::Holding(e) => { match e { + crate::frontend::action::action_state_machine::HeldObject::Blueprint(bp) => { + let Position { x, y } = + ActionStateMachine::::player_mouse_to_tile( + state_machine.zoom_level, + camera_pos, + 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, + ); + }, + crate::frontend::action::action_state_machine::HeldObject::Tile(floor_tile) => { // TODO }, crate::frontend::action::action_state_machine::HeldObject::Entity( place_entity_type, ) => match place_entity_type { - // TODO: - crate::frontend::world::tile::PlaceEntityType::Assembler { ty, pos } => { - entity_layer.draw_sprite( - &texture_atlas.assembler, + crate::frontend::world::tile::PlaceEntityType::Assembler { + ty, + pos, + rotation, + } => { + let size: [u16; 2] = data_store.assembler_info[usize::from(*ty)] + .size(*rotation) + .into(); + texture_atlas.assembler.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Inserter { + pos, + dir, + filter, + } => { + let size: [u16; 2] = [1, 1]; + state_machine_layer.draw_sprite( + &texture_atlas.inserter[*dir], + DrawInstance { + position: [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size: [size[0].into(), size[1].into()], + animation_frame: 0, + }, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Belt { pos, direction, ty } => { + let size: [u16; 2] = [1, 1]; + texture_atlas.belt[*direction].draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + } => { + let size: [u16; 2] = [1, 1]; + // TODO: + texture_atlas.belt[*direction].draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + + texture_atlas.underground[*direction][*underground_dir].draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + [1, 1], + 0, + &mut entity_overlay_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::PowerPole { pos, ty } => { + let size: [u16; 2] = [ + data_store.power_pole_data[usize::from(*ty)].size.0, + data_store.power_pole_data[usize::from(*ty)].size.1, + ]; + texture_atlas.power_pole.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + + let power_range = data_store.power_pole_data[usize::from(*ty)].power_range; + + state_machine_layer.draw_sprite( + &texture_atlas.dark_square, + DrawInstance { + position: [ + (pos.x as f32 - power_range as f32) - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + (pos.y as f32 - power_range as f32) - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size: [ + power_range as f32 * 2.0 + size[0] as f32, + power_range as f32 * 2.0 + size[1] as f32, + ], + animation_frame: 0, + }, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Splitter { + pos, + direction, + in_mode, + out_mode, + + ty, + } => {}, + crate::frontend::world::tile::PlaceEntityType::Chest { pos, ty } => { + let size = data_store.chest_tile_sizes[usize::from(*ty)]; + let size = [size.0, size.1]; + texture_atlas.chest.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::SolarPanel { pos, ty } => { + let size = data_store.solar_panel_info[usize::from(*ty)].size; + state_machine_layer.draw_sprite( + &texture_atlas.default, + DrawInstance { + position: [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size: [size[0] as f32, size[1] as f32], + animation_frame: 0, + }, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Lab { pos, ty } => { + let size = data_store.lab_info[usize::from(*ty)].size; + let size = [size.0, size.1]; + + texture_atlas.lab.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::Beacon { pos, ty } => { + let size = data_store.beacon_info[usize::from(*ty)].size; + let size = [size.0, size.1]; + + texture_atlas.beacon.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + + let effect_range = data_store.beacon_info[usize::from(*ty)].effect_range; + + state_machine_layer.draw_sprite( + &texture_atlas.dark_square, + DrawInstance { + position: [ + (pos.x as f32 - ((effect_range.0 - size[0]) / 2) as f32) + - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + (pos.y as f32 - ((effect_range.1 - size[1]) / 2) as f32) + - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size: [effect_range.0 as f32, effect_range.1 as f32], + animation_frame: 0, + }, + ); + }, + crate::frontend::world::tile::PlaceEntityType::FluidTank { + ty, + pos, + rotation, + } => { + let size: [u16; 2] = [1, 1]; + texture_atlas.belt[*rotation].draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + }, + crate::frontend::world::tile::PlaceEntityType::MiningDrill { + ty, + pos, + rotation, + } => { + // FIXME: Rotation + let size = data_store.mining_drill_info[usize::from(*ty)].size; + + // TODO: + texture_atlas.chest.draw( + [ + pos.x as f32 - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + pos.y as f32 - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, + ], + size, + 0, + &mut state_machine_layer, + ); + + let mining_range = + data_store.mining_drill_info[usize::from(*ty)].mining_range; + + state_machine_layer.draw_sprite( + &texture_atlas.dark_square, DrawInstance { position: [ - pos.x as f32 - state_machine.local_player_pos.0 - + num_tiles_across_screen / 2.0, - pos.y as f32 - state_machine.local_player_pos.1 - + num_tiles_across_screen / 2.0, + (pos.x as f32 - ((mining_range[0] - size[0]) / 2) as f32) + - camera_pos.0 + + num_tiles_across_screen_horizontal / 2.0, + (pos.y as f32 - ((mining_range[1] - size[1]) / 2) as f32) + - camera_pos.1 + + num_tiles_across_screen_vertical / 2.0, ], - size: [3.0, 3.0], + size: [mining_range[0] as f32, mining_range[1] as f32], animation_frame: 0, }, - ) + ); }, - crate::frontend::world::tile::PlaceEntityType::Inserter { - pos, - dir, - filter, - } => {}, - crate::frontend::world::tile::PlaceEntityType::Belt { pos, direction } => {}, - crate::frontend::world::tile::PlaceEntityType::PowerPole { pos, ty } => {}, - crate::frontend::world::tile::PlaceEntityType::Splitter { - pos, - direction, - in_mode, - out_mode, - } => {}, - crate::frontend::world::tile::PlaceEntityType::Chest { pos } => {}, - crate::frontend::world::tile::PlaceEntityType::SolarPanel { pos, ty } => {}, - crate::frontend::world::tile::PlaceEntityType::Lab { pos, ty } => {}, }, } }, - crate::frontend::action::action_state_machine::ActionStateMachineState::Viewing(pos) => { + crate::frontend::action::action_state_machine::ActionStateMachineState::Viewing(_) => { // TODO: }, - crate::frontend::action::action_state_machine::ActionStateMachineState::Decontructing( - position, + crate::frontend::action::action_state_machine::ActionStateMachineState::Deconstructing( + _, _, ) => { // TODO: }, } - for (player_id, player) in game_state - .world - .players - .iter() - .enumerate() - .filter(|(_, p)| p.visible) - .filter(|(i, _)| *i != state_machine.my_player_id as usize) - { - player_layer.draw_sprite( - &texture_atlas.player, - DrawInstance { - position: [ - player.pos.0 - state_machine.local_player_pos.0 + num_tiles_across_screen / 2.0, - player.pos.1 - state_machine.local_player_pos.1 + num_tiles_across_screen / 2.0, - ], - size: [1.0, 2.0], - animation_frame: 0, - }, - ); - info!( - "Rendering other player {} at {:?}", - player_id, - [ - player.pos.0 - state_machine.local_player_pos.0, - player.pos.1 - state_machine.local_player_pos.1, - ] - ); - } - - player_layer.draw_sprite( - &texture_atlas.player, - DrawInstance { - // Always in the middle - position: [num_tiles_across_screen / 2.0, num_tiles_across_screen / 2.0], - size: [1.0, 2.0], - animation_frame: 0, - }, - ); + mem::drop(state_machine); - trace!("Rendering self at {:?}", state_machine.local_player_pos); + { + profiling::scope!("Draw layers"); + { + let Layers { + tile_layer, + entity_layer, + entity_overlay_layer, + item_layer, + warning_layer, + range_layer, + } = folded_layers; + renderer.draw(&tile_layer); - renderer.draw(&tile_layer); + renderer.draw(&entity_layer); - renderer.draw(&range_layer); + renderer.draw(&item_layer); - renderer.draw(&entity_layer); + renderer.draw(&entity_overlay_layer); - renderer.draw(&item_layer); + renderer.draw(&range_layer); - renderer.draw(&warning_layer); + renderer.draw(&warning_layer); + renderer.draw(&player_layer); + } + renderer.draw(&state_machine_layer); + renderer.draw(&entity_overlay_layer); + renderer.draw(&player_layer); + } +} - renderer.draw(&player_layer); +pub(super) enum EscapeMenuOptions { + BackToMainMenu, } pub fn render_ui( ctx: &Context, - ui: &Ui, - state_machine: &mut ActionStateMachine, - game_state: &GameState, - data_store: &DataStore, -) -> impl IntoIterator> { + ui: &mut Ui, + mut state_machine: MutexGuard>, + mut game_state: MutexGuard>, + data_store: MutexGuard>, +) -> Result< + impl Iterator> + use, + EscapeMenuOptions, +> { + let state_machine_ref = &mut *state_machine; + let game_state_ref = &mut *game_state; + let data_store_ref = &*data_store; let mut actions = vec![]; - Window::new("BP").show(ctx, |ui| { - let bp = Blueprint::from_area(&game_state.world, [1590..1700, 1590..1700], data_store); + if state_machine_ref.escape_menu_open { + if let Some(escape_action) = Modal::new("Pause Window".into()) + .show(ctx, |ui| { + ui.heading("Paused"); + if ui.button("Main Menu").clicked() { + return Some(EscapeMenuOptions::BackToMainMenu); + } + + None + }) + .inner + { + match escape_action { + EscapeMenuOptions::BackToMainMenu => return Err(escape_action), + } + } + } + + 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("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(file) = File::open(path) { + if let Ok(bp) = ron::de::from_reader(file) { + state_machine_ref.state = + ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); + } + } + } + } + }); + + Window::new("DEBUG USE WITH CARE") + .default_open(false) + .show(ctx, |ui| { + 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); + } + ui.checkbox( + &mut state_machine_ref.show_graph_dot_output, + "Generate Belt Graph", + ); + if state_machine_ref.show_graph_dot_output { + let mut graph = format!( + "{:?}", + Dot::new(&game_state_ref.simulation_state.factory.belts.belt_graph) + ); + + ui.text_edit_multiline(&mut graph); + } + }); + + Window::new("UPS").default_open(false).show(ctx, |ui| { + let points = &game_state_ref.update_times.get_data_points(0)[0..30]; + ui.label(format!( + "{:.1} UPS", + 1.0 / (points.iter().map(|v| v.dur).sum::() / points.len() as u32) + .as_secs_f32() + )); + }); + + 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 + }; + + if ui + .add_enabled(bp.is_some(), Button::new("Copy Blueprint String")) + .clicked() + { + let s: String = + ron::ser::to_string_pretty(bp.unwrap(), ron::ser::PrettyConfig::default()).unwrap(); + ctx.copy_text(s); + } + }); + + Window::new("RawData").default_open(false).show(ctx, |ui| { + let raw = get_raw_data_test(); let mut s: String = - ron::ser::to_string_pretty(&bp, ron::ser::PrettyConfig::default()).unwrap(); + ron::ser::to_string_pretty(&raw, ron::ser::PrettyConfig::default()).unwrap(); ui.text_edit_multiline(&mut s); }); - match &state_machine.state { + ctx.set_cursor_icon(egui::CursorIcon::Default); + match &state_machine_ref.state { + ActionStateMachineState::CtrlCPressed => { + ctx.set_cursor_icon(egui::CursorIcon::Copy); + }, + ActionStateMachineState::CopyDragInProgress { start_pos } => { + ctx.set_cursor_icon(egui::CursorIcon::Copy); + }, + crate::frontend::action::action_state_machine::ActionStateMachineState::Idle => {}, crate::frontend::action::action_state_machine::ActionStateMachineState::Holding( held_object, @@ -592,57 +1848,60 @@ pub fn render_ui( ) => { let mut viewing = true; Window::new("Viewing").open(&mut viewing).show(ctx, |ui| { - let chunk = game_state + let chunk = game_state_ref .world .get_chunk_for_tile(*position) .expect("Cannot find chunk for viewing"); - let entity = chunk.get_entity_at(*position, data_store); + let entity = chunk.get_entity_at(*position, data_store_ref); if let Some(entity) = entity { match entity { - crate::frontend::world::tile::Entity::Assembler { ty, pos, info, modules } => { + crate::frontend::world::tile::Entity::Assembler { ty, pos, info, modules, rotation } => { + // FIXME: Rotate sprite let mut goal_recipe: Option> = match info { AssemblerInfo::UnpoweredNoRecipe => None, AssemblerInfo::Unpowered(recipe) => Some(*recipe), - AssemblerInfo::PoweredNoRecipe(position) => None, - AssemblerInfo::Powered { id, pole_position } => Some(id.recipe), + AssemblerInfo::PoweredNoRecipe(_) => None, + AssemblerInfo::Powered { id, .. } => Some(id.recipe), }; - ComboBox::new("Recipe list", "Recipes").selected_text(goal_recipe.map(|recipe| data_store.recipe_names[usize_from(recipe.id)].as_str()).unwrap_or("Choose a recipe!")).show_ui(ui, |ui| { - data_store.recipe_names.iter().enumerate().for_each(|(i, recipe_name)| { + ComboBox::new("Recipe list", "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)| { + (game_state_ref.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)| { + ui.selectable_value(&mut goal_recipe, Some(Recipe {id: i.try_into().unwrap()}), recipe_name); }); }); - // TODO: Render module slots - match info { crate::frontend::world::tile::AssemblerInfo::UnpoweredNoRecipe => { - ui.label("Assembler"); + ui.label(&data_store_ref.assembler_info[usize::from(*ty)].display_name); if let Some(goal_recipe) = goal_recipe { actions.push(ActionType::SetRecipe(SetRecipeInfo { pos: *pos, recipe: goal_recipe })); } }, crate::frontend::world::tile::AssemblerInfo::Unpowered(recipe) => { - ui.label("Assembler"); + ui.label(&data_store_ref.assembler_info[usize::from(*ty)].display_name); if let Some(goal_recipe) = goal_recipe { if goal_recipe != *recipe { actions.push(ActionType::SetRecipe(SetRecipeInfo { pos: *pos, recipe: goal_recipe })); } } }, - crate::frontend::world::tile::AssemblerInfo::PoweredNoRecipe(grid) => { - ui.label("Assembler"); + crate::frontend::world::tile::AssemblerInfo::PoweredNoRecipe(_) => { + ui.label(&data_store_ref.assembler_info[usize::from(*ty)].display_name); if let Some(goal_recipe) = goal_recipe { actions.push(ActionType::SetRecipe(SetRecipeInfo { pos: *pos, recipe: goal_recipe })); } }, crate::frontend::world::tile::AssemblerInfo::Powered { - id, - pole_position + id, .. } => { - ui.label("Assembler"); + ui.label(&data_store_ref.assembler_info[usize::from(*ty)].display_name); + + ui.label(format!("{:?}", *id)); if let Some(goal_recipe) = goal_recipe { if goal_recipe != id.recipe { @@ -653,63 +1912,127 @@ pub fn render_ui( // TODO: // ui.label(data_store.recipe_names[usize_from(assembler_id.recipe.id)]); + let time_per_recipe = data_store_ref.recipe_timers[usize_from(id.recipe.id)] as f32; + let AssemblerOnclickInfo { inputs, outputs, timer_percentage, prod_timer_percentage, - } = game_state + base_speed, + speed_mod, + prod_mod, + power_consumption_mod, + base_power_consumption, + } = game_state_ref .simulation_state .factory .power_grids - .get_assembler_info(*id, data_store); + .get_assembler_info(*id, data_store_ref); let main_pb = ProgressBar::new(timer_percentage).show_percentage().corner_radius(CornerRadius::ZERO); ui.add(main_pb); let prod_pb = ProgressBar::new(prod_timer_percentage).fill(Color32::ORANGE).show_percentage().corner_radius(CornerRadius::ZERO); ui.add(prod_pb); + // Render module slots + 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 { + row.col(|ui| { + if let Some(module_id) = module { + ui.label(&data_store_ref.module_info[*module_id].display_name); + } else { + ui.label("Empty Module Slot"); + } + }); + } + }); + }); + + let crafting_speed = base_speed * (1.0 + speed_mod); + let time_per_craft = time_per_recipe / crafting_speed; + TableBuilder::new(ui).columns(Column::auto().resizable(false), inputs.len() + outputs.len()).body(|mut body| { body.row(5.0, |mut row| { - for (item, count) in inputs.iter().chain(outputs.iter()) { + for (item, count) in inputs.iter() { + let (_, _, count_in_recipe) = data_store_ref.recipe_to_items_and_amounts[&id.recipe].iter().find(|(dir, recipe_item, _)| *dir == ItemRecipeDir::Ing && *item == *recipe_item).unwrap(); + row.col(|ui| { + ui.add(egui::Label::new(&data_store_ref.item_display_names[usize_from(item.id)]).wrap_mode(egui::TextWrapMode::Extend)); + ui.add(egui::Label::new(format!("{}", *count)).wrap_mode(egui::TextWrapMode::Extend)); + ui.add(egui::Label::new(format!("{}/s", (*count_in_recipe as f32) / (time_per_craft / 60.0))).wrap_mode(egui::TextWrapMode::Extend)); + }); + } + + for (item, count) in outputs.iter() { + let (_, _, count_in_recipe) = data_store_ref.recipe_to_items_and_amounts[&id.recipe].iter().find(|(dir, recipe_item, _)| *dir == ItemRecipeDir::Out && *item == *recipe_item).unwrap(); row.col(|ui| { - ui.label(&data_store.item_names[usize_from(item.id)]); - ui.label(format!("{}", *count)); + ui.add(egui::Label::new(&data_store_ref.item_display_names[usize_from(item.id)]).wrap_mode(egui::TextWrapMode::Extend)); + ui.add(egui::Label::new(format!("{}", *count)).wrap_mode(egui::TextWrapMode::Extend)); + ui.add(egui::Label::new(format!("{:.2}/s", (*count_in_recipe as f32) / (time_per_craft / 60.0) * (1.0 + prod_mod))).wrap_mode(egui::TextWrapMode::Extend)); }); } }); }); + + + + ui.label(format!("Crafting Speed: {:.2}({:+.0}%)", crafting_speed, speed_mod * 100.0)); + 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)); + } } }, crate::frontend::world::tile::Entity::PowerPole { - ty, pos, - connected_power_poles, + .. } => { // TODO: - let power_range = data_store.power_pole_data[usize::from(*ty)].power_range; - let size = data_store.power_pole_data[usize::from(*ty)].size; + // let power_range = data_store.power_pole_data[usize::from(*ty)].power_range; + // let size = data_store.power_pole_data[usize::from(*ty)].size; - let grid_id = game_state + let grid_id = game_state_ref .simulation_state .factory .power_grids .pole_pos_to_grid_id[pos] as usize; - let pg = &game_state.simulation_state.factory.power_grids.power_grids + let pg = &game_state_ref.simulation_state.factory.power_grids.power_grids [grid_id]; ui.label(format!("Power Grid number: {}", grid_id)); - let pb = ProgressBar::new(pg.last_power_mult as f32 / 64.0); - ui.add(pb); + ui.columns_const(|[ui_consumption, ui_production, ui_storage]| { + // Power Consumption + ui_consumption.add(Label::new(RichText::new("Satisfaction").heading()).wrap_mode(egui::TextWrapMode::Extend)); + ui_consumption.add(ProgressBar::new(pg.last_produced_power.0 as f32 / pg.last_power_consumption.0 as f32).corner_radius(CornerRadius::ZERO).fill(if pg.last_power_mult == MAX_POWER_MULT { + Color32::GREEN + } else if pg.last_power_mult > MAX_POWER_MULT / 2 { + Color32::YELLOW + } else { + Color32::RED + }).text(RichText::new(format!("{}/{}", pg.last_produced_power, pg.last_power_consumption)).color(Color32::BLACK))); + + + // Power Production + ui_production.add(Label::new(RichText::new("Production").heading()).wrap_mode(egui::TextWrapMode::Extend)); + ui_production.add(ProgressBar::new(pg.last_produced_power.0 as f32 / pg.last_ticks_max_power_production.0 as f32).corner_radius(CornerRadius::ZERO).text(RichText::new(format!("{}/{}", pg.last_produced_power, pg.last_ticks_max_power_production)).color(Color32::BLACK))); + + + // Power Storage + let max_charge: Joule = pg.main_accumulator_count.iter().copied().zip(data_store_ref.accumulator_info.iter().map(|info| info.max_charge)).map(|(count, charge)| charge * count).sum(); + let current_charge: Joule = pg.main_accumulator_count.iter().copied().zip(pg.main_accumulator_charge.iter().copied()).map(|(count, charge)| charge * count).sum(); + + ui_storage.add(Label::new(RichText::new("Accumulator charge").heading()).wrap_mode(egui::TextWrapMode::Extend)); + ui_storage.add(ProgressBar::new(current_charge.0 as f32 / max_charge.0 as f32).corner_radius(CornerRadius::ZERO).text(RichText::new(format!("{}/{}", current_charge, max_charge)).color(Color32::BLACK))); + }); let timescale = 1; let max_value_at_timescale = (MAX_POWER_MULT as f64) * (RELATIVE_INTERVAL_MULTS[..=timescale].iter().copied().product::() as f64); let num_samples = NUM_SAMPLES_AT_INTERVALS[timescale]; - let points = pg.power_history.get_series(timescale, data_store, Some(|_| true)).into_iter().map(|series| (series.name, series.data.into_iter() + let points = pg.power_mult_history.get_series(timescale, data_store_ref, Some(|_| true)).into_iter().map(|(_, series)| (series.name, series.data.into_iter() .enumerate() .map(|(i, v)| [i as f64, v.into()]) .collect::>())); @@ -718,6 +2041,29 @@ pub fn render_ui( .stroke(Stroke::new(2.0, Color32::GREEN)) }); + 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(), |mut row| { + let i = row.index(); + + if i < pg.num_assemblers_of_type.len() { + row.col(|ui| { + ui.add(Label::new(&data_store_ref.assembler_info[i].display_name).extend()); + + }); + row.col(|ui| {ui.add(Label::new(format!("{}", pg.num_assemblers_of_type[i])).extend());}); + } else { + let i = i - pg.num_assemblers_of_type.len(); + row.col(|ui| { + ui.add(Label::new(&data_store_ref.solar_panel_info[i].display_name).extend()); + + }); + row.col(|ui| {ui.add(Label::new(format!("{}", pg.num_solar_panels_of_type[i])).extend());}); + } + }); + }); + + ui.label(format!("{}", pg.last_power_consumption)); + Plot::new("power_history_graph").show_x(false).show_y(false) // .auto_bounds([true, false]) .set_margin_fraction([0.0, 0.05].into()) @@ -740,59 +2086,139 @@ pub fn render_ui( }); }, crate::frontend::world::tile::Entity::Belt { - pos, - direction, id, belt_pos, - } => match id { - BeltTileId::AnyBelt(index, phantom_dat_) => todo!(), + .. + } => { + match id { + BeltTileId::AnyBelt(index, _) => { + ui.label("Belt"); + ui.label(format!("Any Belt {}", *index).as_str()); + }, + } + ui.label(format!("Item: {:?}", game_state_ref.simulation_state.factory.belts.get_pure_item(*id)).as_str()); + + let mut dedup = Default::default(); + let mut done = Default::default(); + game_state_ref.simulation_state.factory.belts.get_items_which_could_end_up_on_that_belt(*id, &mut dedup, &mut done); + ui.label(format!("Possible items: {:?}", done[id]).as_str()); + + ui.label(format!("Inner: {:?}", game_state_ref.simulation_state.factory.belts.inner.belt_belt_inserters).as_str()); + + ui.label(format!("Belt Pos: {:?}", *belt_pos)); + }, + crate::frontend::world::tile::Entity::Underground { + id, + underground_dir, + belt_pos, + .. + } => { + match id { + BeltTileId::AnyBelt(index, _) => { + ui.label("Underground Belt"); + ui.label(format!("Any Belt {}", *index).as_str()); + }, + } + ui.label(format!("Item: {:?}", game_state_ref.simulation_state.factory.belts.get_pure_item(*id)).as_str()); + + let mut dedup = Default::default(); + let mut done = Default::default(); + game_state_ref.simulation_state.factory.belts.get_items_which_could_end_up_on_that_belt(*id, &mut dedup, &mut done); + ui.label(format!("Possible items: {:?}", done[id]).as_str()); + + ui.label(format!("Inner: {:?}", game_state_ref.simulation_state.factory.belts.inner.belt_belt_inserters).as_str()); + + ui.label(format!("UndergroundDir: {:?}", *underground_dir)); + + ui.label(format!("Belt Pos: {:?}", *belt_pos)); + }, crate::frontend::world::tile::Entity::Inserter { + user_movetime, + type_movetime, + pos, - direction, info, - } => match info { - crate::frontend::world::tile::InserterInfo::NotAttached { - start_pos, - end_pos, - } => println!("Unattached inserter at {pos:?}"), - crate::frontend::world::tile::InserterInfo::Attached(ins) => match ins { - crate::frontend::world::tile::AttachedInserter::BeltStorage { - id, - belt_pos, - } => { - // TODO: - }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { - item, - inserter, - } => { - // TODO: + .. + } => { + ui.label("Inserter"); + match info { + crate::frontend::world::tile::InserterInfo::NotAttached { .. } => { + ui.label("NotAttached"); }, - crate::frontend::world::tile::AttachedInserter::StorageStorage { .. } => { - // TODO: + crate::frontend::world::tile::InserterInfo::Attached {info: ins, ..} => match ins { + crate::frontend::world::tile::AttachedInserter::BeltStorage { + id, + belt_pos, + } => { + ui.label("BeltStorage"); + + ui.label(format!("belt_id: {:?}", *id)); + ui.label(format!("belt_pos: {}", *belt_pos)); + + ui.label(format!("storage: {:?}", game_state_ref.simulation_state.factory.belts.get_inserter_info_at(*id, *belt_pos).expect("No inserter at pos indicated in entity!"))); + + // TODO: + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { + item, + inserter, + } => { + ui.label("BeltBelt"); + // TODO: + }, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => { + ui.label("StorageStorage"); + ui.label(&data_store_ref.item_display_names[usize_from(item.id)]); + + // TODO: + }, }, - }, + } + + let mut movetime_overridden = user_movetime.is_some(); + + ui.checkbox(&mut movetime_overridden, "Override Swing Time"); + + if movetime_overridden { + let mut movetime = user_movetime.map(|v| v.into()).unwrap_or(*type_movetime); + + ui.add(egui::Slider::new(&mut movetime, (*type_movetime)..=u16::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()) }); + } + } else if movetime_overridden != user_movetime.is_some() { + actions.push(ActionType::OverrideInserterMovetime { pos: *pos, new_movetime: None }); + } }, - Entity::Splitter { .. } => { - warn!("Viewing Splitter. This currently does nothing!"); + Entity::Splitter { id, .. } => { + let [inputs, outputs] = game_state_ref + .simulation_state + .factory + .belts + .get_splitter_belt_ids(*id); + + ui.label(format!("Inputs: {:?}", inputs)); + ui.label(format!("Outputs: {:?}", outputs)); }, Entity::Chest { ty, - pos, item, - index, + .. } => { - let Some(item) = item else { + let Some((item, index)) = item else { todo!() }; + ui.label(&data_store_ref.item_display_names[usize_from(item.id)]); + ui.label(format!("{}", *index)); - let stack_size: u16 = data_store.item_stack_sizes[usize_from(item.id)] as u16; + let stack_size: u16 = data_store_ref.item_stack_sizes[usize_from(item.id)] as u16; - let num_slots = data_store.chest_num_slots[*ty as usize]; - let (current_items, _max_items) = game_state.simulation_state.factory.chests.stores[usize_from(item.id)].get_chest(*index); + let num_slots = data_store_ref.chest_num_slots[*ty as usize]; + let (current_items, _max_items) = game_state_ref.simulation_state.factory.chests.stores[usize_from(item.id)].get_chest(*index); - TableBuilder::new(ui).columns(Column::auto().resizable(false), 10).body(|mut body| { + TableBuilder::new(ui).columns(Column::auto().resizable(false), 10).body(|body| { body.rows(5.0, (num_slots / 10) as usize + (num_slots % 10 > 0) as usize, |mut row| { let idx = row.index(); for col_idx in 0..10 { @@ -801,9 +2227,9 @@ pub fn render_ui( break; } row.col(|ui| { - let this_slots_stack_count = min(current_items.saturating_sub(slot_id as u16 * stack_size), stack_size); + let this_slots_stack_count = min(current_items.saturating_sub(slot_id as ChestSize * stack_size as ChestSize), stack_size as ChestSize); - let clicked = ui.label(format!("{}", this_slots_stack_count)).clicked(); + let clicked = ui.add(Label::new(format!("{}", this_slots_stack_count)).extend()).clicked(); let mut shift = false; ctx.input(|input| {shift = input.modifiers.shift; }); @@ -815,11 +2241,19 @@ pub fn render_ui( }); }); }, - Entity::Lab { pos, ty, pole_position } => { + Entity::Lab { .. } => { // TODO }, - Entity::SolarPanel { pos, ty, pole_position } => { + Entity::SolarPanel { .. } => { + // TODO + } + Entity::Beacon { .. } => { // TODO + }, + Entity::FluidTank { ty, pos, rotation } => { + let id = game_state_ref.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; + + ui.label(format!("{:?}", id)); } _ => todo!(), } @@ -827,11 +2261,11 @@ pub fn render_ui( }); if !viewing { - state_machine.state = ActionStateMachineState::Idle; + state_machine_ref.state = ActionStateMachineState::Idle; } }, - crate::frontend::action::action_state_machine::ActionStateMachineState::Decontructing( - position, + crate::frontend::action::action_state_machine::ActionStateMachineState::Deconstructing( + _, timer, ) => { Window::new("Deconstructing").show(ui.ctx(), |ui| { @@ -867,99 +2301,476 @@ pub fn render_ui( }); }); + Window::new("Technology") + .open(&mut state_machine_ref.technology_panel_open) + .show(ctx, |ui| { + let research_actions = game_state_ref + .simulation_state + .tech_state + .render_tech_window( + ui, + state_machine_ref.tech_tree_render.get_or_insert( + game_state_ref + .simulation_state + .tech_state + .generate_render_graph(data_store_ref), + ), + data_store_ref, + ); + + actions.extend(research_actions); + }); + Window::new("Statistics") - .open(&mut state_machine.statistics_panel_open) - .show(ctx, |ui| match state_machine.statistics_panel { - StatisticsPanel::Production(scale) => { - let points: Vec<(String, usize, PlotPoints)> = game_state - .statistics - .production - .get_series(scale, data_store, Some(|_| true)) - .into_iter() - .enumerate() - .map(|(i, series)| (series.name, i, series.data)) - .map(|(name, i, data)| { - ( - name, - i, - data.into_iter() - .enumerate() - .map(|(i, v)| [i as f64, v.into()]) - .collect(), - ) - }) - .filter(|(_, _, points): &(_, _, PlotPoints)| { - points.points().iter().any(|p| p.y > 0.0) - }) - .collect(); - let lines = points.into_iter().map(|(name, id, points)| { - Line::new(name, points).stroke(Stroke::new(2.0, data_store.item_to_colour[id])) - }); + .open(&mut state_machine_ref.statistics_panel_open) + .show(ctx, |ui| { + let time_scale = match &mut state_machine_ref.statistics_panel { + StatisticsPanel::Items(timescale) => timescale, + StatisticsPanel::Fluids(timescale) => timescale, + }; + ui.with_layout(Layout::left_to_right(egui::Align::Min), |ui| { + ui.radio_value(time_scale, 0, "10 Seconds"); + ui.radio_value(time_scale, 1, "1 Minute"); + ui.radio_value(time_scale, 2, "1 Hour"); + }); + + let time_scale = *time_scale; + + match state_machine_ref.statistics_panel { + StatisticsPanel::Items(scale) | StatisticsPanel::Fluids(scale) => { + let take_fluids = matches!( + state_machine_ref.statistics_panel, + StatisticsPanel::Fluids(_) + ); + + ui.columns_const(|[ui_production, ui_consumption]: &mut [Ui; 2]| { + ui_production.heading("Production"); + ui_production.separator(); + ui_consumption.heading("Consumption"); + ui_consumption.separator(); + + let prod_points: Vec<(String, usize, f32, PlotPoints)> = game_state_ref + .statistics + .production + .get_series( + scale, + data_store_ref, + Some(|item: Item| { + data_store_ref.item_is_fluid[usize_from(item.id)] == take_fluids + }), + ) + .into_iter() + .map(|(item_id, series)| (series.name, item_id, series.data)) + .map(|(name, i, data)| { + ( + name, + i, + data.iter().copied().sum(), + data.into_iter() + .enumerate() + .map(|(i, v)| [i as f64, v.into()]) + .collect(), + ) + }) + .filter(|(_, _, sum, _): &(_, _, f32, _)| *sum > 0.0) + .collect(); + + let max_prod = prod_points + .iter() + .map(|v| v.2) + .max_by(|a, b| { + if a < b { + Ordering::Less + } else { + Ordering::Greater + } + }) + .unwrap_or(0.0); + + let mut sum_list_prod: Vec<_> = prod_points + .iter() + .map(|v| (v.0.clone(), v.1, v.2)) + .collect(); + + sum_list_prod.sort_by(|a, b| { + if a.2 < b.2 { + Ordering::Greater + } else { + Ordering::Less + } + }); + + let lines = prod_points + .into_iter() + .filter(|(_, id, _, _)| state_machine_ref.production_filters[*id]) + .map(|(name, id, _sum, points)| { + Line::new(name, points) + .stroke(Stroke::new(2.0, data_store_ref.item_to_colour[id])) + }); + + let ticks_per_value = RELATIVE_INTERVAL_MULTS[..=scale] + .iter() + .copied() + .product::() + as f64; + + Plot::new("production_graph") + .set_margin_fraction([0.0, 0.05].into()) + .x_grid_spacer(|_grid_input| { + (0..NUM_X_AXIS_TICKS[scale]) + .map(|v| GridMark { + value: v as f64 / (NUM_X_AXIS_TICKS[scale] as f64) + * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), + step_size: 1.0 / (NUM_X_AXIS_TICKS[scale] as f64) + * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), + }) + .collect() + }) + .y_grid_spacer(|grid_input| { + let max: f64 = grid_input.bounds.1; + + let mut lower_dec = 10.0_f64 + .powf((max / ticks_per_value * 60.0 * 60.0).log10().floor()); - let ticks_per_value = RELATIVE_INTERVAL_MULTS[..=scale] - .iter() - .copied() - .product::() as f64; - - Plot::new("production_graph") - .set_margin_fraction([0.0, 0.05].into()) - .x_grid_spacer(|_grid_input| { - (0..NUM_X_AXIS_TICKS[scale]) - .map(|v| GridMark { - value: v as f64 / (NUM_X_AXIS_TICKS[scale] as f64) - * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), - step_size: 1.0 / (NUM_X_AXIS_TICKS[scale] as f64) - * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), + if lower_dec < 1.0 { + lower_dec = 1.0; + } + + lower_dec = lower_dec * ticks_per_value / 60.0 / 60.0; + + (0..40) + .filter_map(|v| { + ((v as f64) / 4.0 * lower_dec < max).then_some(GridMark { + value: (v as f64) / 4.0 * lower_dec, + step_size: lower_dec / 4.0, + }) + }) + .chain((0..10).filter_map(|v| { + ((v as f64) * lower_dec < max).then_some(GridMark { + value: (v as f64) * lower_dec, + step_size: 1.0 * lower_dec, + }) + })) + .collect() }) - .collect() - }) - .y_grid_spacer(|grid_input| { - let mut lower_dec = 10.0_f64.powf( - (grid_input.bounds.1 / ticks_per_value * 60.0 * 60.0) - .log10() - .floor(), + .custom_y_axes( + [AxisHints::new_y().formatter(move |v, _| { + format!("{:.1}/min", v.value / ticks_per_value * 60.0 * 60.0) + })] + .to_vec(), + ) + .custom_x_axes( + [AxisHints::new_x() + .formatter(|v, _| TIMESCALE_LEGEND[scale](v.value))] + .to_vec(), + ) + .include_y(0) + .allow_zoom([false, false]) + .allow_drag([false, false]) + .allow_scroll([false, false]) + .view_aspect(3.0) + .show(ui_production, |ui| { + for line in lines { + ui.line(line); + } + }); + + let ticks_per_sample = RELATIVE_INTERVAL_MULTS[..=time_scale] + .iter() + .copied() + .product::(); + let ticks_total = min( + ticks_per_sample * NUM_SAMPLES_AT_INTERVALS[time_scale], + game_state_ref + .statistics + .production + .num_samples_pushed + .next_multiple_of(ticks_per_sample) + - ticks_per_sample, + ) as f32; + + let row_height = ui_production.spacing().interact_size.y; + ScrollArea::vertical().id_salt("Prod List Scroll").show( + ui_production, + |ui| { + TableBuilder::new(ui) + .id_salt("Production List") + .sense(Sense::click()) + .column(Column::auto()) + .column(Column::remainder()) + .column(Column::auto()) + .body(|body| { + body.rows(row_height, sum_list_prod.len(), |mut row| { + let idx = row.index(); + row.col(|ui| { + if state_machine_ref.production_filters + [sum_list_prod[idx].1] + { + ui.add( + Label::new(egui::RichText::new( + sum_list_prod[idx].0.as_str(), + )) + .extend(), + ); + } else { + ui.add( + Label::new( + egui::RichText::new( + sum_list_prod[idx].0.as_str(), + ) + .strikethrough(), + ) + .extend(), + ); + } + }); + row.col(|ui| { + ui.add( + ProgressBar::new( + sum_list_prod[idx].2 / max_prod, + ) + .fill( + data_store_ref.item_to_colour + [sum_list_prod[idx].1], + ) + .corner_radius(CornerRadius::ZERO), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::right_to_left(egui::Align::Center), + |ui| { + ui.add( + Label::new(format!( + "{:.0}/m", + sum_list_prod[idx].2 / ticks_total + * TICKS_PER_SECOND_LOGIC as f32 + * 60.0 + )) + .extend(), + ); + }, + ); + }); + if row.response().clicked() { + state_machine_ref.production_filters + [sum_list_prod[idx].1] = !state_machine_ref + .production_filters[sum_list_prod[idx].1]; + } + }); + }); + }, ); - if lower_dec < 1.0 { - lower_dec = 1.0; - } + let cons_points: Vec<(String, usize, f32, PlotPoints)> = game_state_ref + .statistics + .consumption + .get_series( + scale, + data_store_ref, + Some(|item: Item| { + data_store_ref.item_is_fluid[usize_from(item.id)] == take_fluids + }), + ) + .into_iter() + .map(|(item_id, series)| (series.name, item_id, series.data)) + .map(|(name, i, data)| { + ( + name, + i, + data.iter().copied().sum(), + data.into_iter() + .enumerate() + .map(|(i, v)| [i as f64, v.into()]) + .collect(), + ) + }) + .filter(|(_, _, sum, _): &(_, _, f32, _)| *sum > 0.0) + .collect(); + + let max_cons = cons_points + .iter() + .map(|v| v.2) + .max_by(|a, b| { + if a < b { + Ordering::Less + } else { + Ordering::Greater + } + }) + .unwrap_or(0.0); + + let mut sum_list_cons: Vec<_> = cons_points + .iter() + .map(|v| (v.0.clone(), v.1, v.2)) + .collect(); + + sum_list_cons.sort_by(|a, b| { + if a.2 < b.2 { + Ordering::Greater + } else { + Ordering::Less + } + }); - lower_dec = lower_dec * ticks_per_value / 60.0 / 60.0; + let lines = cons_points + .into_iter() + .filter(|(_, id, _, _)| state_machine_ref.consumption_filters[*id]) + .map(|(name, id, _sum, points)| { + Line::new(name, points) + .stroke(Stroke::new(2.0, data_store_ref.item_to_colour[id])) + }); - (0..40) - .map(|v| GridMark { - value: (v as f64) / 4.0 * lower_dec, - step_size: lower_dec / 4.0, + let ticks_per_value = RELATIVE_INTERVAL_MULTS[..=scale] + .iter() + .copied() + .product::() + as f64; + + Plot::new("consumption_graph") + .set_margin_fraction([0.0, 0.05].into()) + .x_grid_spacer(|_grid_input| { + (0..NUM_X_AXIS_TICKS[scale]) + .map(|v| GridMark { + value: v as f64 / (NUM_X_AXIS_TICKS[scale] as f64) + * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), + step_size: 1.0 / (NUM_X_AXIS_TICKS[scale] as f64) + * (NUM_SAMPLES_AT_INTERVALS[scale] as f64), + }) + .collect() }) - .chain((0..10).map(|v| GridMark { - value: (v as f64) * lower_dec, - step_size: 1.0 * lower_dec, - })) - .collect() - }) - .custom_y_axes( - [AxisHints::new_y().formatter(move |v, _| { - format!("{:.1}/min", v.value / ticks_per_value * 60.0 * 60.0) - })] - .to_vec(), - ) - .custom_x_axes( - [AxisHints::new_x().formatter(|v, _| TIMESCALE_LEGEND[scale](v.value))] - .to_vec(), - ) - .include_y(0) - .allow_zoom([false, false]) - .allow_drag([false, false]) - .allow_scroll([false, false]) - .show(ui, |ui| { - for line in lines { - ui.line(line); - } - }) - }, + .y_grid_spacer(|grid_input| { + let max: f64 = grid_input.bounds.1; + + let mut lower_dec = 10.0_f64 + .powf((max / ticks_per_value * 60.0 * 60.0).log10().floor()); + + if lower_dec < 1.0 { + lower_dec = 1.0; + } + + lower_dec = lower_dec * ticks_per_value / 60.0 / 60.0; + + (0..40) + .filter_map(|v| { + ((v as f64) / 4.0 * lower_dec < max).then_some(GridMark { + value: (v as f64) / 4.0 * lower_dec, + step_size: lower_dec / 4.0, + }) + }) + .chain((0..10).filter_map(|v| { + ((v as f64) * lower_dec < max).then_some(GridMark { + value: (v as f64) * lower_dec, + step_size: 1.0 * lower_dec, + }) + })) + .collect() + }) + .custom_y_axes( + [AxisHints::new_y().formatter(move |v, _| { + format!("{:.1}/min", v.value / ticks_per_value * 60.0 * 60.0) + })] + .to_vec(), + ) + .custom_x_axes( + [AxisHints::new_x() + .formatter(|v, _| TIMESCALE_LEGEND[scale](v.value))] + .to_vec(), + ) + .include_y(0) + .allow_zoom([false, false]) + .allow_drag([false, false]) + .allow_scroll([false, false]) + .view_aspect(3.0) + .show(ui_consumption, |ui| { + for line in lines { + ui.line(line); + } + }); + + ScrollArea::vertical().id_salt("Cons List Scroll").show( + ui_consumption, + |ui| { + TableBuilder::new(ui) + .id_salt("Consumption List") + .sense(Sense::click()) + .column(Column::auto()) + .column(Column::remainder()) + .column(Column::auto()) + .body(|body| { + body.rows(row_height, sum_list_cons.len(), |mut row| { + let idx = row.index(); + row.col(|ui| { + if state_machine_ref.consumption_filters + [sum_list_cons[idx].1] + { + ui.add( + Label::new(egui::RichText::new( + sum_list_cons[idx].0.as_str(), + )) + .extend(), + ); + } else { + ui.add( + Label::new( + egui::RichText::new( + sum_list_cons[idx].0.as_str(), + ) + .strikethrough(), + ) + .extend(), + ); + } + }); + row.col(|ui| { + ui.add( + ProgressBar::new( + sum_list_cons[idx].2 / max_cons, + ) + .fill( + data_store_ref.item_to_colour + [sum_list_cons[idx].1], + ) + .corner_radius(CornerRadius::ZERO), + ); + }); + row.col(|ui| { + ui.with_layout( + Layout::right_to_left(egui::Align::Center), + |ui| { + ui.add( + Label::new(format!( + "{:.0}/m", + sum_list_cons[idx].2 / ticks_total + * TICKS_PER_SECOND_LOGIC as f32 + * 60.0 + )) + .extend(), + ); + }, + ); + }); + if row.response().clicked() { + state_machine_ref.consumption_filters + [sum_list_cons[idx].1] = !state_machine_ref + .consumption_filters[sum_list_cons[idx].1]; + } + }); + }); + }, + ); + }); + }, + } }); - actions + mem::drop(game_state); + mem::drop(state_machine); + mem::drop(data_store); + + puffin_egui::profiler_window(ctx); + + Ok(actions.into_iter()) } fn render_items_straight( diff --git a/src/rendering/temp_assets/beacon.png b/src/rendering/temp_assets/beacon.png new file mode 100644 index 0000000..0b79aaa Binary files /dev/null and b/src/rendering/temp_assets/beacon.png differ diff --git a/src/rendering/temp_assets/dark_gray.png b/src/rendering/temp_assets/dark_gray.png new file mode 100644 index 0000000..405359f Binary files /dev/null and b/src/rendering/temp_assets/dark_gray.png differ diff --git a/src/rendering/temp_assets/dark_square.png b/src/rendering/temp_assets/dark_square.png new file mode 100644 index 0000000..9c6bf1b Binary files /dev/null and b/src/rendering/temp_assets/dark_square.png differ diff --git a/src/rendering/temp_assets/krastorio/LICENSE b/src/rendering/temp_assets/krastorio/LICENSE new file mode 100644 index 0000000..22a35be --- /dev/null +++ b/src/rendering/temp_assets/krastorio/LICENSE @@ -0,0 +1,56 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. diff --git a/src/rendering/temp_assets/krastorio/PowerPole.png b/src/rendering/temp_assets/krastorio/PowerPole.png new file mode 100644 index 0000000..e59405e Binary files /dev/null and b/src/rendering/temp_assets/krastorio/PowerPole.png differ diff --git a/src/rendering/temp_assets/krastorio/README.md b/src/rendering/temp_assets/krastorio/README.md new file mode 100644 index 0000000..a9e41cf --- /dev/null +++ b/src/rendering/temp_assets/krastorio/README.md @@ -0,0 +1,5 @@ +Licensed under the [LGPL](LICENSE) from Krastorio 2: +- https://codeberg.org/raiguard/Krastorio2Assets +- https://mods.factorio.com/mod/Krastorio2Assets + +Thanks to all artists and contributors! \ No newline at end of file diff --git a/src/rendering/temp_assets/krastorio/advanced-lab.png b/src/rendering/temp_assets/krastorio/advanced-lab.png new file mode 100644 index 0000000..cab129b Binary files /dev/null and b/src/rendering/temp_assets/krastorio/advanced-lab.png differ diff --git a/src/rendering/temp_assets/krastorio/automation-tech-card.png b/src/rendering/temp_assets/krastorio/automation-tech-card.png new file mode 100644 index 0000000..de6e0a8 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/automation-tech-card.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/east-north.png b/src/rendering/temp_assets/krastorio/belt/east-north.png new file mode 100644 index 0000000..4b57668 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/east-north.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/east-south.png b/src/rendering/temp_assets/krastorio/belt/east-south.png new file mode 100644 index 0000000..b2d726e Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/east-south.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/east.png b/src/rendering/temp_assets/krastorio/belt/east.png new file mode 100644 index 0000000..f34c244 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/east.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/north-east.png b/src/rendering/temp_assets/krastorio/belt/north-east.png new file mode 100644 index 0000000..69bf023 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/north-east.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/north-west.png b/src/rendering/temp_assets/krastorio/belt/north-west.png new file mode 100644 index 0000000..ee33e68 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/north-west.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/north.png b/src/rendering/temp_assets/krastorio/belt/north.png new file mode 100644 index 0000000..2eebb36 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/north.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/south-east.png b/src/rendering/temp_assets/krastorio/belt/south-east.png new file mode 100644 index 0000000..f48e82a Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/south-east.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/south-west.png b/src/rendering/temp_assets/krastorio/belt/south-west.png new file mode 100644 index 0000000..10fc0e0 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/south-west.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/south.png b/src/rendering/temp_assets/krastorio/belt/south.png new file mode 100644 index 0000000..53fa917 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/south.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/west-north.png b/src/rendering/temp_assets/krastorio/belt/west-north.png new file mode 100644 index 0000000..8186484 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/west-north.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/west-south.png b/src/rendering/temp_assets/krastorio/belt/west-south.png new file mode 100644 index 0000000..a4ef1b0 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/west-south.png differ diff --git a/src/rendering/temp_assets/krastorio/belt/west.png b/src/rendering/temp_assets/krastorio/belt/west.png new file mode 100644 index 0000000..e567e3c Binary files /dev/null and b/src/rendering/temp_assets/krastorio/belt/west.png differ diff --git a/src/rendering/temp_assets/krastorio/chemical-tech-card.png b/src/rendering/temp_assets/krastorio/chemical-tech-card.png new file mode 100644 index 0000000..c0f5933 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/chemical-tech-card.png differ diff --git a/src/rendering/temp_assets/krastorio/chest.png b/src/rendering/temp_assets/krastorio/chest.png new file mode 100644 index 0000000..6150e02 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/chest.png differ diff --git a/src/rendering/temp_assets/krastorio/copper-plate.png b/src/rendering/temp_assets/krastorio/copper-plate.png new file mode 100644 index 0000000..55b0202 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/copper-plate.png differ diff --git a/src/rendering/temp_assets/krastorio/electronic-circuit.png b/src/rendering/temp_assets/krastorio/electronic-circuit.png new file mode 100644 index 0000000..511ebde Binary files /dev/null and b/src/rendering/temp_assets/krastorio/electronic-circuit.png differ diff --git a/src/rendering/temp_assets/krastorio/enriched-copper.png b/src/rendering/temp_assets/krastorio/enriched-copper.png new file mode 100644 index 0000000..a2b2bb5 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/enriched-copper.png differ diff --git a/src/rendering/temp_assets/krastorio/enriched-iron.png b/src/rendering/temp_assets/krastorio/enriched-iron.png new file mode 100644 index 0000000..1cffd10 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/enriched-iron.png differ diff --git a/src/rendering/temp_assets/krastorio/furnace.png b/src/rendering/temp_assets/krastorio/furnace.png new file mode 100644 index 0000000..1785754 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/furnace.png differ diff --git a/src/rendering/temp_assets/krastorio/iron-gear-wheel.png b/src/rendering/temp_assets/krastorio/iron-gear-wheel.png new file mode 100644 index 0000000..2f5a278 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/iron-gear-wheel.png differ diff --git a/src/rendering/temp_assets/krastorio/iron-plate.png b/src/rendering/temp_assets/krastorio/iron-plate.png new file mode 100644 index 0000000..d7056cc Binary files /dev/null and b/src/rendering/temp_assets/krastorio/iron-plate.png differ diff --git a/src/rendering/temp_assets/krastorio/logistic-tech-card.png b/src/rendering/temp_assets/krastorio/logistic-tech-card.png new file mode 100644 index 0000000..dd4d4fa Binary files /dev/null and b/src/rendering/temp_assets/krastorio/logistic-tech-card.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/east-entrance.png b/src/rendering/temp_assets/krastorio/underground/east-entrance.png new file mode 100644 index 0000000..a3a3c07 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/east-entrance.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/east-exit.png b/src/rendering/temp_assets/krastorio/underground/east-exit.png new file mode 100644 index 0000000..4168efb Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/east-exit.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/north-entrance.png b/src/rendering/temp_assets/krastorio/underground/north-entrance.png new file mode 100644 index 0000000..1f7da09 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/north-entrance.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/north-exit.png b/src/rendering/temp_assets/krastorio/underground/north-exit.png new file mode 100644 index 0000000..3cd9d67 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/north-exit.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/south-entrance.png b/src/rendering/temp_assets/krastorio/underground/south-entrance.png new file mode 100644 index 0000000..8ab2fdb Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/south-entrance.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/south-exit.png b/src/rendering/temp_assets/krastorio/underground/south-exit.png new file mode 100644 index 0000000..2d08429 Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/south-exit.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/west-entrance.png b/src/rendering/temp_assets/krastorio/underground/west-entrance.png new file mode 100644 index 0000000..705afbb Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/west-entrance.png differ diff --git a/src/rendering/temp_assets/krastorio/underground/west-exit.png b/src/rendering/temp_assets/krastorio/underground/west-exit.png new file mode 100644 index 0000000..5789d3f Binary files /dev/null and b/src/rendering/temp_assets/krastorio/underground/west-exit.png differ diff --git a/src/rendering/temp_assets/light_gray.png b/src/rendering/temp_assets/light_gray.png new file mode 100644 index 0000000..27243ee Binary files /dev/null and b/src/rendering/temp_assets/light_gray.png differ diff --git a/src/rendering/temp_assets/no_power.png b/src/rendering/temp_assets/no_power.png index 9a254b0..6d1d5d5 100644 Binary files a/src/rendering/temp_assets/no_power.png and b/src/rendering/temp_assets/no_power.png differ diff --git a/src/rendering/temp_assets/not_connected.png b/src/rendering/temp_assets/not_connected.png index 2f5d026..524ab1c 100644 Binary files a/src/rendering/temp_assets/not_connected.png and b/src/rendering/temp_assets/not_connected.png differ diff --git a/src/rendering/temp_assets/white.png b/src/rendering/temp_assets/white.png new file mode 100644 index 0000000..7272685 Binary files /dev/null and b/src/rendering/temp_assets/white.png differ diff --git a/src/rendering/window.rs b/src/rendering/window.rs index bfe950a..5ee9144 100644 --- a/src/rendering/window.rs +++ b/src/rendering/window.rs @@ -1,12 +1,18 @@ use std::{ - sync::{atomic::AtomicU64, mpsc::Sender, Arc, Mutex}, + sync::{ + Arc, + atomic::{AtomicBool, AtomicU64}, + mpsc::Sender, + }, time::{Duration, Instant}, }; +use parking_lot::Mutex; + use crate::{ data::DataStore, frontend::{ - action::{action_state_machine::ActionStateMachine, ActionType}, + action::{ActionType, action_state_machine::ActionStateMachine}, input::Input, }, item::WeakIdxTrait, @@ -14,17 +20,17 @@ use crate::{ saving::save, }; use log::{info, warn}; -use tilelib::types::{Display, Sprite, Texture}; +use tilelib::types::Display; use winit::{ event::{ElementState, MouseButton, WindowEvent}, window::WindowAttributes, }; use super::{ + TextureAtlas, app_state::{AppState, GameState}, - texture_atlas, TextureAtlas, + texture_atlas, }; -use image::GenericImageView; pub struct App { window: Window, @@ -53,8 +59,19 @@ pub enum LoadedGame { pub struct LoadedGameSized { pub state: Arc>>, pub state_machine: Arc>>, - pub data_store: Arc>, + pub data_store: Arc>>, pub ui_action_sender: Sender>, + + pub stop_update_thread: Arc, +} + +impl Drop + for LoadedGameSized +{ + fn drop(&mut self) { + self.stop_update_thread + .store(true, std::sync::atomic::Ordering::Relaxed); + } } pub struct Window { @@ -81,7 +98,7 @@ impl winit::application::ApplicationHandler for App { fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, + _window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { assert!(self.window.display.is_some()); @@ -93,22 +110,18 @@ impl winit::application::ApplicationHandler for App { info!("EXITING"); if let Some(state) = &self.currently_loaded_game { match &state.state { - LoadedGame::ItemU8RecipeU8(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU8RecipeU16(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU16RecipeU8(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), - LoadedGame::ItemU16RecipeU16(state) => save( - &state.state.lock().unwrap(), - state.data_store.checksum.clone(), - ), + LoadedGame::ItemU8RecipeU8(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU8RecipeU16(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU16RecipeU8(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, + LoadedGame::ItemU16RecipeU16(state) => { + save(&state.state.lock(), &state.data_store.lock()) + }, } } @@ -121,7 +134,7 @@ impl winit::application::ApplicationHandler for App { }, WindowEvent::KeyboardInput { - device_id, + device_id: _, event, is_synthetic, } => { @@ -138,7 +151,7 @@ impl winit::application::ApplicationHandler for App { }, WindowEvent::MouseWheel { - device_id, + device_id: _, delta, phase, } => { @@ -159,14 +172,18 @@ impl winit::application::ApplicationHandler for App { }, WindowEvent::MouseInput { - device_id, + device_id: _, state, button, } => { let input = match (state, button) { - (ElementState::Pressed, MouseButton::Left) => Input::LeftClickPressed, + (ElementState::Pressed, MouseButton::Left) => { + Input::LeftClickPressed { shift: false } + }, (ElementState::Released, MouseButton::Left) => Input::LeftClickReleased, - (ElementState::Pressed, MouseButton::Right) => Input::RightClickPressed, + (ElementState::Pressed, MouseButton::Right) => { + Input::RightClickPressed { shift: false } + }, (ElementState::Released, MouseButton::Right) => Input::RightClickReleased, v => todo!("{:?}", v), }; @@ -183,7 +200,9 @@ impl winit::application::ApplicationHandler for App { let fps = Duration::from_secs(1).div_duration_f32(self.window.last_frame_time.elapsed()); - match self.state { + match &self.state { + AppState::MainMenu { in_ip_box } => todo!(), + AppState::Ingame => { if let Some(loaded) = &self.currently_loaded_game { let current_tick = @@ -197,15 +216,14 @@ impl winit::application::ApplicationHandler for App { match &loaded.state { LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { - let game_state = loaded_game_sized.state.lock().unwrap(); - let state_machine = - loaded_game_sized.state_machine.lock().unwrap(); + let game_state = loaded_game_sized.state.lock(); + let state_machine = loaded_game_sized.state_machine.lock(); render_world( renderer, - &game_state, + game_state, &self.texture_atlas, - &state_machine, - &loaded_game_sized.data_store, + state_machine, + &loaded_game_sized.data_store.lock(), ) }, LoadedGame::ItemU8RecipeU16(loaded_game_sized) => todo!(), @@ -229,7 +247,7 @@ impl winit::application::ApplicationHandler for App { warn!("No Game loaded"); } }, - AppState::Loading => { + AppState::Loading { .. } => { // TODO: }, } @@ -262,7 +280,7 @@ impl Window { impl App { pub fn new(input_sender: Sender) -> Self { Self { - state: AppState::Loading, + state: AppState::MainMenu { in_ip_box: None }, window: Window::new(), last_rendered_update: 0, currently_loaded_game: None, diff --git a/src/replays/mod.rs b/src/replays/mod.rs index a068a2a..506809c 100644 --- a/src/replays/mod.rs +++ b/src/replays/mod.rs @@ -2,10 +2,17 @@ use std::borrow::Borrow; use std::future::Future; use std::ops::ControlFlow; -use genawaiter::rc::{r#gen, Gen}; -use genawaiter::yield_; +use std::sync::Arc; + +use parking_lot::Mutex; +use std::mem; + +use std::path::PathBuf; + use genawaiter::GeneratorState::Complete; use genawaiter::GeneratorState::Yielded; +use genawaiter::rc::{Gen, r#gen}; +use itertools::Itertools; use crate::{ data::DataStore, @@ -21,14 +28,19 @@ pub struct Replay< RecipeIdxType: WeakIdxTrait, DataStor: Borrow>, > { - starting_state: GameState, + /// Compressed binary representation of the starting GameState + starting_state: Box<[u8]>, pub actions: Vec>, - data_store: DataStor, + pub data_store: DataStor, current_timestep: u64, end_timestep: Option, + + storage_location: Option, + + is_dummy: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -38,18 +50,41 @@ pub struct ReplayAction } impl< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - DataStor: Borrow>, - > Replay + ItemIdxType: IdxTrait, + RecipeIdxType: IdxTrait, + DataStor: Borrow>, +> Replay { - pub fn new(game_state: GameState, data_store: DataStor) -> Self { + pub fn new( + game_state: &GameState, + storage_location: Option, + data_store: DataStor, + ) -> Self { + let game_state_bytes = bitcode::serialize(game_state).unwrap(); + Self { + starting_state: game_state_bytes.into_boxed_slice(), + 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: game_state, + starting_state: Box::new([]), actions: vec![], data_store, current_timestep: 0, end_timestep: None, + + storage_location: None, + + is_dummy: true, } } @@ -64,6 +99,9 @@ impl< &mut self, actions: impl IntoIterator>, ) { + if self.is_dummy { + return; + } self.actions .extend(actions.into_iter().map(|a| ReplayAction { timestamp: self.current_timestep, @@ -78,31 +116,37 @@ impl< pub fn run( self, ) -> ReplayViewer< - GameState, - impl Future>, + (GameState, DataStor), + impl Future, DataStor)>, > { + assert!(!self.is_dummy); ReplayViewer { generator: r#gen!({ let data_store = self.data_store; - let mut game_state: GameState = self.starting_state; 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() - .take_while(|a| a.timestamp == current_timestep) + .peeking_take_while(|a| a.timestamp == current_timestep) .map(|ra| ra.action); game_state.apply_actions(this_ticks_actions, data_store.borrow()); game_state.update(data_store.borrow()); - let game_state_opt: Option> = - yield_!(game_state); + // let game_state_opt: Option> = + // yield_!(game_state); - game_state = game_state_opt.unwrap(); + // game_state = game_state_opt.unwrap(); if Some(current_timestep) == self.end_timestep { break; @@ -111,10 +155,83 @@ impl< } } - game_state + (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; + + let mut actions = self.actions.into_iter().peekable(); + let mut current_timestep = 0; + + let 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); + + *(game_state_out.lock()) = game_state; + + loop { + let this_ticks_actions: Vec<_> = actions + .by_ref() + .peeking_take_while(|a| a.timestamp == current_timestep) + .map(|ra| ra.action) + .collect(); + + let mut game_state = game_state_out.lock(); + + game_state.apply_actions(this_ticks_actions, data_store.borrow()); + + game_state.update(data_store.borrow()); + + on_tick(); + + // 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; + } + } + } + + // 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> { @@ -122,7 +239,7 @@ pub struct ReplayViewer> { } impl> ReplayViewer { - pub fn with(mut self, every_step: impl Fn(&V) -> ControlFlow<(), ()>) -> V { + 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 { diff --git a/src/research.rs b/src/research.rs index 0729c91..79df023 100644 --- a/src/research.rs +++ b/src/research.rs @@ -1,21 +1,377 @@ -#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] +use egui::{Button, CornerRadius, ProgressBar, SidePanel}; +use egui::{Color32, Ui}; +use egui_graphs::{DefaultNodeShape, LayoutRandom, LayoutStateRandom}; +use egui_graphs::Graph; +use egui_graphs::GraphView; +use egui_graphs::LayoutHierarchical; +use egui_graphs::LayoutStateHierarchical; +use egui_graphs::SettingsInteraction; +use egui_graphs::SettingsNavigation; +use egui_graphs::{DefaultEdgeShape, SettingsStyle}; +use petgraph::Directed; +use petgraph::graph::NodeIndex; +use petgraph::visit::EdgeRef; + +use std::cmp::min; +use std::collections::{HashMap, HashSet}; +use std::u16; + +use crate::data; +use crate::data::DataStore; +use crate::item::Indexable; + +use crate::IdxTrait; +use crate::frontend::action::ActionType; +use crate::item::Recipe; + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize, +)] pub struct Technology { pub id: u16, //65k Technologies should suffice :) } +#[derive(Debug)] +pub struct LabTickInfo { + pub times_labs_used_science: u64, + pub tech: Technology, +} pub type ResearchProgress = u16; #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] pub struct TechState { pub current_technology: Option, + pub finished_technologies: HashSet, + pub in_progress_technologies: HashMap, // current_tech_mod_lookup: (), + pub recipe_active: Vec, + + science_overflow_buffer: Box<[u32]>, } impl TechState { - pub fn apply_progress(&mut self, tech_progress: u16) { - // TODO: - if tech_progress > 0 { - dbg!(tech_progress); + pub fn new( + data_store: &DataStore, + ) -> Self { + let finished_technologies = HashSet::from_iter( + data_store + .instantly_finished_technologies + .iter() + .map(|index| Technology { + id: index.index().try_into().unwrap(), + }), + ); + + let recipe_active = (0..data_store.recipe_is_intermediate.len()) + .map(|recipe_id| recipe_id.try_into().unwrap()) + .map(|recipe_id| { + finished_technologies.iter().any(|tech| { + data_store + .technology_tree + .node_weight(NodeIndex::from(tech.id)) + .unwrap() + .effect + .unlocked_recipes + .contains(&Recipe { id: recipe_id }) + }) + }) + .collect(); + + Self { + current_technology: None, + finished_technologies, + recipe_active, + + in_progress_technologies: HashMap::new(), + + science_overflow_buffer: vec![0; data_store.science_bottle_items.len()] + .into_boxed_slice(), + } + } + + pub fn get_active_recipes(&self) -> &[bool] { + &self.recipe_active + } + + #[profiling::function] + pub fn apply_progress( + &mut self, + mut tech_progress: u16, + data_store: &DataStore, + ) { + if tech_progress == 0 { + return; + } + + if let Some(current) = &self.current_technology { + let tech_cost = &data_store.technology_costs[current.id as usize]; + + // Apply science from overflow if possible + let mut tech_progress_from_overflow = u16::MAX; + for (overflow_buffer, unit_cost) in self + .science_overflow_buffer + .iter_mut() + .zip(tech_cost.1.iter()) + { + if *unit_cost > 0 { + tech_progress_from_overflow = min( + tech_progress_from_overflow, + u16::try_from(*overflow_buffer / u32::from(*unit_cost)).unwrap(), + ); + } + } + + if tech_progress_from_overflow > 0 { + dbg!(tech_progress_from_overflow); + } + + tech_progress_from_overflow = + tech_progress_from_overflow.clamp(0, u16::MAX - tech_progress); + + for (overflow_buffer, unit_cost) in self + .science_overflow_buffer + .iter_mut() + .zip(tech_cost.1.iter()) + { + *overflow_buffer = overflow_buffer + .checked_sub(u32::from(*unit_cost) * u32::from(tech_progress_from_overflow)) + .expect("Overflow buffer underflowed?!?"); + } + + tech_progress += tech_progress_from_overflow; + + // Apply this ticks progress + let progress = self.in_progress_technologies.entry(*current).or_insert(0); + + *progress += u64::from(tech_progress); + + if *progress >= tech_cost.0 { + // We finished this technology! + let final_science_used = self.in_progress_technologies.remove(¤t).unwrap(); + self.finished_technologies.insert(*current); + for recipe in &data_store + .technology_tree + .node_weight(NodeIndex::from(current.id)) + .unwrap() + .effect + .unlocked_recipes + { + self.recipe_active[recipe.into_usize()] = true; + } + self.current_technology = None; + + // 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. + // To not lose this science (which would mean players sometimes are unable to finish a technology even if they supplied enough science packs), + // We just store the overflow science and apply it if possible + let overshoot = final_science_used - tech_cost.0; + + for (overflow_buffer, unit_cost) in self + .science_overflow_buffer + .iter_mut() + .zip(tech_cost.1.iter()) + { + *overflow_buffer = (*overflow_buffer).checked_add(u32::try_from(overshoot * u64::from(*unit_cost)) + .expect("impossible since the most science unit produced this tick are u16::MAX (see funciton args)")) + .expect("Science Overflow buffer overflowed (Ironic)."); + } + + dbg!(&self.science_overflow_buffer); + } + } else { + assert_eq!( + tech_progress, 0, + "Labs should not be working without a tech set!" + ); } } + + pub fn generate_render_graph( + &self, + data_store: &DataStore, + ) -> Graph, (), Directed, u16, DefaultNodeShape, DefaultEdgeShape> + { + egui_graphs::to_graph_custom::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape>( + &data_store.technology_tree, + |node| { + if self.finished_technologies.contains(&Technology { + id: node.id().index().try_into().unwrap(), + }) { + node.set_color(Color32::GREEN); + } else if data_store + .technology_tree + .edges_directed(node.id(), petgraph::Direction::Incoming) + .all(|edge| { + self.finished_technologies.contains(&Technology { + id: edge.source().index().try_into().unwrap(), + }) + }) + { + node.set_color(Color32::YELLOW); + } else { + node.set_color(Color32::RED); + } + + node.set_label( + data_store + .technology_tree + .node_weight(NodeIndex::from(u16::try_from(node.id().index()).unwrap())) + .unwrap() + .name + .clone(), + ); + }, + |_edge| {}, + ) + } + + pub fn render_tech_window( + &self, + ui: &mut Ui, + render_graph: &mut Graph< + data::Technology, + (), + Directed, + u16, + DefaultNodeShape, + DefaultEdgeShape, + >, + data_store: &DataStore, + ) -> impl Iterator> + use + { + { + profiling::scope!("Update Tech Tree colors"); + for tech in 0..data_store.technology_costs.len() { + let node = render_graph.node_mut(NodeIndex::new(tech)).unwrap(); + + if self.finished_technologies.contains(&Technology { + id: node.id().index().try_into().unwrap(), + }) { + node.set_color(Color32::GREEN); + } else if data_store + .technology_tree + .edges_directed(node.id(), petgraph::Direction::Incoming) + .all(|edge| { + self.finished_technologies.contains(&Technology { + id: edge.source().index().try_into().unwrap(), + }) + }) + { + node.set_color(Color32::YELLOW); + } else { + node.set_color(Color32::RED); + } + } + } + + let mut ret = vec![]; + + SidePanel::new(egui::panel::Side::Left, "Current Technology Info Sidepanel").show_inside( + ui, + |ui| { + if let Some(tech) = &self.current_technology { + ui.label( + &data_store + .technology_tree + .node_weight(NodeIndex::from(tech.id)) + .unwrap() + .name, + ); + + let done = self + .in_progress_technologies + .get(tech) + .copied() + .unwrap_or(0); + let cost = data_store.technology_costs[tech.id as usize].0; + ui.add( + ProgressBar::new((done as f64 / cost as f64) as f32) + .corner_radius(CornerRadius::ZERO) + .text(format!("{}/{}", done, cost)), + ); + + if ui.button("Cancel").clicked() { + ret.push(ActionType::SetActiveResearch { tech: None }); + } else if ui.button("[CHEAT] Unlock Technology").clicked() { + ret.push(ActionType::CheatUnlockTechnology { tech: *tech }); + } + } + }, + ); + + SidePanel::new(egui::panel::Side::Right, "Technology Info Sidepanel").show_inside( + ui, + |ui| { + if ui.button("[CHEAT] Unlock all techs").clicked() { + for (tech, _tech_info) in render_graph.nodes_iter() { + ret.push(ActionType::CheatUnlockTechnology { + tech: Technology { + id: tech.index().try_into().unwrap(), + }, + }); + } + } + + if !render_graph.selected_nodes().is_empty() { + let [selected_node] = render_graph.selected_nodes() else { + unreachable!("We only allow selecting a single node!"); + }; + + ui.label( + &data_store + .technology_tree + .node_weight(NodeIndex::from( + u16::try_from(selected_node.index()).unwrap(), + )) + .unwrap() + .name, + ); + + let already_researched = self.finished_technologies.contains(&Technology { + id: selected_node.index().try_into().unwrap(), + }); + + let possible_to_research = data_store + .technology_tree + .edges_directed(*selected_node, petgraph::Direction::Incoming) + .all(|edge| { + self.finished_technologies.contains(&Technology { + id: edge.source().index().try_into().unwrap(), + }) + }); + + let is_currently_researching = Some(Technology { + id: selected_node.index().try_into().unwrap(), + }) == self.current_technology; + + if ui + .add_enabled( + !already_researched + && !is_currently_researching + && possible_to_research, + Button::new("Research"), + ) + .clicked() + { + ret.push(ActionType::SetActiveResearch { + tech: Some(Technology { + id: selected_node.index().try_into().unwrap(), + }), + }); + } + } + }, + ); + + let mut view = + GraphView::<_, _, _, _, _, _, LayoutStateRandom, LayoutRandom>::new( + render_graph, + ) + .with_navigations(&SettingsNavigation::new().with_zoom_and_pan_enabled(true)) + .with_interactions(&SettingsInteraction::new().with_node_selection_enabled(true)) + .with_styles(&SettingsStyle::new().with_labels_always(true)); + + ui.add(&mut view); + + ret.into_iter() + } } diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 7d50ce0..d09fa75 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -1,18 +1,20 @@ use std::{ borrow::Borrow, env, - fs::{create_dir_all, File}, + fs::{File, create_dir_all}, io::{Read, Write}, marker::PhantomData, + path::PathBuf, }; +use bitcode::Encode; use directories::ProjectDirs; use log::error; use ron::ser::PrettyConfig; -use crate::{item::IdxTrait, rendering::app_state::GameState}; +use crate::{data::DataStore, item::IdxTrait, rendering::app_state::GameState}; -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Encode, serde::Deserialize, serde::Serialize)] pub struct SaveGame< ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait, @@ -20,6 +22,7 @@ pub struct SaveGame< > { pub checksum: String, pub game_state: G, + // pub data_store: D, pub item: PhantomData, pub recipe: PhantomData, } @@ -28,8 +31,9 @@ pub struct SaveGame< /// If File system stuff fails pub fn save( game_state: &GameState, - checksum: String, + data_store: &DataStore, ) { + let checksum = data_store.checksum.clone(); let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); create_dir_all(dir.data_dir()).expect("Could not create data dir"); @@ -56,10 +60,12 @@ pub fn save( } } + let temp_file_dir = dir.data_dir().join("tmp.save"); let save_file_dir = dir.data_dir().join("save.save"); - let mut file = File::create(save_file_dir).expect("Could not open file"); + let mut file = File::create(&temp_file_dir).expect("Could not open file"); + // FIXME: What to do, if the size of the Save in memory + on disk exceeds RAM? let res = bitcode::serialize(&SaveGame { checksum, game_state, @@ -69,6 +75,8 @@ pub fn save( .unwrap(); file.write_all(&res).expect("Could not write to file"); + + std::fs::rename(temp_file_dir, save_file_dir).expect("Could not rename tmp save file!"); } /// # Panics @@ -77,17 +85,45 @@ pub fn save( pub fn load< ItemIdxType: IdxTrait + for<'a> serde::Deserialize<'a>, RecipeIdxType: IdxTrait + for<'a> serde::Deserialize<'a>, ->() -> Option>> { - let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); +>( + path: PathBuf, +) -> Option>> { + let file = File::open(path); - let save_file_dir = dir.data_dir().join("save.save"); + file.map_or(None, |mut file| { + let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); + + file.read_to_end(&mut v).unwrap(); + + match bitcode::deserialize(&mut v) { + Ok(val) => Some(val), + Err(err) => { + error!("Found save, but was unable to deserialize it!!!! \n{}", err); + None + }, + } + }) +} + +/// # Panics +/// If File system stuff fails +#[must_use] +pub fn load_readable< + ItemIdxType: IdxTrait + for<'a> serde::Deserialize<'a>, + RecipeIdxType: IdxTrait + for<'a> serde::Deserialize<'a>, +>( + path: PathBuf, +) -> Option>> { + let file = File::open(path); + + file.map_or(None, |mut file| { + let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); - let file = File::open(save_file_dir); + file.read_to_end(&mut v).unwrap(); - file.map_or(None, |file| { - let v = file.bytes().collect::, _>>().unwrap(); + let mut de = ron::Deserializer::from_bytes(&v).unwrap(); - match bitcode::deserialize(v.as_slice()) { + match serde_path_to_error::deserialize(&mut de) { Ok(val) => Some(val), Err(err) => { error!("Found save, but was unable to deserialize it!!!! \n{}", err); diff --git a/src/split_arbitrary.rs b/src/split_arbitrary.rs index b9ac545..e8ebda4 100644 --- a/src/split_arbitrary.rs +++ b/src/split_arbitrary.rs @@ -1,14 +1,14 @@ use std::{cmp::min, mem}; use rayon::iter::{ - plumbing::{bridge, Producer}, IndexedParallelIterator, ParallelIterator, + plumbing::{Producer, bridge}, }; pub fn split_arbitrary_mut( slice: &mut [T], sizes: impl IntoIterator, -) -> impl IntoIterator { +) -> impl Iterator { SplitArbitraryMut { slice, sizes: sizes.into_iter(), @@ -41,7 +41,7 @@ impl<'a, T, C: Iterator> Iterator for SplitArbitraryMut<'a, T, C> pub fn split_arbitrary_mut_slice<'a, 'b, T: Send>( slice: &'a mut [T], sizes: &'b [usize], -) -> impl IntoIterator + use<'a, 'b, T> + IndexedParallelIterator +) -> impl Iterator + use<'a, 'b, T> + IndexedParallelIterator { SplitArbitraryMutSlice { slice, sizes } } diff --git a/src/statistics/consumption.rs b/src/statistics/consumption.rs new file mode 100644 index 0000000..9097cc3 --- /dev/null +++ b/src/statistics/consumption.rs @@ -0,0 +1,139 @@ +use std::{ + borrow::Borrow, + collections::BTreeMap, + ops::{Add, AddAssign}, +}; + +use charts_rs::Series; +use itertools::Itertools; + +use crate::{ + NewWithDataStore, + data::DataStore, + item::{IdxTrait, Indexable, Item}, +}; + +use crate::research::LabTickInfo; + +use super::{IntoSeries, recipe::RecipeTickInfo}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ConsumptionInfo { + items_consumed: Vec, +} + +impl ConsumptionInfo { + pub fn from_infos( + recipe_info: &RecipeTickInfo, + lab_info: &Option, + data_store: &DataStore, + ) -> Self { + let mut ret = Self { + items_consumed: data_store + .item_to_recipe_count_where_its_ingredient + .iter() + .map(|values| { + values + .iter() + .map(|(recipe, amount)| { + recipe_info.num_crafts_finished[recipe.id.into()].full_crafts + * u64::from(*amount) + }) + .sum() + }) + .collect(), + }; + + if let Some(lab_info) = lab_info { + let costs = &data_store.technology_costs[lab_info.tech.id as usize].1; + + for (cost, item) in costs.iter().zip(data_store.science_bottle_items.iter()) { + ret.items_consumed[item.into_usize()] += + u64::from(*cost) * lab_info.times_labs_used_science; + } + } + + ret + } +} + +impl Add<&ConsumptionInfo> for ConsumptionInfo { + type Output = ConsumptionInfo; + + fn add(mut self, rhs: &ConsumptionInfo) -> Self::Output { + for (s, rhs) in self + .items_consumed + .iter_mut() + .zip(rhs.items_consumed.iter()) + { + *s += rhs; + } + + self + } +} + +impl AddAssign<&ConsumptionInfo> for ConsumptionInfo { + fn add_assign(&mut self, rhs: &ConsumptionInfo) { + for (s, rhs) in self + .items_consumed + .iter_mut() + .zip(rhs.items_consumed.iter()) + { + *s += rhs; + } + } +} + +impl + IntoSeries, ItemIdxType, RecipeIdxType> for ConsumptionInfo +{ + fn into_series( + values: &[Self], + filter: Option) -> bool>, + data_store: &DataStore, + ) -> impl Iterator { + BTreeMap::from_iter( + values + .iter() + .map(|info| { + info.items_consumed + .iter() + .zip(data_store.item_display_names.iter()) + .enumerate() + .filter_map(|(item_id, v)| { + filter + .as_ref() + .map(|filter| { + filter(Item { + id: ItemIdxType::try_from(item_id).unwrap(), + }) + }) + .unwrap_or(true) + .then_some(((item_id, v.1), *v.0)) + }) + }) + .flatten() + .into_group_map() + .into_iter() + .map(|(k, v)| (k.0, (k.1, v))), + ) + .into_iter() + .map(|(item_id, a)| { + ( + item_id.try_into().unwrap(), + (a.0.as_str(), a.1.into_iter().map(|v| v as f32).collect()).into(), + ) + }) + } +} + +impl NewWithDataStore for ConsumptionInfo { + fn new( + data_store: impl Borrow>, + ) -> Self { + Self { + items_consumed: vec![0; data_store.borrow().item_names.len()], + } + } +} diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 4abc538..ae933f3 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -1,15 +1,17 @@ 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, - NewWithDataStore, }; +pub mod consumption; mod power; pub mod production; pub mod recipe; @@ -24,7 +26,7 @@ pub const TIMESCALE_NAMES: [&'static str; NUM_DIFFERENT_TIMESCALES] = ["10 seconds", "1 minute", "1 hour"]; pub const TIMESCALE_LEGEND: [fn(f64) -> String; NUM_DIFFERENT_TIMESCALES] = [ - |t| format!("{:.0}s", dbg!(t / 60.0)), + |t| format!("{:.0}s", t / 60.0), |t| format!("{:.0}s", t), |t| format!("{:.0}m", t), ]; @@ -32,6 +34,7 @@ pub const TIMESCALE_LEGEND: [fn(f64) -> String; NUM_DIFFERENT_TIMESCALES] = [ #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GenStatistics { pub production: Timeline, + pub consumption: Timeline, research: Timeline, } @@ -41,13 +44,19 @@ impl GenStatistics { ) -> Self { GenStatistics { production: Timeline::new(true, data_store), + consumption: Timeline::new(true, data_store), research: Timeline::new(true, data_store), } } - pub fn append_single_set_of_samples(&mut self, samples: (ProductionInfo, ResearchProgress)) { + #[profiling::function] + pub fn append_single_set_of_samples( + &mut self, + samples: (ProductionInfo, ConsumptionInfo, ResearchProgress), + ) { self.production.append_single_set_of_samples(samples.0); - self.research.append_single_set_of_samples(samples.1 as u64); + self.consumption.append_single_set_of_samples(samples.1); + self.research.append_single_set_of_samples(samples.2 as u64); } pub fn get_chart( @@ -60,6 +69,7 @@ impl GenStatistics { .production .get_series(timescale, data_store, filter) .into_iter() + .map(|v| v.1) .collect(); LineChart::new( @@ -74,7 +84,7 @@ pub trait IntoSeries: Sized { values: &[Self], filter: Option bool>, data_store: &DataStore, - ) -> impl IntoIterator; + ) -> impl Iterator; } #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] @@ -114,9 +124,7 @@ impl AddAssign<&'a T>> Timeline { break; } - let (level_to_read_from, current_level) = - // TODO: mid might be wrong here - self.samples.split_at_mut(current_level_idx); + let (level_to_read_from, current_level) = self.samples.split_at_mut(current_level_idx); let (level_to_read_from, current_level) = (level_to_read_from.last().unwrap(), &mut current_level[0]); @@ -156,10 +164,14 @@ impl AddAssign<&'a T>> Timeline { timescale: usize, data_store: &'b DataStore, filter: Option, - ) -> impl IntoIterator + use<'a, 'b, T, Item, ItemIdxType, RecipeIdxType, Filter> + ) -> impl Iterator + use<'a, 'b, T, Item, ItemIdxType, RecipeIdxType, Filter> where T: IntoSeries, { T::into_series(&self.samples[timescale], filter, data_store) } + + pub fn get_data_points(&self, timescale: usize) -> &[T] { + &self.samples[timescale] + } } diff --git a/src/statistics/power.rs b/src/statistics/power.rs index 3aebabc..f6a1b24 100644 --- a/src/statistics/power.rs +++ b/src/statistics/power.rs @@ -14,10 +14,11 @@ impl { fn into_series( values: &[Self], - filter: Option) -> bool>, - data_store: &DataStore, - ) -> impl IntoIterator { - iter::once( + _filter: Option) -> bool>, + _data_store: &DataStore, + ) -> impl Iterator { + iter::once(( + 0, ( "Power Satisfaction", values @@ -26,6 +27,6 @@ impl .collect::>(), ) .into(), - ) + )) } } diff --git a/src/statistics/production.rs b/src/statistics/production.rs index 3e6e930..f077d0f 100644 --- a/src/statistics/production.rs +++ b/src/statistics/production.rs @@ -8,17 +8,16 @@ use charts_rs::Series; use itertools::Itertools; use crate::{ + NewWithDataStore, data::DataStore, item::{IdxTrait, Item}, - NewWithDataStore, }; -use super::{recipe::RecipeTickInfo, IntoSeries}; +use super::{IntoSeries, recipe::RecipeTickInfo}; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct ProductionInfo { pub items_produced: Vec, - items_used: Vec, } impl ProductionInfo { @@ -41,19 +40,6 @@ impl ProductionInfo { .sum() }) .collect(), - items_used: data_store - .item_to_recipe_count_where_its_ingredient - .iter() - .map(|values| { - values - .iter() - .map(|(recipe, amount)| { - info.num_crafts_finished[recipe.id.into()].full_crafts - * u64::from(*amount) - }) - .sum() - }) - .collect(), } } } @@ -70,10 +56,6 @@ impl Add<&ProductionInfo> for ProductionInfo { *s += rhs; } - for (s, rhs) in self.items_used.iter_mut().zip(rhs.items_used.iter()) { - *s += rhs; - } - self } } @@ -87,10 +69,6 @@ impl AddAssign<&ProductionInfo> for ProductionInfo { { *s += rhs; } - - for (s, rhs) in self.items_used.iter_mut().zip(rhs.items_used.iter()) { - *s += rhs; - } } } @@ -101,14 +79,14 @@ impl values: &[Self], filter: Option) -> bool>, data_store: &DataStore, - ) -> impl IntoIterator { + ) -> impl Iterator { BTreeMap::from_iter( values .iter() .map(|info| { info.items_produced .iter() - .zip(data_store.item_names.iter()) + .zip(data_store.item_display_names.iter()) .enumerate() .filter_map(|(item_id, v)| { filter @@ -127,8 +105,13 @@ impl .into_iter() .map(|(k, v)| (k.0, (k.1, v))), ) - .into_values() - .map(|a| (a.0.as_str(), a.1.into_iter().map(|v| v as f32).collect()).into()) + .into_iter() + .map(|(item_id, a)| { + ( + item_id.try_into().unwrap(), + (a.0.as_str(), a.1.into_iter().map(|v| v as f32).collect()).into(), + ) + }) } } @@ -138,7 +121,6 @@ impl NewWithDataStore for ProductionInfo { ) -> Self { Self { items_produced: vec![0; data_store.borrow().item_names.len()], - items_used: vec![0; data_store.borrow().item_names.len()], } } } diff --git a/src/statistics/recipe.rs b/src/statistics/recipe.rs index 6e0439e..77feee4 100644 --- a/src/statistics/recipe.rs +++ b/src/statistics/recipe.rs @@ -17,6 +17,12 @@ pub struct RecipeTickInfoParts { pub recipes_0_1: Vec, pub recipes_1_1: Vec, pub recipes_2_1: Vec, + pub recipes_2_2: Vec, + pub recipes_2_3: Vec, + pub recipes_3_1: Vec, + pub recipes_4_1: Vec, + pub recipes_5_1: Vec, + pub recipes_6_1: Vec, // etc. } @@ -39,6 +45,12 @@ impl RecipeTickInfo { let mut recipes_0_1 = parts.recipes_0_1.into_iter(); let mut recipes_1_1 = parts.recipes_1_1.into_iter(); let mut recipes_2_1 = parts.recipes_2_1.into_iter(); + let mut recipes_2_2 = parts.recipes_2_2.into_iter(); + let mut recipes_2_3 = parts.recipes_2_3.into_iter(); + let mut recipes_3_1 = parts.recipes_3_1.into_iter(); + let mut recipes_4_1 = parts.recipes_4_1.into_iter(); + let mut recipes_5_1 = parts.recipes_5_1.into_iter(); + let mut recipes_6_1 = parts.recipes_6_1.into_iter(); let num_crafts_finished = data_store .recipe_num_ing_lookup @@ -54,6 +66,24 @@ impl RecipeTickInfo { (2, 1) => recipes_2_1.next().expect( "Number of recipes in parts does not match number of recipes in datastore", ), + (2, 2) => recipes_2_2.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), + (2, 3) => recipes_2_3.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), + (3, 1) => recipes_3_1.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), + (4, 1) => recipes_4_1.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), + (5, 1) => recipes_5_1.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), + (6, 1) => recipes_6_1.next().expect( + "Number of recipes in parts does not match number of recipes in datastore", + ), // etc _ => unreachable!(), }) diff --git a/src/statistics/research.rs b/src/statistics/research.rs index 86d8870..0d5abb2 100644 --- a/src/statistics/research.rs +++ b/src/statistics/research.rs @@ -1,23 +1,11 @@ use std::iter; -use crate::{ - item::{IdxTrait, WeakIdxTrait}, - research::ResearchProgress, - NewWithDataStore, -}; +use crate::{item::IdxTrait, research::ResearchProgress}; use super::IntoSeries; pub struct ResearchInfo {} -impl NewWithDataStore for u64 { - fn new( - _data_store: impl std::borrow::Borrow>, - ) -> Self { - Self::default() - } -} - impl IntoSeries<(), ItemIdxType, RecipeIdxType> for ResearchProgress { @@ -25,8 +13,9 @@ impl IntoSeries<(), ItemIdxType, values: &[Self], filter: Option bool>, _data_store: &crate::data::DataStore, - ) -> impl IntoIterator { - iter::once( + ) -> impl Iterator { + iter::once(( + 0, ( "Research", values @@ -36,6 +25,6 @@ impl IntoSeries<(), ItemIdxType, .collect(), ) .into(), - ) + )) } } diff --git a/src/storage_list.rs b/src/storage_list.rs index c761765..2085f0e 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -1,44 +1,31 @@ +use std::iter; use std::u16; use itertools::Itertools; use rayon::iter::IndexedParallelIterator; +use strum::IntoEnumIterator; +use crate::chest::MultiChestStore; use crate::{ assembler::FullAssemblerStore, chest::FullChestStore, - data::{DataStore, ItemRecipeDir}, - inserter::Storage, - item::{usize_from, IdxTrait, Item, ITEMCOUNTTYPE}, + data::{DataStore, ItemRecipeDir, all_item_iter}, + inserter::{FakeUnionStorage, StaticID, Storage}, + item::{ITEMCOUNTTYPE, IdxTrait, Item, usize_from}, lab::MultiLabStore, - power::{power_grid::PowerGridIdentifier, PowerGridStorage}, + power::{PowerGridStorage, power_grid::PowerGridIdentifier}, split_arbitrary::split_arbitrary_mut_slice, }; -const ALWAYS_FULL: &'static [ITEMCOUNTTYPE] = &[0; u16::MAX as usize]; -const PANIC_ON_INSERT: &'static [ITEMCOUNTTYPE] = &[0; 0]; +// 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 PANIC_ON_INSERT_DATA: &'static [ITEMCOUNTTYPE] = &[0; 0]; -const NUM_ITEMS: usize = 0; -static NUM_GRIDS: usize = 0; -const NUM_RECIPES: usize = 0; -static NUM_MACHINES_OF_RECIPE: usize = 0; -static NUMBER_OF_CHESTS: usize = 0; -static NUMBER_OF_BOT_NETWORKS: usize = 0; - -type ItemSlot = ITEMCOUNTTYPE; type SingleGridStorage<'a, 'b> = (&'a [ITEMCOUNTTYPE], &'b mut [ITEMCOUNTTYPE]); 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]; -type ChestStorages<'a> = &'a mut [ITEMCOUNTTYPE; NUMBER_OF_CHESTS]; -/// Provider, Requester, Storage -const NUMBER_OF_DIFFERENT_CHEST_TYPES: usize = 3; -type SingleBotNetworkChestStorage<'a> = - [&'a mut [ITEMCOUNTTYPE; NUMBER_OF_CHESTS]; NUMBER_OF_DIFFERENT_CHEST_TYPES]; -type BotNetworkStorages<'a> = [SingleBotNetworkChestStorage<'a>; NUMBER_OF_BOT_NETWORKS]; - -// Ideally we could have Box<[SingleItem; NUM_ITEMS]> -// SingleItem = Box<[&mut [ITEMCOUNTTYPE]]> - fn num_labs( item: Item, data_store: &DataStore, @@ -58,6 +45,18 @@ pub fn num_recipes( num_recipes } +pub fn static_size( + item: Item, + data_store: &DataStore, +) -> usize { + let mut size = 0; + + // Chests + size += 1; + + size +} + pub fn grid_size( item: Item, data_store: &DataStore, @@ -72,18 +71,23 @@ fn size_of_single_item_slice( num_grids_total: usize, data_store: &DataStore, ) -> usize { - let num_different_static_containers = data_store.num_different_static_containers; let grid_size = grid_size(item, data_store); - num_grids_total * grid_size + num_different_static_containers + let static_size = static_size(item, data_store); + + let first_grid_offs = static_size.div_ceil(grid_size); + + (first_grid_offs + num_grids_total) * grid_size } pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( slice: SingleItemStorages<'a, 'b>, storage_id: Storage, - num_grids_total: usize, num_recipes: usize, grid_size: usize, + static_size: usize, ) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { + let first_grid_offs_in_grids = static_size.div_ceil(grid_size); + match storage_id { Storage::Assembler { grid, @@ -94,37 +98,50 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( usize_from(recipe_idx_with_this_item) < num_recipes, "The recipe stored in an inserter needs to be translated!" ); - let outer = &mut slice[Into::::into(grid) * grid_size + let outer = &mut slice[(first_grid_offs_in_grids + Into::::into(grid)) + * grid_size + Into::::into(recipe_idx_with_this_item)]; ( - &outer.0[Into::::into(index)], - &mut outer.1[Into::::into(index)], + &outer.0[usize::try_from(index).unwrap()], + &mut outer.1[usize::try_from(index).unwrap()], ) }, Storage::Lab { grid, index } => { - let outer = &mut slice[Into::::into(grid) * grid_size + num_recipes]; + let outer = &mut slice + [(first_grid_offs_in_grids + Into::::into(grid)) * grid_size + num_recipes]; ( - &outer.0[Into::::into(index)], - &mut outer.1[Into::::into(index)], + &outer.0[usize::try_from(index).unwrap()], + &mut outer.1[usize::try_from(index).unwrap()], ) }, Storage::Static { static_id, index } => { // debug_assert!(usize::from(static_id) < data_store.num_different_static_containers); - let outer = - &mut slice[num_grids_total * grid_size + Into::::into(static_id as u8)]; + let outer = &mut slice[Into::::into(static_id)]; ( - &outer.0[Into::::into(index)], - &mut outer.1[Into::::into(index)], + &outer.0[usize::try_from(index).unwrap()], + &mut outer.1[usize::try_from(index).unwrap()], ) }, } } +pub fn index_fake_union<'a, 'b>( + slice: SingleItemStorages<'a, 'b>, + storage_id: FakeUnionStorage, + grid_size: usize, +) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { + let (outer, inner) = storage_id.into_inner_and_outer_indices_with_statics_at_zero(grid_size); + + let subslice = &mut slice[outer]; + (&subslice.0[inner], &mut subslice.1[inner]) +} + +#[profiling::function] pub fn sizes<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( data_store: &'a DataStore, num_grids_total: usize, -) -> impl IntoIterator + use<'a, ItemIdxType, RecipeIdxType> { - (0..data_store.item_names.len()) +) -> impl Iterator + use<'a, ItemIdxType, RecipeIdxType> { + (0..data_store.item_display_names.len()) .map(|i| Item { id: ItemIdxType::try_from(i).unwrap(), }) @@ -134,9 +151,9 @@ pub fn sizes<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( pub fn full_to_by_item<'a, 'b, 'c, 'd>( storages: &'d mut FullStorages<'a, 'b>, sizes: &'c [usize], -) -> impl IntoIterator> - + use<'a, 'b, 'c, 'd> - + IndexedParallelIterator> +) -> impl Iterator> ++ use<'a, 'b, 'c, 'd> ++ IndexedParallelIterator> where 'b: 'a, 'd: 'a, @@ -145,6 +162,7 @@ where split_arbitrary_mut_slice(storages, &sizes) } +#[profiling::function] fn get_full_storage_index( item: Item, storage: Storage, @@ -160,24 +178,30 @@ fn get_full_storage_index( .sum(); let num_recipes = num_recipes(item, data_store); - let num_labs = num_labs(item, data_store); let grid_size = grid_size(item, data_store); + let static_size = static_size(item, data_store); + + let first_grid_offs = static_size.div_ceil(grid_size); let ret = match storage { Storage::Assembler { grid, recipe_idx_with_this_item: recipe, - index, - } => item_offs + usize::from(grid) * grid_size + usize_from(recipe), - Storage::Lab { grid, index } => item_offs + usize::from(grid) * grid_size + num_recipes, - Storage::Static { static_id, index } => { - item_offs + num_power_grids * grid_size + usize::from(static_id as u8) + index: _, + } => item_offs + (first_grid_offs + usize::from(grid)) * grid_size + usize_from(recipe), + Storage::Lab { grid, index: _ } => { + item_offs + (first_grid_offs + usize::from(grid)) * grid_size + num_recipes }, + Storage::Static { + static_id, + index: _, + } => item_offs + usize::from(static_id), }; ret } +#[profiling::function] pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( grids: &'a mut PowerGridStorage, chest_store: &'a mut FullChestStore, @@ -217,21 +241,26 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( - 1, "{:?}", { - let mut r = Vec::from_iter( - all_storages(grids, chest_store, data_store) - .into_iter() - .map(|v| { - let idx = get_full_storage_index( - v.0, - v.1, - num_power_grids, - data_store, - ); - (v, idx) - }), - ); - r.sort_by_key(|v| v.1); - r + let mut storages: Vec<_> = all_storages(grids, chest_store, data_store) + .into_iter() + .map(|v| { + ( + get_full_storage_index(v.0, v.1, num_power_grids, data_store), + (v.0, v.1, v.2.len()), + ) + }) + .collect(); + storages.sort_by_key(|v| { + get_full_storage_index(v.1.0, v.1.1, num_power_grids, data_store) + }); + + for i in 0..max_index { + if !storages.iter().any(|v| v.0 == i) { + dbg!(i); + } + } + + storages } ) }, @@ -246,21 +275,82 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( } } - let all_storages = all_storages(grids, chest_store, data_store); + // let all_storages = all_storages(grids, chest_store, data_store); + + // let all_storages_sorted: Box<[_]> = { + // profiling::scope!("Sort Storages"); + // let mut ret: Vec<_> = all_storages.into_iter().collect(); + + // ret.sort_by_cached_key(|v| get_full_storage_index(v.0, v.1, num_power_grids, data_store)); + + // // TODO: Collecting twice here seems wasteful + // ret.into_iter().map(|v| (v.2, v.3)).collect() + // }; + + // let correct_len = all_storages_sorted.len(); + + let all_storages_sorted: Box<[_]> = { + let mut grids_by_item = { + // TODO: This is super slow with a lot of grids. To the point where it takes the majority of the processing time in some cases! + profiling::scope!("grids_by_item"); + grids + .power_grids + .iter_mut() + .enumerate() + .flat_map(|(grid_id, grid)| { + let grid_id = grid_id.try_into().unwrap(); + all_assembler_storages(grid_id, &mut grid.stores, data_store) + .into_iter() + .chain(all_lab_storages(grid_id, &mut grid.lab_stores, data_store)) + }) + .into_group_map_by(|v| v.0) + }; + + for item in all_item_iter(data_store) { + let _vec = grids_by_item.entry(item).or_default(); + // assert!( + // vec.is_sorted_by_key(|v| get_full_storage_index( + // v.0, + // v.1, + // num_power_grids, + // data_store + // )), + // "{:?}", + // vec.iter() + // .map(|v| ( + // v.1, + // get_full_storage_index(v.0, v.1, num_power_grids, data_store) + // )) + // .collect_vec() + // ); + } + + { + profiling::scope!("all_storages_sorted"); + all_item_iter(data_store) + .zip(chest_store.stores.iter_mut()) + .zip(grids_by_item.into_iter().sorted_by_key(|v| v.0)) + .flat_map(|((item, chest_store), (grid_item, grid))| { + assert_eq!(item, grid_item); + chest_storages_pre_sorted(item, chest_store, data_store).chain( + grid.into_iter() + .map(|(_item, _storage, max_insert, data)| (max_insert, data)), + ) + }) + .collect() + } + }; - let all_storages_sorted = all_storages - .into_iter() - .sorted_unstable_by_key(|v| get_full_storage_index(v.0, v.1, num_power_grids, data_store)) - .map(|v| (v.2, v.3)) - .collect(); + // assert_eq!(correct_len, all_storages_sorted.len()); all_storages_sorted } +#[profiling::function] fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( grids: &'a mut PowerGridStorage, chest_store: &'a mut FullChestStore, data_store: &'b DataStore, -) -> impl IntoIterator< +) -> impl Iterator< Item = ( Item, Storage, @@ -278,14 +368,34 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( .into_iter() .chain(all_lab_storages(grid_id, &mut grid.lab_stores, data_store)) }) - .chain(all_chest_storages(chest_store)); + .chain(all_static_storages(chest_store, data_store)); all_storages } + +#[profiling::function] +pub fn chest_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( + item: Item, + chest_store: &'a mut MultiChestStore, + data_store: &'b DataStore, +) -> impl Iterator ++ use<'a, 'b, ItemIdxType, RecipeIdxType> { + let grid_size = grid_size(item, data_store); + let static_size = static_size(item, data_store); + + let first_grid_offs_in_grids = static_size.div_ceil(grid_size); + + let first_grid_offs = grid_size * first_grid_offs_in_grids; + + iter::once(chest_store.storage_list_slices()) + .chain(iter::repeat_with(|| (PANIC_ON_INSERT, [].as_mut_slice()))) + .take(first_grid_offs) +} + fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( grid: PowerGridIdentifier, assembler_store: &'a mut FullAssemblerStore, data_store: &'b DataStore, -) -> impl IntoIterator< +) -> impl Iterator< Item = ( Item, Storage, @@ -465,42 +575,774 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ), ] }), - ); - i -} + ) + .chain( + assembler_store + .assemblers_2_2 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_2_2, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); -fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( - grid: PowerGridIdentifier, - lab_store: &'a mut MultiLabStore, - data_store: &'b DataStore, -) -> impl IntoIterator< - Item = ( - Item, - Storage, - &'a [ITEMCOUNTTYPE], - &'a mut [ITEMCOUNTTYPE], - ), -> + use<'a, 'b, ItemIdxType, RecipeIdxType> { - lab_store - .sciences - .iter_mut() - .zip(lab_store.max_insert.iter()) - .zip(data_store.science_bottle_items.iter().copied()) - .map(move |((science, max_insert), item)| { - ( - item, - Storage::Lab { grid, index: 0 }, - max_insert.as_slice(), - science.as_mut_slice(), - ) - }) -} + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); -fn all_lazy_power_machine_storages() {} + let mut items_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }); + + 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(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 2)][recipe_id_2_2], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 2)][recipe_id_2_2], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_out0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 2)][recipe_id_2_2], + item_out0, + )], + index: 0, + }, + ALWAYS_FULL, + outputs0, + ), + ( + item_out1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 2)][recipe_id_2_2], + item_out1, + )], + index: 0, + }, + ALWAYS_FULL, + outputs1, + ), + ] + }), + ) + .chain( + assembler_store + .assemblers_2_3 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_2_3, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); + + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); + + let mut items_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }); + + let item_out0 = *items_out.next().unwrap(); + 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(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 3)][recipe_id_2_3], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 3)][recipe_id_2_3], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_out0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 3)][recipe_id_2_3], + item_out0, + )], + index: 0, + }, + ALWAYS_FULL, + outputs0, + ), + ( + item_out1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 3)][recipe_id_2_3], + item_out1, + )], + index: 0, + }, + ALWAYS_FULL, + outputs1, + ), + ( + item_out2, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(2, 3)][recipe_id_2_3], + item_out2, + )], + index: 0, + }, + ALWAYS_FULL, + outputs2, + ), + ] + }), + ) + .chain( + assembler_store + .assemblers_3_1 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_3_1, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); + + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); + let item_in2 = *items_in.next().unwrap(); + + let item_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }) + .nth(0) + .copied() + .unwrap(); + + let (([ings0_max, ings1_max, ings2_max], [ings0, ings1, ings2]), [outputs]) = + multi.get_all_mut(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(3, 1)][recipe_id_3_1], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(3, 1)][recipe_id_3_1], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_in2, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(3, 1)][recipe_id_3_1], + item_in2, + )], + index: 0, + }, + ings2_max, + ings2, + ), + ( + item_out, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(3, 1)][recipe_id_3_1], + item_out, + )], + index: 0, + }, + ALWAYS_FULL, + outputs, + ), + ] + }), + ) + .chain( + assembler_store + .assemblers_4_1 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_4_1, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); + + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); + let item_in2 = *items_in.next().unwrap(); + let item_in3 = *items_in.next().unwrap(); + + let item_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }) + .nth(0) + .copied() + .unwrap(); + + let ( + ( + [ings0_max, ings1_max, ings2_max, ings3_max], + [ings0, ings1, ings2, ings3], + ), + [outputs], + ) = multi.get_all_mut(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(4, 1)][recipe_id_4_1], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(4, 1)][recipe_id_4_1], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_in2, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(4, 1)][recipe_id_4_1], + item_in2, + )], + index: 0, + }, + ings2_max, + ings2, + ), + ( + item_in3, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(4, 1)][recipe_id_4_1], + item_in3, + )], + index: 0, + }, + ings3_max, + ings3, + ), + ( + item_out, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(4, 1)][recipe_id_4_1], + item_out, + )], + index: 0, + }, + ALWAYS_FULL, + outputs, + ), + ] + }), + ) + .chain( + assembler_store + .assemblers_5_1 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_5_1, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); + + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); + let item_in2 = *items_in.next().unwrap(); + let item_in3 = *items_in.next().unwrap(); + let item_in4 = *items_in.next().unwrap(); + + let item_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }) + .nth(0) + .copied() + .unwrap(); + + let ( + ( + [ings0_max, ings1_max, ings2_max, ings3_max, ings4_max], + [ings0, ings1, ings2, ings3, ings4], + ), + [outputs], + ) = multi.get_all_mut(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_in2, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_in2, + )], + index: 0, + }, + ings2_max, + ings2, + ), + ( + item_in3, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_in3, + )], + index: 0, + }, + ings3_max, + ings3, + ), + ( + item_in4, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_in4, + )], + index: 0, + }, + ings4_max, + ings4, + ), + ( + item_out, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(5, 1)][recipe_id_5_1], + item_out, + )], + index: 0, + }, + ALWAYS_FULL, + outputs, + ), + ] + }), + ) + .chain( + assembler_store + .assemblers_6_1 + .iter_mut() + .enumerate() + .flat_map(move |(recipe_id_6_1, multi)| { + let mut items_in = data_store.recipe_to_items[&multi.recipe].iter().filter_map( + |(dir, item)| { + if *dir == ItemRecipeDir::Ing { + Some(item) + } else { + None + } + }, + ); + + let item_in0 = *items_in.next().unwrap(); + let item_in1 = *items_in.next().unwrap(); + let item_in2 = *items_in.next().unwrap(); + let item_in3 = *items_in.next().unwrap(); + let item_in4 = *items_in.next().unwrap(); + let item_in5 = *items_in.next().unwrap(); + + let item_out = data_store.recipe_to_items[&multi.recipe] + .iter() + .filter_map(|(dir, item)| { + if *dir == ItemRecipeDir::Out { + Some(item) + } else { + None + } + }) + .nth(0) + .copied() + .unwrap(); + + let ( + ( + [ + ings0_max, + ings1_max, + ings2_max, + ings3_max, + ings4_max, + ings5_max, + ], + [ings0, ings1, ings2, ings3, ings4, ings5], + ), + [outputs], + ) = multi.get_all_mut(); + + [ + ( + item_in0, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in0, + )], + index: 0, + }, + ings0_max, + ings0, + ), + ( + item_in1, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in1, + )], + index: 0, + }, + ings1_max, + ings1, + ), + ( + item_in2, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in2, + )], + index: 0, + }, + ings2_max, + ings2, + ), + ( + item_in3, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in3, + )], + index: 0, + }, + ings3_max, + ings3, + ), + ( + item_in4, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in4, + )], + index: 0, + }, + ings4_max, + ings4, + ), + ( + item_in5, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_in5, + )], + index: 0, + }, + ings5_max, + ings5, + ), + ( + item_out, + Storage::Assembler { + grid, + recipe_idx_with_this_item: data_store.recipe_to_translated_index[&( + data_store.ing_out_num_to_recipe[&(6, 1)][recipe_id_6_1], + item_out, + )], + index: 0, + }, + ALWAYS_FULL, + outputs, + ), + ] + }), + ); + i +} + +fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( + grid: PowerGridIdentifier, + lab_store: &'a mut MultiLabStore, + data_store: &'b DataStore, +) -> impl Iterator< + Item = ( + Item, + Storage, + &'a [ITEMCOUNTTYPE], + &'a mut [ITEMCOUNTTYPE], + ), +> + use<'a, 'b, ItemIdxType, RecipeIdxType> { + lab_store + .sciences + .iter_mut() + .zip(lab_store.max_insert.iter()) + .zip(data_store.science_bottle_items.iter().copied()) + .map(move |((science, max_insert), item)| { + ( + item, + Storage::Lab { grid, index: 0 }, + max_insert.as_slice(), + science.as_mut_slice(), + ) + }) +} + +fn all_lazy_power_machine_storages() {} + +#[profiling::function] +fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( + chest_store: &'a mut FullChestStore, + data_store: &'b DataStore, +) -> impl Iterator< + Item = ( + Item, + Storage, + &'a [ITEMCOUNTTYPE], + &'a mut [ITEMCOUNTTYPE], + ), +> + use<'a, 'b, ItemIdxType, RecipeIdxType> { + (0..data_store.item_display_names.len()) + .zip(chest_store.stores.iter_mut()) + .flat_map(|(id, chest)| { + let item = Item { + id: id.try_into().unwrap(), + }; + + let grid_size = grid_size(item, data_store); + let static_size = static_size(item, data_store); + + let first_grid_offs_in_grids = static_size.div_ceil(grid_size); + + let first_grid_offs = grid_size * first_grid_offs_in_grids; + + assert!(first_grid_offs >= static_size); + assert!(first_grid_offs % grid_size == 0); + + let (max_insert, data) = chest.storage_list_slices(); + + std::iter::repeat(item) + .zip( + std::iter::once(( + Storage::Static { + static_id: crate::inserter::StaticID::Chest as u16, + index: 0, + }, + max_insert, + data, + )) + .chain( + std::iter::repeat_with(|| (PANIC_ON_INSERT, [].as_mut_slice())) + .zip(StaticID::iter().count()..) + .map(|((max, data), static_id)| { + ( + Storage::Static { + static_id: static_id.try_into().unwrap(), + index: 0, + }, + max, + data, + ) + }), + ) + .take(first_grid_offs), + ) + .map(|(item, (a, b, c))| (item, a, b, c)) + }) +} +#[profiling::function] fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( chest_store: &'a mut FullChestStore, -) -> impl IntoIterator< +) -> impl Iterator< Item = ( Item, Storage, @@ -522,7 +1364,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( ( item, Storage::Static { - static_id: crate::inserter::StaticID::Chest, + static_id: crate::inserter::StaticID::Chest as u16, index: 0, }, max_insert, @@ -531,9 +1373,9 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }) } -fn bot_network_storages( -) -> impl IntoIterator, &'static mut [ITEMCOUNTTYPE])> { - vec![todo!()] +fn bot_network_storages() +-> impl Iterator, &'static mut [ITEMCOUNTTYPE])> { + vec![todo!()].into_iter() } fn all_train_station_storages() {} diff --git a/test_blueprints/basic_factory.bp b/test_blueprints/basic_factory.bp deleted file mode 100644 index 75c4c54..0000000 --- a/test_blueprints/basic_factory.bp +++ /dev/null @@ -1,92 +0,0 @@ -( - actions: [ - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 13, - y: 9, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Assembler ( - pos: ( - x: 15, - y: 8, - ), ty: 0 )), - )), - SetRecipe(( - pos: ( - x: 15, - y: 8, - ), - recipe: ( - id: 0, - ), - )), - PlaceEntity(( - entities: Single(Chest( - pos: ( - x: 17, - y: 6, - ), - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 17, - y: 7, - ), - dir: North, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Chest( - pos: ( - x: 16, - y: 6, - ), - )), - )), - PlaceEntity(( - entities: Single(Chest( - pos: ( - x: 15, - y: 6, - ), - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 16, - y: 7, - ), - dir: North, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 15, - y: 7, - ), - dir: North, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(SolarPanel( - pos: ( - x: 10, - y: 8, - ), - ty: 0, - )), - )), - ], -) \ No newline at end of file diff --git a/test_blueprints/huge_bad_purple.bp b/test_blueprints/huge_bad_purple.bp new file mode 100644 index 0000000..acebf8d --- /dev/null +++ b/test_blueprints/huge_bad_purple.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b337337bf54cf272e280e5c8a76adfb1a50c0abc62ea48e7d7b5366b7520d0db +size 56355857 diff --git a/test_blueprints/murphy/adv_oil.bp b/test_blueprints/murphy/adv_oil.bp new file mode 100644 index 0000000..52d5f70 --- /dev/null +++ b/test_blueprints/murphy/adv_oil.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d11af0c84bdb27301a305f6b17e0c4a01e6142b184c9a2f66cfc2502cb3f1b59 +size 304917 diff --git a/test_blueprints/murphy/blue.bp b/test_blueprints/murphy/blue.bp new file mode 100644 index 0000000..61f885a --- /dev/null +++ b/test_blueprints/murphy/blue.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cbf0f17364f1be05e81619ed62620dbdb2e9bfc4ec2c8661357c7c1a33f86b4 +size 496618 diff --git a/test_blueprints/murphy/blue_40k.bp b/test_blueprints/murphy/blue_40k.bp new file mode 100644 index 0000000..c14041c --- /dev/null +++ b/test_blueprints/murphy/blue_40k.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e77efd9f404f3252c35303d897fe3b9dccfd636eb65c447380490d5b876c536 +size 31286843 diff --git a/test_blueprints/murphy/blue_chips_rocket.bp b/test_blueprints/murphy/blue_chips_rocket.bp new file mode 100644 index 0000000..ede57fd --- /dev/null +++ b/test_blueprints/murphy/blue_chips_rocket.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99212fa1edddc40a942848513d82695f13d0a0d96560bba646bf3e088912cc46 +size 2261359 diff --git a/test_blueprints/murphy/blue_chips_science.bp b/test_blueprints/murphy/blue_chips_science.bp new file mode 100644 index 0000000..5b5bfd3 --- /dev/null +++ b/test_blueprints/murphy/blue_chips_science.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77a95c5d1b2f4866277e471160404baead2d68c6e9aecc6d36111b4c48536a52 +size 1315884 diff --git a/test_blueprints/murphy/full_lds.bp b/test_blueprints/murphy/full_lds.bp new file mode 100644 index 0000000..f903963 --- /dev/null +++ b/test_blueprints/murphy/full_lds.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d750fab97e1bbc2da4a0d73b2bc5cfc8a4f67db6cac16db09f232d98354aabd9 +size 4006052 diff --git a/test_blueprints/murphy/green.bp b/test_blueprints/murphy/green.bp new file mode 100644 index 0000000..429c32b --- /dev/null +++ b/test_blueprints/murphy/green.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b70b6426bff05a398388353f99fed94da61f2f9003cf2fadd3cd960a7ecaa74 +size 348458 diff --git a/test_blueprints/murphy/green_40k.bp b/test_blueprints/murphy/green_40k.bp new file mode 100644 index 0000000..18b80c2 --- /dev/null +++ b/test_blueprints/murphy/green_40k.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c046b5a3a2e3e66ba0756bf9d8dbe1422f89d64fd475c3a9f4a8d4b3cc1a3bf3 +size 12973244 diff --git a/test_blueprints/murphy/labs.bp b/test_blueprints/murphy/labs.bp new file mode 100644 index 0000000..a18c11f --- /dev/null +++ b/test_blueprints/murphy/labs.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5487e3e428faee9661ab451ad48db70beabdfff3d503cf08681d769b719fe2e0 +size 71669 diff --git a/test_blueprints/murphy/labs_40k.bp b/test_blueprints/murphy/labs_40k.bp new file mode 100644 index 0000000..f8fe4fd --- /dev/null +++ b/test_blueprints/murphy/labs_40k.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c43f478cf753a18c2032dd8c6b9d6bafd7cc4333893818a54534e458832a1b0 +size 14342344 diff --git a/test_blueprints/murphy/lds.bp b/test_blueprints/murphy/lds.bp new file mode 100644 index 0000000..5317a92 --- /dev/null +++ b/test_blueprints/murphy/lds.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a3f6f5fab7f0b99a0d4999cab8842783adf69c29d49bc1504012a8f830d72d4 +size 314185 diff --git a/test_blueprints/murphy/megabase.bp b/test_blueprints/murphy/megabase.bp new file mode 100644 index 0000000..01a877b --- /dev/null +++ b/test_blueprints/murphy/megabase.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f50f70b65186563a1da4d847d8913c81330046fc600d86aed3f15b0ea8cf4900 +size 278686334 diff --git a/test_blueprints/murphy/prod_mod.bp b/test_blueprints/murphy/prod_mod.bp new file mode 100644 index 0000000..1463620 --- /dev/null +++ b/test_blueprints/murphy/prod_mod.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2dab2e14140b611f30f38d23de7b9b556128abb6164368e87fd9524c3f6b39f +size 180532 diff --git a/test_blueprints/murphy/purple.bp b/test_blueprints/murphy/purple.bp new file mode 100644 index 0000000..b0faf8d --- /dev/null +++ b/test_blueprints/murphy/purple.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b2b910618ab68a237e0312c34c788ab47283071263c5e4c82dc551263fbfcb6 +size 463011 diff --git a/test_blueprints/murphy/purple_10k_no_steel.bp b/test_blueprints/murphy/purple_10k_no_steel.bp new file mode 100644 index 0000000..2112b60 --- /dev/null +++ b/test_blueprints/murphy/purple_10k_no_steel.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d314a8179619e4c4043a5cd334b7368a4857592cc48b6bce35f8edda78ae11 +size 7151256 diff --git a/test_blueprints/murphy/purple_40k.bp b/test_blueprints/murphy/purple_40k.bp new file mode 100644 index 0000000..54f4fe3 --- /dev/null +++ b/test_blueprints/murphy/purple_40k.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c595f4f15932ee650bbfea3a4125e27add5a33c4b7c915dc714dffe0440287a2 +size 85460979 diff --git a/test_blueprints/murphy/purple_40k_no_steel.bp b/test_blueprints/murphy/purple_40k_no_steel.bp new file mode 100644 index 0000000..213d456 --- /dev/null +++ b/test_blueprints/murphy/purple_40k_no_steel.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d0f97aff1bfd5f991d4e5654686da1e152734984ab9d4b7a268a0390d519062 +size 38688514 diff --git a/test_blueprints/murphy/purple_40k_with_furnace_steel.bp b/test_blueprints/murphy/purple_40k_with_furnace_steel.bp new file mode 100644 index 0000000..39628dd --- /dev/null +++ b/test_blueprints/murphy/purple_40k_with_furnace_steel.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85ce54b3d46f0460f524f7fbdebec51e148b90715904293ce437e9c56293f9c3 +size 58139091 diff --git a/test_blueprints/murphy/purple_with_prod.bp b/test_blueprints/murphy/purple_with_prod.bp new file mode 100644 index 0000000..2cbfc93 --- /dev/null +++ b/test_blueprints/murphy/purple_with_prod.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f35efb940792002950d4508860cfff1e6b417a71b53fed6ab23d26f265238db5 +size 1788791 diff --git a/test_blueprints/murphy/rcu.bp b/test_blueprints/murphy/rcu.bp new file mode 100644 index 0000000..0a09bf6 --- /dev/null +++ b/test_blueprints/murphy/rcu.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f288e6f14536c8c7b1476ce765da0a55c664344bb1c3eeddc5fcf57525da09d1 +size 196742 diff --git a/test_blueprints/murphy/red.bp b/test_blueprints/murphy/red.bp new file mode 100644 index 0000000..b4f7310 --- /dev/null +++ b/test_blueprints/murphy/red.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:276289f6b7d876e4888578ba76a72bd5c6cd734d5c46bf65b19fde82a28519a1 +size 111518 diff --git a/test_blueprints/murphy/red_40k.bp b/test_blueprints/murphy/red_40k.bp new file mode 100644 index 0000000..d63f9ec --- /dev/null +++ b/test_blueprints/murphy/red_40k.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:257463873a6d4fcaf102288c0a55e302ec00c5a98316e29059533b2d5b3b6bf5 +size 4222580 diff --git a/test_blueprints/murphy/red_chips.bp b/test_blueprints/murphy/red_chips.bp new file mode 100644 index 0000000..d5f7446 --- /dev/null +++ b/test_blueprints/murphy/red_chips.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6cf0441a248934794593249b0b477d3c46dbf7b5486bd2ff0d57cd8f20def9f +size 513280 diff --git a/test_blueprints/murphy/repeat_factory.bp b/test_blueprints/murphy/repeat_factory.bp new file mode 100644 index 0000000..84ba80f --- /dev/null +++ b/test_blueprints/murphy/repeat_factory.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92162514d0d5dc5458dadbf51183233dbeaf6a927b5fa0ac31dd66b4bbdccfa0 +size 8055231 diff --git a/test_blueprints/murphy/rgb_running.bp b/test_blueprints/murphy/rgb_running.bp new file mode 100644 index 0000000..517bd71 --- /dev/null +++ b/test_blueprints/murphy/rgb_running.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c7335136b75b7fd3f7a2073807d928f14444ebba0ad71c69441a159054c8c22 +size 67810362 diff --git a/test_blueprints/murphy/rgbp_running.bp b/test_blueprints/murphy/rgbp_running.bp new file mode 100644 index 0000000..a4b2609 --- /dev/null +++ b/test_blueprints/murphy/rgbp_running.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518a568dd8cece22c6dcfa18428f70f260e3f2ac32ce881c337a5906495324fc +size 155414863 diff --git a/test_blueprints/murphy/robot_frame.bp b/test_blueprints/murphy/robot_frame.bp new file mode 100644 index 0000000..803deac --- /dev/null +++ b/test_blueprints/murphy/robot_frame.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20d97a5395195384ea2443e05344cd71b732ec5503a4bf287f015593012d2c62 +size 299879 diff --git a/test_blueprints/murphy/rocket_fuel.bp b/test_blueprints/murphy/rocket_fuel.bp new file mode 100644 index 0000000..95442ee --- /dev/null +++ b/test_blueprints/murphy/rocket_fuel.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:256735154dffbc56101868fdbd5a4bd2bb16a486398293374ee89f7f268543bf +size 735025 diff --git a/test_blueprints/murphy/silo.bp b/test_blueprints/murphy/silo.bp new file mode 100644 index 0000000..abf345a --- /dev/null +++ b/test_blueprints/murphy/silo.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc7a01ec50aabdcfec98c436b74ae37f83fd0a2a96180cea127b65596be0e1d8 +size 63806 diff --git a/test_blueprints/murphy/small_yellow.bp b/test_blueprints/murphy/small_yellow.bp new file mode 100644 index 0000000..01c90e1 --- /dev/null +++ b/test_blueprints/murphy/small_yellow.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e0e72da779417a40f260d5a48a147ecef91c0921fe955bce0ffb0b2fe3456c2 +size 177844 diff --git a/test_blueprints/murphy/steel.bp b/test_blueprints/murphy/steel.bp new file mode 100644 index 0000000..ad0753d --- /dev/null +++ b/test_blueprints/murphy/steel.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f4422ac7a7c7311b59de983848a6a69dff087fef19f3861d285c8ffe8873af0 +size 104604 diff --git a/test_blueprints/murphy/steel_for_furnace.bp b/test_blueprints/murphy/steel_for_furnace.bp new file mode 100644 index 0000000..fb7c874 --- /dev/null +++ b/test_blueprints/murphy/steel_for_furnace.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3e541a63519ca4cb659eb4a1fad2e8f0f11c940a8eaf9ae7e35e5fcb51803d6 +size 19224524 diff --git a/test_blueprints/murphy/sulphuric_acid.bp b/test_blueprints/murphy/sulphuric_acid.bp new file mode 100644 index 0000000..c4cf4af --- /dev/null +++ b/test_blueprints/murphy/sulphuric_acid.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e696a5fb1d8b8f18770679541888c306700737a7f5dd1fa2ea3abd13ca3dc56 +size 58719 diff --git a/test_blueprints/murphy/temp_full_repeat_factory.bp b/test_blueprints/murphy/temp_full_repeat_factory.bp new file mode 100644 index 0000000..e69de29 diff --git a/test_blueprints/purple_with_crap.bp b/test_blueprints/purple_with_crap.bp new file mode 100644 index 0000000..2ae7db3 --- /dev/null +++ b/test_blueprints/purple_with_crap.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:983f7e96e672f5070d646e91883b0093b76214543304e6370fc69cf3e13e257e +size 217398 diff --git a/test_blueprints/red_and_green_with_clocking.bp b/test_blueprints/red_and_green_with_clocking.bp new file mode 100644 index 0000000..2718740 --- /dev/null +++ b/test_blueprints/red_and_green_with_clocking.bp @@ -0,0 +1,3 @@ +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 index c2c8d3a..43a502a 100644 --- a/test_blueprints/red_sci.bp +++ b/test_blueprints/red_sci.bp @@ -1,245 +1,3 @@ -( - actions: [ - PlaceEntity(( - entities: Single(Assembler(pos: ( - x: 10, - y: 9, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 10, - y: 9, - ), - recipe: ( - id: 0, - ), - )), - PlaceEntity(( - entities: Single(SolarPanel( - pos: ( - x: 10, - y: 12, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Assembler(pos:( - x: 14, - y: 9, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 14, - y: 9, - ), - recipe: ( - id: 2, - ), - )), - PlaceEntity(( - entities: Single(Assembler(pos: ( - x: 18, - y: 9, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 18, - y: 9, - ), - recipe: ( - id: 4, - ), - )), - PlaceEntity(( - entities: Single(Assembler(pos: ( - x: 22, - y: 11, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 22, - y: 11, - ), - recipe: ( - id: 5, - ), - )), - PlaceEntity(( - entities: Single(SolarPanel( - pos: ( - x: 22, - y: 14, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Assembler(pos: ( - x: 18, - y: 13, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 18, - y: 13, - ), - recipe: ( - id: 3, - ), - )), - PlaceEntity(( - entities: Single(Assembler(pos: ( - x: 14, - y: 13, - ), ty: 0)), - )), - SetRecipe(( - pos: ( - x: 14, - y: 13, - ), - recipe: ( - id: 1, - ), - )), - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 13, - y: 9, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 13, - y: 12, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 17, - y: 12, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 21, - y: 12, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(PowerPole( - pos: ( - x: 25, - y: 12, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 13, - y: 11, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 17, - y: 11, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 21, - y: 11, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 17, - y: 13, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 21, - y: 13, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Lab( - pos: ( - x: 26, - y: 9, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Lab( - pos: ( - x: 26, - y: 13, - ), - ty: 0, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 25, - y: 11, - ), - dir: East, - filter: None, - )), - )), - PlaceEntity(( - entities: Single(Inserter( - pos: ( - x: 25, - y: 13, - ), - dir: East, - filter: None, - )), - )), - ], -) \ No newline at end of file +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 new file mode 100644 index 0000000..2c3c6ab --- /dev/null +++ b/test_blueprints/red_sci_with_beacons_and_belts.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0be6c5a23c94e49f69580da798630aab2664edc5ef4ace0ba2afe14a10dfb606 +size 37415 diff --git a/test_blueprints/test.bp b/test_blueprints/test.bp new file mode 100644 index 0000000..51593fe --- /dev/null +++ b/test_blueprints/test.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0414740ca055f14a12228801c71f90811295cabf78270ff75bcf05214833317b +size 33534510 diff --git a/tested_ideas/storage_list_statics_at_zero.md b/tested_ideas/storage_list_statics_at_zero.md new file mode 100644 index 0000000..4c36b3c --- /dev/null +++ b/tested_ideas/storage_list_statics_at_zero.md @@ -0,0 +1 @@ +Placing the static storages at the start of the storage list, we an skip some calculations in the inserter indexing. I have tested this, and it seems to save ~300us on a full factory. \ No newline at end of file diff --git a/tests/visual_test.rs b/tests/visual_test.rs new file mode 100644 index 0000000..e46de4b --- /dev/null +++ b/tests/visual_test.rs @@ -0,0 +1,158 @@ +use eframe::EventLoopBuilderHook; +use factory::rendering::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::data::DataStore; +use factory::frontend::action::action_state_machine::ActionStateMachine; +use factory::rendering::app_state::GameState; +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()); + } +}