From a8ccf6849670ffdcf3566881eb4ad753e570fff3 Mon Sep 17 00:00:00 2001 From: itsmeow Date: Wed, 20 Dec 2023 16:24:43 -0500 Subject: [PATCH] IconForge Start blending Huge cleanup Finish optimizing the thing Finish the thing!! Clean up a bit Re-add 32-bit thing Fix TOML sorting Add dmsrc Fix clippy suggestions Clippy.. stop being mean Cargo fmt + doc comments Code cleanup More cleanup, remove most unsafe unwrap()s, use Match syntax. Remove unneccesarily verbose casting Fix overlay blending Cleanup with new DMI version Cargo fmt Leaf test, DynamicImage->RgbaImage, better Error handling, DashMap, and cleanup command Fix Further tree optimizations, hashing optimization, cache icostrings more effectively. Optimize unique_icons insertion a little Fix macro Little more cleanup Add to README Update dmi, add caching logic. Address reviews Cleanup panic unwind Fix lint failure --- Cargo.lock | 390 ++++++++-------- Cargo.toml | 20 +- README.md | 1 + dmsrc/iconforge.dm | 59 +++ src/byond.rs | 57 +++ src/error.rs | 5 + src/hash.rs | 9 +- src/iconforge.rs | 1061 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 9 files changed, 1427 insertions(+), 177 deletions(-) create mode 100644 dmsrc/iconforge.dm create mode 100644 src/iconforge.rs diff --git a/Cargo.lock b/Cargo.lock index 98170f30..823bccfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,13 +47,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -138,9 +139,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.68.1" +version = "0.69.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" dependencies = [ "bitflags 2.4.1", "cexpr", @@ -191,47 +192,26 @@ dependencies = [ [[package]] name = "borsh" -version = "0.10.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" dependencies = [ "borsh-derive", - "hashbrown 0.13.2", + "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" dependencies = [ + "once_cell", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.42", + "syn_derive", ] [[package]] @@ -241,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", - "regex-automata 0.4.3", + "regex-automata", "serde", ] @@ -352,6 +332,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.31" @@ -446,9 +432,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -606,6 +592,8 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core", + "rayon", + "serde", ] [[package]] @@ -675,10 +663,11 @@ dependencies = [ [[package]] name = "dmi" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c191706391473812b2c9f11befada49b65e094a19d3d422985f0d1e2daf900" +checksum = "a99b0d2dae3ac6a37fe16dae423852e78b4d3279f86fe0958bd0549540952ffc" dependencies = [ + "bitflags 2.4.1", "deflate", "image", "inflate", @@ -877,9 +866,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", ] @@ -892,20 +881,9 @@ checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.42", -] +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-sink" @@ -915,19 +893,18 @@ checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-io", - "futures-macro", "futures-task", "memchr", "pin-project-lite", @@ -971,9 +948,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gix" @@ -1483,9 +1460,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1493,7 +1470,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -1515,7 +1492,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.6", ] [[package]] @@ -1562,9 +1539,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1573,9 +1550,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1596,9 +1573,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1611,7 +1588,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1620,9 +1597,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -1685,16 +1662,6 @@ dependencies = [ "png", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.1.0" @@ -1744,9 +1711,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" @@ -1992,9 +1959,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2024,7 +1991,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "socket2 0.5.4", + "socket2 0.5.5", "twox-hash", "url", "webpki", @@ -2221,9 +2188,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2253,9 +2220,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -2305,7 +2272,7 @@ checksum = "f6f4a3f5089b981000cb50ec24320faf7a19649a45e8730e4adf49f78f066528" dependencies = [ "deprecate-until", "fixedbitset", - "indexmap 2.1.0", + "indexmap", "integer-sqrt", "num-traits", "rustc-hash", @@ -2391,21 +2358,21 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ - "toml 0.5.11", + "once_cell", + "toml_edit 0.19.15", ] [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "toml_edit 0.20.7", ] [[package]] @@ -2626,38 +2593,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", + "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" - [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rend" @@ -2704,7 +2665,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.2", + "webpki-roots 0.25.3", "winreg", ] @@ -2718,11 +2679,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.11", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rkyv" version = "0.7.43" @@ -2786,7 +2761,8 @@ dependencies = [ "sha-1", "sha2", "thiserror", - "toml 0.8.8", + "toml", + "tracy_full", "twox-hash", "url", "zip", @@ -2794,9 +2770,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.32.0" +version = "1.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" dependencies = [ "arrayvec", "borsh", @@ -2820,6 +2796,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.28" @@ -2835,21 +2820,21 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", - "rustls-webpki 0.101.6", + "ring 0.17.7", + "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] @@ -2860,18 +2845,18 @@ version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -2912,12 +2897,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -3091,9 +3076,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", @@ -3164,6 +3149,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3206,9 +3203,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -3290,9 +3287,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -3300,7 +3297,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.4", + "socket2 0.5.5", "windows-sys 0.48.0", ] @@ -3316,9 +3313,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3328,15 +3325,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.8" @@ -3364,7 +3352,18 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", "toml_datetime", "winnow", ] @@ -3375,7 +3374,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.1.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -3390,29 +3389,48 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] +[[package]] +name = "tracy-client-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db0b1cc1bb12a70457300d9affc07acb587390d971a796dac2f4d9bca8df776" +dependencies = [ + "cc", +] + +[[package]] +name = "tracy_full" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01aaff24a62ad715d80adcf28e47c228dbed3d6285fb85b55bfd9eb47fda4df" +dependencies = [ + "once_cell", + "rustc_version", + "tracy-client-sys", +] + [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" @@ -3421,7 +3439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", - "rand 0.7.3", + "rand 0.8.5", "static_assertions", ] @@ -3473,6 +3491,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3560,9 +3584,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -3601,9 +3625,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3611,12 +3635,12 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.7", + "untrusted 0.9.0", ] [[package]] @@ -3630,9 +3654,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "winapi" @@ -3843,6 +3867,26 @@ dependencies = [ "tap", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.42", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 482865b1..a9d11da3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ serde_json = { version = "1.0", optional = true } lazy_static = { version = "1.4", optional = true } once_cell = { version = "1.19", optional = true } mysql = { version = "24.0", default_features = false, optional = true } -dashmap = { version = "5.5", optional = true } +dashmap = { version = "5.5", optional = true, features = ["rayon", "serde"] } zip = { version = "0.6", optional = true } rand = { version = "0.8", optional = true } toml-dep = { version = "0.8.8", package = "toml", optional = true } @@ -62,7 +62,8 @@ rayon = { version = "1.8", optional = true } dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "4.4", optional = true } num-integer = { version = "0.1.45", optional = true } -dmi = { version = "0.3.1", optional = true } +dmi = { version = "0.3.4", optional = true } +tracy_full = { version = "1.6.1", optional = true } [features] default = [ @@ -89,6 +90,7 @@ all = [ "file", "git", "http", + "iconforge", "json", "log", "noise", @@ -133,6 +135,20 @@ hash = [ "serde", "serde_json", ] +iconforge = [ + "dashmap", + "dep:dmi", + "hash", + "image", + "jobs", + "once_cell", + "png", + "rayon", + "serde", + "serde_json", + "tracy_full", + "twox-hash", +] pathfinder = ["num-integer", "pathfinding", "serde", "serde_json"] redis_pubsub = ["flume", "redis", "serde", "serde_json"] redis_reliablequeue = ["flume", "redis", "serde", "serde_json"] diff --git a/README.md b/README.md index 009cc7ca..c36c3af2 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ The default features are: Additional features are: * batchnoise: Discrete Batched Perlin-like Noise, fast and multi-threaded - sent over once instead of having to query for every tile. * hash: Faster replacement for `md5`, support for SHA-1, SHA-256, and SHA-512. Requires OpenSSL on Linux. +* iconforge: A much faster replacement for the spritesheet generation system used by [/tg/station]. * pathfinder: An a* pathfinder used for finding the shortest path in a static node map. Not to be used for a non-static map. * redis_pubsub: Library for sending and receiving messages through Redis. * redis_reliablequeue: Library for using a reliable queue pattern through Redis. diff --git a/dmsrc/iconforge.dm b/dmsrc/iconforge.dm new file mode 100644 index 00000000..acb52f6d --- /dev/null +++ b/dmsrc/iconforge.dm @@ -0,0 +1,59 @@ +/// Generates a spritesheet at: [file_path][spritesheet_name]_[size_id].png +/// The resulting spritesheet arranges icons in a random order, with the position being denoted in the "sprites" return value. +/// All icons have the same y coordinate, and their x coordinate is equal to `icon_width * position`. +/// +/// hash_icons is a boolean (0 or 1), and determines if the generator will spend time creating hashes for the output field dmi_hashes. +/// These hashes can be heplful for 'smart' caching (see rustg_iconforge_cache_valid), but require extra computation. +/// +/// Spritesheet will contain all sprites listed within "sprites". +/// "sprites" format: +/// list( +/// "sprite_name" = list( // <--- this list is a [SPRITE_OBJECT] +/// icon_file = 'icons/path_to/an_icon.dmi', +/// icon_state = "some_icon_state", +/// dir = SOUTH, +/// frame = 1, +/// transform = list([TRANSFORM_OBJECT], ...) +/// ), +/// ..., +/// ) +/// TRANSFORM_OBJECT format: +/// list("type" = "BlendColor", "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY) +/// list("type" = "BlendIcon", "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY) +/// list("type" = "Scale", "width" = 32, "height" = 32) +/// list("type" = "Crop", "x1" = 0, "y1" = 0, "x2" = 32, "y2" = 32) +/// +/// Returns a SpritesheetResult as JSON, containing fields: +/// list( +/// "sizes" = list("32x32", "64x64", ...), +/// "sprites" = list("sprite_name" = list("size_id" = "32x32", "position" = 0), ...), +/// "dmi_hashes" = list("icons/path_to/an_icon.dmi" = "d6325c5b4304fb03", ...), +/// "sprites_hash" = "a2015e5ff403fb5c", // This is the xxh64 hash of the INPUT field "sprites". +/// "error" = "[A string, empty if there were no errors.]" +/// ) +/// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. +#define rustg_iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate")(file_path, spritesheet_name, sprites, "[hash_icons]") +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) RUSTG_CALL(RUST_G, "iconforge_generate_async")(file_path, spritesheet_name, sprites, "[hash_icons]") +/// Returns the status of a job_id +#define rustg_iconforge_check(job_id) RUSTG_CALL(RUST_G, "iconforge_check")("[job_id]") +/// Clears all cached DMIs and images, freeing up memory. +/// This should be used after spritesheets are done being generated. +#define rustg_iconforge_cleanup RUSTG_CALL(RUST_G, "iconforge_cleanup") +/// Takes in a set of hashes, generate inputs, and DMI filepaths, and compares them to determine cache validity. +/// input_hash: xxh64 hash of "sprites" from the cache. +/// dmi_hashes: xxh64 hashes of the DMIs in a spritesheet, given by `rustg_iconforge_generate` with `hash_icons` enabled. From the cache. +/// sprites: The new input that will be passed to rustg_iconforge_generate(). +/// Returns a CacheResult with the following structure: list( +/// "result": "1" (if cache is valid) or "0" (if cache is invalid) +/// "fail_reason": "" (emtpy string if valid, otherwise a string containing the invalidation reason or an error with ERROR: prefixed.) +/// ) +/// In the case of an unrecoverable panic from within Rust, this function ONLY returns a string containing the error. +#define rustg_iconforge_cache_valid(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid")(input_hash, dmi_hashes, sprites) +/// Returns a job_id for use with rustg_iconforge_check() +#define rustg_iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) RUSTG_CALL(RUST_G, "iconforge_cache_valid_async")(input_hash, dmi_hashes, sprites) + +#define RUSTG_ICONFORGE_BLEND_COLOR "BlendColor" +#define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon" +#define RUSTG_ICONFORGE_CROP "Crop" +#define RUSTG_ICONFORGE_SCALE "Scale" diff --git a/src/byond.rs b/src/byond.rs index 385d5251..0629d27e 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -1,11 +1,17 @@ +use crate::error::Error; use std::{ + backtrace::Backtrace, borrow::Cow, cell::RefCell, ffi::{CStr, CString}, + fs::OpenOptions, + io::Write, os::raw::{c_char, c_int}, slice, + sync::Once, }; +static SET_HOOK: Once = Once::new(); static EMPTY_STRING: c_char = 0; thread_local! { static RETURN_STRING: RefCell = RefCell::new(CString::default()); @@ -50,6 +56,7 @@ macro_rules! byond_fn { pub unsafe extern "C" fn $name( _argc: ::std::os::raw::c_int, _argv: *const *const ::std::os::raw::c_char ) -> *const ::std::os::raw::c_char { + $crate::byond::set_panic_hook(); let closure = || ($body); $crate::byond::byond_return(closure().map(From::from)) } @@ -84,3 +91,53 @@ byond_fn!( Some(env!("CARGO_PKG_VERSION")) } ); + +/// Print any panics before exiting. +pub fn set_panic_hook() { + SET_HOOK.call_once(|| { + std::panic::set_hook(Box::new(|panic_info| { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open("rustg-panic.log") + .unwrap(); + file.write_all( + panic_info + .payload() + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap() + .as_bytes(), + ) + .expect("Failed to extract error payload"); + file.write_all(Backtrace::capture().to_string().as_bytes()) + .expect("Failed to extract error backtrace"); + })) + }); +} + +/// Utility for BYOND functions to catch panic unwinds safely and return a Result, as expected. +/// Usage: catch_panic(|| internal_safe_function(arguments)) +pub fn catch_panic(f: F) -> Result +where + F: FnOnce() -> Result + std::panic::UnwindSafe, +{ + match std::panic::catch_unwind(f) { + Ok(o) => o, + Err(e) => { + let message: Option = e + .downcast_ref::<&'static str>() + .map(|payload| payload.to_string()) + .or_else(|| e.downcast_ref::().cloned()); + Err(Error::Panic( + message + .unwrap_or(String::from( + "Failed to stringify panic! Check rustg-panic.log!", + )) + .to_owned(), + )) + } + } +} diff --git a/src/error.rs b/src/error.rs index d56cb5c3..6c3dbc5d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -61,6 +61,11 @@ pub enum Error { #[cfg(feature = "hash")] #[error("Unable to decode hex value.")] HexDecode, + #[cfg(feature = "iconforge")] + #[error("IconForge error: {0}")] + IconForge(String), + #[error("Panic during function execution: {0}")] + Panic(String), } impl From for Error { diff --git a/src/hash.rs b/src/hash.rs index ee959fe0..7980e741 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -71,16 +71,21 @@ fn hash_algorithm>(name: &str, bytes: B) -> Result { hasher.write(bytes.as_ref()); Ok(format!("{:x}", hasher.finish())) } + "xxh64_fixed" => { + let mut hasher = XxHash64::with_seed(17479268743136991876); + hasher.write(bytes.as_ref()); + Ok(format!("{:x}", hasher.finish())) + } "base64" => Ok(base64::prelude::BASE64_STANDARD.encode(bytes.as_ref())), _ => Err(Error::InvalidAlgorithm), } } -fn string_hash(algorithm: &str, string: &str) -> Result { +pub fn string_hash(algorithm: &str, string: &str) -> Result { hash_algorithm(algorithm, string) } -fn file_hash(algorithm: &str, path: &str) -> Result { +pub fn file_hash(algorithm: &str, path: &str) -> Result { let mut bytes: Vec = Vec::new(); let mut file = BufReader::new(File::open(path)?); file.read_to_end(&mut bytes)?; diff --git a/src/iconforge.rs b/src/iconforge.rs new file mode 100644 index 00000000..0b2804f9 --- /dev/null +++ b/src/iconforge.rs @@ -0,0 +1,1061 @@ +// DMI spritesheet generator +// Developed by itsmeow +use crate::{ + byond::catch_panic, + error::Error, + hash::{file_hash, string_hash}, + jobs, +}; +use dashmap::DashMap; +use dmi::{ + dirs::Dirs, + icon::{Icon, IconState}, +}; +use image::{Pixel, RgbaImage}; +use once_cell::sync::Lazy; +use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::sync::RwLock; +use std::{ + collections::HashMap, + fs::File, + hash::BuildHasherDefault, + io::BufReader, + sync::{Arc, Mutex}, +}; +use tracy_full::{frame, zone}; +use twox_hash::XxHash64; +type SpriteJsonMap = HashMap, BuildHasherDefault>; +/// This is used to save time decoding 'sprites' a second time between the cache step and the generate step. +static SPRITES_TO_JSON: Lazy>> = Lazy::new(|| { + Arc::new(Mutex::new(HashMap::with_hasher(BuildHasherDefault::< + XxHash64, + >::default()))) +}); +/// A cache of DMI filepath -> Icon objects. +static ICON_FILES: Lazy, BuildHasherDefault>> = + Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); +/// A cache of icon_hash_input to RgbaImage (with transforms applied! This can only contain COMPLETED sprites). +static ICON_STATES: Lazy>> = + Lazy::new(|| DashMap::with_hasher(BuildHasherDefault::::default())); + +byond_fn!(fn iconforge_generate(file_path, spritesheet_name, sprites, hash_icons) { + let file_path = file_path.to_owned(); + let spritesheet_name = spritesheet_name.to_owned(); + let sprites = sprites.to_owned(); + let hash_icons = hash_icons.to_owned(); + let result = Some(match catch_panic(|| generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }); + frame!(); + result +}); + +byond_fn!(fn iconforge_generate_async(file_path, spritesheet_name, sprites, hash_icons) { + let file_path = file_path.to_owned(); + let spritesheet_name = spritesheet_name.to_owned(); + let sprites = sprites.to_owned(); + let hash_icons = hash_icons.to_owned(); + Some(jobs::start(move || { + let result = match catch_panic(|| generate_spritesheet(&file_path, &spritesheet_name, &sprites, &hash_icons)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }; + frame!(); + result + })) +}); + +byond_fn!(fn iconforge_check(id) { + Some(jobs::check(id)) +}); + +byond_fn!( + fn iconforge_cleanup() { + ICON_FILES.clear(); + ICON_STATES.clear(); + Some("Ok") + } +); + +byond_fn!(fn iconforge_cache_valid(input_hash, dmi_hashes, sprites) { + let input_hash = input_hash.to_owned(); + let dmi_hashes = dmi_hashes.to_owned(); + let sprites = sprites.to_owned(); + let result = Some(match catch_panic(|| cache_valid(&input_hash, &dmi_hashes, &sprites)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + }); + frame!(); + result +}); + +byond_fn!(fn iconforge_cache_valid_async(input_hash, dmi_hashes, sprites) { + let input_hash = input_hash.to_owned(); + let dmi_hashes = dmi_hashes.to_owned(); + let sprites = sprites.to_owned(); + let result = Some(jobs::start(move || { + match catch_panic(|| cache_valid(&input_hash, &dmi_hashes, &sprites)) { + Ok(o) => o.to_string(), + Err(e) => e.to_string() + } + })); + frame!(); + result +}); + +#[derive(Serialize)] +struct SpritesheetResult { + sizes: Vec, + sprites: DashMap>, + dmi_hashes: DashMap, + sprites_hash: String, + error: String, +} + +#[derive(Serialize, Clone)] +struct SpritesheetEntry { + size_id: String, + position: u32, +} + +#[derive(Serialize, Clone, Eq, PartialEq, Hash)] +struct IconObject { + icon_file: String, + icon_state: String, + dir: u8, + frame: u32, + transform: Vec, + transform_hash_input: String, + icon_hash_input: String, +} + +#[derive(Serialize, Deserialize, Clone)] +struct IconObjectIO { + icon_file: String, + icon_state: String, + dir: u8, + frame: u32, + transform: Vec, +} + +impl std::fmt::Display for IconObject { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "IconObject(icon_file={}, icon_state={}, dir={}, frame={})", + self.icon_file, self.icon_state, self.dir, self.frame + ) + } +} + +impl IconObject { + fn to_base(&self) -> Result { + zone!("to_base"); + // This is a micro-op that ends up saving a lot of time. format!() is quite slow when you get down to microseconds. + let mut str_buf = String::with_capacity(self.icon_file.len() + self.icon_state.len() + 4); + str_buf.push_str(&self.icon_file); + str_buf.push_str(&self.icon_state); + str_buf.push_str(&self.dir.to_string()); + str_buf.push_str(&self.frame.to_string()); + Ok(str_buf) + } + + fn gen_icon_hash_input(&mut self) -> Result<(), Error> { + zone!("gen_icon_hash_input"); + let base = self.to_base()?; + { + zone!("transform_to_json"); + let transform_str = serde_json::to_string(&self.transform)?; + self.transform_hash_input = transform_str; + } + let mut str_buf = String::with_capacity(base.len() + self.transform_hash_input.len()); + str_buf.push_str(&base); + str_buf.push_str(&self.transform_hash_input); + self.icon_hash_input = str_buf; + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(tag = "type")] +enum TransformIO { + BlendColor { color: String, blend_mode: u8 }, + BlendIcon { icon: IconObjectIO, blend_mode: u8 }, + Scale { width: u32, height: u32 }, + Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, +} + +#[derive(Serialize, Clone, Eq, PartialEq, Hash)] +enum Transform { + BlendColor { color: String, blend_mode: u8 }, + BlendIcon { icon: IconObject, blend_mode: u8 }, + Scale { width: u32, height: u32 }, + Crop { x1: i32, y1: i32, x2: i32, y2: i32 }, +} + +#[derive(Serialize)] +struct CacheResult { + result: String, + fail_reason: String, +} + +fn cache_valid(input_hash: &str, dmi_hashes_in: &str, sprites_in: &str) -> Result { + zone!("cache_valid"); + let sprites_hash = string_hash("xxh64_fixed", sprites_in)?; + if sprites_hash != input_hash { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: String::from("Input hash did not match."), + })?); + } + let dmi_hashes: DashMap; + { + zone!("from_json_hashes"); + dmi_hashes = serde_json::from_str::>(dmi_hashes_in)?; + } + let mut sprites_json = SPRITES_TO_JSON.lock().unwrap(); + let sprites = match sprites_json.get(&sprites_hash) { + Some(sprites) => sprites, + None => { + zone!("from_json_sprites"); + { + sprites_json.insert( + sprites_hash.clone(), + serde_json::from_str::>(sprites_in)?, + ); + } + sprites_json.get(&sprites_hash).unwrap() + } + }; + + let dmis: HashSet; + + { + zone!("collect_dmis"); + dmis = sprites + .par_iter() + .flat_map(|(_, icon)| { + icon_to_icons_io(icon) + .into_iter() + .map(|icon| icon.icon_file.clone()) + .collect::>() + }) + .collect(); + } + + drop(sprites_json); + + if dmis.len() > dmi_hashes.len() { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: format!("Input hash matched, but more DMIs exist than DMI hashes provided ({} DMIs, {} DMI hashes).", dmis.len(), dmi_hashes.len()), + })?); + } + + let fail_reason: Arc>> = Arc::new(RwLock::new(None)); + { + zone!("check_dmis"); + dmis.into_par_iter().for_each(|dmi_path| { + zone!("check_dmi"); + if fail_reason.read().unwrap().is_some() { + return; + } + match dmi_hashes.get(&dmi_path) { + Some(hash) => { + zone!("hash_dmi"); + match file_hash("xxh64_fixed", &dmi_path) { + Ok(new_hash) => { + zone!("check_match"); + if new_hash != *hash { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("Input hash matched, but dmi_hash was invalid DMI: '{}' (stored hash: {}, new hash: {})", dmi_path, hash.clone(), new_hash)); + } + }, + Err(err) => { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("ERROR: Error while hashing dmi_path '{}': {}", dmi_path, err)); + } + } + } + None => { + if fail_reason.read().unwrap().is_some() { + return; + } + *fail_reason.write().unwrap() = Some(format!("Input hash matched, but no dmi_hash existed for DMI: '{}'", dmi_path)); + } + } + }); + } + if let Some(err) = fail_reason.read().unwrap().clone() { + return Ok(serde_json::to_string::(&CacheResult { + result: String::from("0"), + fail_reason: err, + })?); + } + Ok(serde_json::to_string::(&CacheResult { + result: String::from("1"), + fail_reason: String::from(""), + })?) +} + +fn generate_spritesheet( + file_path: &str, + spritesheet_name: &str, + sprites: &str, + hash_icons: &str, +) -> std::result::Result { + zone!("generate_spritesheet"); + let hash_icons: bool = hash_icons == "1"; + let error = Arc::new(Mutex::new(Vec::::new())); + let dmi_hashes = DashMap::::new(); + + let size_to_icon_objects = Arc::new(Mutex::new(HashMap::>::new())); + let sprites_objects = + DashMap::>::with_hasher( + BuildHasherDefault::::default(), + ); + + let tree_bases = Arc::new(Mutex::new(HashMap::< + String, + Vec<(&String, &IconObject)>, + BuildHasherDefault, + >::with_hasher( + BuildHasherDefault::::default() + ))); + let sprites_hash; + { + zone!("compute_sprites_hash"); + sprites_hash = string_hash("xxh64_fixed", sprites)?; + } + let input = match SPRITES_TO_JSON.lock().unwrap().get(&sprites_hash) { + Some(sprites) => sprites.clone(), + None => { + zone!("from_json_sprites"); // byondapi, save us + serde_json::from_str::>(sprites)? + } + }; + let mut sprites_map = HashMap::::new(); + { + zone!("io_to_mem"); + sprites_map.extend( + input + .into_par_iter() + .map(|(sprite_name, icon)| (sprite_name, icon_from_io(icon))) + .collect::>(), + ); + } + + // Pre-load all the DMIs now. + // This is much faster than doing it as we go (tested!), because sometimes multiple parallel iterators need the DMI. + sprites_map.par_iter().for_each(|(sprite_name, icon)| { + zone!("sprite_to_icons"); + + icon_to_icons(icon) + .into_par_iter() + .for_each(|icon| match icon_to_dmi(icon) { + Ok(_) => { + if hash_icons { + zone!("hash_dmi"); + match file_hash("xxh64_fixed", &icon.icon_file) { + Ok(hash) => { + zone!("insert_dmi_hash"); + dmi_hashes.insert(icon.icon_file.clone(), hash); + } + Err(err) => { + error.lock().unwrap().push(err.to_string()); + } + }; + } + } + Err(err) => error.lock().unwrap().push(err), + }); + + { + zone!("map_to_base"); + let base = match icon.to_base() { + Ok(base) => base, + Err(err) => { + error.lock().unwrap().push(err.to_string()); + return; + } + }; + tree_bases + .lock() + .unwrap() + .entry(base) + .or_default() + .push((sprite_name, icon)); + } + }); + + // cache this here so we don't generate the same string 5000 times + let sprite_name = String::from("N/A, in tree generation stage"); + + // Map duplicate transform sets into a tree. + // This is beneficial in the case where we have the same base image, and the same set of transforms, but change 1 or 2 things at the end. + // We can greatly reduce the amount of RgbaImages created by first finding these. + tree_bases + .lock() + .unwrap() + .par_iter() + .for_each(|(_, icons)| { + zone!("transform_trees"); + let first_icon = match icons.first() { + Some((_, icon)) => icon, + None => { + error + .lock() + .unwrap() + .push(String::from("Somehow found no icon for a tree.")); + return; + } + }; + let (base_image, _) = match icon_to_image(first_icon, &sprite_name, false, false) { + Ok(image) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; + let mut no_transforms = Option::<&IconObject>::None; + let unique_icons = DashMap::::new(); + { + zone!("map_unique"); + icons.iter().for_each(|(_, icon)| { + // This will ensure we only map unique transform sets. This also means each IconObject is guaranteed a unique icon_hash + // Since all icons share the same 'base'. + // Also check to see if the icon is already cached. If so, we can ignore this transform chain. + if !ICON_STATES.contains_key(&icon.icon_hash_input) { + unique_icons.insert(icon.icon_hash_input.clone(), icon); + } + if icon.transform.is_empty() { + no_transforms = Some(icon); + } + }); + } + if let Some(entry) = no_transforms { + if let Err(err) = return_image(base_image.clone(), entry) { + error.lock().unwrap().push(err.to_string()); + } + } + { + zone!("transform_all_leaves"); + if let Err(err) = transform_leaves( + &unique_icons.into_iter().map(|(_, v)| v).collect(), + base_image, + 0, + ) { + error.lock().unwrap().push(err); + } + } + }); + + // Pick the specific icon states out of the DMI, also generating their transforms, build the spritesheet metadata. + sprites_map.par_iter().for_each(|sprite_entry| { + zone!("map_sprite"); + let (sprite_name, icon) = sprite_entry; + + // get RgbaImage, it should already be transformed, so it must be cached. + let (image, _) = match icon_to_image(icon, sprite_name, true, true) { + Ok(image) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; + + { + zone!("create_game_metadata"); + // Generate the metadata used by the game + let size_id = format!("{}x{}", image.width(), image.height()); + if let Err(err) = return_image(image, icon) { + error.lock().unwrap().push(err.to_string()); + } + let icon_position; + { + zone!("insert_into_size_map"); + // This scope releases the lock on size_to_icon_objects + let mut size_map = size_to_icon_objects.lock().unwrap(); + let vec = (*size_map).entry(size_id.to_owned()).or_default(); + icon_position = vec.len() as u32; + vec.push(icon); + } + + { + zone!("insert_into_sprite_objects"); + sprites_objects.insert( + sprite_name.to_owned(), + SpritesheetEntry { + size_id: size_id.to_owned(), + position: icon_position, + }, + ); + } + } + }); + + // all images have been returned now, so continue... + + // cache this here so we don't generate the same string 5000 times + let sprite_name = String::from("N/A, in final generation stage"); + + // Get all the sprites and spew them onto a spritesheet. + size_to_icon_objects + .lock() + .unwrap() + .par_iter() + .for_each(|(size_id, icon_objects)| { + zone!("join_sprites"); + let file_path = format!("{}{}_{}.png", file_path, spritesheet_name, size_id); + let size_data: Vec<&str> = size_id.split('x').collect(); + let base_width = size_data + .first() + .unwrap() + .to_string() + .parse::() + .unwrap(); + let base_height = size_data + .last() + .unwrap() + .to_string() + .parse::() + .unwrap(); + + let mut final_image = + RgbaImage::new(base_width * icon_objects.len() as u32, base_height); + + for (idx, icon) in icon_objects.iter().enumerate() { + zone!("join_sprite"); + let image = match icon_to_image(icon, &sprite_name, true, true) { + Ok((image, _)) => image, + Err(err) => { + error.lock().unwrap().push(err); + return; + } + }; + let base_x: u32 = base_width * idx as u32; + for x in 0..image.width() { + for y in 0..image.height() { + final_image.put_pixel(base_x + x, y, *image.get_pixel(x, y)) + } + } + if let Err(err) = return_image(image, icon) { + error.lock().unwrap().push(err.to_string()); + } + } + { + zone!("write_spritesheet"); + final_image.save(file_path).err(); + } + }); + + let sizes: Vec = size_to_icon_objects + .lock() + .unwrap() + .iter() + .map(|(k, _v)| k) + .cloned() + .collect(); + + // Collect the game metadata and any errors. + let returned = SpritesheetResult { + sizes, + sprites: sprites_objects, + dmi_hashes, + sprites_hash, + error: error.lock().unwrap().join("\n"), + }; + Ok(serde_json::to_string::(&returned)?) +} + +/// Given an array of 'transform arrays' onto from a shared IconObject base, +/// recursively applies transforms in a tree structure. Maximum transform depth is 128. +fn transform_leaves(icons: &Vec<&IconObject>, image: RgbaImage, depth: u8) -> Result<(), String> { + zone!("transform_leaf"); + if depth > 128 { + return Err(String::from( + "Transform depth exceeded 128. https://www.youtube.com/watch?v=CUjrySBwi5Q", + )); + } + let next_transforms = DashMap::>::new(); + let errors = Mutex::new(Vec::::new()); + + { + zone!("get_next_transforms"); + icons.par_iter().for_each(|icon| { + zone!("collect_icon_transforms"); + if let Some(transform) = icon.transform.get(depth as usize) { + next_transforms + .entry(transform.clone()) + .or_default() + .push(icon); + } + }); + } + + { + zone!("do_next_transforms"); + next_transforms + .into_par_iter() + .for_each(|(transform, mut associated_icons)| { + let mut altered_image; + { + zone!("clone_image"); + altered_image = image.clone(); + } + if let Err(err) = transform_image(&mut altered_image, &transform) { + errors.lock().unwrap().push(err); + } + { + zone!("filter_associated_icons"); + associated_icons + .clone() + .into_iter() + .enumerate() + .for_each(|(idx, icon)| { + if icon.transform.len() as u8 == depth + 1 + && *icon.transform.last().unwrap() == transform + { + associated_icons.swap_remove(idx); + if let Err(err) = return_image(altered_image.clone(), icon) { + errors.lock().unwrap().push(err.to_string()); + } + } + }); + } + if let Err(err) = transform_leaves(&associated_icons, altered_image, depth + 1) { + errors.lock().unwrap().push(err); + } + }); + } + + if !errors.lock().unwrap().is_empty() { + return Err(errors.lock().unwrap().join("\n")); + } + Ok(()) +} + +/// Converts an IO icon to one with icon_hash_input +fn icon_from_io(icon_in: IconObjectIO) -> IconObject { + zone!("icon_from_io"); + // TODO: can probably convert this function to just lazily attaching icostring to a RefCell<> or something + // This alternative type system is too verbose and wasteful of processing time. + // https://doc.rust-lang.org/reference/interior-mutability.html + let mut result = IconObject { + icon_file: icon_in.icon_file, + icon_state: icon_in.icon_state, + dir: icon_in.dir, + frame: icon_in.frame, + transform: icon_in + .transform + .into_iter() + .map(|transform_in| match transform_in { + TransformIO::BlendColor { color, blend_mode } => { + Transform::BlendColor { color, blend_mode } + } + TransformIO::BlendIcon { icon, blend_mode } => Transform::BlendIcon { + icon: icon_from_io(icon), + blend_mode, + }, + TransformIO::Crop { x1, y1, x2, y2 } => Transform::Crop { x1, y1, x2, y2 }, + TransformIO::Scale { width, height } => Transform::Scale { width, height }, + }) + .collect(), + transform_hash_input: String::new(), + icon_hash_input: String::new(), + }; + result.gen_icon_hash_input().unwrap(); // unsafe but idc + result +} + +/// Takes in an icon and gives a list of nested icons. Also returns a reference to the provided icon in the list. +fn icon_to_icons(icon_in: &IconObject) -> Vec<&IconObject> { + zone!("icon_to_icons"); + let mut icons: Vec<&IconObject> = Vec::new(); + icons.push(icon_in); + for transform in &icon_in.transform { + if let Transform::BlendIcon { icon, .. } = transform { + let nested = icon_to_icons(icon); + for icon in nested { + icons.push(icon) + } + } + } + icons +} + +/// icon_to_icons but for IO icons. +fn icon_to_icons_io(icon_in: &IconObjectIO) -> Vec<&IconObjectIO> { + zone!("icon_to_icons_io"); + let mut icons: Vec<&IconObjectIO> = Vec::new(); + icons.push(icon_in); + for transform in &icon_in.transform { + if let TransformIO::BlendIcon { icon, .. } = transform { + let nested = icon_to_icons_io(icon); + for icon in nested { + icons.push(icon) + } + } + } + icons +} + +/// Given an IconObject, returns a DMI Icon structure and caches it. +fn icon_to_dmi(icon: &IconObject) -> Result, String> { + zone!("icon_to_dmi"); + let icon_path = &icon.icon_file; + { + zone!("check_dmi_exists"); + if let Some(found) = ICON_FILES.get(icon_path) { + return Ok(found.clone()); + } + } + let icon_file = match File::open(icon_path) { + Ok(icon_file) => icon_file, + Err(err) => { + return Err(format!("Failed to open DMI '{}' - {}", icon_path, err)); + } + }; + let reader = BufReader::new(icon_file); + let dmi: Icon; + { + zone!("parse_dmi"); + dmi = match Icon::load(reader) { + Ok(dmi) => dmi, + Err(err) => { + return Err(format!("DMI '{}' failed to parse - {}", icon_path, err)); + } + }; + } + { + zone!("insert_dmi"); + let dmi_arc = Arc::new(dmi); + let other_arc = dmi_arc.clone(); + // Cache it for later, saving future DMI parsing operations, which are very slow. + ICON_FILES.insert(icon_path.to_owned(), dmi_arc); + Ok(other_arc) + } +} + +/// Takes an IconObject, gets its DMI, then picks out a RgbaImage for the IconState. +/// Returns with True if the RgbaImage is pre-cached (and shouldn't have new transforms applied) +/// Gives ownership over the image. Please return when you are done <3 (via return_image) +fn icon_to_image( + icon: &IconObject, + sprite_name: &String, + cached: bool, + must_be_cached: bool, +) -> Result<(RgbaImage, bool), String> { + zone!("icon_to_image"); + if cached { + zone!("check_rgba_image_exists"); + if icon.icon_hash_input.is_empty() { + return Err(format!( + "No icon_hash generated for {} {}", + icon, sprite_name + )); + } + if let Some(entry) = ICON_STATES.get(&icon.icon_hash_input) { + return Ok((entry.value().clone(), true)); + } + if must_be_cached { + return Err(String::from("Image not found in cache!")); + } + } + let dmi = icon_to_dmi(icon)?; + let mut matched_state: Option<&IconState> = None; + { + zone!("match_icon_state"); + for icon_state in &dmi.states { + if icon_state.name == icon.icon_state { + matched_state = Some(icon_state); + break; + } + } + } + let state = match matched_state { + Some(state) => state, + None => { + return Err(format!( + "Could not find associated icon state {} for {}", + icon.icon_state, sprite_name + )); + } + }; + + let dir = match Dirs::from_bits(icon.dir) { + Some(dir) => dir, + None => { + return Err(format!( + "Invalid dir number {} for {}", + icon.dir, sprite_name + )); + } + }; + Ok(match state.get_image(&dir, icon.frame) { + Ok(image) => (image.to_rgba8(), false), + Err(err) => { + return Err(format!("Error getting image for {}: {}", sprite_name, err)); + } + }) +} + +/// Gives an image back to the cache, after it is done being used. +fn return_image(image: RgbaImage, icon: &IconObject) -> Result<(), Error> { + zone!("insert_rgba_image"); + if icon.icon_hash_input.is_empty() { + return Err(Error::IconForge(format!( + "No icon_hash_input generated for {}", + icon + ))); + } + ICON_STATES.insert(icon.icon_hash_input.to_owned(), image); + Ok(()) +} + +fn apply_all_transforms(image: &mut RgbaImage, transforms: &Vec) -> Result<(), String> { + let mut errors = Vec::::new(); + for transform in transforms { + if let Err(error) = transform_image(image, transform) { + errors.push(error); + } + } + if !errors.is_empty() { + return Err(errors.join("\n")); + } + Ok(()) +} + +/// Applies transforms to a RgbaImage. +fn transform_image(image: &mut RgbaImage, transform: &Transform) -> Result<(), String> { + zone!("transform_image"); + match transform { + Transform::BlendColor { color, blend_mode } => { + zone!("blend_color"); + let mut color2: [u8; 4] = [0, 0, 0, 255]; + { + zone!("from_hex"); + let mut hex: String = color.to_owned(); + if hex.starts_with('#') { + hex = hex[1..].to_string(); + } + if hex.len() == 6 { + hex += "ff"; + } + + if let Err(err) = hex::decode_to_slice(hex, &mut color2) { + return Err(format!("Decoding hex color {} failed: {}", color, err)); + } + } + for x in 0..image.width() { + for y in 0..image.height() { + let px = image.get_pixel_mut(x, y); + let pixel = px.channels(); + let blended = Rgba::blend_u8(pixel, &color2, *blend_mode); + + *px = image::Rgba::(blended); + } + } + } + Transform::BlendIcon { icon, blend_mode } => { + zone!("blend_icon"); + let (mut other_image, cached) = + icon_to_image(icon, &format!("Transform blend_icon {}", icon), true, false)?; + + if !cached { + apply_all_transforms(&mut other_image, &icon.transform)?; + }; + for x in 0..std::cmp::min(image.width(), other_image.width()) { + for y in 0..std::cmp::min(image.width(), other_image.width()) { + let px1 = image.get_pixel_mut(x, y); + let px2 = other_image.get_pixel(x, y); + let pixel_1 = px1.channels(); + let pixel_2 = px2.channels(); + + let blended = Rgba::blend_u8(pixel_1, pixel_2, *blend_mode); + + *px1 = image::Rgba::(blended); + } + } + if let Err(err) = return_image(other_image, icon) { + return Err(err.to_string()); + } + } + Transform::Scale { width, height } => { + zone!("scale"); + let old_width = image.width() as usize; + let old_height = image.height() as usize; + let x_ratio = old_width as f32 / *width as f32; + let y_ratio = old_height as f32 / *height as f32; + let mut new_image = RgbaImage::new(*width, *height); + for x in 0..(*width) { + for y in 0..(*height) { + let old_x = (x as f32 * x_ratio).floor() as u32; + let old_y = (y as f32 * y_ratio).floor() as u32; + new_image.put_pixel(x, y, *image.get_pixel(old_x, old_y)); + } + } + *image = new_image; + } + Transform::Crop { x1, y1, x2, y2 } => { + zone!("crop"); + let i_width = image.width(); + let i_height = image.height(); + let mut x1 = *x1; + let mut y1 = *y1; + let mut x2 = *x2; + let mut y2 = *y2; + if x2 <= x1 || y2 <= y1 { + return Err(format!( + "Invalid bounds {} {} to {} {} in crop transform", + x1, y1, x2, y2 + )); + } + + // convert from BYOND (0,0 is bottom left) to Rust (0,0 is top left) + let y2_old = y2; + y2 = i_height as i32 - y1; + y1 = i_height as i32 - y2_old; + + let mut width = x2 - x1; + let mut height = y2 - y1; + + if x1 < 0 || x2 > i_width as i32 || y1 < 0 || y2 > i_height as i32 { + let mut blank_img: image::ImageBuffer, Vec> = + RgbaImage::from_fn(width as u32, height as u32, |_x, _y| { + image::Rgba([0, 0, 0, 0]) + }); + image::imageops::overlay( + &mut blank_img, + image, + if x1 < 0 { (x1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, + if y1 < 0 { (y1).abs() as i64 } else { 0 } + - if x1 > i_width as i32 { + (x1 - i_width as i32) as i64 + } else { + 0 + }, + ); + *image = blank_img; + x1 = std::cmp::max(0, x1); + x2 = std::cmp::min(i_width as i32, x2); + y1 = std::cmp::max(0, y1); + y2 = std::cmp::min(i_height as i32, y2); + width = x2 - x1; + height = y2 - y1; + } + *image = + image::imageops::crop_imm(image, x1 as u32, y1 as u32, width as u32, height as u32) + .to_image(); + } + } + Ok(()) +} + +#[derive(Clone)] +struct Rgba { + r: f32, + g: f32, + b: f32, + a: f32, +} + +impl Rgba { + fn into_array(self) -> [u8; 4] { + [ + self.r.round() as u8, + self.g.round() as u8, + self.b.round() as u8, + self.a.round() as u8, + ] + } + + fn from_array(rgba: &[u8]) -> Rgba { + Self { + r: rgba[0] as f32, + g: rgba[1] as f32, + b: rgba[2] as f32, + a: rgba[3] as f32, + } + } + + fn map_each(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba + where + F: Fn(f32, f32) -> f32, + T: Fn(f32, f32) -> f32, + { + Rgba { + r: rgb_fn(color.r, color2.r), + g: rgb_fn(color.g, color2.g), + b: rgb_fn(color.b, color2.b), + a: a_fn(color.a, color2.a), + } + } + + fn map_each_a(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba + where + F: Fn(f32, f32, f32, f32) -> f32, + T: Fn(f32, f32) -> f32, + { + Rgba { + r: rgb_fn(color.r, color2.r, color.a, color2.a), + g: rgb_fn(color.g, color2.g, color.a, color2.a), + b: rgb_fn(color.b, color2.b, color.a, color2.a), + a: a_fn(color.a, color2.a), + } + } + + /// Takes two [u8; 4]s, converts them to Rgba structs, then blends them according to blend_mode by calling blend(). + fn blend_u8(color: &[u8], other_color: &[u8], blend_mode: u8) -> [u8; 4] { + Rgba::from_array(color) + .blend(&Rgba::from_array(other_color), blend_mode) + .into_array() + } + + /// Blends two colors according to blend_mode. The numbers correspond to BYOND blend modes. + fn blend(&self, other_color: &Rgba, blend_mode: u8) -> Rgba { + match blend_mode { + 0 => Rgba::map_each(self, other_color, |c1, c2| c1 + c2, f32::min), + 1 => Rgba::map_each(self, other_color, |c1, c2| c2 - c1, f32::min), + 2 => Rgba::map_each( + self, + other_color, + |c1, c2| c1 * c2 / 255.0, + |a1: f32, a2: f32| a1 * a2 / 255.0, + ), + 3 => Rgba::map_each_a( + self, + other_color, + |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) + }, + ), + 6 => Rgba::map_each_a( + other_color, + self, + |c1, c2, _c1_a, c2_a| c1 + (c2 - c1) * c2_a / 255.0, + |a1, a2| { + let high = f32::max(a1, a2); + let low = f32::min(a1, a2); + high + (high * low / 255.0) + }, + ), + _ => self.clone(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b9fa90a..891d03dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,8 @@ pub mod git; pub mod hash; #[cfg(feature = "http")] pub mod http; +#[cfg(feature = "iconforge")] +pub mod iconforge; #[cfg(feature = "json")] pub mod json; #[cfg(feature = "log")]