diff --git a/.gitignore b/.gitignore index f76e85f..b00084a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ # Generated by Cargo # will have compiled files and executables /target/ -/test_files/ +/temp/ /.vscode/ /EML_gui_config.ini +rustfmt.toml # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index f6025bb..b48e228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "accesskit_macos", "accesskit_unix", "accesskit_windows", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "winit", ] @@ -185,47 +185,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -255,7 +256,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" dependencies = [ - "async-fs 2.1.1", + "async-fs 2.1.2", "async-net", "enumflags2", "futures-channel", @@ -264,7 +265,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus 4.1.2", + "zbus 4.2.0", ] [[package]] @@ -284,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" dependencies = [ "event-listener 5.3.0", - "event-listener-strategy 0.5.1", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] @@ -297,7 +298,7 @@ checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", "event-listener 5.3.0", - "event-listener-strategy 0.5.1", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] @@ -310,7 +311,7 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-lite 2.3.0", "slab", ] @@ -329,9 +330,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock 3.3.0", "blocking", @@ -370,8 +371,8 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.6.0", - "rustix 0.38.32", + "polling 3.7.0", + "rustix 0.38.34", "slab", "tracing", "windows-sys 0.52.0", @@ -421,15 +422,15 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.32", + "rustix 0.38.34", "windows-sys 0.48.0", ] [[package]] name = "async-process" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad07b3443bfa10dcddf86a452ec48949e8e7fedf7392d82de3969fda99e90ed" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" dependencies = [ "async-channel", "async-io 2.3.2", @@ -440,45 +441,45 @@ dependencies = [ "cfg-if", "event-listener 5.3.0", "futures-lite 2.3.0", - "rustix 0.38.32", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] [[package]] name = "async-recursion" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] name = "async-signal" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" dependencies = [ "async-io 2.3.2", - "async-lock 2.8.0", + "async-lock 3.3.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.32", + "rustix 0.38.34", "signal-hook-registry", "slab", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" @@ -488,7 +489,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -554,14 +555,14 @@ dependencies = [ "derive_utils", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" @@ -603,7 +604,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.61", "which", ] @@ -671,18 +672,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", - "fastrand 2.0.2", "futures-io", "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] @@ -714,7 +713,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -737,8 +736,8 @@ checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ "bitflags 2.5.0", "log", - "polling 3.6.0", - "rustix 0.38.32", + "polling 3.7.0", + "rustix 0.38.34", "slab", "thiserror", ] @@ -750,7 +749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ "calloop", - "rustix 0.38.32", + "rustix 0.38.34", "wayland-backend", "wayland-client", ] @@ -763,12 +762,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "clru" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] name = "cocoa" @@ -946,9 +946,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" @@ -962,9 +962,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -987,7 +987,7 @@ checksum = "5387f5bbc9e9e6c96436ea125afa12614cebf8ac67f49abc08c1e7a891466c90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1108,7 +1108,7 @@ dependencies = [ "lazy_static", "proc-macro2", "regex", - "syn 2.0.60", + "syn 2.0.61", "unicode-xid", ] @@ -1120,7 +1120,7 @@ checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1" dependencies = [ "lazy_static", "proc-macro2", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1135,7 +1135,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1255,7 +1255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1302,7 +1302,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1405,7 +1405,7 @@ checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "elden_mod_loader_gui" -version = "0.9.3" +version = "0.9.4" dependencies = [ "criterion", "env_logger", @@ -1445,7 +1445,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1479,9 +1479,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1547,9 +1547,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener 5.3.0", "pin-project-lite", @@ -1582,9 +1582,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -1641,9 +1641,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1721,7 +1721,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1781,7 +1781,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1796,7 +1796,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -2013,9 +2013,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -2237,7 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903d476370e036f8d1560d4aed21ba366e9d83f02c1b27d9621aa1eaf99669b5" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2437,6 +2437,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -2485,9 +2491,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -2561,9 +2567,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" @@ -2641,9 +2647,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2673,11 +2679,12 @@ dependencies = [ [[package]] name = "lyon_extra" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ce2ae38f2480094ec1f0d5df51a75581fa84f0e8f32a0edb1d264630c99f3b" +checksum = "8c4a243ce61e7e5f3ae6c72a88d8fb081b6c69f13500c15e99cfd1159a833b20" dependencies = [ "lyon_path", + "thiserror", ] [[package]] @@ -2795,7 +2802,7 @@ dependencies = [ "ndk-sys", "num_enum", "raw-window-handle 0.5.2", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "thiserror", ] @@ -2862,9 +2869,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2888,7 +2895,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3039,9 +3046,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -3072,7 +3079,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3100,7 +3107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", ] @@ -3169,15 +3176,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.32", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -3205,12 +3212,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3234,9 +3241,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -3317,9 +3324,9 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "raw-window-handle" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b" [[package]] name = "rayon" @@ -3417,7 +3424,7 @@ dependencies = [ "objc-foundation", "objc_id", "pollster", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", @@ -3466,9 +3473,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3501,9 +3508,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -3514,9 +3521,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "rustybuzz" @@ -3552,9 +3559,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3598,35 +3605,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -3641,7 +3648,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3672,9 +3679,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3765,7 +3772,7 @@ dependencies = [ "i-slint-compiler", "spin_on", "thiserror", - "toml_edit 0.22.11", + "toml_edit 0.22.12", ] [[package]] @@ -3808,7 +3815,7 @@ dependencies = [ "libc", "log", "memmap2 0.9.4", - "rustix 0.38.32", + "rustix 0.38.34", "thiserror", "wayland-backend", "wayland-client", @@ -3861,7 +3868,7 @@ dependencies = [ "cfg_aliases 0.1.1", "cocoa", "core-graphics", - "fastrand 2.0.2", + "fastrand 2.1.0", "foreign-types", "js-sys", "log", @@ -3869,7 +3876,7 @@ dependencies = [ "objc", "raw-window-handle 0.5.2", "redox_syscall 0.4.1", - "rustix 0.38.32", + "rustix 0.38.34", "tiny-xlib", "wasm-bindgen", "wayland-backend", @@ -3938,7 +3945,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3964,9 +3971,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -3991,8 +3998,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", - "rustix 0.38.32", + "fastrand 2.1.0", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -4013,22 +4020,22 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4145,7 +4152,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.11", + "toml_edit 0.22.12", ] [[package]] @@ -4183,15 +4190,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.11" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb686a972ccef8537b39eead3968b0e8616cb5040dbb9bba93007c8e07c9215f" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -4213,7 +4220,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4423,7 +4430,7 @@ checksum = "68c1b85ec843d3bc60e9d65fa7e00ce6549416a25c267b5ea93e6c81e3aa66e5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4469,7 +4476,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -4503,7 +4510,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4522,7 +4529,7 @@ checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.32", + "rustix 0.38.34", "scoped-tls", "smallvec", "wayland-sys", @@ -4535,7 +4542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ "bitflags 2.5.0", - "rustix 0.38.32", + "rustix 0.38.34", "wayland-backend", "wayland-scanner", ] @@ -4557,7 +4564,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" dependencies = [ - "rustix 0.38.32", + "rustix 0.38.34", "wayland-client", "xcursor", ] @@ -4668,7 +4675,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.32", + "rustix 0.38.34", ] [[package]] @@ -4689,11 +4696,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4760,7 +4767,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4771,7 +4778,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5016,9 +5023,9 @@ dependencies = [ "orbclient", "percent-encoding", "raw-window-handle 0.5.2", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "redox_syscall 0.3.5", - "rustix 0.38.32", + "rustix 0.38.34", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -5033,7 +5040,7 @@ dependencies = [ "web-time 0.2.4", "windows-sys 0.48.0", "x11-dl", - "x11rb 0.13.0", + "x11rb 0.13.1", "xkbcommon-dl", ] @@ -5048,9 +5055,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -5081,7 +5088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" dependencies = [ "libc", - "x11rb 0.13.0", + "x11rb 0.13.1", ] [[package]] @@ -5114,17 +5121,17 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", "gethostname 0.4.3", "libc", "libloading 0.8.3", "once_cell", - "rustix 0.38.32", - "x11rb-protocol 0.13.0", + "rustix 0.38.34", + "x11rb-protocol 0.13.1", ] [[package]] @@ -5138,9 +5145,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xattr" @@ -5150,7 +5157,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.13", - "rustix 0.38.32", + "rustix 0.38.34", ] [[package]] @@ -5254,21 +5261,20 @@ dependencies = [ [[package]] name = "zbus" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" +checksum = "6aea58d1af0aaa8abf87f3d9ade9b8f46bf13727e5f9fb24bc31ee9d94a9b4ad" dependencies = [ "async-broadcast 0.7.0", "async-executor", - "async-fs 2.1.1", + "async-fs 2.1.2", "async-io 2.3.2", "async-lock 3.3.0", - "async-process 2.2.1", + "async-process 2.2.2", "async-recursion", "async-task", "async-trait", "blocking", - "derivative", "enumflags2", "event-listener 5.3.0", "futures-core", @@ -5286,9 +5292,9 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros 4.1.2", + "zbus_macros 4.2.0", "zbus_names 3.0.0", - "zvariant 4.0.2", + "zvariant 4.0.3", ] [[package]] @@ -5307,14 +5313,13 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" +checksum = "1bf2b496ec1e2d3c4a7878e351607f7a2bec1e1029b353683dfc28a22999e369" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "regex", "syn 1.0.109", "zvariant_utils", ] @@ -5338,27 +5343,27 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant 4.0.2", + "zvariant 4.0.3", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5386,16 +5391,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +checksum = "4e9282c6945d9e27742ba7ad7191325546636295de7b83f6735af73159b32ac7" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "zvariant_derive 4.0.2", + "zvariant_derive 4.0.3", ] [[package]] @@ -5413,9 +5418,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +checksum = "0142549e559746ff09d194dd43d256a554299d286cc56460a082b8ae24652aa1" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", @@ -5426,9 +5431,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +checksum = "75fa7291bdd68cd13c4f97cc9d78cbf16d96305856dfc7ac942aeff4c2de7d5a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5b870bd..1c0ade1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "elden_mod_loader_gui" -version = "0.9.3" +version = "0.9.4" authors = ["WardLordRuby"] edition = "2021" build = "build.rs" @@ -10,7 +10,7 @@ build = "build.rs" [package.metadata.winresource] ProductName = "Elden Mod Loader GUI" FileDescription = "Elden Ring mod manager made by: WardLordRuby" -ProductVersion = "0.9.3-beta" +ProductVersion = "0.9.4-beta" [profile.release] opt-level = "z" # Optimize for size. diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index 8a46dc6..aa70bbe 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -1,13 +1,13 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use elden_mod_loader_gui::utils::ini::{parser::RegMod, writer::*}; +use elden_mod_loader_gui::{utils::ini::writer::*, Cfg, INI_SECTIONS}; use rand::{distributions::Alphanumeric, Rng}; use std::{ fs::remove_file, path::{Path, PathBuf}, }; -const BENCH_TEST_FILE: &str = "test_files\\benchmark_test.ini"; +const BENCH_TEST_FILE: &str = "temp\\benchmark_test.ini"; const NUM_ENTRIES: u32 = 25; fn populate_non_valid_ini(len: u32, file: &Path) { @@ -18,11 +18,11 @@ fn populate_non_valid_ini(len: u32, file: &Path) { let paths = generate_test_paths(); let path_refs = paths.iter().map(|p| p.as_path()).collect::>(); - save_bool(file, Some("registered-mods"), &key, bool_value).unwrap(); + save_bool(file, INI_SECTIONS[2], &key, bool_value).unwrap(); if paths.len() > 1 { - save_path_bufs(file, &key, &path_refs).unwrap(); + save_paths(file, INI_SECTIONS[3], &key, &path_refs).unwrap(); } else { - save_path(file, Some("mod-files"), &key, paths[0].as_path()).unwrap(); + save_path(file, INI_SECTIONS[3], &key, paths[0].as_path()).unwrap(); } } } @@ -45,10 +45,11 @@ fn generate_test_paths() -> Vec { fn data_collection_benchmark(c: &mut Criterion) { let test_file = Path::new(BENCH_TEST_FILE); + let ini = Cfg::read(test_file).unwrap(); populate_non_valid_ini(NUM_ENTRIES, test_file); c.bench_function("data_collection", |b| { - b.iter(|| black_box(RegMod::collect(test_file, true))); + b.iter(|| black_box(ini.collect_mods(None, true))); }); remove_file(test_file).unwrap(); } diff --git a/build.rs b/build.rs index f7257ec..576c095 100644 --- a/build.rs +++ b/build.rs @@ -4,7 +4,7 @@ extern crate winresource; /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE` const MAJOR: u64 = 0; const MINOR: u64 = 9; -const PATCH: u64 = 3; +const PATCH: u64 = 4; const RELEASE: u64 = 2; fn main() { diff --git a/src/lib.rs b/src/lib.rs index 4fe1c7c..3abec77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod utils { pub mod installer; pub mod ini { + pub mod mod_loader; pub mod parser; pub mod writer; } @@ -9,15 +10,17 @@ pub mod utils { use ini::Ini; use log::{error, info, trace, warn}; use utils::ini::{ - parser::{IniProperty, RegMod}, - writer::{remove_array, save_bool, save_path, save_path_bufs}, + parser::{IniProperty, IntoIoError, RegMod, Setup}, + writer::{new_cfg, remove_array, save_bool, save_path, save_paths}, }; use std::{ + collections::HashSet, io::ErrorKind, path::{Path, PathBuf}, }; +// changing the order of any of the following consts would not be good const DEFAULT_GAME_DIR: [&str; 6] = [ "Program Files (x86)", "Steam", @@ -26,16 +29,35 @@ const DEFAULT_GAME_DIR: [&str; 6] = [ "ELDEN RING", "Game", ]; + pub const REQUIRED_GAME_FILES: [&str; 3] = [ "eldenring.exe", "oo2core_6_win64.dll", "eossdk-win64-shipping.dll", ]; -pub const LOADER_FILES: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll"]; -pub const LOADER_FILES_DISABLED: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll.disabled"]; +pub const OFF_STATE: &str = ".disabled"; + +pub const INI_NAME: &str = "EML_gui_config.ini"; +pub const INI_SECTIONS: [Option<&str>; 4] = [ + Some("app-settings"), + Some("paths"), + Some("registered-mods"), + Some("mod-files"), +]; +pub const INI_KEYS: [&str; 2] = ["dark_mode", "game_dir"]; +pub const DEFAULT_INI_VALUES: [&str; 1] = ["true"]; +pub const ARRAY_KEY: &str = "array[]"; +pub const ARRAY_VALUE: &str = "array"; + +pub const LOADER_FILES: [&str; 3] = [ + "dinput8.dll.disabled", + "dinput8.dll", + "mod_loader_config.ini", +]; pub const LOADER_SECTIONS: [Option<&str>; 2] = [Some("modloader"), Some("loadorder")]; pub const LOADER_KEYS: [&str; 2] = ["load_delay", "show_terminal"]; +pub const DEFAULT_LOADER_VALUES: [&str; 2] = ["5000", "0"]; #[macro_export] macro_rules! new_io_error { @@ -60,16 +82,14 @@ impl PathErrors { pub fn shorten_paths(paths: &[PathBuf], remove: &PathBuf) -> Result, PathErrors> { let mut results = PathErrors::new(paths.len()); - paths - .iter() - .for_each(|path| match path.strip_prefix(remove) { - Ok(file) => { - results.ok_paths_short.push(PathBuf::from(file)); - } - Err(_) => { - results.err_paths_long.push(PathBuf::from(path)); - } - }); + paths.iter().for_each(|path| match path.strip_prefix(remove) { + Ok(file) => { + results.ok_paths_short.push(PathBuf::from(file)); + } + Err(_) => { + results.err_paths_long.push(PathBuf::from(path)); + } + }); if results.err_paths_long.is_empty() { Ok(results.ok_paths_short) } else { @@ -77,50 +97,7 @@ pub fn shorten_paths(paths: &[PathBuf], remove: &PathBuf) -> Result } } -#[derive(Default)] -pub struct ModLoader { - pub installed: bool, - pub disabled: bool, - pub cfg: PathBuf, -} - -pub fn elden_mod_loader_properties(game_dir: &Path) -> std::io::Result { - let disabled: bool; - let cfg: PathBuf; - let installed = match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { - Ok(true) => { - info!("Found mod loader files"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = false; - true - } - Ok(false) => { - warn!("Checking if mod loader is disabled"); - match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { - Ok(true) => { - info!("Found mod loader files in the disabled state"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = true; - true - } - Ok(false) => { - error!("Mod Loader Files not found in selected path"); - cfg = PathBuf::new(); - disabled = false; - false - } - Err(err) => return Err(err), - } - } - Err(err) => return Err(err), - }; - Ok(ModLoader { - installed, - disabled, - cfg, - }) -} - +/// returns all the modified _partial_paths_ pub fn toggle_files( game_dir: &Path, new_state: bool, @@ -132,20 +109,19 @@ pub fn toggle_files( file_paths .iter() .map(|path| { - let off_state = ".disabled"; let file_name = match path.file_name() { Some(name) => name, None => path.as_os_str(), }; let mut new_name = file_name.to_string_lossy().to_string(); - if let Some(index) = new_name.to_lowercase().find(off_state) { + if let Some(index) = new_name.to_lowercase().find(OFF_STATE) { if new_state { - new_name.replace_range(index..index + off_state.len(), ""); + new_name.replace_range(index..index + OFF_STATE.len(), ""); } } else if !new_state { - new_name.push_str(off_state); + new_name.push_str(OFF_STATE); } - let mut new_path = path.clone(); + let mut new_path = PathBuf::from(path); new_path.set_file_name(new_name); new_path }) @@ -166,13 +142,10 @@ pub fn toggle_files( ); } - paths - .iter() - .zip(new_paths.iter()) - .try_for_each(|(path, new_path)| { - std::fs::rename(path, new_path)?; - Ok(()) - }) + paths.iter().zip(new_paths.iter()).try_for_each(|(path, new_path)| { + std::fs::rename(path, new_path)?; + Ok(()) + }) } fn update_cfg( num_file: &usize, @@ -182,18 +155,18 @@ pub fn toggle_files( save_file: &Path, ) -> std::io::Result<()> { if *num_file == 1 { - save_path(save_file, Some("mod-files"), key, path_to_save[0])?; + save_path(save_file, INI_SECTIONS[3], key, path_to_save[0])?; } else { remove_array(save_file, key)?; - save_path_bufs(save_file, key, path_to_save)?; + save_paths(save_file, INI_SECTIONS[3], key, path_to_save)?; } - save_bool(save_file, Some("registered-mods"), key, state)?; + save_bool(save_file, INI_SECTIONS[2], key, state)?; Ok(()) } - let num_rename_files = reg_mod.files.len(); - let num_total_files = num_rename_files + reg_mod.other_files_len(); + let num_rename_files = reg_mod.files.dll.len(); + let num_total_files = num_rename_files + reg_mod.files.other_files_len(); - let file_paths = std::sync::Arc::new(reg_mod.files.clone()); + let file_paths = std::sync::Arc::new(reg_mod.files.dll.clone()); let file_paths_clone = file_paths.clone(); let game_dir_clone = game_dir.to_path_buf(); @@ -203,8 +176,8 @@ pub fn toggle_files( std::thread::spawn(move || join_paths(&game_dir_clone, &file_paths_clone)); let short_path_new = new_short_paths_thread.join().unwrap_or(Vec::new()); - let all_short_paths = reg_mod.add_other_files_to_files(&short_path_new); - let full_path_new = join_paths(Path::new(game_dir), &short_path_new); + let all_short_paths = reg_mod.files.add_other_files_to_files(&short_path_new); + let full_path_new = join_paths(game_dir, &short_path_new); let full_path_original = original_full_paths_thread.join().unwrap_or(Vec::new()); rename_files(&num_rename_files, &full_path_original, &full_path_new)?; @@ -221,32 +194,96 @@ pub fn toggle_files( Ok(short_path_new) } -pub fn get_cfg(input_file: &Path) -> std::io::Result { - Ini::load_from_file_noescape(input_file) - .map_err(|err| std::io::Error::new(ErrorKind::AddrNotAvailable, err)) +// MARK: TODO +// make get_cfg() private and move over to get_or_setup_cfg() | would just need to figure out how to set the first startup flag + +/// If cfg file does not exist or is not set up with provided sections this function will +/// create a new ".ini" file in the given path +pub fn get_or_setup_cfg(from_path: &Path, sections: &[Option<&str>]) -> std::io::Result { + match get_cfg(from_path) { + Ok(ini) => { + if ini.is_setup(sections) { + trace!("{:?} found, and is already setup", from_path.file_name()); + return Ok(ini); + } + trace!( + "ini: {:?} is not setup, creating new", + from_path.file_name() + ); + } + Err(err) => error!("{err} : {:?}", from_path.file_name()), + }; + new_cfg(from_path) +} + +pub fn get_cfg(from_path: &Path) -> std::io::Result { + Ini::load_from_file_noescape(from_path).map_err(|err| err.into_io_error()) } pub enum Operation { All, Any, + Count, } -pub fn does_dir_contain(path: &Path, operation: Operation, list: &[&str]) -> std::io::Result { +pub enum OperationResult<'a, T: ?Sized> { + Bool(bool), + Count((usize, HashSet<&'a T>)), +} + +/// `Operation::All` and `Operation::Any` map to `OperationResult::bool(_result_)` +/// `Operation::Count` maps to `OperationResult::Count((_num_found_, _HashSet<_&input_list_>))` +/// when matching you will always have to `_ => unreachable()` for the return type you will never get +pub fn does_dir_contain<'a, T>( + path: &Path, + operation: Operation, + list: &'a [&T], +) -> std::io::Result> +where + T: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash + ?Sized, + for<'b> &'b str: std::borrow::Borrow, +{ let entries = std::fs::read_dir(path)?; let file_names = entries - .map(|entry| Ok(entry?.file_name())) - .collect::>>()?; + .filter_map(|entry| Some(entry.ok()?.file_name())) + .collect::>(); + let str_names = file_names.iter().filter_map(|f| f.to_str()).collect::>(); - let result = match operation { - Operation::All => list - .iter() - .all(|check_file| file_names.iter().any(|file_name| file_name == check_file)), - Operation::Any => list - .iter() - .any(|check_file| file_names.iter().any(|file_name| file_name == check_file)), - }; - Ok(result) + match operation { + Operation::All => Ok(OperationResult::Bool( + list.iter().all(|&check_file| str_names.contains(check_file)), + )), + Operation::Any => Ok(OperationResult::Bool( + list.iter().any(|&check_file| str_names.contains(check_file)), + )), + Operation::Count => { + let collection = list + .iter() + .filter(|&check_file| str_names.contains(check_file)) + .copied() + .collect::>(); + Ok(OperationResult::Count((collection.len(), collection))) + } + } } + +/// returns a collection of references to entries in list that are not found in the supplied path +/// returns an empty Vec if all files were found +pub fn files_not_found<'a, T>(in_path: &Path, list: &'a [&T]) -> std::io::Result> +where + T: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash + ?Sized, + for<'b> &'b str: std::borrow::Borrow, +{ + match does_dir_contain(in_path, Operation::Count, list) { + Ok(OperationResult::Count((c, _))) if c == list.len() => Ok(Vec::new()), + Ok(OperationResult::Count((_, found_files))) => { + Ok(list.iter().filter(|&&e| !found_files.contains(e)).copied().collect()) + } + Err(err) => Err(err), + _ => unreachable!(), + } +} + pub struct FileData<'a> { pub name: &'a str, pub extension: &'a str, @@ -254,9 +291,11 @@ pub struct FileData<'a> { } impl FileData<'_> { + /// To get an accurate FileData.name function input needs .file_name() called before hand + /// FileData.extension && FileData.enabled are accurate with any &Path str as input pub fn from(name: &str) -> FileData { - match name.find(".disabled") { - Some(index) => { + match FileData::state_data(name) { + (false, index) => { let first_split = name.split_at(name[..index].rfind('.').expect("is file")); FileData { name: first_split.0, @@ -267,8 +306,7 @@ impl FileData<'_> { enabled: false, } } - - None => { + (true, _) => { let split = name.split_at(name.rfind('.').expect("is file")); FileData { name: split.0, @@ -279,20 +317,36 @@ impl FileData<'_> { } } - pub fn is_enabled(path: &Path) -> std::io::Result { - let file_name = file_name_or_err(path)?.to_string_lossy(); - Ok(FileData::from(&file_name).enabled) + #[inline] + /// index is only used in the _disabled_ state to locate where `OFF_STATE` begins + /// saftey check to make sure `OFF_STATE` is found at the end of a `&str` + fn state_data(path: &str) -> (bool, usize) { + if let Some(index) = path.find(OFF_STATE) { + (index != path.len() - OFF_STATE.len(), index) + } else { + (true, 0) + } + } + + #[inline] + pub fn is_enabled>(path: &T) -> bool { + FileData::state_data(&path.as_ref().to_string_lossy()).0 + } + + #[inline] + pub fn is_disabled>(path: &T) -> bool { + !FileData::state_data(&path.as_ref().to_string_lossy()).0 } } -/// Convience function to map Option None to an io Error +/// convience function to map Option None to an io Error pub fn parent_or_err(path: &Path) -> std::io::Result<&Path> { path.parent().ok_or(std::io::Error::new( ErrorKind::InvalidData, "Could not get parent_dir", )) } -/// Convience function to map Option None to an io Error +/// convience function to map Option None to an io Error pub fn file_name_or_err(path: &Path) -> std::io::Result<&std::ffi::OsStr> { path.file_name().ok_or(std::io::Error::new( ErrorKind::InvalidData, @@ -300,65 +354,87 @@ pub fn file_name_or_err(path: &Path) -> std::io::Result<&std::ffi::OsStr> { )) } +#[derive(Debug, Default)] +pub struct Cfg { + pub data: Ini, + pub dir: PathBuf, +} pub enum PathResult { Full(PathBuf), Partial(PathBuf), None(PathBuf), } -pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { - let config: Ini = match get_cfg(file_name) { - Ok(ini) => { - trace!( - "Success: (attempt_locate_game) Read ini from \"{}\"", - file_name.display() - ); - ini - } - Err(err) => { - error!( - "Failure: (attempt_locate_game) Could not complete. Could not read ini from \"{}\"", - file_name.display() - ); - error!("Error: {err}"); - return Ok(PathResult::None(PathBuf::new())); + +impl Cfg { + pub fn from(ini: Ini, ini_path: &Path) -> Self { + Cfg { + data: ini, + dir: PathBuf::from(ini_path), } - }; - if let Some(path) = IniProperty::::read(&config, Some("paths"), "game_dir", false) - .and_then(|ini_property| { - match does_dir_contain(&ini_property.value, Operation::All, &REQUIRED_GAME_FILES) { - Ok(true) => Some(ini_property.value), - Ok(false) => { - error!( - "{}", - format!( - "Required Game files not found in:\n\"{}\"", - ini_property.value.display() - ) - ); - None - } - Err(err) => { - error!("Error: {err}"); - None - } - } + } + + pub fn read(ini_path: &Path) -> std::io::Result { + let data = get_or_setup_cfg(ini_path, &INI_SECTIONS)?; + Ok(Cfg { + data, + dir: PathBuf::from(ini_path), }) - { - info!("Success: \"game_dir\" from ini is valid"); - return Ok(PathResult::Full(path)); } - let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); - if does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES).unwrap_or(false) { - info!("Success: located \"game_dir\" on drive"); - save_path(file_name, Some("paths"), "game_dir", try_locate.as_path())?; - return Ok(PathResult::Full(try_locate)); + + pub fn update(&mut self) -> std::io::Result<()> { + self.data = get_or_setup_cfg(&self.dir, &INI_SECTIONS)?; + Ok(()) } - if try_locate.components().count() > 1 { - info!("Partial \"game_dir\" found"); - return Ok(PathResult::Partial(try_locate)); + + /// returns the number of registered mods currently saved in the ".ini" + pub fn mods_registered(&self) -> usize { + if self.data.section(INI_SECTIONS[2]).is_none() + || self.data.section(INI_SECTIONS[2]).unwrap().is_empty() + { + 0 + } else { + self.data.section(INI_SECTIONS[2]).unwrap().len() + } + } + + /// returns true if registered mods saved in the ".ini" is None + #[inline] + pub fn mods_empty(&self) -> bool { + self.data.section(INI_SECTIONS[2]).is_none() + || self.data.section(INI_SECTIONS[2]).unwrap().is_empty() + } + + pub fn attempt_locate_game(&mut self) -> std::io::Result { + if let Ok(path) = + IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false) + { + info!("Success: \"game_dir\" from ini is valid"); + return Ok(PathResult::Full(path.value)); + } + let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); + if matches!( + does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES), + Ok(OperationResult::Bool(true)) + ) { + info!("Success: located \"game_dir\" on drive"); + save_path( + &self.dir, + INI_SECTIONS[1], + INI_KEYS[1], + try_locate.as_path(), + )?; + self.data + .with_section(INI_SECTIONS[1]) + .set(INI_KEYS[1], try_locate.to_string_lossy().to_string()); + return Ok(PathResult::Full(try_locate)); + } + if try_locate.components().count() > 1 { + info!("Partial \"game_dir\" found"); + return Ok(PathResult::Partial(try_locate)); + } + warn!("Could not locate \"game_dir\""); + Ok(PathResult::None(try_locate)) } - warn!("Could not locate \"game_dir\""); - Ok(PathResult::None(try_locate)) } fn attempt_locate_dir(target_path: &[&str]) -> std::io::Result { diff --git a/src/main.rs b/src/main.rs index 5308661..e0710b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ use elden_mod_loader_gui::{ utils::{ ini::{ - parser::{file_registered, IniProperty, RegMod, Valitidity}, + mod_loader::{ModLoader, ModLoaderCfg, Countable}, + parser::{file_registered, IniProperty, RegMod, Setup}, writer::*, }, installer::{remove_mod_files, InstallData, scan_for_mods} @@ -13,10 +14,10 @@ use elden_mod_loader_gui::{ *, }; use i_slint_backend_winit::WinitWindowAccessor; -use log::{error, info, warn}; -use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; +use log::{error, info, warn, debug}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, Timer, VecModel}; use std::{ - ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ + collections::{HashMap, HashSet}, ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ atomic::{AtomicU32, Ordering}, OnceLock, } @@ -28,13 +29,13 @@ use tokio::sync::{ slint::include_modules!(); -const CONFIG_NAME: &str = "EML_gui_config.ini"; static GLOBAL_NUM_KEY: AtomicU32 = AtomicU32::new(0); static RESTRICTED_FILES: OnceLock<[&'static OsStr; 6]> = OnceLock::new(); static RECEIVER: OnceLock>> = OnceLock::new(); fn main() -> Result<(), slint::PlatformError> { env_logger::init(); + slint::platform::set_platform(Box::new( i_slint_backend_winit::Backend::new().expect("This app is being run on windows"), )) @@ -53,53 +54,106 @@ fn main() -> Result<(), slint::PlatformError> { { let current_ini = get_ini_dir(); let first_startup: bool; - let ini_valid = match get_cfg(current_ini) { + let mut errors= Vec::new(); + let ini = match get_cfg(current_ini) { Ok(ini) => { - if ini.is_setup() { + if ini.is_setup(&INI_SECTIONS) { info!("Config file found at \"{}\"", current_ini.display()); first_startup = false; - true + Some(ini) } else { first_startup = false; - false + None } } Err(err) => { + // io::Open error or | parse error with type ErrorKind::InvalidData error!("Error: {err}"); + if err.kind() == ErrorKind::InvalidData { + debug!("error 1"); + errors.push(err); + } first_startup = true; - false + None + } + }; + let mut ini = match ini { + Some(ini_data) => Cfg::from(ini_data, current_ini), + None => { + Cfg::read(current_ini).unwrap_or_else(|err| { + // io::write error + debug!("error 2"); + errors.push(err); + Cfg { data: ini::Ini::new(), dir: current_ini.to_owned() } + }) } }; - if !ini_valid { - warn!("Ini not setup correctly. Creating new Ini"); - new_cfg(current_ini).unwrap(); - } let game_verified: bool; - let game_dir = match attempt_locate_game(current_ini) { + let mod_loader: ModLoader; + let mut mod_loader_cfg: ModLoaderCfg; + let mut reg_mods = None; + let order_data: HashMap; + let game_dir = match ini.attempt_locate_game() { Ok(path_result) => match path_result { - PathResult::Full(path) => match RegMod::collect(current_ini, false) { - Ok(reg_mods) => { - reg_mods.iter().for_each(|data| { - data.verify_state(&path, current_ini) - .unwrap_or_else(|err| ui.display_msg(&err.to_string())) + PathResult::Full(path) => { + mod_loader = ModLoader::properties(&path).unwrap_or_else(|err| { + debug!("error 3"); + errors.push(err); + ModLoader::default() + }); + if mod_loader.installed() { + mod_loader_cfg = ModLoaderCfg::read_section(mod_loader.path(), LOADER_SECTIONS[1]).unwrap_or_else(|err| { + debug!("error 4"); + errors.push(err); + ModLoaderCfg::default(mod_loader.path()) }); - game_verified = true; - Some(path) + } else { + mod_loader_cfg = ModLoaderCfg::default(mod_loader.path()); } - Err(err) => { - ui.display_msg(&err.to_string()); - game_verified = true; - Some(path) + match mod_loader_cfg.parse_section() { + Ok(data) => order_data = data, + Err(err) => { + debug!("error 5"); + errors.push(err); + order_data = HashMap::new() + } + }; + match ini.collect_mods( Some(&order_data), false) { + Ok(mod_data) => { + reg_mods = Some(mod_data); + } + Err(err) => { + // io::Write error | PermissionDenied + debug!("error 6"); + errors.push(err); + } + }; + if reg_mods.is_some() && reg_mods.as_ref().unwrap().len() != ini.mods_registered() { + ini = Cfg::read(current_ini).unwrap_or_else(|err| { + debug!("error 7"); + errors.push(err); + Cfg { data: ini::Ini::new(), dir: current_ini.to_owned() } + }) } + game_verified = true; + Some(path) }, PathResult::Partial(path) | PathResult::None(path) => { + mod_loader_cfg = ModLoaderCfg::empty(); + mod_loader = ModLoader::default(); + order_data = HashMap::new(); game_verified = false; Some(path) } }, Err(err) => { - ui.display_msg(&err.to_string()); + // io::Write error + debug!("error 8"); + errors.push(err); + mod_loader_cfg = ModLoaderCfg::empty(); + mod_loader = ModLoader::default(); + order_data = HashMap::new(); game_verified = false; None } @@ -107,15 +161,21 @@ fn main() -> Result<(), slint::PlatformError> { match IniProperty::::read( &get_cfg(current_ini).expect("ini file is verified"), - Some("app-settings"), - "dark_mode", - false, + INI_SECTIONS[0], + INI_KEYS[0], ) { - Some(bool) => ui.global::().set_dark_mode(bool.value), - None => { + Ok(bool) => ui.global::().set_dark_mode(bool.value), + Err(err) => { + // io::Read error + debug!("error 9"); + errors.push(err); ui.global::().set_dark_mode(true); - save_bool(current_ini, Some("app-settings"), "dark_mode", true) - .unwrap_or_else(|err| ui.display_msg(&err.to_string())); + save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], true) + // io::Write error + .unwrap_or_else(|err| { + debug!("error 10"); + errors.push(err) + }); } }; @@ -128,100 +188,129 @@ fn main() -> Result<(), slint::PlatformError> { .to_string() .into(), ); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, !game_verified).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), - )); - let mod_loader: ModLoader; + let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); + if !game_verified { ui.global::().set_current_subpage(1); - if !first_startup { - ui.display_msg( - "Failed to locate Elden Ring\nPlease Select the install directory for Elden Ring", - ); - } - mod_loader = ModLoader::default(); } else { - let game_dir = game_dir.clone().expect("game dir verified"); - mod_loader = elden_mod_loader_properties(&game_dir).unwrap_or_default(); - ui.global::() - .set_loader_disabled(mod_loader.disabled); - if mod_loader.installed { + deserialize_current_mods( + &if let Some(mod_data) = reg_mods { + mod_data + } else { + ini.collect_mods(Some(&order_data),!mod_loader.installed()).unwrap_or_else(|err| { + // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error + debug!("error 11"); + errors.push(err); + vec![RegMod::default()] + }) + },ui.as_weak() + ); + ui.global::().set_loader_disabled(mod_loader.disabled()); + + if mod_loader.installed() { ui.global::().set_loader_installed(true); - if let Ok(mod_loader_ini) = get_cfg(&mod_loader.cfg) { - match IniProperty::::read( - &mod_loader_ini, - LOADER_SECTIONS[0], - LOADER_KEYS[0], - false, - ) { - Some(delay_time) => ui - .global::() - .set_load_delay(SharedString::from(format!("{}ms", delay_time.value))), - None => { - error!("Found an unexpected character saved in \"load_delay\" Reseting to default value"); - save_value_ext( - &mod_loader.cfg, - LOADER_SECTIONS[0], - LOADER_KEYS[0], - "5000", - ) - .unwrap_or_else(|err| error!("{err}")); - } - } - } else { - error!("Error: could not read \"mod_loader_config.ini\""); - } - } - if !first_startup && !mod_loader.installed { - ui.display_msg("This tool requires Elden Mod Loader by TechieW to be installed!"); + let delay = mod_loader_cfg.get_load_delay().unwrap_or_else(|_| { + // parse error ErrorKind::InvalidData + let err = std::io::Error::new(ErrorKind::InvalidData, format!( + "Found an unexpected character saved in \"{}\" Reseting to default value", + LOADER_KEYS[0] + )); + error!("{err}"); + debug!("error 12"); + errors.push(err); + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_LOADER_VALUES[0]) + .unwrap_or_else(|err| { + // io::write error + error!("{err}"); + debug!("error 13"); + errors.push(err); + }); + DEFAULT_LOADER_VALUES[0].parse().unwrap() + }); + let show_terminal = mod_loader_cfg.get_show_terminal().unwrap_or_else(|_| { + // parse error ErrorKind::InvalidData + let err = std::io::Error::new(ErrorKind::InvalidData, format!( + "Found an unexpected character saved in \"{}\" Reseting to default value", + LOADER_KEYS[1] + )); + error!("{err}"); + debug!("error 14"); + errors.push(err); + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_LOADER_VALUES[1]) + .unwrap_or_else(|err| { + // io::write error + error!("{err}"); + debug!("error 15"); + errors.push(err); + }); + false + }); + + ui.global::().set_load_delay(SharedString::from(format!("{}ms", delay))); + ui.global::().set_show_terminal(show_terminal); } } - if first_startup { - if !game_verified && !mod_loader.installed { - ui.display_msg( - "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease select the game directory containing \"eldenring.exe\"", - ); - } else if game_verified && !mod_loader.installed { - ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); - } else if game_verified { - let ui_handle = ui.as_weak(); + // we need to wait for slint event loop to start `ui.run()` before making calls to `ui.display_msg()` + // otherwise calculations for the positon of display_msg_popup are not correct + let ui_handle = ui.as_weak(); + slint::invoke_from_event_loop(move || { + Timer::single_shot(std::time::Duration::from_millis(200), move || { slint::spawn_local(async move { let ui = ui_handle.unwrap(); - let ui_handle = ui.as_weak(); - match confirm_scan_mods(ui_handle, &game_dir.expect("game verified"), current_ini, false).await { - Ok(len) => { - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), - )); - ui.display_msg(&format!("Successfully Found {len} mod(s)")); + if !errors.is_empty() { + for err in errors { + ui.display_msg(&err.to_string()); let _ = receive_msg().await; } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); + } + if first_startup { + if !game_verified { + ui.display_msg( + "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nPlease select the game directory containing \"eldenring.exe\"", + ); + } else if game_verified && !mod_loader.installed() { + ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); + } else if game_verified { + ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), Some(ini)).await { + ui.display_msg(&err.to_string()); + }; } - }; - ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); + } else if game_verified { + if !mod_loader.installed() { + ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); + } else if ini.mods_empty() { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), Some(ini)).await { + ui.display_msg(&err.to_string()); + } + } + } else { + ui.display_msg( + "Failed to locate Elden Ring\nPlease Select the install directory for Elden Ring", + ); + } }).unwrap(); - } - } + }); + }).unwrap(); } // TODO: Error check input text for invalid symbols ui.global::().on_select_mod_files({ let ui_handle = ui.as_weak(); move |mod_name| { - let current_ini = get_ini_dir(); let ui = ui_handle.unwrap(); + let ini_dir = get_ini_dir(); + let mut ini = match Cfg::read(ini_dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; let format_key = mod_name.trim().replace(' ', "_"); let mut results: Vec> = Vec::with_capacity(2); - let registered_mods = RegMod::collect(current_ini, false).unwrap_or_else(|err| { + let registered_mods = ini.collect_mods(None, false).unwrap_or_else(|err| { results.push(Err(err)); vec![RegMod::default()] }); @@ -229,21 +318,19 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&results[0].as_ref().unwrap_err().to_string()); return; } + if registered_mods + .iter() + .any(|mod_data| format_key.to_lowercase() == mod_data.name.to_lowercase()) { - if registered_mods - .iter() - .any(|mod_data| format_key.to_lowercase() == mod_data.name.to_lowercase()) - { - ui.display_msg(&format!( - "There is already a registered mod with the name\n\"{mod_name}\"" - )); - ui.global::() - .set_line_edit_text(SharedString::new()); - return; - } + ui.display_msg(&format!( + "There is already a registered mod with the name\n\"{mod_name}\"" + )); + ui.global::() + .set_line_edit_text(SharedString::new()); + return; } - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); slint::spawn_local(async move { + let game_dir = get_or_update_game_dir(None); let file_paths = match get_user_files(&game_dir) { Ok(files) => files, Err(err) => { @@ -289,26 +376,24 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg("A selected file is already registered to a mod"); return; } - let state = !files.iter().all(|file| { - matches!(FileData::is_enabled(file), Ok(false)) - }); + let state = !files.iter().all(FileData::is_disabled); results.push(save_bool( - current_ini, - Some("registered-mods"), + &ini.dir, + INI_SECTIONS[2], &format_key, state, )); match files.len() { 0 => return, 1 => results.push(save_path( - current_ini, - Some("mod-files"), + &ini.dir, + INI_SECTIONS[3], &format_key, files[0].as_path(), )), 2.. => { let path_refs = files.iter().map(|p| p.as_path()).collect::>(); - results.push(save_path_bufs(current_ini, &format_key, &path_refs)) + results.push(save_paths(&ini.dir, INI_SECTIONS[3], &format_key, &path_refs)) }, } if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { @@ -316,37 +401,38 @@ fn main() -> Result<(), slint::PlatformError> { // If something fails to save attempt to create a corrupt entry so // sync keys will take care of any invalid ini entries let _ = - remove_entry(current_ini, Some("registered-mods"), &format_key); + remove_entry(&ini.dir, INI_SECTIONS[2], &format_key); } let new_mod = RegMod::new(&format_key, state, files); new_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .unwrap_or_else(|err| { // Toggle files returned an error lets try it again - if new_mod.verify_state(&game_dir, current_ini).is_err() { + if new_mod.verify_state(&game_dir, &ini.dir).is_err() { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, - Some("registered-mods"), + &ini.dir, + INI_SECTIONS[2], &new_mod.name, ); }; }); - ui.global::() - .set_line_edit_text(SharedString::new()); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { + ui.global::().set_line_edit_text(SharedString::new()); + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors - match RegMod::collect(current_ini, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } - }), - )); + }) + }),ui.as_weak() + ); }).unwrap(); } }); @@ -354,10 +440,17 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; slint::spawn_local(async move { + let game_dir = get_or_update_game_dir(None); let path_result = get_user_folder(&game_dir); + drop(game_dir); let path = match path_result { Ok(path) => path, Err(err) => { @@ -369,46 +462,46 @@ fn main() -> Result<(), slint::PlatformError> { info!("User Selected Path: \"{}\"", path.display()); let try_path: PathBuf = match does_dir_contain(&path, Operation::All, &["Game"]) { - Ok(true) => PathBuf::from(&format!("{}\\Game", path.display())), - Ok(false) => path, + Ok(OperationResult::Bool(true)) => PathBuf::from(&format!("{}\\Game", path.display())), + Ok(OperationResult::Bool(false)) => path, Err(err) => { error!("{err}"); ui.display_msg(&err.to_string()); return; } + _ => unreachable!(), }; - match does_dir_contain(Path::new(&try_path), Operation::All, &REQUIRED_GAME_FILES) { - Ok(true) => { - let result = save_path(current_ini, Some("paths"), "game_dir", &try_path); - if result.is_err() && save_path(current_ini, Some("paths"), "game_dir", &try_path).is_err() { + match files_not_found(&try_path, &REQUIRED_GAME_FILES) { + Ok(not_found) => if not_found.is_empty() { + let result = save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], &try_path); + if result.is_err() && save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { let err = result.unwrap_err(); error!("Failed to save directory. {err}"); ui.display_msg(&err.to_string()); return; }; info!("Success: Files found, saved diretory"); - let mod_loader = match elden_mod_loader_properties(&try_path) { - Ok(loader) => loader, - Err(err) => { - error!("{err}"); - ui.display_msg(&err.to_string()); - return; - } - }; + let mod_loader = ModLoader::properties(&try_path).unwrap_or_default(); ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); ui.global::().set_game_path_valid(true); ui.global::().set_current_subpage(0); - ui.global::().set_loader_installed(mod_loader.installed); - ui.global::().set_loader_disabled(mod_loader.disabled); - if mod_loader.installed { - ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!") + ui.global::().set_loader_installed(mod_loader.installed()); + ui.global::().set_loader_disabled(mod_loader.disabled()); + if mod_loader.installed() { + ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); + let _ = receive_msg().await; + if ini.mods_empty() { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, Some(ini)).await { + ui.display_msg(&err.to_string()); + }; + } } else { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } - } - Ok(false) => { - let err = format!("Required Game files not found in:\n\"{}\"", try_path.display()); + let _ = get_or_update_game_dir(Some(try_path)); + } else { + let err = format!("{} files not found in:\n\"{}\"", not_found.join("\n"), try_path.display()); error!("{err}"); ui.display_msg(&err); } @@ -423,24 +516,30 @@ fn main() -> Result<(), slint::PlatformError> { }).unwrap(); } }); - ui.global::().on_toggleMod({ + ui.global::().on_toggle_mod({ let ui_handle = ui.as_weak(); - move |key, state| { + move |key, state| -> bool { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let ini_dir = get_ini_dir(); + let mut ini = match Cfg::read(ini_dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return !state; + } + }; + let game_dir = get_or_update_game_dir(None); let format_key = key.replace(' ', "_"); - match RegMod::collect(current_ini, false) { + match ini.collect_mods(None, false) { Ok(reg_mods) => { if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { - let result = toggle_files(&game_dir, state, found_mod, Some(current_ini)); + let result = toggle_files(&game_dir, state, found_mod, Some(&ini.dir)); if result.is_ok() { - return; + return state; } - let err = result.unwrap_err(); - ui.display_msg(&err.to_string()); + ui.display_msg(&result.unwrap_err().to_string()); } else { error!("Mod: \"{key}\" not found"); ui.display_msg(&format!("Mod: \"{key}\" not found")) @@ -448,19 +547,20 @@ fn main() -> Result<(), slint::PlatformError> { } Err(err) => ui.display_msg(&err.to_string()), } - ui.global::().set_if_err_bool(!state); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { - // if error lets try it again and see if we can get sync-keys to cleanup any errors - match RegMod::collect(current_ini, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } - }), - )); + }) + }),ui.as_weak() + ); + !state } }); ui.global::().on_force_app_focus({ @@ -474,20 +574,24 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); - let format_key = key.replace(' ', "_"); - let game_dir = PathBuf::from( - ui.global::().get_game_path().to_string(), - ); - let registered_mods = match RegMod::collect(current_ini, false) { + let ini_dir = get_ini_dir(); + let mut ini = match Cfg::read(ini_dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; + let registered_mods = match ini.collect_mods(None, false) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); return; } }; - // let reciever_clone = reciever_clone.clone(); slint::spawn_local(async move { + let game_dir = get_or_update_game_dir(None); + let format_key = key.replace(' ', "_"); let file_paths = match get_user_files(&game_dir) { Ok(paths) => paths, Err(err) => { @@ -536,25 +640,26 @@ fn main() -> Result<(), slint::PlatformError> { if file_registered(®istered_mods, &files) { ui.display_msg("A selected file is already registered to a mod"); } else { - let mut new_data = found_mod.files.clone(); + let num_files = files.len(); + let mut new_data = found_mod.files.dll.clone(); new_data.extend(files); - let mut results = Vec::with_capacity(2); - let new_data_refs = found_mod.add_other_files_to_files(&new_data); - if found_mod.files.len() + found_mod.other_files_len() == 1 { + let mut results = Vec::with_capacity(3); + let new_data_refs = found_mod.files.add_other_files_to_files(&new_data); + if found_mod.files.len() == 1 { results.push(remove_entry( - current_ini, - Some("mod-files"), + &ini.dir, + INI_SECTIONS[3], &found_mod.name, )); } else { - results.push(remove_array(current_ini, &found_mod.name)); + results.push(remove_array(&ini.dir, &found_mod.name)); } - results.push(save_path_bufs(current_ini, &found_mod.name, &new_data_refs)); + results.push(save_paths(&ini.dir, INI_SECTIONS[3], &found_mod.name, &new_data_refs)); if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, - Some("registered-mods"), + &ini.dir, + INI_SECTIONS[2], &format_key, ); } @@ -562,31 +667,37 @@ fn main() -> Result<(), slint::PlatformError> { let updated_mod = RegMod::new(&found_mod.name, found_mod.state, new_data_owned); updated_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .unwrap_or_else(|err| { if updated_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .is_err() { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, - Some("registered-mods"), + &ini.dir, + INI_SECTIONS[2], &updated_mod.name, ); }; + results.push(Err(err)); }); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { - match RegMod::collect(current_ini, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + if !results.iter().any(|r| r.is_err()) { + ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); + } + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } - }), - )); + }) + }),ui.as_weak() + ); } } else { error!("Mod: \"{key}\" not found"); @@ -600,32 +711,50 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); + let ini_dir = get_ini_dir(); + let mut ini = match Cfg::read(ini_dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; let format_key = key.replace(' ', "_"); ui.display_confirm(&format!("Are you sure you want to de-register: \"{key}\""), false); slint::spawn_local(async move { if receive_msg().await != Message::Confirm { return } - let reg_mods = match RegMod::collect(current_ini, false) { + let order_map: Option>; + let loader_dir = get_loader_ini_dir(); + let loader = match ModLoaderCfg::read_section(loader_dir, LOADER_SECTIONS[1]) { + Ok(mut data) => { + order_map = data.parse_section().ok(); + data + }, + Err(err) => { + ui.display_msg(&err.to_string()); + order_map = None; + ModLoaderCfg::default(loader_dir) + } + }; + let mut reg_mods = match ini.collect_mods(order_map.as_ref(), false) { Ok(reg_mods) => reg_mods, Err(err) => { ui.display_msg(&err.to_string()); return; } - }; - let game_dir = - PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = get_or_update_game_dir(None); if let Some(found_mod) = - reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) + reg_mods.iter_mut().find(|reg_mod| format_key == reg_mod.name) { - let mut found_files = found_mod.files.clone(); - if found_files.iter().any(|file| { - matches!(FileData::is_enabled(file), Ok(false)) - }) { - match toggle_files(&game_dir, true, found_mod, Some(current_ini)) { - Ok(files) => found_files = files, + if found_mod.files.dll.iter().any(FileData::is_disabled) { + match toggle_files(&game_dir, true, found_mod, Some(ini_dir)) { + Ok(files) => { + found_mod.files.dll = files; + found_mod.state = true; + }, Err(err) => { ui.display_msg(&format!("Failed to set mod to enabled state on removal\naborted before removal\n\n{err}")); return; @@ -633,11 +762,10 @@ fn main() -> Result<(), slint::PlatformError> { } } // we can let sync keys take care of removing files from ini - remove_entry(current_ini, Some("registered-mods"), &found_mod.name) + remove_entry(ini_dir, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); - let file_refs = found_mod.add_other_files_to_files(&found_files); let ui_handle = ui.as_weak(); - match confirm_remove_mod(ui_handle, &game_dir, file_refs).await { + match confirm_remove_mod(ui_handle, &game_dir, loader.path(), found_mod).await { Ok(_) => ui.display_msg(&format!("Successfully removed all files associated with the previously registered mod \"{key}\"")), Err(err) => { match err.kind() { @@ -653,17 +781,19 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("{err}\nRemoving invalid entries")) }; ui.global::().set_current_subpage(0); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { - match RegMod::collect(current_ini, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } - }), - )); + }) + }),ui.as_weak() + ); }).unwrap(); } }); @@ -672,72 +802,63 @@ fn main() -> Result<(), slint::PlatformError> { move |state| { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - save_bool(current_ini, Some("app-settings"), "dark_mode", state).unwrap_or_else( + save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], state).unwrap_or_else( |err| ui.display_msg(&format!("Failed to save theme preference\n\n{err}")), ); } }); + ui.global::().on_edit_config_item({ + let ui_handle = ui.as_weak(); + move |config_item| { + let ui = ui_handle.unwrap(); + let game_dir = get_or_update_game_dir(None); + let item = config_item.text.to_string(); + if !matches!(FileData::from(&item).extension, ".txt" | ".ini") { + return; + }; + let os_file = vec![std::ffi::OsString::from(format!("{}\\{item}", game_dir.display()))]; + open_text_files(ui.as_weak(), os_file); + } + }); ui.global::().on_edit_config({ let ui_handle = ui.as_weak(); move |config_file| { let ui = ui_handle.unwrap(); - let game_dir = ui.global::().get_game_path(); + let game_dir = get_or_update_game_dir(None); let downcast_config_file = config_file .as_any() .downcast_ref::>() .expect("We know we set a VecModel earlier"); - let string_file = downcast_config_file + let os_files = downcast_config_file .iter() - .map(|path| std::ffi::OsString::from(format!("{game_dir}\\{path}"))) + .map(|path| std::ffi::OsString::from(format!("{}\\{path}", game_dir.display()))) .collect::>(); - for file in string_file { - let file_clone = file.clone(); - let jh = std::thread::spawn(move || { - std::process::Command::new("notepad") - .arg(&file) - .spawn() - }); - match jh.join() { - Ok(result) => match result { - Ok(_) => (), - Err(err) => { - error!("{err}"); - ui.display_msg(&format!( - "Failed to open config file {file_clone:?}\n\nError: {err}" - )); - } - }, - Err(err) => { - error!("Thread panicked! {err:?}"); - ui.display_msg(&format!("{err:?}")); - } - } - } + open_text_files(ui.as_weak(), os_files); } }); ui.global::().on_toggle_terminal({ let ui_handle = ui.as_weak(); - move |state| { + move |state| -> bool { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; - let ext_ini = PathBuf::from(ui.global::().get_game_path().to_string()) - .join(LOADER_FILES[0]); - save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( + let ext_ini = get_loader_ini_dir(); + let mut result = state; + save_value_ext(ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { ui.display_msg(&err.to_string()); - ui.global::().set_show_terminal(!state); + result = !state; }, ); + result } }); ui.global::().on_set_load_delay({ let ui_handle = ui.as_weak(); move |time| { let ui = ui_handle.unwrap(); - let ext_ini = PathBuf::from(ui.global::().get_game_path().to_string()) - .join(LOADER_FILES[0]); + let ext_ini = get_loader_ini_dir(); ui.global::().invoke_force_app_focus(); - if let Err(err) = save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { + if let Err(err) = save_value_ext(ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { ui.display_msg(&format!("Failed to set load delay\n\n{err}")); return; } @@ -749,20 +870,20 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::().on_toggle_all({ let ui_handle = ui.as_weak(); - move |state| { + move |state| -> bool { let ui = ui_handle.unwrap(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = get_or_update_game_dir(None); let files = if state { vec![PathBuf::from(LOADER_FILES[1])] } else { - vec![PathBuf::from(LOADER_FILES_DISABLED[1])] + vec![PathBuf::from(LOADER_FILES[0])] }; let main_dll = RegMod::new("main", !state, files); match toggle_files(&game_dir, !state, &main_dll, None) { - Ok(_) => ui.global::().set_loader_disabled(state), + Ok(_) => state, Err(err) => { ui.display_msg(&format!("{err}")); - ui.global::().set_loader_disabled(!state) + !state } } } @@ -771,9 +892,9 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let game_dir = ui.global::().get_game_path().to_string(); let jh = std::thread::spawn(move || { - std::process::Command::new("explorer").arg(game_dir).spawn() + let game_dir = get_or_update_game_dir(None); + std::process::Command::new("explorer").arg(game_dir.as_path()).spawn() }); match jh.join() { Ok(result) => match result { @@ -802,33 +923,208 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); slint::spawn_local(async move { - let ui_handle = ui.as_weak(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); - match confirm_scan_mods(ui_handle, &game_dir, current_ini, true).await { - Ok(len) => { - ui.global::().set_current_subpage(0); - ui.global::().set_current_mods(deserialize( - &RegMod::collect(current_ini, false).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), - )); - ui.display_msg(&format!("Successfully Found {len} mod(s)")); - } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); - } + let game_dir = get_or_update_game_dir(None); + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, None).await { + ui.display_msg(&err.to_string()); }; }).unwrap(); } }); + ui.global::().on_add_remove_order({ + let ui_handle = ui.as_weak(); + move |state, key, value| -> i32 { + let ui = ui_handle.unwrap(); + let error = 42069_i32; + let cfg_dir = get_loader_ini_dir(); + let result: i32 = if state { 1 } else { -1 }; + let mut load_order = match ModLoaderCfg::read_section(cfg_dir, LOADER_SECTIONS[1]) { + Ok(data) => data, + Err(err) => { + ui.display_msg(&err.to_string()); + return error; + } + }; + let load_orders = load_order.mut_section(); + let stable_k = match state { + true => { + load_orders.insert(&key, &value.to_string()); + Some(key.as_str()) + } + false => { + if !load_orders.contains_key(&key) { + return error; + } + load_orders.remove(&key); + None + } + }; + if let Err(err) = load_order.update_order_entries(stable_k) { + ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + return error; + }; + let model = ui.global::().get_current_mods(); + let mut selected_mod = model.row_data(value as usize).unwrap(); + selected_mod.order.set = state; + if !state { selected_mod.order.at = 0 } + model.set_row_data(value as usize, selected_mod); + if let Err(err) = model.update_order(&mut load_order, value, ui.as_weak()) { + ui.display_msg(&err.to_string()); + return error; + }; + result + } + }); + ui.global::().on_modify_order({ + let ui_handle = ui.as_weak(); + move |to_k, from_k, value, row, dll_i| -> i32 { + let ui = ui_handle.unwrap(); + let mut result = 0_i32; + let cfg_dir = get_loader_ini_dir(); + let mut load_order = match ModLoaderCfg::read_section(cfg_dir, LOADER_SECTIONS[1]) { + Ok(data) => data, + Err(err) => { + ui.display_msg(&err.to_string()); + return -1; + } + }; + let load_orders = load_order.mut_section(); + if to_k != from_k && load_orders.contains_key(&from_k) { + load_orders.remove(&from_k); + load_orders.append(&to_k, value.to_string()) + } else if load_orders.contains_key(&to_k) { + load_orders.insert(&to_k, value.to_string()) + } else { + load_orders.append(&to_k, value.to_string()); + result = 1 + }; + + load_order.update_order_entries(Some(&to_k)).unwrap_or_else(|err| { + ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + if !result.is_negative() { result = -1 } + }); + if to_k != from_k { + let model = ui.global::().get_current_mods(); + let mut selected_mod = model.row_data(row as usize).unwrap(); + selected_mod.order.i = dll_i; + if !selected_mod.order.set { selected_mod.order.set = true } + model.set_row_data(row as usize, selected_mod); + if value != row { + if let Err(err) = model.update_order(&mut load_order, row, ui.as_weak()) { + ui.display_msg(&err.to_string()); + return -1; + }; + } + } else if value != row { + let model = ui.global::().get_current_mods(); + let mut curr_row = model.row_data(row as usize).unwrap(); + let mut replace_row = model.row_data(value as usize).unwrap(); + std::mem::swap(&mut curr_row.order.at, &mut replace_row.order.at); + model.set_row_data(row as usize, replace_row); + model.set_row_data(value as usize, curr_row); + ui.invoke_update_mod_index(value, 1); + ui.invoke_redraw_checkboxes(); + } + result + } + }); + ui.global::().on_force_deserialize({ + let ui_handle = ui.as_weak(); + move || { + let ui = ui_handle.unwrap(); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }) + }),ui.as_weak() + ); + info!("deserialized after encountered error"); + } + }); ui.invoke_focus_app(); ui.run() } +trait Sortable { + fn update_order(&self, cfg: &mut ModLoaderCfg, selected_row: i32, ui_handle: slint::Weak) -> std::io::Result<()>; +} + +impl Sortable for ModelRc { + fn update_order(&self, cfg: &mut ModLoaderCfg, selected_row: i32, ui_handle: slint::Weak) -> std::io::Result<()> { + let ui = ui_handle.unwrap(); + let order_map = cfg.parse_section()?; + + let mut unsorted_idx = (0..self.row_count()).collect::>(); + let selected_key = self.row_data(selected_row as usize).expect("front end gives us a valid row").name; + let mut i = 0_usize; + let mut selected_i = 0_i32; + let mut no_order_count = 0_usize; + let mut seen_names = HashSet::new(); + while !unsorted_idx.is_empty() { + if i >= unsorted_idx.len() { + i = 0 + } + let unsorted_i = unsorted_idx[i]; + let mut curr_row = self.row_data(unsorted_i).expect("unsorted_idx is valid ranges"); + let curr_key = curr_row.dll_files.row_data(curr_row.order.i as usize); + let new_order: Option<&usize>; + if curr_key.is_some() && {new_order = order_map.get(&curr_key.unwrap().to_string()); new_order}.is_some() { + let new_order = new_order.unwrap(); + curr_row.order.at = *new_order as i32 + 1; + if curr_row.name == selected_key { + selected_i = *new_order as i32; + } + if unsorted_i == *new_order { + self.set_row_data(*new_order, curr_row); + unsorted_idx.swap_remove(i); + continue; + } + if let Some(index) = unsorted_idx.iter().position(|&x| x == *new_order) { + let swap_row = self.row_data(*new_order).unwrap(); + if swap_row.name == selected_key { + selected_i = unsorted_i as i32; + } + self.set_row_data(*new_order, curr_row); + self.set_row_data(unsorted_i, swap_row); + unsorted_idx.swap_remove(index); + continue; + } + } else { + if curr_row.dll_files.row_count() != 1 { + curr_row.order.i = -1; + } + if curr_row.name == selected_key { + selected_i = unsorted_i as i32; + } + if !seen_names.contains(&curr_row.name) { + seen_names.insert(curr_row.name.clone()); + no_order_count += 1; + } + self.set_row_data(unsorted_i, curr_row); + if no_order_count >= unsorted_idx.len() { + // alphabetical sort would go here + break; + } + i += 1; + } + } + ui.invoke_update_mod_index(selected_i, 1); + ui.invoke_redraw_checkboxes(); + Ok(()) + } +} + impl App { pub fn display_msg(&self, msg: &str) { self.set_display_message(SharedString::from(msg)); @@ -896,69 +1192,138 @@ fn get_ini_dir() -> &'static PathBuf { static CONFIG_PATH: OnceLock = OnceLock::new(); CONFIG_PATH.get_or_init(|| { let exe_dir = std::env::current_dir().expect("Failed to get current dir"); - exe_dir.join(CONFIG_NAME) + exe_dir.join(INI_NAME) }) } +fn get_loader_ini_dir() -> &'static PathBuf { + static LOADER_CONFIG_PATH: OnceLock = OnceLock::new(); + LOADER_CONFIG_PATH.get_or_init(|| { + get_or_update_game_dir(None).join(LOADER_FILES[2]) + }) +} + +fn get_or_update_game_dir(update: Option) -> tokio::sync::RwLockReadGuard<'static, std::path::PathBuf> { + static GAME_DIR: OnceLock> = OnceLock::new(); + + if let Some(path) = update { + let gd = GAME_DIR.get_or_init(|| { + RwLock::new(PathBuf::new()) + }); + let mut gd_lock = gd.blocking_write(); + *gd_lock = path; + } + + GAME_DIR.get().unwrap().blocking_read() +} + fn populate_restricted_files() -> [&'static OsStr; 6] { - let mut restricted_files: [&OsStr; 6] = [&OsStr::new(""); 6]; + let mut restricted_files: [&OsStr; 6] = [OsStr::new(""); 6]; for (i, file) in LOADER_FILES.iter().map(OsStr::new).enumerate() { restricted_files[i] = file; } for (i, file) in REQUIRED_GAME_FILES.iter().map(OsStr::new).enumerate() { restricted_files[i + LOADER_FILES.len()] = file; } - restricted_files[LOADER_FILES.len() + REQUIRED_GAME_FILES.len()] = - OsStr::new(LOADER_FILES_DISABLED[1]); restricted_files } -fn deserialize(data: &[RegMod]) -> ModelRc { - let display_mod: Rc> = Default::default(); - for mod_data in data.iter() { - let has_config = !mod_data.config_files.is_empty(); - let config_files: Rc> = Default::default(); - if has_config { - mod_data.config_files.iter().for_each(|file| { - config_files.push(SharedString::from(file.to_string_lossy().to_string())) +fn open_text_files(ui_handle: slint::Weak, files: Vec) { + let ui = ui_handle.unwrap(); + for file in files { + let file_clone = file.clone(); + let jh = std::thread::spawn(move || { + std::process::Command::new("notepad") + .arg(&file) + .spawn() + }); + match jh.join() { + Ok(result) => match result { + Ok(_) => (), + Err(err) => { + error!("{err}"); + ui.display_msg(&format!( + "Failed to open config file {file_clone:?}\n\nError: {err}" + )); + } + }, + Err(err) => { + error!("Thread panicked! {err:?}"); + ui.display_msg(&format!("{err:?}")); + } + } + } +} + +fn order_data_or_default(ui_handle: slint::Weak, from_path: Option<&Path>) -> HashMap { + let ui = ui_handle.unwrap(); + let path: &Path; + if let Some(dir) = from_path { + path = dir + } else { path = get_loader_ini_dir() }; + match ModLoaderCfg::read_section(path, LOADER_SECTIONS[1]) { + Ok(mut data) => { + data.parse_section().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + HashMap::new() }) - } else { - config_files.push(SharedString::new()) + }, + Err(err) => { + ui.display_msg(&err.to_string()); + HashMap::new() + } + } +} + +fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { + let ui = ui_handle.unwrap(); + let display_mods: Rc> = Default::default(); + for mod_data in mods.iter() { + let files: Rc> = Default::default(); + let dll_files: Rc> = Default::default(); + let config_files: Rc> = Default::default(); + if !mod_data.files.dll.is_empty() { + files.extend(mod_data.files.dll.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); + dll_files.extend(mod_data.files.dll.iter().map(|f| SharedString::from(f.file_name().unwrap().to_string_lossy().replace(OFF_STATE, "")))); + }; + if !mod_data.files.config.is_empty() { + files.extend(mod_data.files.config.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); + config_files.extend(mod_data.files.config.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()))); + }; + if !mod_data.files.other.is_empty() { + files.extend(mod_data.files.other.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); }; let name = mod_data.name.replace('_', " "); - display_mod.push(DisplayMod { + display_mods.push(DisplayMod { displayname: SharedString::from(if mod_data.name.chars().count() > 20 { format!("{}...", &name[..17]) } else { name.clone() }), - name: SharedString::from(name.clone()), + name: SharedString::from(name), enabled: mod_data.state, - files: SharedString::from({ - let files: Vec = { - let mut files = Vec::with_capacity(mod_data.all_files_len()); - files.extend(mod_data.files.iter().map(|f| f.to_string_lossy().replace(".disabled", ""))); - files.extend(mod_data.config_files.iter().map(|f| f.to_string_lossy().to_string())); - files.extend(mod_data.other_files.iter().map(|f| f.to_string_lossy().to_string())); - files - }; - files.join("\n") - }), - has_config, + files: ModelRc::from(files), config_files: ModelRc::from(config_files), + dll_files: ModelRc::from(dll_files), + order: LoadOrder { + at: if !mod_data.order.set { 0 } else { mod_data.order.at as i32 + 1 }, + i: if !mod_data.order.set && mod_data.files.dll.len() != 1 { -1 } else { mod_data.order.i as i32 }, + set: mod_data.order.set + }, }) } - ModelRc::from(display_mod) + ui.global::().set_current_mods(ModelRc::from(display_mods)); + ui.global::().set_orders_set(mods.order_count() as i32); } async fn install_new_mod( name: &str, files: Vec, game_dir: &Path, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); let mod_name = name.trim(); ui.display_confirm( &format!( @@ -970,7 +1335,7 @@ async fn install_new_mod( return new_io_error!(ErrorKind::ConnectionAborted, "Mod install canceled"); } match InstallData::new(mod_name, files, game_dir) { - Ok(data) => add_dir_to_install_data(data, ui_weak).await, + Ok(data) => add_dir_to_install_data(data, ui_handle).await, Err(err) => Err(err) } } @@ -979,24 +1344,24 @@ async fn install_new_files_to_mod( mod_data: &RegMod, files: Vec, game_dir: &Path, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm("Selected files are not installed? Would you like to try and install them?", true); if receive_msg().await != Message::Confirm { return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to install files"); }; match InstallData::amend(mod_data, files, game_dir) { - Ok(data) => confirm_install(data, ui_weak).await, + Ok(data) => confirm_install(data, ui_handle).await, Err(err) => Err(err) } } async fn add_dir_to_install_data( mut install_files: InstallData, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm(&format!( "Current Files to install:\n{}\n\nWould you like to add a directory eg. Folder containing a config file?", install_files.display_paths), true); @@ -1023,24 +1388,23 @@ async fn add_dir_to_install_data( if result.len() == 1 && err.kind() == ErrorKind::InvalidInput { ui.display_msg(&format!("Error:\n\n{err}")); let _ = receive_msg().await; - let future = async { - add_dir_to_install_data(install_files, ui_weak).await - }; - let reselect_dir = Box::pin(future); + let reselect_dir = Box::pin(async { + add_dir_to_install_data(install_files, ui_handle).await + }); reselect_dir.await } else { new_io_error!(ErrorKind::Other, format!("Error: Could not Install\n\n{err}")) } } - true => confirm_install(install_files, ui_weak).await, + true => confirm_install(install_files, ui_handle).await, } } async fn confirm_install( install_files: InstallData, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm( &format!( "Confirm install of mod \"{}\"\n\nSelected files:\n{}\n\nInstall at:\n{}", @@ -1065,10 +1429,10 @@ async fn confirm_install( } async fn confirm_remove_mod( - ui_weak: slint::Weak, - game_dir: &Path, files: Vec<&Path>) -> std::io::Result<()> { - let ui = ui_weak.unwrap(); - let install_dir = match files.iter().min_by_key(|file| file.ancestors().count()) { + ui_handle: slint::Weak, + game_dir: &Path, loader_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { + let ui = ui_handle.unwrap(); + let install_dir = match reg_mod.files.file_refs().iter().min_by_key(|file| file.ancestors().count()) { Some(path) => game_dir.join(parent_or_err(path)?), None => PathBuf::from("Error: Failed to display a parent_dir"), }; @@ -1080,29 +1444,103 @@ async fn confirm_remove_mod( if receive_msg().await != Message::Confirm { return new_io_error!(ErrorKind::ConnectionAborted, format!("Mod files are still installed at \"{}\"", install_dir.display())); }; - remove_mod_files(game_dir, files) + remove_mod_files(game_dir, loader_dir, reg_mod) } async fn confirm_scan_mods( - ui_weak: slint::Weak, + ui_handle: slint::Weak, game_dir: &Path, - ini_file: &Path, - ini_exists: bool) -> std::io::Result { - let ui = ui_weak.unwrap(); + ini: Option) -> std::io::Result<()> { + let ui = ui_handle.unwrap(); + ui.display_confirm("Would you like to attempt to auto-import already installed mods to Elden Mod Loader GUI?", true); if receive_msg().await != Message::Confirm { - return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to scan for mods"); + return Ok(()); + }; + + let ini = match ini { + Some(data) => data, + None => Cfg::read(get_ini_dir())?, + }; + let order_map: Option>; + let loader_dir = get_loader_ini_dir(); + let loader = match ModLoaderCfg::read_section(loader_dir, LOADER_SECTIONS[1]) { + Ok(mut data) => { + order_map = data.parse_section().ok(); + data + }, + Err(err) => { + ui.display_msg(&err.to_string()); + order_map = None; + ModLoaderCfg::default(loader_dir) + } }; - if ini_exists { + + let mut old_mods: Vec; + if !ini.mods_empty() { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); if receive_msg().await != Message::Confirm { - return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to scan for mods"); + return Ok(()); }; + old_mods = ini.collect_mods(order_map.as_ref(), false)?; let dark_mode = ui.global::().get_dark_mode(); - std::fs::remove_file(ini_file)?; - new_cfg(ini_file)?; - save_bool(ini_file, Some("app-settings"), "dark_mode", dark_mode)?; - save_path(ini_file, Some("paths"), "game_dir", game_dir)?; + + std::fs::remove_file(&ini.dir)?; + new_cfg(&ini.dir)?; + save_bool(&ini.dir, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; + save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; + } else { + old_mods = Vec::new(); + } + let new_ini: Cfg; + match scan_for_mods(game_dir, &ini.dir) { + Ok(len) => { + new_ini = match Cfg::read(&ini.dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return Err(err); + } + }; + ui.global::().set_current_subpage(0); + let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); + let order_data = order_data_or_default(ui.as_weak(), Some(mod_loader.path())); + deserialize_current_mods( + &new_ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + new_ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }) + }),ui.as_weak() + ); + ui.display_msg(&format!("Successfully Found {len} mod(s)")); + } + Err(err) => { + ui.display_msg(&format!("Error: {err}")); + new_ini = Cfg::default(); + }, + }; + if new_ini.dir != Path::new("") && !old_mods.is_empty() { + let new_mods = new_ini.collect_mods(None, false)?; + let all_new_files = new_mods.iter().flat_map(|m| m.files.file_refs()).collect::>(); + old_mods.retain(|m| m.files.dll.iter().any(|f| !all_new_files.contains(f.as_path()))); + if old_mods.is_empty() { return Ok(()) } + + old_mods.iter().try_for_each(|m| { + if m.order.set && !all_new_files.contains(m.files.dll[m.order.i].as_path()) { + remove_order_entry(m, loader.path()) + } else { + Ok(()) + } + })?; + + old_mods.iter_mut().for_each(|m| m.files.dll.retain(|f| !all_new_files.contains(f.as_path()))); + old_mods.retain(|m| !m.files.dll.is_empty()); + if old_mods.is_empty() { return Ok(()) } + old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); + if old_mods.is_empty() { return Ok(()) } + + old_mods.iter().try_for_each(|m| toggle_files(game_dir, true, m, None).map(|_| ()))?; } - scan_for_mods(game_dir, ini_file) + Ok(()) } \ No newline at end of file diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs new file mode 100644 index 0000000..e1fddae --- /dev/null +++ b/src/utils/ini/mod_loader.rs @@ -0,0 +1,269 @@ +use ini::Ini; +use log::{trace, warn}; +use std::{ + collections::HashMap, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use crate::{ + does_dir_contain, get_or_setup_cfg, new_io_error, + utils::ini::{ + parser::{IniProperty, ModError, RegMod}, + writer::{new_cfg, EXT_OPTIONS}, + }, + Operation, OperationResult, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, +}; + +#[derive(Debug, Default)] +pub struct ModLoader { + installed: bool, + disabled: bool, + path: PathBuf, +} + +impl ModLoader { + pub fn properties(game_dir: &Path) -> std::io::Result { + let mut cfg_dir = game_dir.join(LOADER_FILES[2]); + let mut properties = ModLoader::default(); + match does_dir_contain(game_dir, Operation::Count, &LOADER_FILES) { + Ok(OperationResult::Count((_, files))) => { + if files.contains(LOADER_FILES[1]) && !files.contains(LOADER_FILES[0]) { + trace!("Mod loader found in the Enabled state"); + properties.installed = true; + } else if files.contains(LOADER_FILES[0]) && !files.contains(LOADER_FILES[1]) { + trace!("Mod loader found in the Disabled state"); + properties.installed = true; + properties.disabled = true; + } + if files.contains(LOADER_FILES[2]) { + std::mem::swap(&mut cfg_dir, &mut properties.path); + } + } + Err(err) => return Err(err), + _ => unreachable!(), + }; + if properties.installed && properties.path == Path::new("") { + trace!("{} not found, creating new", LOADER_FILES[2]); + new_cfg(&cfg_dir)?; + properties.path = cfg_dir; + } + if !properties.installed { + warn!("Mod loader dll hook not found"); + } + Ok(properties) + } + + #[inline] + pub fn installed(&self) -> bool { + self.installed + } + + #[inline] + pub fn disabled(&self) -> bool { + self.disabled + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path + } + + #[inline] + pub fn own_path(self) -> PathBuf { + self.path + } +} + +#[derive(Debug)] +pub struct ModLoaderCfg { + data: Ini, + dir: PathBuf, + section: Option, +} + +impl ModLoaderCfg { + pub fn read_section(cfg_dir: &Path, section: Option<&str>) -> std::io::Result { + if section.is_none() { + return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); + } + + let data = get_or_setup_cfg(cfg_dir, &LOADER_SECTIONS)?; + Ok(ModLoaderCfg { + data, + dir: PathBuf::from(cfg_dir), + section: section.map(String::from), + }) + } + + pub fn get_load_delay(&self) -> std::io::Result { + match IniProperty::::read(&self.data, LOADER_SECTIONS[0], LOADER_KEYS[0]) { + Ok(delay_time) => Ok(delay_time.value), + Err(err) => Err(err.add_msg(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[0] + ))), + } + } + + pub fn get_show_terminal(&self) -> std::io::Result { + match IniProperty::::read(&self.data, LOADER_SECTIONS[0], LOADER_KEYS[1]) { + Ok(delay_time) => Ok(delay_time.value), + Err(err) => Err(err.add_msg(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[1] + ))), + } + } + + #[inline] + pub fn mut_section(&mut self) -> &mut ini::Properties { + self.data.section_mut(self.section.as_ref()).unwrap() + } + + #[inline] + fn section(&self) -> &ini::Properties { + self.data.section(self.section.as_ref()).unwrap() + } + + #[inline] + /// updates the current section, general sections `None` are not supported + pub fn set_section(&mut self, new: Option<&str>) { + if new.is_some() { + self.section = new.map(String::from) + } + } + + #[inline] + pub fn iter(&self) -> ini::PropertyIter { + self.section().iter() + } + + /// Returns an owned `HashMap` with values parsed into K: `String`, V: `usize` + /// this function also fixes usize.parse() errors and if values are out of order + pub fn parse_section(&mut self) -> std::io::Result> { + let map = self.parse_into_map(); + if self.section().len() != map.len() { + trace!("fixing usize parse error in \"{}\"", LOADER_FILES[2]); + self.update_order_entries(None)?; + return Ok(self.parse_into_map()); + } + let mut values = self.iter().filter_map(|(k, _)| map.get(k)).collect::>(); + values.sort(); + for (i, value) in values.iter().enumerate() { + if i != **value { + trace!( + "values in \"{}\" are not in order, sorting entries", + LOADER_FILES[2] + ); + self.update_order_entries(None)?; + return Ok(self.parse_into_map()); + } + } + Ok(map) + } + + fn parse_into_map(&self) -> HashMap { + self.iter() + .filter_map(|(k, v)| Some((k.to_string(), v.parse::().ok()?))) + .collect::>() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.section().is_empty() + } + + #[inline] + pub fn len(&self) -> usize { + self.section().len() + } + + #[inline] + pub fn path(&self) -> &Path { + &self.dir + } + + pub fn write_to_file(&self) -> std::io::Result<()> { + self.data.write_to_file_opt(&self.dir, EXT_OPTIONS) + } + + /// updates the load order values in `Some("loadorder")` so they are always `0..` + /// if you want a key's value to remain the unedited you can supply `Some(stable_key)` + /// then writes the updated key values to file + /// + /// error cases: + /// section is not set to "loadorder" + /// fails to write to file + pub fn update_order_entries(&mut self, stable: Option<&str>) -> std::io::Result<()> { + if self.section.as_deref() != LOADER_SECTIONS[1] { + return new_io_error!( + ErrorKind::InvalidInput, + format!( + "This function is only supported to modify Section: \"{}\"", + LOADER_SECTIONS[1].unwrap() + ) + ); + } + let mut k_v = Vec::with_capacity(self.section().len()); + let (mut stable_k, mut stable_v) = ("", 0_usize); + for (k, v) in self.iter() { + if let Some(new_k) = stable { + if k == new_k { + (stable_k, stable_v) = (k, v.parse::().unwrap_or(usize::MAX)); + continue; + } + } + k_v.push((k, v.parse::().unwrap_or(usize::MAX))); + } + k_v.sort_by_key(|(_, v)| *v); + + let mut new_section = ini::Properties::new(); + + if k_v.is_empty() && !stable_k.is_empty() { + new_section.append(stable_k, "0"); + } else { + let mut offset = 0_usize; + for (k, _) in k_v { + if !stable_k.is_empty() && stable_v == offset { + new_section.append(std::mem::take(&mut stable_k), stable_v.to_string()); + offset += 1; + } + new_section.append(k, offset.to_string()); + offset += 1; + } + if !stable_k.is_empty() { + new_section.append(stable_k, offset.to_string()) + } + } + std::mem::swap(self.mut_section(), &mut new_section); + self.write_to_file() + } + + pub fn default(path: &Path) -> Self { + ModLoaderCfg { + data: ini::Ini::new(), + dir: PathBuf::from(path), + section: None, + } + } + + pub fn empty() -> Self { + ModLoaderCfg { + data: ini::Ini::new(), + dir: PathBuf::new(), + section: None, + } + } +} + +pub trait Countable { + fn order_count(&self) -> usize; +} + +impl Countable for &[RegMod] { + #[inline] + fn order_count(&self) -> usize { + self.iter().filter(|m| m.order.set).count() + } +} diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 21392f1..844df02 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -1,109 +1,102 @@ use ini::{Ini, Properties}; -use log::{error, trace, warn}; +use log::{error, warn}; use std::{ collections::HashMap, io::ErrorKind, path::{Path, PathBuf}, - str::ParseBoolError, }; use crate::{ - get_cfg, new_io_error, toggle_files, - utils::ini::writer::{remove_array, remove_entry, INI_SECTIONS}, - FileData, + files_not_found, new_io_error, toggle_files, + utils::ini::writer::{remove_array, remove_entry}, + Cfg, FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, OFF_STATE, REQUIRED_GAME_FILES, }; -pub trait ValueType: Sized { - type ParseError: std::fmt::Display; +pub trait Parsable: Sized { fn parse_str( ini: &Ini, section: Option<&str>, + partial_path: Option<&Path>, key: &str, skip_validation: bool, - ) -> Result; - #[allow(unused_variables)] - fn validate( - self, - ini: &Ini, - section: Option<&str>, - disable: bool, - ) -> Result { - Ok(self) - } + ) -> std::io::Result; } -impl ValueType for bool { - type ParseError = ParseBoolError; +impl Parsable for bool { fn parse_str( ini: &Ini, section: Option<&str>, + _partial_path: Option<&Path>, key: &str, _skip_validation: bool, - ) -> Result { - ini.get_from(section, key) + ) -> std::io::Result { + match ini + .get_from(section, key) .expect("Validated by IniProperty::is_valid") - .to_lowercase() - .parse::() + { + "0" => Ok(false), + "1" => Ok(true), + c => c.to_lowercase().parse::().map_err(|err| err.into_io_error()), + } } } -impl ValueType for u32 { - type ParseError = std::num::ParseIntError; +impl Parsable for u32 { fn parse_str( ini: &Ini, section: Option<&str>, + _partial_path: Option<&Path>, key: &str, _skip_validation: bool, - ) -> Result { + ) -> std::io::Result { ini.get_from(section, key) .expect("Validated by IniProperty::is_valid") .parse::() + .map_err(|err| err.into_io_error()) } } -impl ValueType for PathBuf { - type ParseError = std::io::Error; +impl Parsable for PathBuf { fn parse_str( ini: &Ini, section: Option<&str>, + partial_path: Option<&Path>, key: &str, skip_validation: bool, ) -> std::io::Result { - let parsed_value = PathBuf::from( - ini.get_from(section, key) - .expect("Validated by IniProperty::is_valid"), - ); + let parsed_value = PathBuf::from({ + let value = ini.get_from(section, key); + if matches!(value, Some(ARRAY_VALUE)) { + return new_io_error!( + ErrorKind::InvalidData, + "Invalid type found. Expected: Path, Found: Vec" + ); + } + value.expect("Validated by IniProperty::is_valid") + }); if skip_validation { - Ok(parsed_value) - } else { - parsed_value.validate(ini, section, skip_validation) + return Ok(parsed_value); } - } - fn validate(self, ini: &Ini, section: Option<&str>, disable: bool) -> std::io::Result { - if !disable { - if section == Some("mod-files") { - let game_dir = - match IniProperty::::read(ini, Some("paths"), "game_dir", false) { - Some(ini_property) => ini_property.value, - None => return new_io_error!(ErrorKind::NotFound, "game_dir is not valid"), - }; - validate_file(&game_dir.join(&self))?; - Ok(self) - } else { - validate_existance(&self)?; - Ok(self) + parsed_value.as_path().validate(partial_path)?; + if key == INI_KEYS[1] { + match files_not_found(&parsed_value, &REQUIRED_GAME_FILES) { + Ok(not_found) => { + if !not_found.is_empty() { + return new_io_error!(ErrorKind::NotFound, format!("Could not verify the install directory of Elden Ring, the following files were not found: \n{}", not_found.join("\n"))); + } + } + Err(err) => return Err(err), } - } else { - Ok(self) } + Ok(parsed_value) } } -impl ValueType for Vec { - type ParseError = std::io::Error; +impl Parsable for Vec { fn parse_str( ini: &Ini, section: Option<&str>, + partial_path: Option<&Path>, key: &str, skip_validation: bool, ) -> std::io::Result { @@ -112,39 +105,68 @@ impl ValueType for Vec { .iter() .skip_while(|(k, _)| *k != key) .skip_while(|(k, _)| *k == key) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| PathBuf::from(v)) .collect() } + if !matches!(ini.get_from(section, key), Some(ARRAY_VALUE)) { + return new_io_error!( + ErrorKind::InvalidData, + "Invalid type found. Expected: Vec, Found: Path" + ); + } let parsed_value = read_array( - ini.section(section) - .expect("Validated by IniProperty::is_valid"), + ini.section(section).expect("Validated by IniProperty::is_valid"), key, ); if skip_validation { - Ok(parsed_value) + return Ok(parsed_value); + } + parsed_value.validate(partial_path)?; + Ok(parsed_value) + } +} + +pub trait Valitidity { + /// _full_paths_ are assumed to Point to directories, where as _partial_paths_ are assumed to point to files + /// if you want to validate a _partial_path_ you must supply the _path_prefix_ + fn validate>(&self, partial_path: Option

) -> std::io::Result<()>; +} + +impl> Valitidity for T { + fn validate>(&self, partial_path: Option

) -> std::io::Result<()> { + if let Some(prefix) = partial_path { + validate_file(&prefix.as_ref().join(self))?; + Ok(()) } else { - parsed_value.validate(ini, section, skip_validation) + validate_existance(self.as_ref())?; + Ok(()) } } - fn validate(self, ini: &Ini, _section: Option<&str>, disable: bool) -> std::io::Result { - if !disable { - let game_dir = match IniProperty::::read(ini, Some("paths"), "game_dir", false) - { - Some(ini_property) => ini_property.value, - None => return new_io_error!(ErrorKind::NotFound, "game_dir is not valid"), - }; - if let Some(err) = self - .iter() - .find_map(|path| validate_file(&game_dir.join(path)).err()) - { - Err(err) - } else { - Ok(self) +} + +impl> Valitidity for [T] { + fn validate>(&self, partial_path: Option

) -> std::io::Result<()> { + let mut add_errors = String::new(); + let mut init_err = std::io::Error::new(ErrorKind::WriteZero, ""); + self.iter().for_each(|f| { + if let Err(err) = f.validate(partial_path.as_ref()) { + if init_err.kind() == ErrorKind::WriteZero { + init_err = err; + } else if add_errors.is_empty() { + add_errors = err.to_string() + } else { + add_errors.push_str(&format!("\n{err}")) + } } - } else { - Ok(self) + }); + if init_err.kind() != ErrorKind::WriteZero { + if add_errors.is_empty() { + return Err(init_err); + } + return Err(init_err.add_msg(add_errors)); } + Ok(()) } } @@ -152,13 +174,12 @@ fn validate_file(path: &Path) -> std::io::Result<()> { if path.extension().is_none() { let input_file = path.to_string_lossy().to_string(); let split = input_file.rfind('\\').unwrap_or(0); - input_file - .split_at(if split != 0 { split + 1 } else { split }) - .1 - .to_string(); return new_io_error!( ErrorKind::InvalidInput, - format!("\"{input_file}\" does not have an extention") + format!( + "\"{}\" does not have an extention", + input_file.split_at(if split != 0 { split + 1 } else { split }).1 + ) ); } validate_existance(path) @@ -183,215 +204,424 @@ fn validate_existance(path: &Path) -> std::io::Result<()> { } } -pub struct IniProperty { +pub trait Setup { + fn is_setup(&self, sections: &[Option<&str>]) -> bool; +} + +impl Setup for Ini { + fn is_setup(&self, sections: &[Option<&str>]) -> bool { + sections.iter().all(|§ion| self.section(section).is_some()) + } +} + +#[derive(Debug)] +pub struct IniProperty { //section: Option, //key: String, pub value: T, } -impl IniProperty { +impl IniProperty { + pub fn read(ini: &Ini, section: Option<&str>, key: &str) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, false, None)?, + }) + } +} +impl IniProperty { + pub fn read(ini: &Ini, section: Option<&str>, key: &str) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, false, None)?, + }) + } +} +impl IniProperty { pub fn read( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, - ) -> Option> { - match IniProperty::is_valid(ini, section, key, skip_validation) { - Ok(value) => { - trace!( - "Success: read key: \"{key}\" Section: \"{}\" from ini", - section.expect("Passed in section should be valid") - ); - Some(IniProperty { - //section: Some(section.unwrap().to_string()), - //key: key.to_string(), - value, - }) - } - Err(err) => { - error!( - "{}", - format!( - "Value stored in Section: \"{}\", Key: \"{key}\" is not valid", - section.expect("Passed in section should be valid") - ) - ); - error!("Error: {err}"); - None - } - } + ) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, skip_validation, None)?, + }) } +} + +impl IniProperty> { + pub fn read( + ini: &Ini, + section: Option<&str>, + key: &str, + path_prefix: &Path, + skip_validation: bool, + ) -> std::io::Result>> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, skip_validation, Some(path_prefix))?, + }) + } +} +impl IniProperty { fn is_valid( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, - ) -> Result { + path_prefix: Option<&Path>, + ) -> std::io::Result { match &ini.section(section) { Some(s) => match s.contains_key(key) { - true => { - T::parse_str(ini, section, key, skip_validation).map_err(|err| err.to_string()) - } - false => Err(format!("Key: \"{key}\" not found in {ini:?}")), + true => T::parse_str(ini, section, path_prefix, key, skip_validation), + false => new_io_error!( + ErrorKind::NotFound, + format!("Key: \"{key}\" not found in {ini:?}") + ), }, - None => Err(format!( - "Section: \"{}\" not found in {ini:?}", - section.expect("Passed in section should be valid") - )), + None => new_io_error!( + ErrorKind::NotFound, + format!( + "Section: \"{}\" not found in {ini:?}", + section.expect("Passed in section should be valid") + ) + ), } } } -pub trait Valitidity { - fn is_setup(&self) -> bool; +#[derive(Debug, Default)] +pub struct RegMod { + /// user defined Key in snake_case + pub name: String, + + /// true = enabled | false = disabled + pub state: bool, + + /// files associated with the Registered Mod + pub files: SplitFiles, + + /// contains properties related to if a mod has a set load order + pub order: LoadOrder, } -impl Valitidity for Ini { - fn is_setup(&self) -> bool { - INI_SECTIONS.iter().all(|section| { - let trimmed_section: String = section.trim_matches(|c| c == '[' || c == ']').to_owned(); - self.section(Some(trimmed_section)).is_some() - }) +#[derive(Debug, Default)] +pub struct SplitFiles { + /// files with extension `.dll` | also possible they end in `.dll.disabled` + /// saved as short paths with `game_dir` truncated + pub dll: Vec, + + /// files with extension `.ini` + /// saved as short paths with `game_dir` truncated + pub config: Vec, + + /// files with any extension other than `.dll` or `.ini` + /// saved as short paths with `game_dir` truncated + pub other: Vec, +} + +#[derive(Debug, Default)] +pub struct LoadOrder { + /// if one of `SplitFiles.dll` has a set load_order + pub set: bool, + + /// the index of the selected `mod_file` within `SplitFiles.dll` + /// derialization will set this to -1 if `set` is false and `SplitFiles.dll` is not len 1 + pub i: usize, + + /// current set value of `load_order` + /// `self.at` is stored as 0 index | front end uses 1 index + pub at: usize, +} + +impl LoadOrder { + fn from(dll_files: &[PathBuf], parsed_order_val: &HashMap) -> Self { + let mut order = LoadOrder::default(); + if dll_files.is_empty() { + return order; + } + if let Some(files) = dll_files + .iter() + .map(|f| { + let file_name = f.file_name(); + Some(file_name?.to_string_lossy().replace(OFF_STATE, "")) + }) + .collect::>>() + { + for (i, dll) in files.iter().enumerate() { + if let Some(v) = parsed_order_val.get(dll) { + order.set = true; + order.i = i; + order.at = *v; + break; + } + } + } else { + error!("Failed to retrieve file_name for Path in: {dll_files:?} Returning LoadOrder::default") + }; + order } } -#[derive(Default)] -pub struct RegMod { - pub name: String, - pub state: bool, - pub files: Vec, - pub config_files: Vec, - pub other_files: Vec, +impl SplitFiles { + fn from(in_files: Vec) -> Self { + let len = in_files.len(); + let mut dll = Vec::with_capacity(len); + let mut config = Vec::with_capacity(len); + let mut other = Vec::with_capacity(len); + in_files.into_iter().for_each(|file| { + match FileData::from(&file.to_string_lossy()).extension { + ".dll" => dll.push(file), + ".ini" => config.push(file), + _ => other.push(file), + } + }); + SplitFiles { dll, config, other } + } + + /// returns references to all files + pub fn file_refs(&self) -> Vec<&Path> { + let mut path_refs = Vec::with_capacity(self.len()); + path_refs.extend(self.dll.iter().map(|f| f.as_path())); + path_refs.extend(self.config.iter().map(|f| f.as_path())); + path_refs.extend(self.other.iter().map(|f| f.as_path())); + path_refs + } + + pub fn dll_refs(&self) -> Vec<&Path> { + self.dll.iter().map(|f| f.as_path()).collect() + } + + /// returns references to `input_files` + `self.config` + `self.other` + pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { + let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); + path_refs.extend(files.iter().map(|f| f.as_path())); + path_refs.extend(self.config.iter().map(|f| f.as_path())); + path_refs.extend(self.other.iter().map(|f| f.as_path())); + path_refs + } + + #[inline] + /// total number of files + pub fn len(&self) -> usize { + self.dll.len() + self.config.len() + self.other.len() + } + + #[inline] + /// returns true if all fields contain no PathBufs + pub fn is_empty(&self) -> bool { + self.dll.is_empty() && self.config.is_empty() && self.other.is_empty() + } + + #[inline] + /// number of `config` and `other` + pub fn other_files_len(&self) -> usize { + self.config.len() + self.other.len() + } } impl RegMod { + /// this function omits the population of the `order` field pub fn new(name: &str, state: bool, in_files: Vec) -> Self { - fn split_out_config_files( - in_files: Vec, - ) -> (Vec, Vec, Vec) { - let mut files = Vec::with_capacity(in_files.len()); - let mut config_files = Vec::with_capacity(in_files.len()); - let mut other_files = Vec::with_capacity(in_files.len()); - in_files.into_iter().for_each(|file| { - let file_name = file.file_name().expect("is file").to_string_lossy(); - let name_data = FileData::from(&file_name); - match name_data.extension { - ".txt" => other_files.push(file), - ".ini" => config_files.push(file), - _ => files.push(file), - } - }); - (files, config_files, other_files) + RegMod { + name: name.trim().replace(' ', "_"), + state, + files: SplitFiles::from(in_files), + order: LoadOrder::default(), + } + } + + /// unlike `new` this function returns a `RegMod` with all fields populated + /// `parsed_order_val` can be obtained from `ModLoaderCfg::parse_section()` + pub fn with_load_order( + name: &str, + state: bool, + in_files: Vec, + parsed_order_val: &HashMap, + ) -> Self { + let split_files = SplitFiles::from(in_files); + let load_order = LoadOrder::from(&split_files.dll, parsed_order_val); + RegMod { + name: name.trim().replace(' ', "_"), + state, + files: split_files, + order: load_order, } - let (files, config_files, other_files) = split_out_config_files(in_files); + } + + fn from_split_files(name: &str, state: bool, in_files: SplitFiles, order: LoadOrder) -> Self { RegMod { name: String::from(name), state, - files, - config_files, - other_files, + files: in_files, + order, + } + } + + pub fn verify_state(&self, game_dir: &Path, ini_path: &Path) -> std::io::Result<()> { + if (!self.state && self.files.dll.iter().any(FileData::is_enabled)) + || (self.state && self.files.dll.iter().any(FileData::is_disabled)) + { + warn!( + "wrong file state for \"{}\" chaning file extentions", + self.name + ); + let _ = toggle_files(game_dir, self.state, self, Some(ini_path))?; } + Ok(()) } - pub fn collect(path: &Path, skip_validation: bool) -> std::io::Result> { - type ModData<'a> = Vec<(&'a str, Result, Vec)>; - fn sync_keys<'a>(ini: &'a Ini, path: &Path) -> std::io::Result> { - fn collect_file_data(section: &Properties) -> HashMap<&str, Vec<&str>> { +} + +impl Cfg { + // MARK: FIXME + // when is the best time to verify parsed data? currently we verify data after shaping it + // the code would most likely be cleaner if we verified it apon parsing before doing any shaping + + // should we have two collections? one for deserialization(full) one for just collect and verify + + // collect needs to be completely recoverable, runing into an error and then returning a default is not good enough + pub fn collect_mods( + &self, + include_load_order: Option<&HashMap>, + skip_validation: bool, + ) -> std::io::Result> { + type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); + type ModData<'a> = Vec<( + &'a str, + Result, + SplitFiles, + LoadOrder, + )>; + + fn sync_keys<'a>(ini: &'a Ini, ini_path: &Path) -> std::io::Result> { + fn collect_paths(section: &Properties) -> HashMap<&str, Vec<&str>> { section .iter() .enumerate() - .filter(|(_, (k, _))| *k != "array[]") + .filter(|(_, (k, _))| *k != ARRAY_KEY) .map(|(i, (k, v))| { let paths = section .iter() .skip(i + 1) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| v) .collect(); - (k, if v == "array" { paths } else { vec![v] }) + (k, if v == ARRAY_VALUE { paths } else { vec![v] }) }) .collect() } - fn combine_map_data<'a>( - state_map: HashMap<&'a str, &str>, - file_map: HashMap<&str, Vec<&str>>, - ) -> ModData<'a> { - let mut mod_data = state_map - .iter() - .filter_map(|(&key, &state_str)| { - file_map.get(&key).map(|file_strs| { - ( - key, - state_str.to_lowercase().parse::(), - file_strs.iter().map(PathBuf::from).collect::>(), - ) - }) - }) - .collect::(); - mod_data.sort_by_key(|(key, _, _)| *key); - mod_data - } + let mod_state_data = ini - .section(Some("registered-mods")) + .section(INI_SECTIONS[2]) .expect("Validated by Ini::is_setup on startup"); - let mod_files_data = ini - .section(Some("mod-files")) + let dll_data = ini + .section(INI_SECTIONS[3]) .expect("Validated by Ini::is_setup on startup"); let mut state_data = mod_state_data.iter().collect::>(); - let mut file_data = collect_file_data(mod_files_data); + let mut file_data = collect_paths(dll_data); let invalid_state = state_data .keys() .filter(|k| !file_data.contains_key(*k)) .cloned() .collect::>(); + for key in invalid_state { state_data.remove(key); - remove_entry(path, Some("registered-mods"), key)?; + remove_entry(ini_path, INI_SECTIONS[2], key)?; warn!("\"{key}\" has no matching files"); } + let invalid_files = file_data .keys() .filter(|k| !state_data.contains_key(*k)) .cloned() .collect::>(); + for key in invalid_files { if file_data.get(key).expect("key exists").len() > 1 { - remove_array(path, key)?; + remove_array(ini_path, key)?; } else { - remove_entry(path, Some("mod-files"), key)?; + remove_entry(ini_path, INI_SECTIONS[3], key)?; } file_data.remove(key); warn!("\"{key}\" has no matching state"); } - Ok(combine_map_data(state_data, file_data)) + + assert_eq!(state_data.len(), file_data.len()); + Ok((state_data, file_data)) } - fn collect_data_unsafe(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { + + fn combine_map_data<'a>( + map_data: CollectedMaps<'a>, + parsed_order_val: Option<&HashMap>, + ) -> ModData<'a> { + let mut count = 0_usize; + let mut mod_data = map_data + .0 + .iter() + .filter_map(|(&key, &state_str)| { + map_data.1.get(&key).map(|file_strs| { + let split_files = SplitFiles::from( + file_strs.iter().map(PathBuf::from).collect::>(), + ); + let load_order = match parsed_order_val { + Some(data) => LoadOrder::from(&split_files.dll, data), + None => LoadOrder::default(), + }; + if load_order.set { + count += 1 + } + ( + key, + state_str.to_lowercase().parse::(), + split_files, + load_order, + ) + }) + }) + .collect::(); + + // if this fails `sync_keys()` did not do its job + assert_eq!(map_data.1.len(), mod_data.len()); + + mod_data.sort_by_key(|(_, _, _, l)| if l.set { l.at } else { usize::MAX }); + mod_data[count..].sort_by_key(|(key, _, _, _)| *key); + mod_data + } + + fn collect_data_unchecked(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { let mod_state_data = ini - .section(Some("registered-mods")) + .section(INI_SECTIONS[2]) .expect("Validated by Ini::is_setup on startup"); - let mod_files_data = ini - .section(Some("mod-files")) + let dll_data = ini + .section(INI_SECTIONS[3]) .expect("Validated by Ini::is_setup on startup"); - mod_files_data + dll_data .iter() .enumerate() - .filter(|(_, (k, _))| *k != "array[]") + .filter(|(_, (k, _))| *k != ARRAY_KEY) .map(|(i, (k, v))| { - let paths = mod_files_data + let paths = dll_data .iter() .skip(i + 1) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| v) .collect::>(); let s = mod_state_data.get(k).expect("key exists"); - (k, s, if v == "array" { paths } else { vec![v] }) + (k, s, if v == ARRAY_VALUE { paths } else { vec![v] }) }) .collect() } - let ini = get_cfg(path).expect("Validated by Ini::is_setup on startup"); if skip_validation { - let parsed_data = collect_data_unsafe(&ini); + let parsed_data = collect_data_unchecked(&self.data); Ok(parsed_data .iter() .map(|(n, s, f)| { @@ -403,93 +633,44 @@ impl RegMod { }) .collect()) } else { - let parsed_data = sync_keys(&ini, path)?; - Ok(parsed_data - .iter() - .filter_map(|(k, s, f)| match &s { - Ok(bool) => match f.len() { - 0 => unreachable!(), - 1 => { - match f[0] - .to_owned() - .validate(&ini, Some("mod-files"), skip_validation) - { - Ok(path) => Some(RegMod::new(k, *bool, vec![path])), - Err(err) => { - error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k) - .expect("Key is valid"); - None - } - } + let parsed_data = sync_keys(&self.data, &self.dir)?; + let game_dir = + IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false)? + .value; + // parse_section is non critical write error | read_section is also non critical write error + let parsed_data = combine_map_data(parsed_data, include_load_order); + let mut output = Vec::with_capacity(parsed_data.len()); + for (k, s, f, l) in parsed_data { + match &s { + Ok(bool) => { + if let Err(err) = f.file_refs().validate(Some(&game_dir)) { + error!("Error: {err}"); + remove_entry(&self.dir, INI_SECTIONS[2], k).expect("Key is valid"); + } else { + let reg_mod = RegMod::from_split_files(k, *bool, f, l); + // MARK: FIXME + // verify_state should be ran within collect, but this call is too late, we should handle verification earilier + // when sync keys hits an error we should give it a chance to correct by calling verify_state before it deletes an entry + reg_mod.verify_state(&game_dir, &self.dir)?; + output.push(reg_mod) } - 2.. => match f - .to_owned() - .validate(&ini, Some("mod-files"), skip_validation) - { - Ok(paths) => Some(RegMod::new(k, *bool, paths)), - Err(err) => { - error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k) - .expect("Key is valid"); - None - } - }, - }, + } Err(err) => { error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k).expect("Key is valid"); - None + remove_entry(&self.dir, INI_SECTIONS[2], k).expect("Key is valid"); } - }) - .collect()) - } - } - pub fn verify_state(&self, game_dir: &Path, ini_file: &Path) -> std::io::Result<()> { - if (!self.state - && self - .files - .iter() - .any(|path| matches!(FileData::is_enabled(path), Ok(true)))) - || (self.state - && self - .files - .iter() - .any(|path| matches!(FileData::is_enabled(path), Ok(false)))) - { - warn!( - "wrong file state for \"{}\" chaning file extentions", - self.name - ); - toggle_files(game_dir, self.state, self, Some(ini_file)).map(|_| ())? + } + } + Ok(output) } - Ok(()) - } - pub fn file_refs(&self) -> Vec<&Path> { - let mut path_refs = Vec::with_capacity(self.all_files_len()); - path_refs.extend(self.files.iter().map(|f| f.as_path())); - path_refs.extend(self.config_files.iter().map(|f| f.as_path())); - path_refs.extend(self.other_files.iter().map(|f| f.as_path())); - path_refs - } - pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { - let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); - path_refs.extend(files.iter().map(|f| f.as_path())); - path_refs.extend(self.config_files.iter().map(|f| f.as_path())); - path_refs.extend(self.other_files.iter().map(|f| f.as_path())); - path_refs - } - pub fn all_files_len(&self) -> usize { - self.files.len() + self.config_files.len() + self.other_files.len() - } - pub fn other_files_len(&self) -> usize { - self.config_files.len() + self.other_files.len() } } + pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { files.iter().any(|path| { mod_data.iter().any(|registered_mod| { registered_mod + .files .file_refs() .iter() .any(|mod_file| path == mod_file) @@ -497,78 +678,51 @@ pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { }) } -// ----------------------Optimized original implementation------------------------------- -// let mod_state_data = ini.section(Some("registered-mods")).unwrap(); -// mod_state_data -// .iter() -// .map(|(key, _)| RegMod { -// name: key.replace('_', " ").to_string(), -// state: IniProperty::::read(&ini, Some("registered-mods"), key) -// .unwrap() -// .value, -// files: if ini.get_from(Some("mod-files"), key).unwrap() == "array" { -// IniProperty::>::read(&ini, Some("mod-files"), key) -// .unwrap() -// .value -// } else { -// vec![ -// IniProperty::::read(&ini, Some("mod-files"), key) -// .unwrap() -// .value, -// ] -// }, -// }) -// .collect() -// ----------------------------------Multi-threaded attempt---------------------------------------- -// SLOW ASF -- Prolly because of how parser is setup -- try setting up parse_str to only take a str as input -// then pass each thread the strings it needs to parse -// let ini = Arc::new(get_cfg(path).unwrap()); -// let mod_state_data = ini.section(Some("registered-mods")).unwrap(); -// let (tx, rx) = mpsc::channel(); -// let mut found_data: Vec = Vec::with_capacity(mod_state_data.len()); -// for (key, _) in mod_state_data.iter() { -// let ini_clone = Arc::clone(&ini); -// let key_clone = String::from(key); -// let tx_clone = tx.clone(); -// thread::spawn(move || { -// tx_clone -// .send(RegMod { -// name: key_clone.replace('_', " "), -// state: IniProperty::::read( -// &ini_clone, -// Some("registered-mods"), -// &key_clone, -// ) -// .unwrap() -// .value, -// files: if ini_clone.get_from(Some("mod-files"), &key_clone).unwrap() -// == "array" -// { -// IniProperty::>::read( -// &ini_clone, -// Some("mod-files"), -// &key_clone, -// ) -// .unwrap() -// .value -// } else { -// vec![ -// IniProperty::::read( -// &ini_clone, -// Some("mod-files"), -// &key_clone, -// ) -// .unwrap() -// .value, -// ] -// }, -// }) -// .unwrap() -// }); -// } -// drop(tx); - -// for received in rx { -// found_data.push(received); -// } -// found_data +pub trait IntoIoError { + fn into_io_error(self) -> std::io::Error; +} + +impl IntoIoError for ini::Error { + fn into_io_error(self) -> std::io::Error { + match self { + ini::Error::Io(err) => err, + ini::Error::Parse(err) => std::io::Error::new(ErrorKind::InvalidData, err), + } + } +} + +impl IntoIoError for std::str::ParseBoolError { + #[inline] + fn into_io_error(self) -> std::io::Error { + std::io::Error::new(ErrorKind::InvalidData, self.to_string()) + } +} + +impl IntoIoError for std::num::ParseIntError { + #[inline] + fn into_io_error(self) -> std::io::Error { + std::io::Error::new(ErrorKind::InvalidData, self.to_string()) + } +} + +pub trait ModError { + fn add_msg(self, msg: String) -> std::io::Error; +} + +impl ModError for std::io::Error { + #[inline] + fn add_msg(self, msg: String) -> std::io::Error { + std::io::Error::new(self.kind(), format!("{msg}\n\n{self}")) + } +} + +pub trait ErrorClone { + fn clone_err(self) -> std::io::Error; +} + +impl ErrorClone for &std::io::Error { + #[inline] + fn clone_err(self) -> std::io::Error { + std::io::Error::new(self.kind(), self.to_string()) + } +} diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 230034d..d520acc 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -6,14 +6,11 @@ use std::{ path::Path, }; -use crate::{get_cfg, parent_or_err}; - -pub const INI_SECTIONS: [&str; 4] = [ - "[app-settings]", - "[paths]", - "[registered-mods]", - "[mod-files]", -]; +use crate::{ + file_name_or_err, get_cfg, parent_or_err, utils::ini::parser::RegMod, DEFAULT_INI_VALUES, + DEFAULT_LOADER_VALUES, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, + OFF_STATE, +}; const WRITE_OPTIONS: WriteOption = WriteOption { escape_policy: EscapePolicy::Nothing, @@ -21,74 +18,94 @@ const WRITE_OPTIONS: WriteOption = WriteOption { kv_separator: "=", }; -const EXT_OPTIONS: WriteOption = WriteOption { +pub const EXT_OPTIONS: WriteOption = WriteOption { escape_policy: EscapePolicy::Nothing, line_separator: LineSeparator::CRLF, kv_separator: " = ", }; -pub fn save_path_bufs(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; +pub fn save_paths( + file_path: &Path, + section: Option<&str>, + key: &str, + files: &[&Path], +) -> std::io::Result<()> { + let mut config: Ini = get_cfg(file_path)?; let save_paths = files .iter() .map(|path| path.to_string_lossy()) .collect::>() .join("\r\narray[]="); config - .with_section(Some("mod-files")) + .with_section(section) .set(key, format!("array\r\narray[]={save_paths}")); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_path( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, path: &Path, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config .with_section(section) .set(key, path.to_string_lossy().to_string()); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_bool( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, value: bool, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config.with_section(section).set(key, value.to_string()); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_value_ext( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, value: &str, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config.with_section(section).set(key, value); - config.write_to_file_opt(file_name, EXT_OPTIONS) + config.write_to_file_opt(file_path, EXT_OPTIONS) } -pub fn new_cfg(path: &Path) -> std::io::Result<()> { +pub fn new_cfg(path: &Path) -> std::io::Result { + let file_name = file_name_or_err(path)?; let parent = parent_or_err(path)?; + fs::create_dir_all(parent)?; let mut new_ini = File::create(path)?; - for section in INI_SECTIONS { - writeln!(new_ini, "{section}")?; + if file_name == LOADER_FILES[2] { + for (i, section) in LOADER_SECTIONS.iter().enumerate() { + writeln!(new_ini, "[{}]", section.unwrap())?; + if i == 0 { + for (j, _) in LOADER_KEYS.iter().enumerate() { + writeln!(new_ini, "{} = {}", LOADER_KEYS[j], DEFAULT_LOADER_VALUES[j])? + } + } + } + } else { + for (i, section) in INI_SECTIONS.iter().enumerate() { + writeln!(new_ini, "[{}]", section.unwrap())?; + if i == 0 { + writeln!(new_ini, "{}={}", INI_KEYS[i], DEFAULT_INI_VALUES[i])? + } + } } - - Ok(()) + get_cfg(path) } -pub fn remove_array(file_name: &Path, key: &str) -> std::io::Result<()> { - let content = read_to_string(file_name)?; +pub fn remove_array(file_path: &Path, key: &str) -> std::io::Result<()> { + let content = read_to_string(file_path)?; let mut skip_next_line = false; let mut key_found = false; @@ -105,16 +122,13 @@ pub fn remove_array(file_name: &Path, key: &str) -> std::io::Result<()> { !skip_next_line }; - let lines = content - .lines() - .filter(|&line| filter_lines(line)) - .collect::>(); + let lines = content.lines().filter(|&line| filter_lines(line)).collect::>(); - write(file_name, lines.join("\r\n")) + write(file_path, lines.join("\r\n")) } -pub fn remove_entry(file_name: &Path, section: Option<&str>, key: &str) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; +pub fn remove_entry(file_path: &Path, section: Option<&str>, key: &str) -> std::io::Result<()> { + let mut config: Ini = get_cfg(file_path)?; config.delete_from(section, key).ok_or(std::io::Error::new( ErrorKind::Other, format!( @@ -122,5 +136,18 @@ pub fn remove_entry(file_name: &Path, section: Option<&str>, key: &str) -> std:: §ion.expect("Passed in section should be valid") ), ))?; - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) +} + +pub fn remove_order_entry(entry: &RegMod, loader_dir: &Path) -> std::io::Result<()> { + let file_name = file_name_or_err(&entry.files.dll[entry.order.i])?; + let file_name = file_name + .to_str() + .ok_or(std::io::Error::new( + ErrorKind::InvalidData, + format!("{file_name:?} is not valid UTF-8"), + ))? + .replace(OFF_STATE, ""); + remove_entry(loader_dir, LOADER_SECTIONS[1], &file_name)?; + Ok(()) } diff --git a/src/utils/installer.rs b/src/utils/installer.rs index fab9b41..5e17f69 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -9,9 +9,9 @@ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ parser::RegMod, - writer::{save_bool, save_path, save_path_bufs}, + writer::{remove_order_entry, save_bool, save_path, save_paths}, }, - FileData, + FileData, INI_SECTIONS, }; /// Returns the deepest occurance of a directory that contains at least 1 file @@ -33,6 +33,9 @@ fn get_parent_dir(input: &Path) -> std::io::Result { } } +// MARK: TODO +// create a directory_tree_is_empty() function for more efficent boolean checks + fn check_dir_contains_files(path: &Path) -> std::io::Result { let num_of_dirs = items_in_directory(path, FileType::Dir)?; if files_in_directory_tree(path)? == 0 { @@ -132,7 +135,6 @@ fn parent_dir_from_vec(in_files: &[PathBuf]) -> std::io::Result { } } -#[derive(PartialEq)] pub enum DisplayItems { Limit(usize), All, @@ -217,7 +219,7 @@ impl InstallData { file_paths: Vec, game_dir: &Path, ) -> std::io::Result { - let amend_mod_split_file_names = amend_to.files.iter().try_fold( + let amend_mod_split_file_names = amend_to.files.dll.iter().try_fold( Vec::with_capacity(amend_to.files.len()), |mut acc, file| { let file_name = file_name_or_err(file)?.to_string_lossy(); @@ -378,7 +380,7 @@ impl InstallData { format_loop(self, &mut files_to_display, directory, &mut cut_off_data)?; - if *cutoff != DisplayItems::None { + if let DisplayItems::All | DisplayItems::Limit(_) = *cutoff { self.display_paths = files_to_display.join("\n"); } @@ -399,7 +401,10 @@ impl InstallData { let game_dir = self_clone.install_dir.parent().expect("has parent"); if valid_dir.strip_prefix(game_dir).is_ok() { return new_io_error!(ErrorKind::InvalidInput, "Files are already installed"); - } else if does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])? { + } else if matches!( + does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])?, + crate::OperationResult::Bool(true) + ) { return new_io_error!(ErrorKind::InvalidData, "Invalid file structure"); } @@ -426,7 +431,7 @@ impl InstallData { trace!("New directory selected contains unique files, entire folder will be moved"); match items_in_directory(&valid_dir, FileType::Dir)? == 0 { true => self_clone.parent_dir = parent_or_err(&valid_dir)?.to_path_buf(), - false => self_clone.parent_dir = valid_dir.clone(), + false => self_clone.parent_dir.clone_from(&valid_dir), } } @@ -439,9 +444,8 @@ impl InstallData { }); match jh.join() { Ok(result) => match result { - Ok(data) => { - let mut new_self = data; - std::mem::swap(&mut new_self, self); + Ok(mut data) => { + std::mem::swap(&mut data, self); Ok(()) } Err(err) => Err(err), @@ -454,13 +458,19 @@ impl InstallData { } } -pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<()> { - let remove_files = files.iter().map(|f| game_dir.join(f)).collect::>(); - - if remove_files +pub fn remove_mod_files( + game_dir: &Path, + loader_dir: &Path, + reg_mod: &RegMod, +) -> std::io::Result<()> { + let remove_files = reg_mod + .files + .file_refs() .iter() - .any(|file| !matches!(file.try_exists(), Ok(true))) - { + .map(|f| game_dir.join(f)) + .collect::>(); + + if remove_files.iter().any(|file| !matches!(file.try_exists(), Ok(true))) { return new_io_error!( ErrorKind::InvalidInput, "Could not confirm existance of all files to remove" @@ -500,6 +510,9 @@ pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<( } })?; + if reg_mod.order.set { + remove_order_entry(reg_mod, loader_dir)?; + } Ok(()) } @@ -530,10 +543,10 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result for file in files.iter() { let name = file_name_or_err(file)?.to_string_lossy(); let file_data = FileData::from(&name); - if let Some(dir) = dirs - .iter() - .find(|d| d.file_name().expect("is dir") == file_data.name) - { + if file_data.extension != ".dll" { + continue; + }; + if let Some(dir) = dirs.iter().find(|d| d.file_name().expect("is dir") == file_data.name) { let mut data = InstallData::new(file_data.name, vec![file.clone()], game_dir)?; data.import_files_from_dir(dir, &DisplayItems::None)?; file_sets.push(RegMod::new( @@ -541,36 +554,24 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result file_data.enabled, data.from_paths .into_iter() - .map(|p| { - p.strip_prefix(game_dir) - .expect("file found here") - .to_path_buf() - }) + .map(|p| p.strip_prefix(game_dir).expect("file found here").to_path_buf()) .collect::>(), )); } else { file_sets.push(RegMod::new( file_data.name, file_data.enabled, - vec![file - .strip_prefix(game_dir) - .expect("file found here") - .to_path_buf()], + vec![file.strip_prefix(game_dir).expect("file found here").to_path_buf()], )); } } for mod_data in &file_sets { - save_bool( - ini_file, - Some("registered-mods"), - &mod_data.name, - mod_data.state, - )?; - let file_refs = mod_data.file_refs(); + save_bool(ini_file, INI_SECTIONS[2], &mod_data.name, mod_data.state)?; + let file_refs = mod_data.files.file_refs(); if file_refs.len() == 1 { - save_path(ini_file, Some("mod-files"), &mod_data.name, file_refs[0])?; + save_path(ini_file, INI_SECTIONS[3], &mod_data.name, file_refs[0])?; } else { - save_path_bufs(ini_file, &mod_data.name, &file_refs)?; + save_paths(ini_file, INI_SECTIONS[3], &mod_data.name, &file_refs)?; } mod_data.verify_state(game_dir, ini_file)?; } diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..cffc56c --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,27 @@ +use std::{ + fs::{create_dir_all, metadata, File}, + io::Write, + path::Path, +}; + +pub const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; + +pub fn new_cfg_with_sections(path: &Path, sections: &[Option<&str>]) -> std::io::Result<()> { + let parent = path.parent().unwrap(); + + create_dir_all(parent)?; + let mut new_ini = File::create(path)?; + + for section in sections.iter() { + writeln!(new_ini, "[{}]", section.unwrap())?; + } + Ok(()) +} + +pub fn file_exists(file_path: &Path) -> bool { + if let Ok(metadata) = metadata(file_path) { + metadata.is_file() + } else { + false + } +} diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 0b8c7cc..c3b8471 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -1,28 +1,35 @@ +pub mod common; + #[cfg(test)] mod tests { use std::{ - fs::remove_file, + fs::{remove_file, File}, path::{Path, PathBuf}, }; use elden_mod_loader_gui::{ get_cfg, utils::ini::{ - parser::{IniProperty, RegMod, Valitidity}, + mod_loader::{Countable, ModLoaderCfg}, + parser::{IniProperty, RegMod, Setup}, writer::*, }, + Cfg, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, }; + use crate::common::{new_cfg_with_sections, GAME_DIR}; + #[test] fn does_u32_parse() { let test_nums: [u32; 3] = [2342652342, 2343523423, 69420]; - let test_file = Path::new("test_files\\test_nums.ini"); + let test_file = Path::new("temp\\test_nums.ini"); + let test_section = [Some("u32s")]; - new_cfg(test_file).unwrap(); + new_cfg_with_sections(test_file, &test_section).unwrap(); for (i, num) in test_nums.iter().enumerate() { save_value_ext( test_file, - Some("paths"), + test_section[0], &format!("test_num_{i}"), &num.to_string(), ) @@ -31,10 +38,42 @@ mod tests { let config = get_cfg(test_file).unwrap(); - for (i, _) in test_nums.iter().enumerate() { + for (i, num) in test_nums.iter().enumerate() { assert_eq!( - test_nums[i], - IniProperty::::read(&config, Some("paths"), &format!("test_num_{i}"), false) + *num, + IniProperty::::read(&config, test_section[0], &format!("test_num_{i}")) + .unwrap() + .value + ) + } + + remove_file(test_file).unwrap(); + } + + #[test] + fn does_bool_parse() { + let test_bools: [&str; 6] = [" True ", "false", "faLSe", "0 ", "0", "1"]; + let bool_results: [bool; 6] = [true, false, false, false, false, true]; + let test_file = Path::new("temp\\test_bools.ini"); + let test_section = [Some("bools")]; + + new_cfg_with_sections(test_file, &test_section).unwrap(); + for (i, bool_str) in test_bools.iter().enumerate() { + save_value_ext( + test_file, + test_section[0], + &format!("test_bool_{i}"), + bool_str, + ) + .unwrap(); + } + + let config = get_cfg(test_file).unwrap(); + + for (i, bool) in bool_results.iter().enumerate() { + assert_eq!( + *bool, + IniProperty::::read(&config, test_section[0], &format!("test_bool_{i}")) .unwrap() .value ) @@ -45,23 +84,24 @@ mod tests { #[test] fn does_path_parse() { - let test_path_1 = - Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); + let test_path_1 = Path::new(GAME_DIR); let test_path_2 = Path::new("C:\\Windows\\System32"); - let test_file = Path::new("test_files\\test_path.ini"); + let test_file = Path::new("temp\\test_path.ini"); + let test_section = [Some("path")]; { - new_cfg(test_file).unwrap(); - save_path(test_file, Some("paths"), "game_dir", test_path_1).unwrap(); - save_path(test_file, Some("paths"), "random_dir", test_path_2).unwrap(); + new_cfg_with_sections(test_file, &test_section).unwrap(); + save_path(test_file, test_section[0], INI_KEYS[1], test_path_1).unwrap(); + save_path(test_file, test_section[0], "random_dir", test_path_2).unwrap(); } let config = get_cfg(test_file).unwrap(); - let parse_test_1 = IniProperty::::read(&config, Some("paths"), "game_dir", false) - .unwrap() - .value; + let parse_test_1 = + IniProperty::::read(&config, test_section[0], INI_KEYS[1], false) + .unwrap() + .value; let parse_test_2 = - IniProperty::::read(&config, Some("paths"), "random_dir", false) + IniProperty::::read(&config, test_section[0], "random_dir", false) .unwrap() .value; @@ -71,23 +111,127 @@ mod tests { remove_file(test_file).unwrap(); } + #[test] + fn test_sort_by_order() { + let test_keys = ["a_mod", "b_mod", "c_mod", "d_mod", "f_mod", "e_mod"]; + let test_files = test_keys + .iter() + .map(|k| PathBuf::from(format!("{k}.dll"))) + .collect::>(); + let test_values = ["69420", "2", "1", "0"]; + let sorted_order = ["d_mod", "c_mod", "b_mod", "a_mod", "e_mod", "f_mod"]; + + let test_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[2])); + let required_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[1])); + + let test_sections = [LOADER_SECTIONS[0], LOADER_SECTIONS[1], Some("paths")]; + { + new_cfg_with_sections(&test_file, &test_sections).unwrap(); + for (i, key) in test_keys.iter().enumerate() { + save_path(&test_file, test_sections[2], key, &test_files[i]).unwrap(); + } + for (i, value) in test_values.iter().enumerate() { + save_value_ext( + &test_file, + test_sections[1], + test_files[i].to_str().unwrap(), + value, + ) + .unwrap(); + } + File::create(&required_file).unwrap(); + } + + let mut cfg = ModLoaderCfg::read_section(&test_file, test_sections[1]).unwrap(); + + let parsed_cfg = cfg.parse_section().unwrap(); + + cfg.update_order_entries(None).unwrap(); + let order = test_keys + .iter() + .enumerate() + .map(|(i, key)| { + RegMod::with_load_order(key, true, vec![test_files[i].clone()], &parsed_cfg) + }) + .collect::>(); + + // this tests to make sure the two without an order set are marked as order.set = false + assert_eq!(order.as_slice().order_count(), test_values.len()); + + // this tests that the order is set correclty for the mods that have a order entry + order + .iter() + .filter(|m| m.order.set) + .for_each(|m| assert_eq!(m.name, sorted_order[m.order.at])); + + remove_file(test_file).unwrap(); + remove_file(required_file).unwrap(); + } + + #[test] + #[allow(unused_variables)] + fn type_check() { + let test_path = Path::new(GAME_DIR); + let test_array = [Path::new("temp\\test"), Path::new("temp\\test")]; + let test_file = Path::new("temp\\test_type_check.ini"); + let test_sections = [Some("path"), Some("paths")]; + let array_key = "test_array"; + + new_cfg(test_file).unwrap(); + save_path(test_file, test_sections[0], INI_KEYS[1], test_path).unwrap(); + save_paths(test_file, test_sections[1], array_key, &test_array).unwrap(); + + let config = get_cfg(test_file).unwrap(); + + let pathbuf_err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid type found. Expected: Path, Found: Vec", + ); + let vec_pathbuf_err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid type found. Expected: Vec, Found: Path", + ); + + let vec_result = IniProperty::>::read( + &config, + test_sections[0], + INI_KEYS[1], + test_path, + false, + ); + assert_eq!( + vec_result.unwrap_err().to_string(), + vec_pathbuf_err.to_string() + ); + + let path_result = IniProperty::::read(&config, test_sections[1], array_key, false); + assert_eq!( + path_result.unwrap_err().to_string(), + pathbuf_err.to_string() + ); + + remove_file(test_file).unwrap(); + } + #[test] fn read_write_delete_from_ini() { - let test_file = Path::new("test_files\\test_collect_mod_data.ini"); - let mod_1_key = "Unlock The Fps "; - let mod_1_state = false; - let mod_2_key = "Skip The Intro"; - let mod_2_state = true; + let test_file = Path::new("temp\\test_collect_mod_data.ini"); + let game_path = Path::new(GAME_DIR); + let mod_1 = vec![ - Path::new("mods\\UnlockTheFps.dll"), - Path::new("mods\\UnlockTheFps\\config.ini"), + PathBuf::from("mods\\UnlockTheFps.dll"), + PathBuf::from("mods\\UnlockTheFps\\config.ini"), ]; let mod_2 = PathBuf::from("mods\\SkipTheIntro.dll"); + // test_mod_2 state is set incorrectly + let test_mod_1 = RegMod::new("Unlock The Fps ", true, mod_1); + let mut test_mod_2 = RegMod::new(" Skip The Intro", false, vec![mod_2]); + { // Test if new_cfg will write all Sections to the file with .is_setup() new_cfg(test_file).unwrap(); - assert!(get_cfg(test_file).unwrap().is_setup()); + assert!(get_cfg(test_file).unwrap().is_setup(&INI_SECTIONS)); let invalid_format_1 = vec![ Path::new("mods\\UnlockTheFps.dll"), @@ -98,50 +242,105 @@ mod tests { // We must save a working game_dir in the ini before we can parse entries in Section("mod-files") // -----------------------parser is set up to only parse valid entries--------------------------- // ----use case for entries in Section("mod-files") is to keep track of files within game_dir---- - let game_path = - Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); - - save_path_bufs(test_file, mod_1_key, &mod_1).unwrap(); - save_bool(test_file, Some("registered-mods"), mod_1_key, mod_1_state).unwrap(); - save_path(test_file, Some("mod-files"), mod_2_key, &mod_2).unwrap(); - save_bool(test_file, Some("registered-mods"), mod_2_key, mod_2_state).unwrap(); - save_path_bufs(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); + + save_paths( + test_file, + INI_SECTIONS[3], + &test_mod_1.name, + &test_mod_1.files.file_refs(), + ) + .unwrap(); + save_bool( + test_file, + INI_SECTIONS[2], + &test_mod_1.name, + test_mod_1.state, + ) + .unwrap(); save_path( test_file, - Some("mod-files"), + INI_SECTIONS[3], + &test_mod_2.name, + &test_mod_2.files.dll[0], + ) + .unwrap(); + save_bool( + test_file, + INI_SECTIONS[2], + &test_mod_2.name, + test_mod_2.state, + ) + .unwrap(); + save_paths( + test_file, + INI_SECTIONS[3], + "no_matching_state_1", + &invalid_format_1, + ) + .unwrap(); + save_path( + test_file, + INI_SECTIONS[3], "no_matching_state_2", &invalid_format_2, ) .unwrap(); - save_bool(test_file, Some("registered-mods"), "no_matching_path", true).unwrap(); + save_bool(test_file, INI_SECTIONS[2], "no_matching_path", true).unwrap(); - save_path(test_file, Some("paths"), "game_dir", game_path).unwrap(); + save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], game_path).unwrap(); } - // -------------------------------------sync_keys runs from inside RegMod::collect()------------------------------------------------ + // -------------------------------------sync_keys() runs from inside RegMod::collect()------------------------------------------------ // ----this deletes any keys that do not have a matching state eg. (key has state but no files, or key has files but no state)----- // this tests delete_entry && delete_array in this case we delete "no_matching_path", "no_matching_state_1", and "no_matching_state_2" - let registered_mods = RegMod::collect(test_file, false).unwrap(); + let cfg = Cfg::read(test_file).unwrap(); + let registered_mods = cfg.collect_mods(None, false).unwrap(); assert_eq!(registered_mods.len(), 2); + // verify_state() also runs from within RegMod::collect() lets see if changed the state of the mods .dll file + let mut disabled_state = game_path.join(format!( + "{}{}", + test_mod_2.files.dll[0].display(), + OFF_STATE + )); + assert!(matches!(&disabled_state.try_exists(), Ok(true))); + std::mem::swap(&mut test_mod_2.files.dll[0], &mut disabled_state); + + // lets set it correctly now + test_mod_2.state = true; + test_mod_2.verify_state(game_path, test_file).unwrap(); + std::mem::swap(&mut test_mod_2.files.dll[0], &mut disabled_state); + assert!(matches!( + game_path.join(&test_mod_2.files.dll[0]).try_exists(), + Ok(true) + )); + + test_mod_2.state = IniProperty::::read( + &get_cfg(test_file).unwrap(), + INI_SECTIONS[2], + &test_mod_2.name, + ) + .unwrap() + .value; + // Tests name format is correct - let reg_mod_1: &RegMod = registered_mods + let reg_mod_1 = registered_mods .iter() - .find(|data| data.name == mod_1_key.trim()) + .find(|data| data.name == test_mod_1.name.trim()) .unwrap(); - let reg_mod_2: &RegMod = registered_mods + let reg_mod_2 = registered_mods .iter() - .find(|data| data.name == mod_2_key.trim()) + .find(|data| data.name == test_mod_2.name.trim()) .unwrap(); // Tests if PathBuf and Vec's from Section("mod-files") parse correctly | these are partial paths - assert_eq!(mod_1[0], reg_mod_1.files[0]); - assert_eq!(mod_1[1], reg_mod_1.config_files[0]); - assert_eq!(mod_2, reg_mod_2.files[0]); + assert_eq!(test_mod_1.files.dll[0], reg_mod_1.files.dll[0]); + assert_eq!(test_mod_1.files.config[0], reg_mod_1.files.config[0]); + assert_eq!(test_mod_2.files.dll[0], reg_mod_2.files.dll[0]); // Tests if bool was parsed correctly - assert_eq!(mod_1_state, reg_mod_1.state); - assert_eq!(mod_2_state, reg_mod_2.state); + assert_eq!(test_mod_1.state, reg_mod_1.state); + assert!(test_mod_2.state); remove_file(test_file).unwrap(); } diff --git a/tests/test_lib.rs b/tests/test_lib.rs index aa341ab..ded2eb1 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -1,88 +1,140 @@ +pub mod common; + #[cfg(test)] mod tests { use elden_mod_loader_gui::{ - utils::ini::{parser::RegMod, writer::new_cfg}, - *, + does_dir_contain, get_cfg, toggle_files, + utils::ini::{ + parser::{IniProperty, RegMod}, + writer::{new_cfg, save_path, save_paths}, + }, + Operation, OperationResult, INI_SECTIONS, OFF_STATE, }; use std::{ - fs::{metadata, remove_file, File}, + fs::{self, remove_file, File}, path::{Path, PathBuf}, }; + use crate::common::{file_exists, GAME_DIR}; + #[test] fn do_files_toggle() { - fn file_exists(file_path: &Path) -> bool { - if let Ok(metadata) = metadata(file_path) { - metadata.is_file() - } else { - false - } - } - - let dir_to_test_files = - Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui"); - let save_file = Path::new("test_files\\file_toggle_test.ini"); - new_cfg(save_file).unwrap(); + let save_file = Path::new("temp\\file_toggle_test.ini"); let test_files = vec![ - PathBuf::from("test_files\\test1.txt"), - PathBuf::from("test_files\\test2.bhd"), - PathBuf::from("test_files\\test3.dll"), - PathBuf::from("test_files\\test4.exe"), - PathBuf::from("test_files\\test5.bin"), - PathBuf::from("test_files\\config.ini"), + Path::new("test1.txt"), + Path::new("test2.bhd"), + Path::new("test3.dll"), + Path::new("test4.exe"), + Path::new("test5.bin"), + Path::new("config.ini"), ]; + let test_key = "test_files"; + let prefix_key = "test_dir"; + let prefix = Path::new("temp\\"); + + new_cfg(save_file).unwrap(); + save_path(save_file, INI_SECTIONS[1], prefix_key, prefix).unwrap(); + save_paths(save_file, INI_SECTIONS[3], test_key, &test_files).unwrap(); - let test_mod = RegMod::new("Test", true, test_files.clone()); - let test_files_disabled = test_mod + let test_mod = RegMod::new( + test_key, + true, + test_files.iter().map(PathBuf::from).collect(), + ); + let mut test_files_disabled = test_mod .files + .dll .iter() - .map(|file| PathBuf::from(format!("{}.disabled", file.display()))) + .map(|file| PathBuf::from(format!("{}{OFF_STATE}", file.display()))) .collect::>(); - assert_eq!(test_mod.files.len(), 4); - assert_eq!(test_mod.config_files.len(), 1); - assert_eq!(test_mod.other_files.len(), 1); + assert_eq!(test_mod.files.dll.len(), 1); + assert_eq!(test_mod.files.config.len(), 1); + assert_eq!(test_mod.files.other.len(), 4); for test_file in test_files.iter() { - File::create(test_file.to_string_lossy().to_string()).unwrap(); + File::create(test_file).unwrap(); } - toggle_files( - dir_to_test_files, - !test_mod.state, - &test_mod, - Some(save_file), - ) - .unwrap(); + toggle_files(Path::new(""), !test_mod.state, &test_mod, Some(save_file)).unwrap(); for path_to_test in test_files_disabled.iter() { assert!(file_exists(path_to_test.as_path())); } - let test_mod = RegMod { - name: test_mod.name, - state: false, - files: test_files_disabled, - config_files: test_mod.config_files, - other_files: test_mod.other_files, - }; - - toggle_files( - dir_to_test_files, - !test_mod.state, - &test_mod, - Some(save_file), + test_files_disabled.extend(test_mod.files.config); + test_files_disabled.extend(test_mod.files.other); + + let read_disabled_ini = IniProperty::>::read( + &get_cfg(save_file).unwrap(), + INI_SECTIONS[3], + test_key, + prefix, + true, ) - .unwrap(); + .unwrap() + .value; + + assert!(read_disabled_ini + .iter() + .all(|read| test_files_disabled.contains(read))); + + let test_mod = RegMod::new(&test_mod.name, false, test_files_disabled); + + toggle_files(Path::new(""), !test_mod.state, &test_mod, Some(save_file)).unwrap(); for path_to_test in test_files.iter() { - assert!(file_exists(path_to_test.as_path())); + assert!(file_exists(path_to_test)); } + let read_enabled_ini = IniProperty::>::read( + &get_cfg(save_file).unwrap(), + INI_SECTIONS[3], + test_key, + prefix, + true, + ) + .unwrap() + .value; + + assert!(read_enabled_ini + .iter() + .all(|read| test_files.contains(&read.as_path()))); + for test_file in test_files.iter() { - remove_file(test_file.as_path()).unwrap(); + remove_file(test_file).unwrap(); } remove_file(save_file).unwrap(); } + + #[test] + #[allow(unused_variables)] + fn does_dir_contain_work() { + let mods_dir = PathBuf::from(&format!("{GAME_DIR}\\mods")); + let entries = fs::read_dir(&mods_dir) + .unwrap() + .map(|f| f.unwrap().file_name().into_string().unwrap()) + .collect::>(); + let num_entries = entries.len(); + + assert!(matches!( + does_dir_contain( + &mods_dir, + Operation::Count, + entries.iter().map(|e| e.as_ref()).collect::>().as_slice() + ), + Ok(OperationResult::Count((num_entries, _))) + )); + + assert!(matches!( + does_dir_contain(&mods_dir, Operation::Any, &[&entries[1]]), + Ok(OperationResult::Bool(true)) + )); + + assert!(matches!( + does_dir_contain(&mods_dir, Operation::Any, &["this_should_not_exist"]), + Ok(OperationResult::Bool(false)) + )); + } } diff --git a/ui/appwindow.slint b/ui/appwindow.slint index fe3ca4b..f8900be 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -1,5 +1,5 @@ import { MainPage } from "main.slint"; -import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message } from "common.slint"; +import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message, Formatting } from "common.slint"; import { StandardButton } from "std-widgets.slint"; export { MainLogic, SettingsLogic, DisplayMod } @@ -13,14 +13,16 @@ export component App inherits Window { // popup-window-width = text-width + dialog boarder property popup-window-width: msg-size.width + 13px; // window-height = main-page-height -? page-title-height - property window-height: mp.height - 48px; + property window-height: mp.height - Formatting.header-height; property popup-window-x-pos: { - if ((mp.width - popup-window-width) / 2) < 10px { - 10px + if ((mp.width - popup-window-width) / 2) < Formatting.side-padding - 1px { + Formatting.side-padding - 1px } else { (mp.width - popup-window-width) / 2 } }; + // Still possible to run into the - px range + // only noticed on startup popups property popup-window-y-pos: { if ((window-height - popup-window-height) / 2) < 0px { 60px @@ -33,16 +35,19 @@ export component App inherits Window { // property debug-msg: "height calc: " + popup-window-y-pos / 1px + "\nPage Height: " + debug-mp-height / 1px + "\nDialog Height: " + popup-window-height / 1px; title: @tr("Elden Mod Loader"); icon: @image-url("assets/EML-icon.png"); - // preferred-height: 381px; - min-height: 381px; - min-width: 315px; - max-width: 315px; + preferred-height: Formatting.app-preferred-height; + min-height: Formatting.app-preferred-height; + min-width: Formatting.app-width; + max-width: Formatting.app-width; - mp := MainPage {} - callback focus-app; callback show-error-popup; callback show-confirm-popup; + callback update-mod-index(int, int); + callback redraw-checkboxes; + + redraw-checkboxes => { mp.redraw-checkboxes() } + update-mod-index(i, t) => { mp.update-mod-index(i, t) } focus-app => { fs.focus() } show-error-popup => { popup-visible = true; @@ -50,12 +55,10 @@ export component App inherits Window { } show-confirm-popup => { popup-visible = true; - if (!alt-std-buttons) { - confirm-popup.show() - } else { - confirm-popup-2.show() - } + alt-std-buttons ? confirm-popup-2.show() : confirm-popup.show() } + + mp := MainPage {} msg-size := Text { visible: false; @@ -189,7 +192,17 @@ export component App inherits Window { } if (event.text == Key.Tab) { if (!popup-visible) { - mp.focus-line-edit() + if (MainLogic.current-subpage == 0) { + if (MainLogic.game-path-valid) { + mp.focus-line-edit() + } + } else if (MainLogic.current-subpage == 1) { + if (SettingsLogic.loader-installed) { + mp.focus-settings() + } + } else if (MainLogic.current-subpage == 2) { + mp.swap-tab() + } } } accept diff --git a/ui/common.slint b/ui/common.slint index 81338b2..1338cbe 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -1,29 +1,42 @@ +struct LoadOrder { + set: bool, + i: int, + at: int, +} + export struct DisplayMod { displayname: string, name: string, enabled: bool, - files: string, - has-config: bool, + files: [StandardListViewItem], config-files: [string], + dll-files: [string], + order: LoadOrder, } export enum Message { confirm, deny, esc } export global MainLogic { - callback toggleMod(string, bool); + callback toggle-mod(string, bool) -> bool; callback select-mod-files(string); callback add-to-mod(string); callback remove-mod(string); callback edit-config([string]); + callback edit-config-item(StandardListViewItem); + callback add-remove-order(bool, string, int) -> int; + callback modify-order(string, string, int, int, int) -> int; callback force-app-focus(); + callback force-deserialize(); callback send-message(Message); in property line-edit-text; - in-out property if-err-bool; in-out property game-path-valid; + in-out property orders-set; in-out property current-subpage: 0; - in-out property <[DisplayMod]> current-mods: [ - {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, files: "\\placeholder\\path\\data\\really\\long\\paths"}, - ]; + in-out property <[DisplayMod]> current-mods; + // Placeholder data for easy live editing + // : [ + // {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, order: {set: false}}, + // ]; } export global SettingsLogic { @@ -31,10 +44,11 @@ export global SettingsLogic { callback open-game-dir(); callback scan-for-mods(); callback toggle-theme(bool); - callback toggle-terminal(bool); + callback toggle-terminal(bool) -> bool; callback set-load-delay(string); - callback toggle-all(bool); - in property game-path: "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; + callback toggle-all(bool) -> bool; + in property game-path; + // : "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; in property loader-installed; in-out property dark-mode: true; in-out property loader-disabled; @@ -48,8 +62,11 @@ struct ButtonColors { hovered: color, } +// MARK: TODO +// make light mode look not like shit export global ColorPalette { - out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #60a0a4; + out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #adbabb; + out property alt-page-background-color: SettingsLogic.dark-mode ? #132b4e : #38474e; out property popup-background-color: SettingsLogic.dark-mode ? #00393d : #1b1b1b; out property popup-border-color: SettingsLogic.dark-mode ? #17575c : #1b1b1b; @@ -60,28 +77,47 @@ export global ColorPalette { hovered: root.text-base.brighter(20%), }; - out property button-image-base: SettingsLogic.dark-mode ? #505150 : #aeaeae; + out property button-image-base: SettingsLogic.dark-mode ? #505150 : #ffffff; out property button-image-colors: { pressed: root.button-image-base.darker(40%), hovered: root.button-image-base.brighter(20%), }; - out property button-background-base: SettingsLogic.dark-mode ? #4b4b4b83 : #6fc5ffaf; + out property button-background-base: SettingsLogic.dark-mode ? #4b4b4b83 : #3e728b9e; out property button-background-colors: { pressed: root.button-background-base.darker(40%), hovered: root.button-background-base.darker(20%), }; } +export global Formatting { + out property app-width: 315px; + out property app-preferred-height: 381px; + out property header-height: 48px; + out property tab-bar-height: 30px; + out property default-padding: 3px; + out property default-spacing: 3px; + out property side-padding: 8px; + out property button-spacing: 5px; + out property default-element-height: 35px; + out property rectangle-radius: 10px; + out property group-box-width: app-width - Formatting.side-padding; + out property group-box-r1-height: 85px; + out property font-size-h1: 18pt; + out property font-size-h2: 14pt; + out property font-size-h3: 10pt; +} + export component Page inherits Rectangle { in property title: "title"; in property description: "description"; in property has-back-button; - width: 315px; - background: ColorPalette.page-background-color; + in property alt-background; + width: Formatting.app-width; + background: alt-background ? ColorPalette.alt-page-background-color : ColorPalette.page-background-color; callback back; callback settings; - back => { MainLogic.current-subpage = 0 } + back => { MainLogic.current-subpage = 0; MainLogic.force-app-focus() } settings => { MainLogic.current-subpage = 1 } TouchArea {} // Protect underneath controls @@ -89,7 +125,7 @@ export component Page inherits Rectangle { HorizontalLayout { x: 0; y: 0; - height: 48px; + height: Formatting.header-height; padding-left: 5px; padding-right: 8px; padding-top: 8px; @@ -99,7 +135,7 @@ export component Page inherits Rectangle { source: @image-url("assets/back-arrow.png"); image-fit: contain; colorize: ColorPalette.text-foreground-color; - source-clip-y: -50; + source-clip-y: - 50; width: 30px; height: 24px; @@ -111,7 +147,7 @@ export component Page inherits Rectangle { } } title := Text { - font-size: 24px; + font-size: Formatting.font-size-h1; max-width: root.width - 10px; text: root.title; color: ColorPalette.text-foreground-color; @@ -134,7 +170,7 @@ export component Page inherits Rectangle { if (!root.has-back-button) : HorizontalLayout { Text { - font-size: 24px; + font-size: Formatting.font-size-h1; max-width: root.width * 0.8; text: root.title; color: ColorPalette.text-foreground-color; @@ -172,4 +208,82 @@ export component Page inherits Rectangle { } } @children +} + +export component Tab inherits Rectangle { + background: ColorPalette.page-background-color; + width: Formatting.app-width; + + TouchArea {} // Protect underneath controls + + @children +} + +component TabItem inherits Rectangle { + in property selected; + in-out property text <=> label.text; + + callback clicked <=> touch.clicked; + + height: Formatting.tab-bar-height; + + states [ + pressed when touch.pressed : { + state.opacity: 0.9; + } + hover when touch.has-hover : { + state.opacity: 1; + } + un-selected when !root.selected : { + state.opacity: 0.80; + } + selected when root.selected : { + state.opacity: 1; + } + ] + + state := Rectangle { + background: ColorPalette.page-background-color; + border-top-left-radius: 13px; + border-top-right-radius: 13px; + + animate opacity { duration: 150ms; } + } + + HorizontalLayout { + y: (parent.height - self.height) / 2; + padding: Formatting.default-padding; + padding-right: Formatting.side-padding; + + label := Text { + color: ColorPalette.text-foreground-color; + font-size: Formatting.font-size-h3; + vertical-alignment: center; + horizontal-alignment: right; + } + } + + touch := TouchArea { + width: 100%; + height: 100%; + } +} + +export component TabBar inherits Rectangle { + in property <[string]> model: []; + in-out property current-item: 0; + + width: Formatting.app-width; + height: Formatting.tab-bar-height; + + HorizontalLayout { + alignment: start; + horizontal-stretch: 0; + for item[index] in root.model : TabItem { + clicked => { root.current-item = index; } + text: item; + width: root.width / root.model.length; + selected: index == root.current-item; + } + } } \ No newline at end of file diff --git a/ui/editmod.slint b/ui/editmod.slint index 9c64923..607ba73 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -1,113 +1,44 @@ -import { GroupBox, Button, ScrollView } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page } from "common.slint"; +import { MainLogic, SettingsLogic, Page, Formatting, TabBar} from "common.slint"; +import { ModDetails, ModEdit } from "tabs.slint"; export component ModDetailsPage inherits Page { + alt-background: true; has-back-button: true; title: MainLogic.current-mods[mod-index].name; description: @tr("Edit registered mods here"); + // values for live preview editing - This will mess + // ------------up alignment if left on------------- + // height: 400px; in property mod-index; - property box-width: root.width - 10px; + in-out property current-tab; property state-color: SettingsLogic.loader-disabled ? #d01616 : - MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; + MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; property state: SettingsLogic.loader-disabled ? @tr("Mod Loader Disabled") : - MainLogic.current-mods[mod-index].enabled ? @tr("Yes") : @tr("No"); - property button-width: MainLogic.current-mods[mod-index].has-config ? 93px : 105px; - property button-layout: MainLogic.current-mods[mod-index].has-config ? center : end; + MainLogic.current-mods[mod-index].enabled ? @tr("Mod Enabled") : @tr("Mod Disabled"); + property header-offset: 12px; + property tab-height: self.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; + property tab-y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; - details := VerticalLayout { - y: 45px; - height: root.height - edit-mod-box.height - 55px; - padding-top: 4px; - padding-left: 12px; - padding-right: 12px; - alignment: start; - a := Text { - font-size: 10pt; - text: @tr("Enabled:"); + info-text := HorizontalLayout { + y: Formatting.header-height - header-offset; + height: 27px; + padding-right: Formatting.side-padding; + Text { + font-size: Formatting.font-size-h2; + color: state-color; + text: state; + horizontal-alignment: right; } - b := HorizontalLayout { - padding-left: 8px; - padding-top: 3px; - padding-bottom: 3px; - Text { - font-size: 16pt; - color: state-color; - text: state; - } - } - c := Text { - font-size: 10pt; - text: @tr("Name:"); - } - d := HorizontalLayout { - padding-left: 8px; - padding-top: 3px; - padding-bottom: 3px; - Text { - font-size: 13pt; - wrap: word-wrap; - text: MainLogic.current-mods[mod-index].name; - } - } - e := Text { - font-size: 10pt; - text: @tr("Files:"); - } - - } - ScrollView { - y: details.y + a.height + b.height + c.height + d.height + e.height + 9px; - height: root.height - self.y - edit-mod-box.height + 3px; - width: box-width; - viewport-height: files-txt.height; - viewport-width: files-txt.width; - files-txt := Text { - x: 15px; - width: parent.width - self.x; - font-size: 13pt; - wrap: word-wrap; - text: MainLogic.current-mods[mod-index].files; - } - } - VerticalLayout { - y: root.height - edit-mod-box.height; - height: root.height - self.y; - padding-left: 8px; - alignment: end; - - edit-mod-box := GroupBox { - title: @tr("Edit Mod"); - height: 95px; - HorizontalLayout { - width: box-width; - spacing: 7px; - padding-right: 8px; - padding-bottom: 8px; - alignment: button-layout; - Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("Add Files"); - clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } - } - if (MainLogic.current-mods[mod-index].has-config) : Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("Edit config"); - clicked => { MainLogic.edit-config(MainLogic.current-mods[mod-index].config-files) } - } - Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("De-register"); - clicked => { MainLogic.remove-mod(MainLogic.current-mods[mod-index].name) } - } - } - } + + tab-bar := TabBar { + y: Formatting.header-height + 15px; + model: [@tr("Tab" => "Details"), @tr("Tab" => "Edit")]; + current-item <=> current-tab; } + + if tab-bar.current-item == 0 : ModDetails { mod-index: mod-index; height: tab-height; y: tab-y; } + if tab-bar.current-item == 1 : ModEdit { mod-index: mod-index; height: tab-height; y: tab-y; } + } \ No newline at end of file diff --git a/ui/main.slint b/ui/main.slint index e1dd085..e10635d 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -1,8 +1,9 @@ import { CheckBox, GroupBox, ListView, LineEdit, Button } from "std-widgets.slint"; -import { SettingsPage, ModDetailsPage } from "subpages.slint"; -import { MainLogic, SettingsLogic, Page, ColorPalette } from "common.slint"; +import { SettingsPage, ModDetailsPage } from "sub-pages.slint"; +import { MainLogic, SettingsLogic, Page, ColorPalette, Formatting } from "common.slint"; export component MainPage inherits Page { + property update-toggle: true; has-back-button: false; title: @tr("Mods"); description: @tr("Edit state of registered mods here"); @@ -11,27 +12,40 @@ export component MainPage inherits Page { // height: 400px; callback focus-line-edit; - callback edit-mod(int); + callback focus-settings; + callback swap-tab; + callback edit-mod(int, int); + callback update-mod-index(int, int); + callback redraw-checkboxes; focus-line-edit => { input-mod.focus() } - edit-mod(i) => { + focus-settings => { app-settings.focus-settings-scope() } + swap-tab => { mod-settings.current-tab = mod-settings.current-tab == 0 ? 1 : 0 } + + update-mod-index(i, t) => { edit-mod(i, t) } + edit-mod(i, t) => { + mod-settings.current-tab = t; mod-settings.mod-index = i; MainLogic.current-subpage = 2 } + redraw-checkboxes => { + update-toggle = false; + update-toggle = true; + } VerticalLayout { y: 27px; height: parent.height - self.y; - preferred-width: 460px; - padding: 8px; + padding: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; - reg-mod-box := GroupBox { + if update-toggle : reg-mod-box := GroupBox { title: @tr("Registered-Mods:"); - enabled: MainLogic.game-path-valid && !SettingsLogic.loader-disabled; + enabled: SettingsLogic.loader-installed && !SettingsLogic.loader-disabled; - list-view := ListView { + ListView { for mod[idx] in MainLogic.current-mods: re := Rectangle { height: 31px; - border-radius: 10px; + border-radius: Formatting.rectangle-radius; // ----- ------mod-boxes need to have a max text length------------- // implmented a static way to elide text adding displayname property mod-box := CheckBox { @@ -40,18 +54,15 @@ export component MainPage inherits Page { checked: mod.enabled; enabled: reg-mod-box.enabled; toggled => { - MainLogic.if-err-bool = self.checked; - MainLogic.current-mods[idx].enabled = self.checked; - MainLogic.toggleMod(mod.name, self.checked); - if (MainLogic.if-err-bool != self.checked) { - self.checked = MainLogic.if-err-bool; - MainLogic.current-mods[idx].enabled = MainLogic.if-err-bool; + MainLogic.current-mods[idx].enabled = MainLogic.toggle-mod(mod.name, self.checked); + if MainLogic.current-mods[idx].enabled != self.checked { + self.checked = !self.checked; } } } im := Image { - x: 274px; - y: 6px; + x: 282px; + y: 5px; image-fit: contain; height: 20px; source: @image-url("assets/arrow.png"); @@ -61,7 +72,7 @@ export component MainPage inherits Page { x: mod-box.width - 284px; height: 28px; width: root.width - mod-box.width; - clicked => { edit-mod(idx) } + clicked => { edit-mod(idx, 0) } } } states [ @@ -78,37 +89,31 @@ export component MainPage inherits Page { } } add-mod-box := GroupBox { - height: 88px; + height: Formatting.group-box-r1-height; title: @tr("Add Mod"); - enabled: MainLogic.game-path-valid; + enabled: SettingsLogic.loader-installed; FocusScope { + enabled: add-mod-box.enabled; key-pressed(event) => { if (event.text == Key.Escape) { MainLogic.force-app-focus() } - // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { - if (input-mod.has-focus) { - add-mod.focus() - } else { - input-mod.focus() - } + input-mod.has-focus ? add-mod.focus() : input-mod.focus() } accept } HorizontalLayout { - spacing: 7px; + spacing: Formatting.button-spacing; input-mod := LineEdit { - height: 35px; - preferred-width: 100px; - horizontal-alignment: left; + height: Formatting.default-element-height; placeholder-text: @tr("Mod Name"); enabled: add-mod-box.enabled; text <=> MainLogic.line-edit-text; } add-mod := Button { - width: 89px; - height: 35px; + height: Formatting.default-element-height; + width: 95px; text: @tr("Select Files"); primary: !SettingsLogic.dark-mode; enabled: add-mod-box.enabled; @@ -125,7 +130,7 @@ export component MainPage inherits Page { } } } - SettingsPage { + app-settings := SettingsPage { x: MainLogic.current-subpage == 1 ? 0 : parent.width + parent.x + 2px; animate x { duration: 150ms; easing: ease; } } diff --git a/ui/settings.slint b/ui/settings.slint index 7f5f708..3490bc8 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -1,29 +1,29 @@ -import { GroupBox, Button, HorizontalBox, Switch, LineEdit } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page } from "common.slint"; +import { GroupBox, Button, Switch, LineEdit } from "std-widgets.slint"; +import { MainLogic, SettingsLogic, Page, Formatting } from "common.slint"; export component SettingsPage inherits Page { - property horizontal-box-width: self.width - 15px; has-back-button: true; title: @tr("Settings"); description: @tr("Set path to eldenring.exe and app settings here"); + callback focus-settings-scope; + focus-settings-scope => { load-delay.focus() } + VerticalLayout { y: 34px; height: parent.height - self.y; - padding-left: 8px; - padding-right: 0; - spacing: 0px; + padding-left: Formatting.side-padding; alignment: space-between; GroupBox { title: @tr("General"); height: 70px; + width: Formatting.group-box-width; HorizontalLayout { - padding-top: 7px; - padding-left: 6px; - padding-right: 6px; - width: horizontal-box-width; + padding-top: Formatting.side-padding / 2; + padding-left: Formatting.side-padding; + padding-right: Formatting.side-padding; Switch { text: @tr("Dark Mode"); checked <=> SettingsLogic.dark-mode; @@ -44,12 +44,12 @@ export component SettingsPage inherits Page { GroupBox { title: @tr("Game Path"); height: 110px; + width: Formatting.group-box-width; - HorizontalBox { + HorizontalLayout { row: 1; - width: horizontal-box-width; - padding-top: 3px; - padding-bottom: 0; + padding-top: 2px; + padding-left: Formatting.side-padding; Text { vertical-alignment: center; @@ -58,14 +58,14 @@ export component SettingsPage inherits Page { text: SettingsLogic.game-path; } } - HorizontalBox { + HorizontalLayout { row: 2; - padding-top: 9px; - padding-bottom: 0; - width: horizontal-box-width; + padding-top: Formatting.side-padding + 1px; + padding-right: Formatting.side-padding; + spacing: Formatting.button-spacing; alignment: end; Button { - width: 45px; + width: 42px; height: 30px; icon: @image-url("assets/folder.png"); colorize-icon: true; @@ -73,7 +73,7 @@ export component SettingsPage inherits Page { clicked => { SettingsLogic.open-game-dir() } } Button { - width: 105px; + width: 106px; height: 30px; primary: !SettingsLogic.dark-mode; text: @tr("Set Path"); @@ -84,15 +84,10 @@ export component SettingsPage inherits Page { FocusScope { key-pressed(event) => { if (event.text == Key.Escape) { - MainLogic.force-app-focus() + root.back() } - // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { - if (load-delay.has-focus) { - set-delay.focus() - } else { - load-delay.focus() - } + load-delay.has-focus ? set-delay.focus() : load-delay.focus() } accept } @@ -100,42 +95,48 @@ export component SettingsPage inherits Page { GroupBox { title: @tr("Mod Loader Options"); enabled: SettingsLogic.loader-installed; - height: 150px; + width: Formatting.group-box-width; + height: 140px; - HorizontalBox { + HorizontalLayout { row: 1; - padding-top: 4px; - padding-left: 2px; - padding-bottom: 5px; - width: horizontal-box-width; + padding-left: Formatting.side-padding - 2px; Switch { text: @tr("Show Terminal"); enabled: SettingsLogic.loader-installed; checked <=> SettingsLogic.show-terminal; - toggled => { SettingsLogic.toggle-terminal(self.checked) } + toggled => { + SettingsLogic.show-terminal = SettingsLogic.toggle-terminal(self.checked); + if SettingsLogic.show-terminal != self.checked { + self.checked = !self.checked; + } + } } } - HorizontalBox { + HorizontalLayout { row: 2; - padding-top: 4px; - padding-left: 2px; - padding-bottom: 5px; - - width: horizontal-box-width; + padding-top: Formatting.side-padding; + padding-left: Formatting.side-padding - 2px; Switch { text: @tr("Disable All mods"); enabled: SettingsLogic.loader-installed; checked <=> SettingsLogic.loader-disabled; - toggled => { SettingsLogic.toggle-all(self.checked) } + toggled => { + SettingsLogic.loader-disabled = SettingsLogic.toggle-all(self.checked); + if SettingsLogic.loader-disabled != self.checked { + self.checked = !self.checked; + } + } } } - HorizontalBox { + HorizontalLayout { row: 3; - padding-left: 2px; - padding-bottom: 8px; - width: horizontal-box-width; + padding-top: Formatting.side-padding + 2px; + padding-right: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; + spacing: Formatting.button-spacing; load-delay := LineEdit { - width: 128px; + width: 132px; height: 30px; horizontal-alignment: right; enabled: SettingsLogic.loader-installed; diff --git a/ui/subpages.slint b/ui/sub-pages.slint similarity index 100% rename from ui/subpages.slint rename to ui/sub-pages.slint diff --git a/ui/tabs.slint b/ui/tabs.slint new file mode 100644 index 0000000..7eea8f6 --- /dev/null +++ b/ui/tabs.slint @@ -0,0 +1,206 @@ +import { GroupBox, Button, StandardListView, Switch, ComboBox, SpinBox } from "std-widgets.slint"; +import { Tab, SettingsLogic, MainLogic, Formatting } from "common.slint"; + +export component ModDetails inherits Tab { + in property mod-index; + property details-height: a.height + b.height + c.height + (3*Formatting.default-spacing); + VerticalLayout { + y: 0px; + padding-top: Formatting.default-padding; + padding-bottom: Formatting.side-padding / 2; + padding: Formatting.side-padding; + spacing: Formatting.default-spacing; + alignment: start; + + a := Text { + font-size: Formatting.font-size-h3; + text: @tr("Name:"); + } + b := HorizontalLayout { + padding-left: Formatting.side-padding; + Text { + font-size: Formatting.font-size-h2; + wrap: word-wrap; + text: MainLogic.current-mods[mod-index].name; + } + } + c := Text { + font-size: Formatting.font-size-h3; + text: @tr("Files:"); + } + } + StandardListView { + y: details-height; + height: root.height - details-height - Formatting.side-padding; + width: Formatting.group-box-width - Formatting.side-padding; + model: MainLogic.current-mods[mod-index].files; + item-pointer-event(i, event) => { + if event.kind == PointerEventKind.up && event.button == PointerEventButton.left { + MainLogic.edit-config-item(MainLogic.current-mods[mod-index].files[i]) + } + } + } +} + +export component ModEdit inherits Tab { + in property mod-index; + property update-toggle: true; + property has-config: MainLogic.current-mods[mod-index].config-files.length > 0; + property selected-order: MainLogic.current-mods[mod-index].order.at; + property selected-index: MainLogic.current-mods[mod-index].order.i; + property selected-dll: MainLogic.current-mods[mod-index].dll-files[selected-index]; + property button-width: has-config ? 96px : 105px; + property button-layout: has-config ? space-between : end; + VerticalLayout { + y: 0px; + padding: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; + alignment: space-between; + + load-order-box := GroupBox { + property temp; + title: @tr("Load Order"); + enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; + + function init-selected-index() { + if !MainLogic.current-mods[mod-index].order.set { + MainLogic.current-mods[mod-index].order.at = 0; + if MainLogic.current-mods[mod-index].dll-files.length != 1 { + MainLogic.current-mods[mod-index].order.i = -1; + } + } + } + + HorizontalLayout { + row: 1; + padding-top: Formatting.default-padding; + + function redraw-elements() { + update-toggle = false; + update-toggle = true; + } + + load-order := Switch { + text: @tr("Set Load Order"); + enabled: load-order-box.enabled; + checked: MainLogic.current-mods[mod-index].order.set; + toggled => { + if self.checked { + MainLogic.current-mods[mod-index].order.at = MainLogic.orders-set + 1; + if selected-index != -1 && selected-order > 0 { + // Front end `order.at` is 1 based and back end is 0 based + temp = MainLogic.add-remove-order(self.checked, selected-dll, mod-index); + if temp != 42069 { + MainLogic.orders-set = MainLogic.orders-set + temp; + } else { + self.checked = !self.checked; + MainLogic.force-deserialize() + } + temp = 0 + } + redraw-elements() + } else { + if selected-index != -1 && selected-order > 0 { + temp = MainLogic.add-remove-order(self.checked, selected-dll, mod-index); + if temp != 42069 { + MainLogic.orders-set = MainLogic.orders-set + temp; + } else { + self.checked = !self.checked; + MainLogic.force-deserialize() + } + } else { + MainLogic.current-mods[mod-index].order.at = 0 + } + temp = 0; + redraw-elements() + } + } + } + } + HorizontalLayout { + row: 2; + padding-top: Formatting.side-padding; + spacing: Formatting.default-spacing; + + function modify-file(file: string, i: int) { + if file != selected-dll && selected-order > 0 { + temp = MainLogic.modify-order(file, selected-dll, selected-order - 1, mod-index, i); + if temp != -1 { + MainLogic.orders-set = MainLogic.orders-set + temp; + } else { + init-selected-index(); + MainLogic.force-deserialize() + } + temp = 0 + } + MainLogic.force-app-focus() + } + function modify-index(v: int) { + if selected-index != -1 && v > 0 { + temp = MainLogic.modify-order(selected-dll, selected-dll, v - 1, mod-index, selected-index); + if temp != -1 { + MainLogic.orders-set = MainLogic.orders-set + temp + } else { + MainLogic.force-deserialize() + } + } else { + MainLogic.current-mods[mod-index].order.at = v + } + if temp != -1 { + MainLogic.current-mods[mod-index].order.at = v + } + temp = 0 + } + + // Might be able to remove this hack after properly having sorting data parsed + if update-toggle : ComboBox { + enabled: load-order.checked && load-order-box.enabled; + current-index: selected-index; + model: MainLogic.current-mods[mod-index].dll-files; + selected(file) => { modify-file(file, self.current-index) } + } + + // MARK: TODO + // Create a focus scope to handle up and down arrow inputs + if update-toggle : SpinBox { + width: 106px; + enabled: load-order.checked && load-order-box.enabled; + minimum: 1; + maximum: MainLogic.orders-set; + value: selected-order; + edited(int) => { modify-index(int) } + } + } + } + + edit-mod-box := GroupBox { + title: @tr("Mod Actions"); + height: Formatting.group-box-r1-height; + HorizontalLayout { + spacing: Formatting.button-spacing; + alignment: button-layout; + Button { + width: button-width; + height: Formatting.default-element-height; + primary: !SettingsLogic.dark-mode; + text: @tr("Add Files"); + clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } + } + if has-config : Button { + width: button-width; + height: Formatting.default-element-height; + primary: !SettingsLogic.dark-mode; + text: @tr("Edit config"); + clicked => { MainLogic.edit-config(MainLogic.current-mods[mod-index].config-files) } + } + Button { + width: button-width; + height: Formatting.default-element-height; + primary: !SettingsLogic.dark-mode; + text: @tr("De-register"); + clicked => { MainLogic.remove-mod(MainLogic.current-mods[mod-index].name) } + } + } + } + } +} \ No newline at end of file