From 204ce1e16353dfb21edb07abab4b6278543c752c Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 17 Jan 2023 10:33:19 +0100 Subject: [PATCH 01/90] feat(dtmm): Create initial mod manager window --- Cargo.lock | 1079 ++++++++++++++++++++++ crates/dtmm/Cargo.toml | 19 + crates/dtmm/notes.adoc | 49 + crates/dtmm/src/main.rs | 42 + crates/dtmm/src/main_window.rs | 237 +++++ crates/dtmm/src/state.rs | 119 +++ crates/dtmm/src/theme.rs | 4 + crates/dtmm/src/widget/container.rs | 7 + crates/dtmm/src/widget/fill_container.rs | 63 ++ crates/dtmm/src/widget/mod.rs | 14 + 10 files changed, 1633 insertions(+) create mode 100644 crates/dtmm/Cargo.toml create mode 100644 crates/dtmm/notes.adoc create mode 100644 crates/dtmm/src/main.rs create mode 100644 crates/dtmm/src/main_window.rs create mode 100644 crates/dtmm/src/state.rs create mode 100644 crates/dtmm/src/theme.rs create mode 100644 crates/dtmm/src/widget/container.rs create mode 100644 crates/dtmm/src/widget/fill_container.rs create mode 100644 crates/dtmm/src/widget/mod.rs diff --git a/Cargo.lock b/Cargo.lock index acbedb7..23846e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,48 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "associative-cache" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46016233fc1bb55c23b856fe556b7db6ccd05119a0a392e04f0b3b7c79058f16" + +[[package]] +name = "atk" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" +dependencies = [ + "atk-sys", + "bitflags", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -71,6 +113,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.3" @@ -92,6 +149,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + [[package]] name = "bytecount" version = "0.6.3" @@ -131,6 +194,31 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cairo-rs" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cc" version = "1.0.78" @@ -140,6 +228,15 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cfg-expr" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -205,6 +302,37 @@ dependencies = [ "winapi", ] +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +dependencies = [ + "bitflags", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + [[package]] name = "color-eyre" version = "0.6.2" @@ -244,12 +372,75 @@ dependencies = [ "toml", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +dependencies = [ + "bitflags", + "core-foundation", + "foreign-types", + "libc", +] + +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.5" @@ -365,6 +556,99 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "druid" +version = "0.7.0" +source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +dependencies = [ + "console_error_panic_hook", + "druid-derive", + "druid-shell", + "fluent-bundle", + "fluent-langneg", + "fluent-syntax", + "fnv", + "im", + "instant", + "tracing", + "tracing-subscriber", + "tracing-wasm", + "unic-langid", + "unicode-segmentation", + "xi-unicode", +] + +[[package]] +name = "druid-derive" +version = "0.4.0" +source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "druid-shell" +version = "0.7.0" +source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +dependencies = [ + "anyhow", + "bitflags", + "block", + "cairo-rs", + "cfg-if", + "cocoa", + "core-graphics", + "foreign-types", + "gdk-sys", + "glib-sys", + "gtk", + "gtk-sys", + "instant", + "js-sys", + "keyboard-types", + "kurbo", + "objc", + "once_cell", + "piet-common", + "scopeguard", + "time", + "tracing", + "wasm-bindgen", + "web-sys", + "winapi", + "wio", +] + +[[package]] +name = "dtmm" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "confy", + "druid", + "sdk", + "serde", + "tokio", + "toml", + "tracing", + "tracing-error", + "tracing-subscriber", +] + [[package]] name = "dtmt" version = "0.2.0" @@ -394,6 +678,18 @@ dependencies = [ "zip", ] +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -461,6 +757,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "field-offset" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "flate2" version = "1.0.25" @@ -471,6 +777,61 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-bundle" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" +dependencies = [ + "thiserror", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.25" @@ -560,6 +921,65 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -587,12 +1007,158 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" +[[package]] +name = "gio" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" +dependencies = [ + "atk", + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "heck" version = "0.4.0" @@ -617,6 +1183,20 @@ dependencies = [ "digest", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indenter" version = "0.3.3" @@ -630,6 +1210,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", ] [[package]] @@ -669,6 +1271,33 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags", +] + +[[package]] +name = "kurbo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e119590a03caff1f7a582e8ee8c2164ddcc975791701188132fd1d1b518d3871" +dependencies = [ + "arrayvec", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -706,6 +1335,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -715,6 +1353,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.5.0" @@ -826,6 +1470,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "object" version = "0.30.1" @@ -874,6 +1527,59 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pango" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" +dependencies = [ + "bitflags", + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pangocairo" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ad2ec87789371b551fd2367c10aa37060412ffd3e60abd99491b21b93a3f9b" +dependencies = [ + "bitflags", + "cairo-rs", + "glib", + "libc", + "pango", + "pangocairo-sys", +] + +[[package]] +name = "pangocairo-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848d2df9b7f1a8c7a19d994de443bcbe5d4382610ccb8e64247f932be74fcf76" +dependencies = [ + "cairo-sys-rs", + "glib-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -897,6 +1603,102 @@ dependencies = [ "sha2", ] +[[package]] +name = "pest" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "piet" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd2a0027c9ea84b51f42ea3148864e6fc58970ad927e78390d5f9cf7cfa2c78" +dependencies = [ + "kurbo", + "unic-bidi", +] + +[[package]] +name = "piet-cairo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b98e21ec69eefb09d34e34050beb700749547dfc5da7a6cff27089dd59af1b3" +dependencies = [ + "cairo-rs", + "pango", + "pangocairo", + "piet", + "unicode-segmentation", + "xi-unicode", +] + +[[package]] +name = "piet-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7285cc595907be29db4d7f3fb331ae273250365cebf536a4a7b1dafc62978e" +dependencies = [ + "cairo-rs", + "cairo-sys-rs", + "cfg-if", + "core-graphics", + "piet", + "piet-cairo", + "piet-coregraphics", + "piet-direct2d", + "piet-web", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "piet-coregraphics" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32553fa0566ed2f23dadec1078a1e6a5930672f350db54d4267e9358f578ab4f" +dependencies = [ + "associative-cache", + "core-foundation", + "core-foundation-sys", + "core-graphics", + "core-text", + "foreign-types", + "piet", +] + +[[package]] +name = "piet-direct2d" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d65edc9fa5d1710daa2e55e9cc7b65e65794304ffa870a565fb7128f3110ac5" +dependencies = [ + "associative-cache", + "dwrote", + "piet", + "utf16_lit", + "winapi", + "wio", +] + +[[package]] +name = "piet-web" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "303452b9813560d4e4ea37b5792b18578b12f1e47111f1aea1a4cb9392f6fa95" +dependencies = [ + "js-sys", + "piet", + "unicode-segmentation", + "wasm-bindgen", + "web-sys", + "xi-unicode", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -915,6 +1717,17 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -982,6 +1795,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1043,6 +1865,21 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.36.6" @@ -1116,6 +1953,30 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.152" @@ -1185,6 +2046,16 @@ dependencies = [ "libc", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.7" @@ -1238,6 +2109,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-deps" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -1317,6 +2201,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aeafdfd935e4a7fe16a91ab711fa52d54df84f9c8f7ca5837a9d1d902ef4c2" +dependencies = [ + "displaydoc", +] + [[package]] name = "tokio" version = "1.24.1" @@ -1439,12 +2332,107 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "type-map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unic-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356b759fb6a82050666f11dce4b6fe3571781f1449f3ef78074e408d468ec09" +dependencies = [ + "matches", + "unic-ucd-bidi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-langid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1472,6 +2460,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "utf16_lit" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14706d2a800ee8ff38c1d3edb873cd616971ea59eb7c0d046bb44ef59b06a1ae" + [[package]] name = "utf8parse" version = "0.2.0" @@ -1484,6 +2478,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + [[package]] name = "version_check" version = "0.9.4" @@ -1496,6 +2496,70 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1584,6 +2648,21 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "xi-unicode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" + [[package]] name = "zip" version = "0.6.3" diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml new file mode 100644 index 0000000..64361c4 --- /dev/null +++ b/crates/dtmm/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dtmm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } +color-eyre = "0.6.2" +confy = "0.5.1" +druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } +sdk = { path = "../../lib/sdk", version = "0.2.0" } +serde = "1.0.152" +tokio = "1.23.0" +toml = "0.5.10" +tracing = "0.1.37" +tracing-error = "0.2.0" +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/crates/dtmm/notes.adoc b/crates/dtmm/notes.adoc new file mode 100644 index 0000000..bbf29e7 --- /dev/null +++ b/crates/dtmm/notes.adoc @@ -0,0 +1,49 @@ += Notes + +== Layout + +- top bar: + - left aligned: a tab bar with "mods", "settings", "about" + - right aligned: a button to start the game + - in the future: center aligned a dropdown to select profiles, and button to edit them +- main view: + - left side: list view of mods + - right side: details pane and buttons + - always visible, first mod in list is selected by default + - buttons: + - add mod + - deploy mods + - remove selected mod + - enable/disable (text changes based on state) + +== Mod list + +- name +- description? +- image? +- click to get details pane? + +== Managing mods + +- for each mod in the list, have a checkbox +- need a button to remove mods +- need a button to add mods from downloaded files +- search + +== Misc + +- settings +- open mod storage + +== Managing the game + +- deploy mods +- + +== Preparing the game + +- click "Install mods" to prepare the game files with the enabled mods + +== Playing the game + +- if overlay file systems are used, the game has to be started through the mod manager diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs new file mode 100644 index 0000000..d73b440 --- /dev/null +++ b/crates/dtmm/src/main.rs @@ -0,0 +1,42 @@ +use clap::command; +use color_eyre::Report; +use color_eyre::Result; +use druid::AppLauncher; +use tracing_error::ErrorLayer; +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +use crate::state::State; + +mod main_window; +mod state; +mod theme; +mod widget; + +#[tracing::instrument] +#[tokio::main] +async fn main() -> Result<()> { + color_eyre::install()?; + + let _matches = command!().get_matches(); + + { + let fmt_layer = tracing_subscriber::fmt::layer().pretty(); + let filter_layer = + EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::new( + tracing_subscriber::fmt::format::Pretty::default(), + )) + .init(); + } + + let initial_state = State::new(); + + AppLauncher::with_window(main_window::new()) + .launch(initial_state) + .map_err(Report::new) +} diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs new file mode 100644 index 0000000..a8b1e71 --- /dev/null +++ b/crates/dtmm/src/main_window.rs @@ -0,0 +1,237 @@ +use druid::im::Vector; +use druid::widget::{ + Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, + ViewSwitcher, +}; +use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; + +use crate::state::{ModInfo, State, View}; +use crate::theme; +use crate::widget::ExtraWidgetExt; + +const TITLE: &str = "Darktide Mod Manager"; +const WINDOW_WIDTH: f64 = 800.0; +const WINDOW_HEIGHT: f64 = 600.0; +const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; + +pub(crate) fn new() -> WindowDesc { + WindowDesc::new(build_window()) + .title(TITLE) + .window_size((WINDOW_WIDTH, WINDOW_HEIGHT)) +} + +fn build_top_bar() -> impl Widget { + Flex::row() + .must_fill_main_axis(true) + .main_axis_alignment(MainAxisAlignment::SpaceBetween) + .with_child( + Flex::row() + .with_child( + Button::new("Mods").on_click(|_ctx, state: &mut State, _env| { + state.set_current_view(View::Mods) + }), + ) + .with_default_spacer() + .with_child( + Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { + state.set_current_view(View::Settings) + }), + ) + .with_default_spacer() + .with_child( + Button::new("About").on_click(|_ctx, state: &mut State, _env| { + state.set_current_view(View::About) + }), + ), + ) + .with_child( + Flex::row() + .with_child(Button::new("Deploy Mods").on_click( + |_ctx, _state: &mut State, _env| { + todo!(); + }, + )) + .with_default_spacer() + .with_child( + Button::new("Run Game").on_click(|_ctx, _state: &mut State, _env| { + todo!(); + }), + ), + ) + .padding(theme::TOP_BAR_INSETS) + .background(theme::TOP_BAR_BACKGROUND_COLOR) + // TODO: Add bottom border. Need a custom widget for that, as the built-in only provides + // uniform borders on all sides +} + +fn build_mod_list() -> impl Widget { + let list = List::new(|| { + Flex::row() + .must_fill_main_axis(true) + // .with_child( + // Label::dynamic(|enabled, _env| { + // if *enabled { + // "Enabled".into() + // } else { + // "Disabled".into() + // } + // }) + // .lens( + // lens::Identity + // .map( + // |(i, info)| info, + // |(i, info), new_info| { + // todo!(); + // }, + // ) + // .then(ModInfo::enabled), + // ), + // ) + // .with_child(Label::raw().lens(ModInfo::name)) + .on_click(|_ctx, state, _env| { + todo!(); + }) + }); + + Scroll::new(list) + .vertical() + .lens(State::mods.map( + |mods| { + mods.iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect::>() + }, + |mods, infos| { + infos.into_iter().for_each(|(i, info)| { + mods.set(i, info); + }); + }, + )) + .content_must_fill() +} + +fn build_mod_details() -> impl Widget { + let details_container = Maybe::new( + || { + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(Label::raw().lens(ModInfo::name)) + .with_flex_child(Label::raw().lens(ModInfo::description), 1.0) + }, + Flex::column, + ) + .lens(State::selected_mod); + + let button_move_up = Button::new("Move Up") + .on_click(|_ctx, index: &mut Option, _env| { + if let Some(i) = index.as_mut() { + *i = i.saturating_sub(1) + } + }) + .lens(State::selected_mod_index); + + let button_move_down = Button::new("Move Down") + .on_click(|_ctx, index: &mut Option, _env| { + if let Some(i) = index.as_mut() { + *i = i.saturating_add(1) + } + }) + .lens(State::selected_mod_index); + + let button_toggle_mod = Maybe::new( + || { + Button::dynamic(|enabled, _env| { + if *enabled { + "Disable Mod".into() + } else { + "Enabled Mod".into() + } + }) + .on_click(|_ctx, info: &mut bool, _env| { + *info = !*info; + }) + .lens(ModInfo::enabled) + }, + // TODO: Gray out + || Button::new("Enable Mod"), + ) + .lens(State::selected_mod); + + let button_add_mod = Button::new("Add Mod").on_click(|_ctx, state: &mut State, _env| { + // TODO: Implement properly + let info = ModInfo::new(); + state.add_mod(info); + }); + + let button_delete_mod = Button::new("Delete Mod") + .on_click(|_ctx, data: &mut State, _env| data.delete_selected_mod()); + + let buttons = Flex::column() + .with_child( + Flex::row() + .main_axis_alignment(MainAxisAlignment::End) + .with_child(button_move_up) + .with_default_spacer() + .with_child(button_move_down) + .padding(Insets::uniform_xy(5.0, 2.0)), + ) + .with_child( + Flex::row() + .main_axis_alignment(MainAxisAlignment::End) + .with_child(button_toggle_mod) + .with_default_spacer() + .with_child(button_add_mod) + .with_default_spacer() + .with_child(button_delete_mod) + .padding(Insets::uniform_xy(5.0, 2.0)), + ) + .with_default_spacer(); + + Flex::column() + .must_fill_main_axis(true) + .main_axis_alignment(MainAxisAlignment::SpaceBetween) + .with_flex_child(details_container, 1.0) + .with_child(buttons) +} + +fn build_view_mods() -> impl Widget { + Split::columns(build_mod_list(), build_mod_details()) + .split_point(0.75) + .min_size(0.0, MOD_DETAILS_MIN_WIDTH) + .solid_bar(true) + .bar_size(2.0) + .draggable(true) +} + +fn build_view_settings() -> impl Widget { + Label::new("Settings") +} + +fn build_view_about() -> impl Widget { + Align::centered( + Flex::column() + .with_child(Label::new("Darktide Mod Manager")) + .with_child(Label::new( + "Website: https://git.sclu1034.dev/bitsquid_dt/dtmt", + )), + ) +} + +fn build_main() -> impl Widget { + ViewSwitcher::new( + |state: &State, _env| state.get_current_view(), + |selector, _state, _env| match selector { + View::Mods => Box::new(build_view_mods()), + View::Settings => Box::new(build_view_settings()), + View::About => Box::new(build_view_about()), + }, + ) +} + +fn build_window() -> impl Widget { + Flex::column() + .must_fill_main_axis(true) + .with_child(build_top_bar()) + .with_flex_child(build_main(), 1.0) +} diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs new file mode 100644 index 0000000..0639818 --- /dev/null +++ b/crates/dtmm/src/state.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use druid::im::Vector; +use druid::{Data, Lens}; + +#[derive(Copy, Clone, Data, PartialEq)] +pub(crate) enum View { + Mods, + Settings, + About, +} + +impl Default for View { + fn default() -> Self { + Self::Mods + } +} + +#[derive(Clone, Data, Lens)] +pub(crate) struct ModInfo { + name: String, + description: Arc, + enabled: bool, +} +impl ModInfo { + pub fn new() -> Self { + Self { + name: format!("Test Mod: {:?}", std::time::SystemTime::now()), + description: Arc::new(String::from("A test dummy")), + enabled: false, + } + } +} + +impl PartialEq for ModInfo { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +#[derive(Clone, Data, Default, Lens)] +pub(crate) struct State { + current_view: View, + mods: Vector, + selected_mod_index: Option, +} + +pub(crate) struct SelectedModLens; + +impl Lens> for SelectedModLens { + fn with) -> V>(&self, data: &State, f: F) -> V { + let info = data + .selected_mod_index + .and_then(|i| data.mods.get(i).cloned()); + + f(&info) + } + + fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { + let mut info = data + .selected_mod_index + .and_then(|i| data.mods.get_mut(i).cloned()); + f(&mut info) + } +} + +/// A Lens that maps an `im::Vector` to `im::Vector<(usize, T)>`, +/// where each element in the destination vector includes its index in the +/// source vector. +pub(crate) struct IndexedVectorLens; + +impl Lens, Vector<(usize, T)>> for IndexedVectorLens { + fn with) -> V>(&self, data: &Vector, f: F) -> V { + let data = data + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect(); + f(&data) + } + + fn with_mut) -> V>( + &self, + data: &mut Vector, + f: F, + ) -> V { + todo!() + } +} + +impl State { + #[allow(non_upper_case_globals)] + pub const selected_mod: SelectedModLens = SelectedModLens; + + pub fn new() -> Self { + Default::default() + } + + pub fn get_current_view(&self) -> View { + self.current_view + } + + pub fn set_current_view(&mut self, view: View) { + self.current_view = view; + } + + pub fn delete_selected_mod(&mut self) { + let Some(index) = self.selected_mod_index else { + return; + }; + + self.mods.remove(index); + } + + pub fn add_mod(&mut self, info: ModInfo) { + self.mods.push_back(info); + self.selected_mod_index = Some(self.mods.len() - 1); + } +} diff --git a/crates/dtmm/src/theme.rs b/crates/dtmm/src/theme.rs new file mode 100644 index 0000000..7658f3f --- /dev/null +++ b/crates/dtmm/src/theme.rs @@ -0,0 +1,4 @@ +use druid::{Color, Insets}; + +pub const TOP_BAR_BACKGROUND_COLOR: Color = Color::rgba8(255, 255, 255, 50); +pub const TOP_BAR_INSETS: Insets = Insets::uniform(5.0); diff --git a/crates/dtmm/src/widget/container.rs b/crates/dtmm/src/widget/container.rs new file mode 100644 index 0000000..e58e64c --- /dev/null +++ b/crates/dtmm/src/widget/container.rs @@ -0,0 +1,7 @@ +use druid::{Data, Widget, WidgetPod}; + +pub struct Container { + child: WidgetPod>>, +} + +impl Container {} diff --git a/crates/dtmm/src/widget/fill_container.rs b/crates/dtmm/src/widget/fill_container.rs new file mode 100644 index 0000000..540715e --- /dev/null +++ b/crates/dtmm/src/widget/fill_container.rs @@ -0,0 +1,63 @@ +use std::f64::INFINITY; + +use druid::widget::prelude::*; +use druid::{Point, WidgetPod}; + +pub struct FillContainer { + child: WidgetPod>>, +} + +impl FillContainer { + pub fn new(child: impl Widget + 'static) -> Self { + Self { + child: WidgetPod::new(child).boxed(), + } + } +} + +impl Widget for FillContainer { + #[tracing::instrument(name = "FillContainer", level = "trace", skip_all)] + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + self.child.event(ctx, event, data, env); + } + + #[tracing::instrument(name = "FillContainer", level = "trace", skip_all)] + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.child.lifecycle(ctx, event, data, env) + } + + #[tracing::instrument(name = "FillContainer", level = "trace", skip_all)] + fn update(&mut self, ctx: &mut UpdateCtx, _: &T, data: &T, env: &Env) { + self.child.update(ctx, data, env); + } + + #[tracing::instrument(name = "FillContainer", level = "trace", skip_all)] + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + bc.debug_check("FillContainer"); + + let child_size = self.child.layout(ctx, bc, data, env); + + let w = if bc.is_width_bounded() { + INFINITY + } else { + child_size.width + }; + + let h = if bc.is_height_bounded() { + INFINITY + } else { + child_size.height + }; + + let my_size = bc.constrain(Size::new(w, h)); + + self.child.set_origin(ctx, Point::new(0.0, 0.0)); + tracing::trace!("Computed layout: size={}", my_size); + my_size + } + + #[tracing::instrument(name = "FillContainer", level = "trace", skip_all)] + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + self.child.paint(ctx, data, env); + } +} diff --git a/crates/dtmm/src/widget/mod.rs b/crates/dtmm/src/widget/mod.rs new file mode 100644 index 0000000..9262d0a --- /dev/null +++ b/crates/dtmm/src/widget/mod.rs @@ -0,0 +1,14 @@ +use druid::{Data, Widget}; + +use self::fill_container::FillContainer; + +pub mod container; +pub mod fill_container; + +pub trait ExtraWidgetExt: Widget + Sized + 'static { + fn content_must_fill(self) -> FillContainer { + FillContainer::new(self) + } +} + +impl + 'static> ExtraWidgetExt for W {} -- 2.45.3 From 61b3a07666ee07b0efad20e4e1e9340ffd17d6e7 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 23 Jan 2023 16:22:16 +0100 Subject: [PATCH 02/90] feat(sdk): Implement bundle database handling --- lib/sdk/src/binary.rs | 44 +++++++ lib/sdk/src/bundle/database.rs | 228 +++++++++++++++++++++++++++++++++ lib/sdk/src/bundle/mod.rs | 1 + lib/sdk/src/lib.rs | 2 + 4 files changed, 275 insertions(+) create mode 100644 lib/sdk/src/bundle/database.rs diff --git a/lib/sdk/src/binary.rs b/lib/sdk/src/binary.rs index 4782440..9ce3f11 100644 --- a/lib/sdk/src/binary.rs +++ b/lib/sdk/src/binary.rs @@ -1,3 +1,47 @@ +use std::io::{Cursor, Read, Seek, Write}; + +use color_eyre::Result; + +use self::sync::{ReadExt, WriteExt}; + +pub trait FromBinary: Sized { + fn from_binary(r: &mut R) -> Result; +} + +pub trait ToBinary { + fn to_binary(&self) -> Result>; +} + +impl ToBinary for Vec { + fn to_binary(&self) -> Result> { + // TODO: Allocations for the vector could be optimized by first + // serializing one value, then calculating the size from that. + let mut bin = Cursor::new(Vec::new()); + bin.write_u32(self.len() as u32)?; + + for val in self.iter() { + let buf = val.to_binary()?; + bin.write_all(&buf)?; + } + + Ok(bin.into_inner()) + } +} + +impl FromBinary for Vec { + fn from_binary(r: &mut R) -> Result { + let size = r.read_u32()? as usize; + + let mut list = Vec::with_capacity(size); + + for _ in 0..size { + list.push(T::from_binary(r)?); + } + + Ok(list) + } +} + pub mod sync { use std::io::{self, Read, Seek, SeekFrom}; diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs new file mode 100644 index 0000000..b3d9296 --- /dev/null +++ b/lib/sdk/src/bundle/database.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; +use std::io::Cursor; +use std::io::Read; +use std::io::Seek; +use std::io::Write; + +use color_eyre::eyre; +use color_eyre::Result; + +use crate::binary::sync::*; +use crate::binary::FromBinary; +use crate::binary::ToBinary; +use crate::murmur::Murmur64; +use crate::Bundle; + +use super::file::BundleFileType; + +const DATABASE_VERSION: u32 = 0x6; +const FILE_VERSION: u32 = 0x4; + +pub struct BundleFile { + name: String, + stream: String, + platform_specific: bool, + file_time: u64, +} + +pub struct FileName { + extension: BundleFileType, + name: Murmur64, +} + +pub struct BundleDatabase { + stored_files: HashMap>, + resource_hashes: HashMap, + bundle_contents: HashMap>, +} + +impl BundleDatabase { + pub fn add_bundle(&mut self, bundle: &Bundle) { + let hash = Murmur64::hash(bundle.name().as_bytes()); + let name = hash.to_string(); + let stream = format!("{}.stream", &name); + let file = BundleFile { + name, + stream, + file_time: 0, + platform_specific: false, + }; + + self.stored_files.entry(hash).or_default().push(file); + + // TODO: Resource hashes + + for f in bundle.files() { + let file_name = FileName { + extension: f.file_type(), + name: Murmur64::hash(f.name(false, None).as_bytes()), + }; + + self.bundle_contents + .entry(hash) + .or_default() + .push(file_name); + } + } +} + +impl FromBinary for BundleDatabase { + #[tracing::instrument(name = "BundleDatabase::from_binary", skip_all)] + fn from_binary(r: &mut R) -> Result { + { + let format = r.read_u32()?; + eyre::ensure!( + format == DATABASE_VERSION, + "invalid file format, expected {:#X}, got {:#X}", + DATABASE_VERSION, + format + ); + } + + let num_entries = r.read_u32()? as usize; + let mut stored_files = HashMap::with_capacity(num_entries); + + for _ in 0..num_entries { + let hash = Murmur64::from(r.read_u64()?); + + let num_files = r.read_u32()? as usize; + let mut files = Vec::with_capacity(num_files); + + for _ in 0..num_files { + { + let version = r.read_u32()?; + eyre::ensure!( + version == FILE_VERSION, + "invalid file version, expected {:#X}, got {:#X}", + FILE_VERSION, + version + ); + } + + let len_name = r.read_u32()? as usize; + let mut buf = vec![0; len_name]; + r.read_exact(&mut buf)?; + + let name = String::from_utf8(buf)?; + + let len_stream = r.read_u32()? as usize; + let mut buf = vec![0; len_stream]; + r.read_exact(&mut buf)?; + + let stream = String::from_utf8(buf)?; + + let platform_specific = r.read_u8()? != 0; + + // TODO: Unknown what this is. In VT2's SDK, it's simply ignored, + // and always written as `0`, but in DT, it seems to be used. + let mut buffer = [0; 20]; + r.read_exact(&mut buffer)?; + + let file_time = r.read_u64()?; + + let file = BundleFile { + name, + stream, + platform_specific, + file_time, + }; + + files.push(file); + } + + stored_files.insert(hash, files); + } + + let num_hashes = r.read_u32()? as usize; + let mut resource_hashes = HashMap::with_capacity(num_hashes); + + for _ in 0..num_hashes { + let name = Murmur64::from(r.read_u64()?); + let hash = r.read_u64()?; + + resource_hashes.insert(name, hash); + } + + let num_contents = r.read_u32()? as usize; + let mut bundle_contents = HashMap::with_capacity(num_contents); + + for _ in 0..num_contents { + let hash = Murmur64::from(r.read_u64()?); + + let num_files = r.read_u32()? as usize; + let mut files = Vec::with_capacity(num_files); + + for _ in 0..num_files { + let extension = BundleFileType::from(r.read_u64()?); + let name = Murmur64::from(r.read_u64()?); + + files.push(FileName { extension, name }); + } + + bundle_contents.insert(hash, files); + } + + Ok(Self { + stored_files, + resource_hashes, + bundle_contents, + }) + } +} + +impl ToBinary for BundleDatabase { + #[tracing::instrument(name = "BundleDatabase::to_binary", skip_all)] + fn to_binary(&self) -> Result> { + let mut binary = Vec::new(); + + { + let mut w = Cursor::new(&mut binary); + + w.write_u32(DATABASE_VERSION)?; + + w.write_u32(self.stored_files.len() as u32)?; + + for (hash, files) in self.stored_files.iter() { + w.write_u64((*hash).into())?; + w.write_u32(files.len() as u32)?; + + for f in files.iter() { + w.write_u32(FILE_VERSION)?; + w.write_u32(f.name.len() as u32)?; + w.write_all(f.name.as_bytes())?; + w.write_u32(f.stream.len() as u32)?; + w.write_all(f.stream.as_bytes())?; + + w.write_u8(if f.platform_specific { 1 } else { 0 })?; + + // TODO: Don't know what goes here + let buffer = [0; 20]; + w.write_all(&buffer)?; + + w.write_u64(f.file_time)?; + } + } + + w.write_u32(self.resource_hashes.len() as u32)?; + + for (name, hash) in self.resource_hashes.iter() { + w.write_u64((*name).into())?; + w.write_u64(*hash)?; + } + + w.write_u32(self.bundle_contents.len() as u32)?; + + for (hash, contents) in self.bundle_contents.iter() { + w.write_u64((*hash).into())?; + w.write_u32(contents.len() as u32)?; + + for FileName { extension, name } in contents.iter() { + w.write_u64((*extension).into())?; + w.write_u64((*name).into())?; + } + } + } + + Ok(binary) + } +} diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 000df1c..4cdc88e 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -8,6 +8,7 @@ use oodle_sys::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; use crate::binary::sync::*; use crate::murmur::{HashGroup, Murmur64}; +pub(crate) mod database; pub(crate) mod file; pub use file::{BundleFile, BundleFileType}; diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 6890317..01b87ee 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -4,6 +4,8 @@ mod context; pub mod filetype; pub mod murmur; +pub use binary::{FromBinary, ToBinary}; +pub use bundle::database::BundleDatabase; pub use bundle::decompress; pub use bundle::{Bundle, BundleFile, BundleFileType}; pub use context::Context; -- 2.45.3 From 073a91d788c4e76095f93548fb9282b07ed77ba9 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 31 Jan 2023 09:19:14 +0100 Subject: [PATCH 03/90] bug(sdk): Fix type conversion recursion The compiler doesn't complain about this, so I assumed it was able to correctly resolve a conversion `BundleFileType` -> `Murmur64` via their shared `From` impl: `u64`. But it appears that is not the case, and the simple `t.into()` just calls itself. So I need to do the conversion via the intermediary value manually. --- lib/sdk/src/bundle/file.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index d5f0c58..2c68185 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -397,7 +397,8 @@ impl From for u64 { } impl From for Murmur64 { fn from(t: BundleFileType) -> Murmur64 { - t.into() + let hash: u64 = t.into(); + Murmur64::from(hash) } } -- 2.45.3 From 6b01511d22fcc54938b495a8692606c57dda29ce Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 31 Jan 2023 09:27:18 +0100 Subject: [PATCH 04/90] refactor(main_window): Combine window size into one variable --- crates/dtmm/src/main_window.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index a8b1e71..0284dd5 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -10,14 +10,13 @@ use crate::theme; use crate::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; -const WINDOW_WIDTH: f64 = 800.0; -const WINDOW_HEIGHT: f64 = 600.0; +const WINDOW_SIZE: (f64, f64) = (800.0, 600.0); const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; pub(crate) fn new() -> WindowDesc { WindowDesc::new(build_window()) .title(TITLE) - .window_size((WINDOW_WIDTH, WINDOW_HEIGHT)) + .window_size(WINDOW_SIZE) } fn build_top_bar() -> impl Widget { -- 2.45.3 From 9077d791b24d21a424be6c398b12ffaf6bc308cc Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 22:46:54 +0100 Subject: [PATCH 05/90] feat(sdk): Implement file properties --- Cargo.lock | 1 + lib/sdk/Cargo.toml | 1 + lib/sdk/src/bundle/file.rs | 23 +++++++--- lib/sdk/src/bundle/mod.rs | 93 ++++++++++++-------------------------- lib/sdk/src/lib.rs | 2 +- 5 files changed, 49 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23846e0..4dddb00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1934,6 +1934,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" name = "sdk" version = "0.2.0" dependencies = [ + "bitflags", "byteorder", "color-eyre", "csv-async", diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index 02f3964..e504a76 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -4,6 +4,7 @@ version = "0.2.0" edition = "2021" [dependencies] +bitflags = "1.3.2" byteorder = "1.4.3" color-eyre = "0.6.2" csv-async = { version = "1.2.4", features = ["tokio", "serde"] } diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index 2c68185..dd3d1a0 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -1,6 +1,7 @@ use std::io::{Cursor, Read, Seek, Write}; use std::path::Path; +use bitflags::bitflags; use color_eyre::eyre::Context; use color_eyre::{eyre, Result}; use futures::future::join_all; @@ -489,10 +490,18 @@ impl BundleFileVariant { } } +bitflags! { + #[derive(Default)] + pub struct Properties: u32 { + const DATA = 0b100; + } +} + pub struct BundleFile { file_type: BundleFileType, name: String, variants: Vec, + props: Properties, } impl BundleFile { @@ -501,6 +510,7 @@ impl BundleFile { file_type, name, variants: Vec::new(), + props: Properties::empty(), } } @@ -508,12 +518,8 @@ impl BundleFile { self.variants.push(variant) } - #[tracing::instrument( - name = "File::read", - skip_all, - fields(name = %meta.name_hash, ext = %meta.extension_hash, flags = meta.flags) - )] - pub fn from_reader(ctx: &crate::Context, r: &mut R, meta: &EntryHeader) -> Result + #[tracing::instrument(name = "File::read", skip(ctx, r))] + pub fn from_reader(ctx: &crate::Context, r: &mut R, props: Properties) -> Result where R: Read + Seek, { @@ -561,6 +567,7 @@ impl BundleFile { variants, file_type, name, + props, }) } @@ -617,6 +624,10 @@ impl BundleFile { } } + pub fn props(&self) -> Properties { + self.props + } + pub fn base_name(&self) -> &String { &self.name } diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 4cdc88e..b9534e4 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -1,4 +1,5 @@ use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write}; +use std::mem::size_of; use std::path::Path; use color_eyre::eyre::{self, Context, Result}; @@ -6,12 +7,13 @@ use color_eyre::{Help, Report, SectionExt}; use oodle_sys::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; use crate::binary::sync::*; +use crate::bundle::file::Properties; use crate::murmur::{HashGroup, Murmur64}; pub(crate) mod database; pub(crate) mod file; -pub use file::{BundleFile, BundleFileType}; +pub use file::{BundleFile, BundleFileType, BundleFileVariant}; #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] enum BundleFormat { @@ -40,56 +42,9 @@ impl From for u32 { } } -pub struct EntryHeader { - name_hash: Murmur64, - extension_hash: Murmur64, - flags: u32, -} - -impl EntryHeader { - #[tracing::instrument(name = "EntryHeader::from_reader", skip_all)] - fn from_reader(r: &mut R) -> Result - where - R: Read + Seek, - { - let extension_hash = Murmur64::from(r.read_u64()?); - let name_hash = Murmur64::from(r.read_u64()?); - let flags = r.read_u32()?; - - // NOTE: Known values so far: - // - 0x0: seems to be the default - // - 0x4: seems to be used for files that point to something in `data/` - // seems to correspond to a change in value in the header's 'unknown_3' - if flags != 0x0 { - tracing::debug!( - flags, - "Unexpected meta flags for file {name_hash:016X}.{extension_hash:016X}", - ); - } - - Ok(Self { - name_hash, - extension_hash, - flags, - }) - } - - #[tracing::instrument(name = "EntryHeader::to_writer", skip_all)] - fn to_writer(&self, w: &mut W) -> Result<()> - where - W: Write + Seek, - { - w.write_u64(self.extension_hash.into())?; - w.write_u64(self.name_hash.into())?; - w.write_u32(self.flags)?; - Ok(()) - } -} - pub struct Bundle { format: BundleFormat, properties: [Murmur64; 32], - headers: Vec, files: Vec, name: String, } @@ -100,7 +55,6 @@ impl Bundle { name, format: BundleFormat::F8, properties: [0.into(); 32], - headers: Vec::new(), files: Vec::new(), } } @@ -119,15 +73,22 @@ impl Bundle { pub fn add_file(&mut self, file: BundleFile) { tracing::trace!("Adding file {}", file.name(false, None)); - let header = EntryHeader { - extension_hash: file.file_type().into(), - name_hash: Murmur64::hash(file.base_name().as_bytes()), - // TODO: Hard coded until we know what this is - flags: 0x0, - }; + let existing_index = self + .files + .iter() + .enumerate() + .find(|(_, f)| **f == file) + .map(|val| val.0); self.files.push(file); - self.headers.push(header); + + if let Some(i) = existing_index { + self.files.swap_remove(i); + } + } + + pub fn get_file>(&self, name: S) -> Option<&BundleFile> { + self.files.iter().find(|f| f.base_name().eq(name.as_ref())) } #[tracing::instrument(skip(ctx, binary), fields(len_binary = binary.as_ref().len()))] @@ -154,9 +115,13 @@ impl Bundle { *prop = Murmur64::from(r.read_u64()?); } - let mut headers = Vec::with_capacity(num_entries); + let mut file_props = Vec::with_capacity(num_entries); for _ in 0..num_entries { - headers.push(EntryHeader::from_reader(&mut r)?); + // Skip two u64 that contain the extension hash and file name hash. + // We don't need them here, since we're reading the whole bundle into memory + // anyways. + r.seek(SeekFrom::Current((2 * size_of::()) as i64))?; + file_props.push(Properties::from_bits_truncate(r.read_u32()?)); } let num_chunks = r.read_u32()? as usize; @@ -227,9 +192,8 @@ impl Bundle { let mut r = Cursor::new(decompressed); let mut files = Vec::with_capacity(num_entries); - for i in 0..num_entries { - let meta = headers.get(i).unwrap(); - let file = BundleFile::from_reader(ctx, &mut r, meta) + for (i, props) in file_props.iter().enumerate() { + let file = BundleFile::from_reader(ctx, &mut r, *props) .wrap_err_with(|| format!("failed to read file {i}"))?; files.push(file); } @@ -237,7 +201,6 @@ impl Bundle { Ok(Self { name: bundle_name, format, - headers, files, properties, }) @@ -255,8 +218,10 @@ impl Bundle { w.write_u64((*prop).into())?; } - for meta in self.headers.iter() { - meta.to_writer(&mut w)?; + for file in self.files.iter() { + w.write_u64(file.file_type().into())?; + w.write_u64(Murmur64::hash(file.base_name().as_bytes()).into())?; + w.write_u32(file.props().bits())?; } let unpacked_data = { diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 01b87ee..e229e28 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -7,5 +7,5 @@ pub mod murmur; pub use binary::{FromBinary, ToBinary}; pub use bundle::database::BundleDatabase; pub use bundle::decompress; -pub use bundle::{Bundle, BundleFile, BundleFileType}; +pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant}; pub use context::Context; -- 2.45.3 From 1d084981311133e14646b65eef2ee251cd6a9de1 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 11:10:56 +0100 Subject: [PATCH 06/90] feat(dtmt): Add command to print the dictionary This is mostly helpful to check/debug whether the internal dictionary actually contains the expected data. For manually looking through the entire dictionary, opening the CSV file is still more convenient. --- Cargo.lock | 23 +++++++++++++++++++++ crates/dtmt/Cargo.toml | 1 + crates/dtmt/src/cmd/dictionary.rs | 34 +++++++++++++++++++++++++++++++ lib/sdk/src/murmur/dictionary.rs | 22 ++++++++++++++++++++ lib/sdk/src/murmur/mod.rs | 3 +-- 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dddb00..2e3679a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,28 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cli-table" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbb116d9e2c4be7011360d0c0bee565712c11e969c9609b25b619366dc379d" +dependencies = [ + "cli-table-derive", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cli-table-derive" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af3bfb9da627b0a6c467624fb7963921433774ed435493b5c08a3053e829ad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clipboard-win" version = "4.4.2" @@ -654,6 +676,7 @@ name = "dtmt" version = "0.2.0" dependencies = [ "clap", + "cli-table", "color-eyre", "confy", "csv-async", diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index 73d2d72..cb8f646 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -26,6 +26,7 @@ confy = "0.5.1" zip = "0.6.3" string_template = "0.2.1" promptly = "0.3.1" +cli-table = { version = "0.4.7", default-features = false, features = ["derive"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/dtmt/src/cmd/dictionary.rs b/crates/dtmt/src/cmd/dictionary.rs index 22a225b..e94e2ad 100644 --- a/crates/dtmt/src/cmd/dictionary.rs +++ b/crates/dtmt/src/cmd/dictionary.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum}; +use cli_table::{print_stdout, WithTitle}; use color_eyre::eyre::{Context, Result}; use color_eyre::{Help, SectionExt}; +use sdk::murmur::{IdString64, Murmur32, Murmur64}; use tokio::fs::File; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio_stream::wrappers::LinesStream; @@ -27,6 +29,29 @@ impl From for sdk::murmur::HashGroup { } } +#[derive(cli_table::Table)] +struct TableRow { + #[table(title = "Value")] + value: String, + #[table(title = "Murmur64")] + long: Murmur64, + #[table(title = "Murmur32")] + short: Murmur32, + #[table(title = "Group")] + group: sdk::murmur::HashGroup, +} + +impl From<&sdk::murmur::Entry> for TableRow { + fn from(entry: &sdk::murmur::Entry) -> Self { + Self { + value: entry.value().clone(), + long: entry.long(), + short: entry.short(), + group: entry.group(), + } + } +} + pub(crate) fn command_definition() -> Command { Command::new("dictionary") .about("Manipulate a hash dictionary file.") @@ -67,6 +92,7 @@ pub(crate) fn command_definition() -> Command { .value_parser(value_parser!(PathBuf)), ), ) + .subcommand(Command::new("show").about("Show the contents of the dictionary")) .subcommand(Command::new("save").about( "Save back the currently loaded dictionary, with hashes pre-computed. \ Pre-computing hashes speeds up loading large dictionaries, as they would \ @@ -176,6 +202,14 @@ pub(crate) async fn run(mut ctx: sdk::Context, matches: &ArgMatches) -> Result<( .await .wrap_err("Failed to write dictionary to disk") } + Some(("show", _)) => { + let lookup = &ctx.lookup; + let rows: Vec<_> = lookup.entries().iter().map(TableRow::from).collect(); + + print_stdout(rows.with_title())?; + + Ok(()) + } _ => unreachable!( "clap is configured to require a subcommand, and they're all handled above" ), diff --git a/lib/sdk/src/murmur/dictionary.rs b/lib/sdk/src/murmur/dictionary.rs index 322dded..2d51af1 100644 --- a/lib/sdk/src/murmur/dictionary.rs +++ b/lib/sdk/src/murmur/dictionary.rs @@ -55,6 +55,24 @@ pub struct Entry { group: HashGroup, } +impl Entry { + pub fn value(&self) -> &String { + &self.value + } + + pub fn long(&self) -> Murmur64 { + self.long + } + + pub fn short(&self) -> Murmur32 { + self.short + } + + pub fn group(&self) -> HashGroup { + self.group + } +} + pub struct Dictionary { entries: Vec, } @@ -172,4 +190,8 @@ impl Dictionary { pub fn is_empty(&self) -> bool { self.entries.is_empty() } + + pub fn entries(&self) -> &Vec { + &self.entries + } } diff --git a/lib/sdk/src/murmur/mod.rs b/lib/sdk/src/murmur/mod.rs index 95e66fa..784a6df 100644 --- a/lib/sdk/src/murmur/mod.rs +++ b/lib/sdk/src/murmur/mod.rs @@ -13,8 +13,7 @@ mod murmurhash64; pub const SEED: u32 = 0; -pub use dictionary::Dictionary; -pub use dictionary::HashGroup; +pub use dictionary::{Dictionary, Entry, HashGroup}; pub use murmurhash64::hash; pub use murmurhash64::hash32; pub use murmurhash64::hash_inverse as inverse; -- 2.45.3 From 036c20bd8cb4e5b15a96c09ae723eef399c60b1f Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 11:13:47 +0100 Subject: [PATCH 07/90] feat(lib): Implement IdString type This type is similar to an `Either` between a `Murmur64` hash and a `String`. This is necessary to be able to retain hash information where the hash is not in the dictionary, but at the same time allow string names where they are available. Up until now, when reading a bundle, all hashes would be converted to strings, which made sense for displaying those names. But when writing the same bundle back, those strings ended up being re-hashed, resulting in incorrect hashes. --- crates/dtmt/src/cmd/bundle/inject.rs | 8 +-- crates/dtmt/src/cmd/bundle/list.rs | 2 +- crates/dtmt/src/cmd/dictionary.rs | 16 +++-- lib/sdk/src/bundle/file.rs | 26 +++++--- lib/sdk/src/bundle/mod.rs | 8 ++- lib/sdk/src/context.rs | 8 +-- lib/sdk/src/filetype/package.rs | 5 +- lib/sdk/src/murmur/mod.rs | 90 ++++++++++++++++++++++++++++ 8 files changed, 137 insertions(+), 26 deletions(-) diff --git a/crates/dtmt/src/cmd/bundle/inject.rs b/crates/dtmt/src/cmd/bundle/inject.rs index 6d583b7..9c47686 100644 --- a/crates/dtmt/src/cmd/bundle/inject.rs +++ b/crates/dtmt/src/cmd/bundle/inject.rs @@ -58,14 +58,14 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { Bundle::from_binary(&ctx, name, binary).wrap_err("Failed to open bundle file")? }; - if let Some(_name) = matches.get_one::("replace") { + if let Some(name) = matches.get_one::("replace") { let mut file = File::open(&file_path) .await .wrap_err_with(|| format!("failed to open '{}'", file_path.display()))?; if let Some(variant) = bundle .files_mut() - .filter(|file| file.matches_name(_name)) + .filter(|file| file.matches_name(name.clone())) // TODO: Handle file variants .find_map(|file| file.variants_mut().next()) { @@ -75,7 +75,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { .wrap_err("failed to read input file")?; variant.set_data(data); } else { - let err = eyre::eyre!("No file '{}' in this bundle.", _name) + let err = eyre::eyre!("No file '{}' in this bundle.", name) .with_suggestion(|| { format!( "Run '{} bundle list {}' to list the files in this bundle.", @@ -87,7 +87,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { format!( "Use '{} bundle inject --add {} {} {}' to add it as a new file", clap::crate_name!(), - _name, + name, bundle_path.display(), file_path.display() ) diff --git a/crates/dtmt/src/cmd/bundle/list.rs b/crates/dtmt/src/cmd/bundle/list.rs index ec869ba..a206af3 100644 --- a/crates/dtmt/src/cmd/bundle/list.rs +++ b/crates/dtmt/src/cmd/bundle/list.rs @@ -64,7 +64,7 @@ where let v = &f.variants()[0]; println!( "\t{}.{}: {} bytes", - f.base_name(), + f.base_name().display(), f.file_type().ext_name(), v.size() ); diff --git a/crates/dtmt/src/cmd/dictionary.rs b/crates/dtmt/src/cmd/dictionary.rs index e94e2ad..0a2491e 100644 --- a/crates/dtmt/src/cmd/dictionary.rs +++ b/crates/dtmt/src/cmd/dictionary.rs @@ -104,17 +104,23 @@ pub(crate) fn command_definition() -> Command { pub(crate) async fn run(mut ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { match matches.subcommand() { Some(("lookup", sub_matches)) => { - let hash = sub_matches - .get_one::("hash") - .expect("required argument not found"); + let hash = { + let s = sub_matches + .get_one::("hash") + .expect("required argument not found"); + + u64::from_str_radix(s, 16) + .wrap_err("failed to parse argument as hexadecimal string")? + }; let groups = sub_matches .get_many::("group") .unwrap_or_default(); for group in groups { - let value = ctx.lookup_hash(*hash, (*group).into()); - println!("{value}"); + if let IdString64::String(value) = ctx.lookup_hash(hash, (*group).into()) { + println!("{group}: {value}"); + } } Ok(()) diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index dd3d1a0..383f42a 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -9,7 +9,7 @@ use serde::Serialize; use crate::binary::sync::*; use crate::filetype::*; -use crate::murmur::{HashGroup, Murmur64}; +use crate::murmur::{HashGroup, IdString64, Murmur64}; use super::EntryHeader; @@ -499,7 +499,7 @@ bitflags! { pub struct BundleFile { file_type: BundleFileType, - name: String, + name: IdString64, variants: Vec, props: Properties, } @@ -508,7 +508,7 @@ impl BundleFile { pub fn new(name: String, file_type: BundleFileType) -> Self { Self { file_type, - name, + name: name.into(), variants: Vec::new(), props: Properties::empty(), } @@ -576,7 +576,7 @@ impl BundleFile { let mut w = Cursor::new(Vec::new()); w.write_u64(self.file_type.hash().into())?; - w.write_u64(Murmur64::hash(self.name.as_bytes()).into())?; + w.write_u64(self.name.to_murmur64().into())?; w.write_u32(self.variants.len() as u32)?; // TODO: Figure out what this is @@ -628,12 +628,12 @@ impl BundleFile { self.props } - pub fn base_name(&self) -> &String { + pub fn base_name(&self) -> &IdString64 { &self.name } pub fn name(&self, decompiled: bool, variant: Option) -> String { - let mut s = self.name.clone(); + let mut s = self.name.display().to_string(); s.push('.'); if let Some(variant) = variant { @@ -652,10 +652,18 @@ impl BundleFile { pub fn matches_name(&self, name: S) -> bool where - S: AsRef, + S: Into, { - let name = name.as_ref(); - self.name == name || self.name(false, None) == name || self.name(true, None) == name + let name = name.into(); + if self.name == name { + return true; + } + + if let IdString64::String(name) = name { + self.name(false, None) == name || self.name(true, None) == name + } else { + false + } } pub fn file_type(&self) -> BundleFileType { diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index b9534e4..d54ba33 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -67,7 +67,11 @@ impl Bundle { path.file_name() .and_then(|name| name.to_str()) .and_then(|name| Murmur64::try_from(name).ok()) - .map(|hash| ctx.lookup_hash(hash, HashGroup::Filename)) + .map(|hash| { + ctx.lookup_hash(hash, HashGroup::Filename) + .display() + .to_string() + }) .unwrap_or_else(|| path.display().to_string()) } @@ -220,7 +224,7 @@ impl Bundle { for file in self.files.iter() { w.write_u64(file.file_type().into())?; - w.write_u64(Murmur64::hash(file.base_name().as_bytes()).into())?; + w.write_u64(file.base_name().to_murmur64().into())?; w.write_u32(file.props().bits())?; } diff --git a/lib/sdk/src/context.rs b/lib/sdk/src/context.rs index 0116c4a..b0de6dc 100644 --- a/lib/sdk/src/context.rs +++ b/lib/sdk/src/context.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::murmur::{Dictionary, HashGroup, Murmur32, Murmur64}; +use crate::murmur::{Dictionary, HashGroup, IdString64, Murmur32, Murmur64}; pub struct Context { pub lookup: Dictionary, @@ -21,17 +21,17 @@ impl Context { } } - pub fn lookup_hash(&self, hash: M, group: HashGroup) -> String + pub fn lookup_hash(&self, hash: M, group: HashGroup) -> IdString64 where M: Into, { let hash = hash.into(); if let Some(s) = self.lookup.lookup(hash, group) { tracing::debug!(%hash, string = s, "Murmur64 lookup successful"); - s.to_owned() + s.to_string().into() } else { tracing::debug!(%hash, "Murmur64 lookup failed"); - format!("{hash:016X}") + hash.into() } } diff --git a/lib/sdk/src/filetype/package.rs b/lib/sdk/src/filetype/package.rs index 36e3575..8b42116 100644 --- a/lib/sdk/src/filetype/package.rs +++ b/lib/sdk/src/filetype/package.rs @@ -201,7 +201,10 @@ impl Package { let t = BundleFileType::from(r.read_u64()?); let hash = Murmur64::from(r.read_u64()?); let path = ctx.lookup_hash(hash, HashGroup::Filename); - inner.entry(t).or_default().insert(PathBuf::from(path)); + inner + .entry(t) + .or_default() + .insert(PathBuf::from(path.display().to_string())); } let pkg = Self { diff --git a/lib/sdk/src/murmur/mod.rs b/lib/sdk/src/murmur/mod.rs index 784a6df..d054b48 100644 --- a/lib/sdk/src/murmur/mod.rs +++ b/lib/sdk/src/murmur/mod.rs @@ -236,3 +236,93 @@ impl<'de> Deserialize<'de> for Murmur32 { deserializer.deserialize_any(Self(0)) } } + +// This type encodes the fact that when reading in a bundle, we don't always have a dictionary +// entry for every hash in there. So we do want to have the real string available when needed, +// but at the same time retain the original hash information for when we don't. +// This is especially important when wanting to write back the read bundle, as the hashes need to +// stay the same. +// The previous system of always turning hashes into strings worked well for the purpose of +// displaying hashes, but would have made it very hard to turn a stringyfied hash back into +// an actual hash. +#[derive(Clone, Debug, Eq)] +pub enum IdString64 { + Hash(Murmur64), + String(String), +} + +impl IdString64 { + pub fn to_murmur64(&self) -> Murmur64 { + match self { + Self::Hash(hash) => *hash, + Self::String(s) => Murmur64::hash(s.as_bytes()), + } + } + + pub fn display(&self) -> IdString64Display { + let s = match self { + IdString64::Hash(hash) => hash.to_string(), + IdString64::String(s) => s.clone(), + }; + + IdString64Display(s) + } + + pub fn is_string(&self) -> bool { + match self { + IdString64::Hash(_) => false, + IdString64::String(_) => true, + } + } + + pub fn is_hash(&self) -> bool { + match self { + IdString64::Hash(_) => true, + IdString64::String(_) => false, + } + } +} + +impl From for IdString64 { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From for IdString64 { + fn from(value: Murmur64) -> Self { + Self::Hash(value) + } +} + +impl From for Murmur64 { + fn from(value: IdString64) -> Self { + value.to_murmur64() + } +} + +impl PartialEq for IdString64 { + fn eq(&self, other: &Self) -> bool { + self.to_murmur64() == other.to_murmur64() + } +} + +pub struct IdString64Display(String); + +impl std::fmt::Display for IdString64Display { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::UpperHex for IdString64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::UpperHex::fmt(&self.to_murmur64(), f) + } +} + +impl std::fmt::LowerHex for IdString64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::LowerHex::fmt(&self.to_murmur64(), f) + } +} -- 2.45.3 From 110108004d651edf3ec610f88640d2dd7cf09fe8 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 11:18:50 +0100 Subject: [PATCH 08/90] fix(dtmt): Fix dictionary lookup for all groups Rather than checking all groups, when no cli flag was given, no groups were checked. --- crates/dtmt/src/cmd/dictionary.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/dtmt/src/cmd/dictionary.rs b/crates/dtmt/src/cmd/dictionary.rs index 0a2491e..20ec1fc 100644 --- a/crates/dtmt/src/cmd/dictionary.rs +++ b/crates/dtmt/src/cmd/dictionary.rs @@ -29,6 +29,17 @@ impl From for sdk::murmur::HashGroup { } } +impl std::fmt::Display for HashGroup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HashGroup::Filename => write!(f, "filename"), + HashGroup::Filetype => write!(f, "filetype"), + HashGroup::Strings => write!(f, "strings"), + HashGroup::Other => write!(f, "other"), + } + } +} + #[derive(cli_table::Table)] struct TableRow { #[table(title = "Value")] @@ -68,7 +79,8 @@ pub(crate) fn command_definition() -> Command { .short('g') .long("group") .action(ArgAction::Append) - .value_parser(value_parser!(HashGroup)), + .value_parser(value_parser!(HashGroup)) + .default_values(["other", "filename", "filetype", "strings"]), ), ) .subcommand( -- 2.45.3 From df06182ca044e90695962f69e3faf7bd96f5885b Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 11:20:07 +0100 Subject: [PATCH 09/90] fix(sdk): Fix file header binary format The file header format is a bit more complex than I first realized, especially around when a path to `data/` is included, and which size field determines its file name length. --- lib/sdk/src/bundle/file.rs | 91 +++++++++++++++++++++++++++++--------- lib/sdk/src/bundle/mod.rs | 6 ++- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index 383f42a..853003a 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -412,6 +412,7 @@ impl std::fmt::Display for BundleFileType { #[derive(Debug)] struct BundleFileHeader { variant: u32, + unknown_1: u8, size: usize, len_data_file_name: usize, } @@ -420,6 +421,8 @@ pub struct BundleFileVariant { property: u32, data: Vec, data_file_name: Option, + // Seems to be related to whether there is a data path. + unknown_1: u8, } impl BundleFileVariant { @@ -432,6 +435,7 @@ impl BundleFileVariant { property: 0, data: Vec::new(), data_file_name: None, + unknown_1: 0, } } @@ -461,30 +465,38 @@ impl BundleFileVariant { R: Read + Seek, { let variant = r.read_u32()?; - r.skip_u8(0)?; + let unknown_1 = r.read_u8()?; let size = r.read_u32()? as usize; r.skip_u8(1)?; let len_data_file_name = r.read_u32()? as usize; Ok(BundleFileHeader { size, + unknown_1, variant, len_data_file_name, }) } #[tracing::instrument(skip_all)] - fn write_header(&self, w: &mut W) -> Result<()> + fn write_header(&self, w: &mut W, props: Properties) -> Result<()> where W: Write + Seek, { w.write_u32(self.property)?; - w.write_u8(0)?; - w.write_u32(self.data.len() as u32)?; - w.write_u8(1)?; + w.write_u8(self.unknown_1)?; let len_data_file_name = self.data_file_name.as_ref().map(|s| s.len()).unwrap_or(0); - w.write_u32(len_data_file_name as u32)?; + + if props.contains(Properties::DATA) { + w.write_u32(len_data_file_name as u32)?; + w.write_u8(1)?; + w.write_u32(0)?; + } else { + w.write_u32(self.data.len() as u32)?; + w.write_u8(1)?; + w.write_u32(len_data_file_name as u32)?; + } Ok(()) } @@ -528,36 +540,63 @@ impl BundleFile { let name = ctx.lookup_hash(hash, HashGroup::Filename); let header_count = r.read_u32()? as usize; + tracing::trace!(header_count); let mut headers = Vec::with_capacity(header_count); r.skip_u32(0)?; - for _ in 0..header_count { - let header = BundleFileVariant::read_header(r)?; + for i in 0..header_count { + let span = tracing::info_span!("Read file header", i); + let _enter = span.enter(); + + let header = BundleFileVariant::read_header(r) + .wrap_err_with(|| format!("failed to read header {i}"))?; + + if props.contains(Properties::DATA) { + tracing::debug!("props: {props:?} | unknown_1: {}", header.unknown_1) + } + headers.push(header); } let mut variants = Vec::with_capacity(header_count); for (i, header) in headers.into_iter().enumerate() { - let span = tracing::info_span!("Read file header {}", i, size = header.size); + let span = tracing::info_span!( + "Read file data {}", + i, + size = header.size, + len_data_file_name = header.len_data_file_name + ); let _enter = span.enter(); - let mut data = vec![0; header.size]; - r.read_exact(&mut data) - .wrap_err_with(|| format!("failed to read header {i}"))?; - - let data_file_name = if header.len_data_file_name > 0 { + let (data, data_file_name) = if props.contains(Properties::DATA) { + let data = vec![]; let s = r - .read_string_len(header.len_data_file_name) + .read_string_len(header.size) .wrap_err("failed to read data file name")?; - Some(s) + + (data, Some(s)) } else { - None + let mut data = vec![0; header.size]; + r.read_exact(&mut data) + .wrap_err_with(|| format!("failed to read file {i}"))?; + + let data_file_name = if header.len_data_file_name > 0 { + let s = r + .read_string_len(header.len_data_file_name) + .wrap_err("failed to read data file name")?; + Some(s) + } else { + None + }; + + (data, data_file_name) }; let variant = BundleFileVariant { property: header.variant, data, data_file_name, + unknown_1: header.unknown_1, }; variants.push(variant); @@ -584,16 +623,26 @@ impl BundleFile { for variant in self.variants.iter() { w.write_u32(variant.property())?; - w.write_u8(0)?; - w.write_u32(variant.size() as u32)?; - w.write_u8(1)?; + w.write_u8(variant.unknown_1)?; let len_data_file_name = variant.data_file_name().map(|s| s.len()).unwrap_or(0); - w.write_u32(len_data_file_name as u32)?; + + if self.props.contains(Properties::DATA) { + w.write_u32(len_data_file_name as u32)?; + w.write_u8(1)?; + w.write_u32(0)?; + } else { + w.write_u32(variant.size() as u32)?; + w.write_u8(1)?; + w.write_u32(len_data_file_name as u32)?; + } } for variant in self.variants.iter() { w.write_all(&variant.data)?; + if let Some(s) = &variant.data_file_name { + w.write_all(s.as_bytes())?; + } } Ok(w.into_inner()) diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index d54ba33..9e1a699 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -180,8 +180,6 @@ impl Bundle { unpacked_size_tracked -= CHUNK_SIZE; } - tracing::trace!(raw_size = raw_buffer.len()); - decompressed.append(&mut raw_buffer); } } @@ -196,7 +194,11 @@ impl Bundle { let mut r = Cursor::new(decompressed); let mut files = Vec::with_capacity(num_entries); + tracing::trace!(num_files = num_entries); for (i, props) in file_props.iter().enumerate() { + let span = tracing::trace_span!("Read file {}", i); + let _enter = span.enter(); + let file = BundleFile::from_reader(ctx, &mut r, *props) .wrap_err_with(|| format!("failed to read file {i}"))?; files.push(file); -- 2.45.3 From 16bfe8810125420e4ba3a688140577ea7931297a Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 22:27:18 +0100 Subject: [PATCH 10/90] feat(sdk): Link LuaJIT for bytecode compilation This removes the LuaJIT binary as a runtime dependency and decreases the complexity of the compilation, by not needing to juggle a bunch of temp files anymore. However, it was a bit of a pain to get everything set up in the end. Closes #4. --- Cargo.lock | 18 +++++ lib/sdk/Cargo.toml | 1 + lib/sdk/src/bundle/file.rs | 7 +- lib/sdk/src/filetype/lua.rs | 138 +++++++++++++++++++----------------- 4 files changed, 98 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e3679a..ef8b80e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.25" @@ -1358,6 +1364,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "luajit2-sys" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33bb7acccd5a0224645ba06eba391af5f7194ff1762c2545860b43afcfd41af2" +dependencies = [ + "cc", + "fs_extra", + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1966,6 +1983,7 @@ dependencies = [ "futures-util", "glob", "libloading", + "luajit2-sys", "nanorand", "oodle-sys", "pin-project-lite", diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index e504a76..5c2ef1a 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -22,3 +22,4 @@ tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "m tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } tracing = { version = "0.1.37", features = ["async-await"] } tracing-error = "0.2.0" +luajit2-sys = "0.0.2" diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index 853003a..2efa9b2 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::io::{Cursor, Read, Seek, Write}; use std::path::Path; @@ -660,7 +661,11 @@ impl BundleFile { S: AsRef, { match file_type { - BundleFileType::Lua => lua::compile(name, sjson).await, + BundleFileType::Lua => { + let sjson = + CString::new(sjson.as_ref()).wrap_err("failed to build CString from SJSON")?; + lua::compile(name, sjson) + } BundleFileType::Unknown(_) => { eyre::bail!("Unknown file type. Cannot compile from SJSON"); } diff --git a/lib/sdk/src/filetype/lua.rs b/lib/sdk/src/filetype/lua.rs index 87ac629..3ce8bdd 100644 --- a/lib/sdk/src/filetype/lua.rs +++ b/lib/sdk/src/filetype/lua.rs @@ -1,13 +1,12 @@ -use std::io::{Cursor, Write}; +use std::ffi::CStr; +use std::io::Cursor; -use color_eyre::{eyre::Context, Result}; -use tokio::{fs, process::Command}; +use color_eyre::eyre; +use color_eyre::Result; +use luajit2_sys as lua; -use crate::{ - binary::sync::WriteExt, - bundle::file::{BundleFileVariant, UserFile}, - BundleFile, BundleFileType, -}; +use crate::bundle::file::{BundleFileVariant, UserFile}; +use crate::{BundleFile, BundleFileType}; #[tracing::instrument(skip_all, fields(buf_len = data.as_ref().len()))] pub(crate) async fn decompile(_ctx: &crate::Context, data: T) -> Result> @@ -19,72 +18,81 @@ where } #[tracing::instrument(skip_all)] -pub(crate) async fn compile(name: String, code: S) -> Result +pub fn compile(name: String, code: S) -> Result where - S: AsRef, + S: AsRef, { - let in_file_path = { - let mut path = std::env::temp_dir(); - let name: String = std::iter::repeat_with(fastrand::alphanumeric) - .take(10) - .collect(); - path.push(name + "-dtmt.lua"); + let bytecode = unsafe { + let state = lua::luaL_newstate(); + lua::luaL_openlibs(state); - path + lua::lua_pushstring(state, code.as_ref().as_ptr() as _); + lua::lua_setglobal(state, b"code\0".as_ptr() as _); + + lua::lua_pushstring(state, name.as_ptr() as _); + lua::lua_setglobal(state, b"name\0".as_ptr() as _); + + let run = b"return string.dump(loadstring(code, \"@\" .. name), false)\0"; + match lua::luaL_loadstring(state, run.as_ptr() as _) as u32 { + lua::LUA_OK => {} + lua::LUA_ERRSYNTAX => { + let err = lua::lua_tostring(state, -1); + let err = CStr::from_ptr(err).to_string_lossy().to_string(); + + lua::lua_close(state); + + eyre::bail!("Invalid syntax: {}", err); + } + lua::LUA_ERRMEM => { + lua::lua_close(state); + eyre::bail!("Failed to allocate sufficient memory to compile LuaJIT bytecode") + } + _ => unreachable!(), + } + + match lua::lua_pcall(state, 0, 1, 0) as u32 { + lua::LUA_OK => { + // The binary data is pretty much guaranteed to contain NUL bytes, + // so we can't rely on `lua_tostring` and `CStr` here. Instead we have to + // explicitely query the string length and build our vector from that. + // However, on the bright side, we don't have to go through any string types anymore, + // and can instead treat it as raw bytes immediately. + let mut len = 0; + let data = lua::lua_tolstring(state, -1, &mut len) as *const u8; + let data = std::slice::from_raw_parts(data, len).to_vec(); + + lua::lua_close(state); + + data + } + lua::LUA_ERRRUN => { + let err = lua::lua_tostring(state, -1); + let err = CStr::from_ptr(err).to_string_lossy().to_string(); + + lua::lua_close(state); + + eyre::bail!("Failed to compile LuaJIT bytecode: {}", err); + } + lua::LUA_ERRMEM => { + lua::lua_close(state); + eyre::bail!("Failed to allocate sufficient memory to compile LuaJIT bytecode") + } + // We don't use an error handler function, so this should be unreachable + lua::LUA_ERRERR => unreachable!(), + _ => unreachable!(), + } }; - let out_file_path = { - let mut path = std::env::temp_dir(); - - let name: String = std::iter::repeat_with(fastrand::alphanumeric) - .take(10) - .collect(); - path.push(name + "-dtmt.luab"); - - path - }; - - fs::write(&in_file_path, code.as_ref().as_bytes()) - .await - .wrap_err_with(|| format!("failed to write file {}", in_file_path.display()))?; - - // TODO: Make executable name configurable - Command::new("luajit") - .arg("-bg") - .arg("-F") - .arg(name.clone() + ".lua") - .arg("-o") - .arg("Windows") - .arg(&in_file_path) - .arg(&out_file_path) - .status() - .await - .wrap_err("failed to compile to LuaJIT byte code")?; - - let mut data = Cursor::new(Vec::new()); - - let bytecode = { - let mut data = fs::read(&out_file_path) - .await - .wrap_err_with(|| format!("failed to read file {}", out_file_path.display()))?; - - // Add Fatshark's custom magic bytes - data[1] = 0x46; - data[2] = 0x53; - data[3] = 0x82; - - data - }; - - data.write_u32(bytecode.len() as u32)?; - // I believe this is supposed to be a uleb128, but it seems to be always 0x2 in binary. - data.write_u64(0x2)?; - data.write_all(&bytecode)?; + // TODO: How are these twelve bytes generated? + let mut data = vec![0x0; 12]; + // Set Fatshark's custom LuaJIT magic bytes + data.extend_from_slice(&[0x1b, 0x46, 0x53, 0x82]); + data.extend_from_slice(&bytecode[4..]); let mut file = BundleFile::new(name, BundleFileType::Lua); let mut variant = BundleFileVariant::new(); - variant.set_data(data.into_inner()); + variant.set_data(data); file.add_variant(variant); Ok(file) -- 2.45.3 From 8b2ca0e45a4c641b977d9158e0891ca0e7ea637b Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 23:00:18 +0100 Subject: [PATCH 11/90] feat(dtmm): Improve logging output format --- crates/dtmm/src/main.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index d73b440..e3e8b29 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -21,17 +21,30 @@ async fn main() -> Result<()> { let _matches = command!().get_matches(); { - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); + if cfg!(debug_assertions) { + let fmt_layer = tracing_subscriber::fmt::layer().pretty(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::new( + tracing_subscriber::fmt::format::Pretty::default(), + )) + .init(); + } else { + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::new( + tracing_subscriber::fmt::format::Pretty::default(), + )) + .init(); + } } let initial_state = State::new(); -- 2.45.3 From 2cda35032c4d5a1c321bdd77b0c7669fceb326ef Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 17 Feb 2023 23:06:10 +0100 Subject: [PATCH 12/90] feat(dtmm): Implement rudimentary mod managing UI --- crates/dtmm/src/main_window.rs | 104 ++++++----- crates/dtmm/src/state.rs | 313 +++++++++++++++++++++++++++------ 2 files changed, 320 insertions(+), 97 deletions(-) diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 0284dd5..b8bf488 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -1,11 +1,14 @@ use druid::im::Vector; use druid::widget::{ Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, - ViewSwitcher, + TextBox, ViewSwitcher, }; use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; -use crate::state::{ModInfo, State, View}; +use crate::state::{ + ModInfo, PathBufFormatter, State, StateController, View, ACTION_DELETE_SELECTED_MOD, + ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, +}; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -67,32 +70,21 @@ fn build_mod_list() -> impl Widget { let list = List::new(|| { Flex::row() .must_fill_main_axis(true) - // .with_child( - // Label::dynamic(|enabled, _env| { - // if *enabled { - // "Enabled".into() - // } else { - // "Disabled".into() - // } - // }) - // .lens( - // lens::Identity - // .map( - // |(i, info)| info, - // |(i, info), new_info| { - // todo!(); - // }, - // ) - // .then(ModInfo::enabled), - // ), - // ) - // .with_child(Label::raw().lens(ModInfo::name)) - .on_click(|_ctx, state, _env| { - todo!(); - }) + .with_child( + Label::dynamic(|enabled, _env| { + if *enabled { + "Enabled".into() + } else { + "Disabled".into() + } + }) + .lens(lens!((usize, ModInfo), 1).then(ModInfo::enabled)), + ) + .with_child(Label::raw().lens(lens!((usize, ModInfo), 1).then(ModInfo::name))) + .on_click(|ctx, (i, _info), _env| ctx.submit_notification(ACTION_SELECT_MOD.with(*i))) }); - Scroll::new(list) + let scroll = Scroll::new(list) .vertical() .lens(State::mods.map( |mods| { @@ -107,7 +99,12 @@ fn build_mod_list() -> impl Widget { }); }, )) - .content_must_fill() + .content_must_fill(); + + Flex::column() + .must_fill_main_axis(true) + .with_child(Flex::row()) + .with_flex_child(scroll, 1.0) } fn build_mod_details() -> impl Widget { @@ -120,23 +117,16 @@ fn build_mod_details() -> impl Widget { }, Flex::column, ) + .padding(Insets::uniform_xy(5.0, 1.0)) .lens(State::selected_mod); let button_move_up = Button::new("Move Up") - .on_click(|_ctx, index: &mut Option, _env| { - if let Some(i) = index.as_mut() { - *i = i.saturating_sub(1) - } - }) - .lens(State::selected_mod_index); + .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_UP)) + .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_up()); let button_move_down = Button::new("Move Down") - .on_click(|_ctx, index: &mut Option, _env| { - if let Some(i) = index.as_mut() { - *i = i.saturating_add(1) - } - }) - .lens(State::selected_mod_index); + .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_DOWN)) + .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_down()); let button_toggle_mod = Maybe::new( || { @@ -144,17 +134,18 @@ fn build_mod_details() -> impl Widget { if *enabled { "Disable Mod".into() } else { - "Enabled Mod".into() + "Enable Mod".into() } }) - .on_click(|_ctx, info: &mut bool, _env| { - *info = !*info; + .on_click(|_ctx, enabled: &mut bool, _env| { + *enabled = !(*enabled); }) .lens(ModInfo::enabled) }, // TODO: Gray out || Button::new("Enable Mod"), ) + .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); let button_add_mod = Button::new("Add Mod").on_click(|_ctx, state: &mut State, _env| { @@ -164,7 +155,9 @@ fn build_mod_details() -> impl Widget { }); let button_delete_mod = Button::new("Delete Mod") - .on_click(|_ctx, data: &mut State, _env| data.delete_selected_mod()); + .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_DELETE_SELECTED_MOD)) + .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) + .lens(State::selected_mod); let buttons = Flex::column() .with_child( @@ -204,7 +197,29 @@ fn build_view_mods() -> impl Widget { } fn build_view_settings() -> impl Widget { - Label::new("Settings") + let game_dir_setting = Flex::row() + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(Label::new("Game Directory:")) + .with_default_spacer() + .with_child( + TextBox::new() + .with_formatter(PathBufFormatter::new()) + .lens(State::game_dir), + ); + let data_dir_setting = Flex::row() + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(Label::new("Data Directory:")) + .with_default_spacer() + .with_child( + TextBox::new() + .with_formatter(PathBufFormatter::new()) + .lens(State::data_dir), + ); + + Flex::column() + .with_child(data_dir_setting) + .with_child(game_dir_setting) + .padding(Insets::uniform(5.0)) } fn build_view_about() -> impl Widget { @@ -233,4 +248,5 @@ fn build_window() -> impl Widget { .must_fill_main_axis(true) .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) + .controller(StateController::new()) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 0639818..c22bdd7 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -1,9 +1,21 @@ +use std::path::PathBuf; use std::sync::Arc; use druid::im::Vector; -use druid::{Data, Lens}; +use druid::text::Formatter; +use druid::widget::Controller; +use druid::{ + AppDelegate, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, + Widget, +}; +use tokio::sync::mpsc::UnboundedSender; -#[derive(Copy, Clone, Data, PartialEq)] +pub const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action..select-mod"); +pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); +pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); +pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); + +#[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { Mods, Settings, @@ -16,20 +28,48 @@ impl Default for View { } } -#[derive(Clone, Data, Lens)] +#[derive(Clone, Data, Debug)] +pub struct PackageInfo { + name: String, + files: Vector, +} + +impl PackageInfo { + pub fn get_name(&self) -> &String { + &self.name + } + + pub fn get_files(&self) -> &Vector { + &self.files + } +} + +#[derive(Clone, Data, Debug, Lens)] pub(crate) struct ModInfo { name: String, description: Arc, enabled: bool, + #[lens(ignore)] + packages: Vector, } + impl ModInfo { pub fn new() -> Self { Self { name: format!("Test Mod: {:?}", std::time::SystemTime::now()), description: Arc::new(String::from("A test dummy")), enabled: false, + packages: Vector::new(), } } + + pub fn get_packages(&self) -> &Vector { + &self.packages + } + + pub(crate) fn get_name(&self) -> &String { + &self.name + } } impl PartialEq for ModInfo { @@ -38,54 +78,15 @@ impl PartialEq for ModInfo { } } -#[derive(Clone, Data, Default, Lens)] +#[derive(Clone, Data, Lens)] pub(crate) struct State { current_view: View, mods: Vector, selected_mod_index: Option, -} - -pub(crate) struct SelectedModLens; - -impl Lens> for SelectedModLens { - fn with) -> V>(&self, data: &State, f: F) -> V { - let info = data - .selected_mod_index - .and_then(|i| data.mods.get(i).cloned()); - - f(&info) - } - - fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { - let mut info = data - .selected_mod_index - .and_then(|i| data.mods.get_mut(i).cloned()); - f(&mut info) - } -} - -/// A Lens that maps an `im::Vector` to `im::Vector<(usize, T)>`, -/// where each element in the destination vector includes its index in the -/// source vector. -pub(crate) struct IndexedVectorLens; - -impl Lens, Vector<(usize, T)>> for IndexedVectorLens { - fn with) -> V>(&self, data: &Vector, f: F) -> V { - let data = data - .iter() - .enumerate() - .map(|(i, val)| (i, val.clone())) - .collect(); - f(&data) - } - - fn with_mut) -> V>( - &self, - data: &mut Vector, - f: F, - ) -> V { - todo!() - } + is_deployment_in_progress: bool, + game_dir: Arc, + data_dir: Arc, + ctx: Arc, } impl State { @@ -93,7 +94,26 @@ impl State { pub const selected_mod: SelectedModLens = SelectedModLens; pub fn new() -> Self { - Default::default() + let ctx = sdk::Context::new(); + + let (game_dir, data_dir) = if cfg!(debug_assertions) { + ( + std::env::current_dir().expect("PWD is borked").join("data"), + PathBuf::from("/tmp/dtmm"), + ) + } else { + (PathBuf::new(), PathBuf::new()) + }; + + Self { + ctx: Arc::new(ctx), + current_view: View::default(), + mods: Vector::new(), + selected_mod_index: None, + is_deployment_in_progress: false, + game_dir: Arc::new(game_dir), + data_dir: Arc::new(data_dir), + } } pub fn get_current_view(&self) -> View { @@ -104,16 +124,203 @@ impl State { self.current_view = view; } - pub fn delete_selected_mod(&mut self) { - let Some(index) = self.selected_mod_index else { - return; - }; + pub fn get_mods(&self) -> Vector { + self.mods.clone() + } - self.mods.remove(index); + pub fn select_mod(&mut self, index: usize) { + self.selected_mod_index = Some(index); } pub fn add_mod(&mut self, info: ModInfo) { self.mods.push_back(info); self.selected_mod_index = Some(self.mods.len() - 1); } + + pub fn can_move_mod_down(&self) -> bool { + self.selected_mod_index + .map(|i| i < (self.mods.len().saturating_sub(1))) + .unwrap_or(false) + } + + pub fn can_move_mod_up(&self) -> bool { + self.selected_mod_index.map(|i| i > 0).unwrap_or(false) + } + + pub(crate) fn get_game_dir(&self) -> &PathBuf { + &self.game_dir + } + + pub(crate) fn get_mod_dir(&self) -> PathBuf { + self.data_dir.join("mods") + } + + pub(crate) fn get_ctx(&self) -> Arc { + self.ctx.clone() + } +} + +pub(crate) struct SelectedModLens; + +impl Lens> for SelectedModLens { + #[tracing::instrument(name = "SelectedModLens::with", skip_all)] + fn with) -> V>(&self, data: &State, f: F) -> V { + let info = data + .selected_mod_index + .and_then(|i| data.mods.get(i).cloned()); + + f(&info) + } + + #[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)] + fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { + match data.selected_mod_index { + Some(i) => { + let mut info = data.mods.get_mut(i).cloned(); + let ret = f(&mut info); + + if let Some(info) = info { + // TODO: Figure out a way to check for equality and + // only update when needed + data.mods.set(i, info); + } else { + data.selected_mod_index = None; + } + + ret + } + None => f(&mut None), + } + } +} + +/// A Lens that maps an `im::Vector` to `im::Vector<(usize, T)>`, +/// where each element in the destination vector includes its index in the +/// source vector. +pub(crate) struct IndexedVectorLens; + +impl Lens, Vector<(usize, T)>> for IndexedVectorLens { + #[tracing::instrument(name = "IndexedVectorLens::with", skip_all)] + fn with) -> V>(&self, values: &Vector, f: F) -> V { + let indexed = values + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect(); + f(&indexed) + } + + #[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)] + fn with_mut) -> V>( + &self, + values: &mut Vector, + f: F, + ) -> V { + let mut indexed = values + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect(); + let ret = f(&mut indexed); + + *values = indexed.into_iter().map(|(_i, val)| val).collect(); + + ret + } +} + +pub struct StateController {} + +impl StateController { + pub fn new() -> Self { + Self {} + } +} + +// TODO: Turn notifications into commands on the AppDelegate +impl> Controller for StateController { + #[tracing::instrument(name = "StateController::event", skip_all)] + fn event( + &mut self, + child: &mut W, + ctx: &mut EventCtx, + event: &Event, + state: &mut State, + env: &Env, + ) { + match event { + Event::Notification(notif) if notif.is(ACTION_SELECT_MOD) => { + ctx.set_handled(); + let index = notif + .get(ACTION_SELECT_MOD) + .expect("notification type didn't match after check"); + + state.select_mod(*index); + } + Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_UP) => { + ctx.set_handled(); + let Some(i) = state.selected_mod_index else { + return; + }; + + let len = state.mods.len(); + if len == 0 || i == 0 { + return; + } + + state.mods.swap(i, i - 1); + state.selected_mod_index = Some(i - 1); + } + Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_DOWN) => { + ctx.set_handled(); + let Some(i) = state.selected_mod_index else { + return; + }; + + let len = state.mods.len(); + if len == 0 || i == usize::MAX || i >= len - 1 { + return; + } + + state.mods.swap(i, i + 1); + state.selected_mod_index = Some(i + 1); + } + Event::Notification(notif) if notif.is(ACTION_DELETE_SELECTED_MOD) => { + ctx.set_handled(); + let Some(index) = state.selected_mod_index else { + return; + }; + + state.mods.remove(index); + } + _ => child.event(ctx, event, state, env), + } + } +} + +pub(crate) struct PathBufFormatter; + +impl PathBufFormatter { + pub fn new() -> Self { + Self {} + } +} + +impl Formatter> for PathBufFormatter { + fn format(&self, value: &Arc) -> String { + value.display().to_string() + } + + fn validate_partial_input( + &self, + _input: &str, + _sel: &druid::text::Selection, + ) -> druid::text::Validation { + druid::text::Validation::success() + } + + fn value(&self, input: &str) -> Result, druid::text::ValidationError> { + let p = PathBuf::from(input); + Ok(Arc::new(p)) + } } -- 2.45.3 From cb9f154f1eed7397ee899078cf47ad7ce9504475 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 10:18:53 +0100 Subject: [PATCH 13/90] fix(sdk): Fix lua file compilation Aussiemon provided the last missing piece of information about the binary format. --- lib/sdk/src/filetype/lua.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/sdk/src/filetype/lua.rs b/lib/sdk/src/filetype/lua.rs index 3ce8bdd..ea072b6 100644 --- a/lib/sdk/src/filetype/lua.rs +++ b/lib/sdk/src/filetype/lua.rs @@ -1,10 +1,12 @@ use std::ffi::CStr; use std::io::Cursor; +use std::io::Write; use color_eyre::eyre; use color_eyre::Result; use luajit2_sys as lua; +use crate::binary::sync::WriteExt; use crate::bundle::file::{BundleFileVariant, UserFile}; use crate::{BundleFile, BundleFileType}; @@ -83,16 +85,19 @@ where } }; - // TODO: How are these twelve bytes generated? - let mut data = vec![0x0; 12]; - // Set Fatshark's custom LuaJIT magic bytes - data.extend_from_slice(&[0x1b, 0x46, 0x53, 0x82]); - data.extend_from_slice(&bytecode[4..]); + let mut data = Cursor::new(Vec::with_capacity(bytecode.len() + 12)); + data.write_u32(bytecode.len() as u32)?; + // TODO: Figure out what these two values are + data.write_u32(0x2)?; + data.write_u32(0x0)?; + // Use Fatshark's custom magic bytes + data.write_all(&[0x1b, 0x46, 0x53, 0x82])?; + data.write_all(&bytecode[4..])?; let mut file = BundleFile::new(name, BundleFileType::Lua); let mut variant = BundleFileVariant::new(); - variant.set_data(data); + variant.set_data(data.into_inner()); file.add_variant(variant); Ok(file) -- 2.45.3 From e65579d8aa27b6326a7d235c9d7efc1e112bf44e Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 10:20:10 +0100 Subject: [PATCH 14/90] feat(dtmm): Implement rudimentary mod deployment --- Cargo.lock | 4 + crates/dtmm/Cargo.toml | 8 +- crates/dtmm/src/controller.rs | 47 ++++ crates/dtmm/src/engine.rs | 373 +++++++++++++++++++++++++ crates/dtmm/src/main.rs | 77 ++++- crates/dtmm/src/main_window.rs | 14 +- crates/dtmm/src/state.rs | 55 +++- crates/dtmm/src/widget/table_select.rs | 73 +++++ lib/sdk/src/bundle/database.rs | 5 +- lib/sdk/src/bundle/file.rs | 8 +- lib/sdk/src/bundle/mod.rs | 4 - lib/sdk/src/filetype/package.rs | 8 + lib/sdk/src/murmur/mod.rs | 6 + 13 files changed, 660 insertions(+), 22 deletions(-) create mode 100644 crates/dtmm/src/controller.rs create mode 100644 crates/dtmm/src/engine.rs create mode 100644 crates/dtmm/src/widget/table_select.rs diff --git a/Cargo.lock b/Cargo.lock index ef8b80e..8e9ca57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,12 +658,16 @@ dependencies = [ name = "dtmm" version = "0.1.0" dependencies = [ + "bitflags", "clap", "color-eyre", "confy", "druid", + "futures", + "oodle-sys", "sdk", "serde", + "serde_sjson", "tokio", "toml", "tracing", diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 64361c4..006aa9e 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -6,13 +6,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitflags = "1.3.2" clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } color-eyre = "0.6.2" confy = "0.5.1" druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } +futures = "0.3.25" sdk = { path = "../../lib/sdk", version = "0.2.0" } -serde = "1.0.152" -tokio = "1.23.0" +serde = { version = "1.0.152", features = ["derive"] } +serde_sjson = { path = "../../lib/serde_sjson", version = "*" } +oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } toml = "0.5.10" tracing = "0.1.37" tracing-error = "0.2.0" diff --git a/crates/dtmm/src/controller.rs b/crates/dtmm/src/controller.rs new file mode 100644 index 0000000..a2bf429 --- /dev/null +++ b/crates/dtmm/src/controller.rs @@ -0,0 +1,47 @@ +use druid::widget::{Button, Controller}; +use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, UpdateCtx, Widget}; + +pub struct DisabledButtonController; + +impl Controller> for DisabledButtonController { + fn event( + &mut self, + child: &mut Button, + ctx: &mut EventCtx, + event: &Event, + data: &mut T, + env: &Env, + ) { + if !ctx.is_disabled() { + ctx.set_disabled(true); + ctx.request_paint(); + } + child.event(ctx, event, data, env) + } + + fn lifecycle( + &mut self, + child: &mut Button, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &T, + env: &Env, + ) { + child.lifecycle(ctx, event, data, env) + } + + fn update( + &mut self, + child: &mut Button, + ctx: &mut UpdateCtx, + old_data: &T, + data: &T, + env: &Env, + ) { + if !ctx.is_disabled() { + ctx.set_disabled(true); + ctx.request_paint(); + } + child.update(ctx, old_data, data, env) + } +} diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs new file mode 100644 index 0000000..818bd8f --- /dev/null +++ b/crates/dtmm/src/engine.rs @@ -0,0 +1,373 @@ +use std::ffi::CString; +use std::io::{Cursor, ErrorKind}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; + +use color_eyre::eyre::Context; +use color_eyre::{eyre, Result}; +use futures::stream; +use futures::StreamExt; +use sdk::filetype::lua; +use sdk::filetype::package::Package; +use sdk::murmur::Murmur64; +use sdk::{ + Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, +}; +use tokio::io::AsyncWriteExt; +use tokio::{fs, try_join}; +use tracing::Instrument; + +use crate::state::{PackageInfo, State}; + +const MOD_BUNDLE_NAME: &str = "packages/mods"; +const BOOT_BUNDLE_NAME: &str = "packages/boot"; +const BUNDLE_DATABASE_NAME: &str = "bundle_database.data"; +const MOD_BOOT_SCRIPT: &str = "scripts/mod_main"; + +#[tracing::instrument] +async fn read_file_with_backup

(path: P) -> Result> +where + P: AsRef + std::fmt::Debug, +{ + let path = path.as_ref(); + let backup_path = { + let mut p = PathBuf::from(path); + let ext = if let Some(ext) = p.extension() { + ext.to_string_lossy().to_string() + ".bak" + } else { + String::from("bak") + }; + p.set_extension(ext); + p + }; + + let file_name = path + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| String::from("file")); + + let bin = match fs::read(&backup_path).await { + Ok(bin) => bin, + Err(err) if err.kind() == ErrorKind::NotFound => { + // TODO: This doesn't need to be awaited here, yet. + // I only need to make sure it has finished before writing the changed bundle. + tracing::debug!( + "Backup does not exist. Backing up original {} to '{}'", + file_name, + backup_path.display() + ); + fs::copy(path, &backup_path).await.wrap_err_with(|| { + format!( + "failed to back up {} '{}' to '{}'", + file_name, + path.display(), + backup_path.display() + ) + })?; + + tracing::debug!("Reading {} from original '{}'", file_name, path.display()); + fs::read(path).await.wrap_err_with(|| { + format!("failed to read {} file: {}", file_name, path.display()) + })? + } + Err(err) => { + return Err(err).wrap_err_with(|| { + format!( + "failed to read {} from backup '{}'", + file_name, + backup_path.display() + ) + }); + } + }; + Ok(bin) +} + +#[tracing::instrument(skip_all)] +async fn patch_game_settings(state: Arc) -> Result<()> { + let settings_path = state + .get_game_dir() + .join("bundle/application_settings/settings_common.ini"); + + let settings = read_file_with_backup(&settings_path) + .await + .wrap_err("failed to read settings.ini")?; + let settings = String::from_utf8(settings).wrap_err("settings.ini is not valid UTF-8")?; + + let mut f = fs::File::create(&settings_path) + .await + .wrap_err_with(|| format!("failed to open {}", settings_path.display()))?; + + let Some(i) = settings.find("boot_script =") else { + eyre::bail!("couldn't find 'boot_script' field"); + }; + + f.write_all(settings[0..i].as_bytes()).await?; + f.write_all(b"boot_script = \"scripts/mod_main\"").await?; + + let Some(j) = settings[i..].find('\n') else { + eyre::bail!("couldn't find end of 'boot_script' field"); + }; + + f.write_all(settings[(i + j)..].as_bytes()).await?; + + tracing::info!("Patched game settings"); + Ok(()) +} + +#[tracing::instrument(skip_all, fields(package = info.get_name()))] +fn make_package(info: &PackageInfo) -> Result { + let mut pkg = Package::new(info.get_name().clone(), PathBuf::new()); + + for f in info.get_files().iter() { + let mut it = f.rsplit('.'); + let file_type = it + .next() + .ok_or_else(|| eyre::eyre!("missing file extension")) + .and_then(BundleFileType::from_str) + .wrap_err("invalid file name in package info")?; + let name: String = it.collect(); + pkg.add_file(file_type, name); + } + + Ok(pkg) +} + +#[tracing::instrument(skip_all)] +async fn build_bundles(state: Arc) -> Result<()> { + let mut bundle = Bundle::new(MOD_BUNDLE_NAME.into()); + let mut tasks = Vec::new(); + + let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); + + let mut db = { + let bin = read_file_with_backup(&database_path) + .await + .wrap_err("failed to read bundle database")?; + let mut r = Cursor::new(bin); + let db = BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database")?; + tracing::trace!("Finished parsing bundle database"); + db + }; + + for mod_info in state.get_mods() { + let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); + let _enter = span.enter(); + + let mod_dir = state.get_mod_dir().join(mod_info.get_name()); + for pkg_info in mod_info.get_packages() { + let span = tracing::trace_span!("building package", name = pkg_info.get_name()); + let _enter = span.enter(); + + let pkg = make_package(pkg_info).wrap_err("failed to make package")?; + let mut variant = BundleFileVariant::new(); + let bin = pkg + .to_binary() + .wrap_err("failed to serialize package to binary")?; + variant.set_data(bin); + let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package); + file.add_variant(variant); + + bundle.add_file(file); + + let src = mod_dir.join(pkg_info.get_name()); + let dest = bundle_dir.clone(); + let pkg_name = pkg_info.get_name().clone(); + let mod_name = mod_info.get_name().clone(); + + tracing::trace!( + "Adding package {} for mod {} to bundle database", + pkg_info.get_name(), + mod_info.get_name() + ); + + // Explicitely drop the guard, so that we can move the span + // into the async operation + drop(_enter); + + let task = async move { + tracing::debug!( + "Copying bundle {} for mod {}: {} -> {}", + pkg_name, + mod_name, + src.display(), + dest.display() + ); + fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| { + format!("failed to hard link bundle {pkg_name} for mod {mod_name}") + }) + } + .instrument(span); + + tasks.push(task); + } + } + + tracing::debug!("Copying {} mod bundles", tasks.len()); + + let mut tasks = stream::iter(tasks).buffer_unordered(10); + + while let Some(res) = tasks.next().await { + res?; + } + + db.add_bundle(&bundle); + + { + let path = bundle_dir.join(format!("{:x}", Murmur64::hash(bundle.name()))); + tracing::trace!("Writing mod bundle to '{}'", path.display()); + fs::write(&path, bundle.to_binary()?) + .await + .wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; + } + + { + tracing::trace!("Writing bundle database to '{}'", database_path.display()); + let bin = db + .to_binary() + .wrap_err("failed to serialize bundle database")?; + fs::write(&database_path, bin).await.wrap_err_with(|| { + format!( + "failed to write bundle database to '{}'", + database_path.display() + ) + })?; + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +async fn patch_boot_bundle(state: Arc) -> Result<()> { + let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + + let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); + let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); + + let (mut db, mut bundle) = try_join!( + async { + let bin = read_file_with_backup(&database_path) + .await + .wrap_err("failed to read bundle database")?; + let mut r = Cursor::new(bin); + + BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database") + } + .instrument(tracing::trace_span!("read bundle database")), + async { + let bin = read_file_with_backup(&bundle_path) + .await + .wrap_err("failed to read boot bundle")?; + + Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin) + .wrap_err("failed to parse boot bundle") + } + .instrument(tracing::trace_span!("read boot bundle")) + )?; + + { + tracing::trace!("Adding mod package file to boot bundle"); + let span = tracing::trace_span!("create mod package file"); + let _enter = span.enter(); + + let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new()); + + for mod_info in state.get_mods() { + for pkg_info in mod_info.get_packages() { + pkg.add_file(BundleFileType::Package, pkg_info.get_name()); + } + } + + let mut variant = BundleFileVariant::new(); + variant.set_data(pkg.to_binary()?); + let mut f = BundleFile::new(MOD_BUNDLE_NAME.to_string(), BundleFileType::Package); + f.add_variant(variant); + + bundle.add_file(f); + } + + { + tracing::trace!("Adding main mod Lua file to boot bundle"); + let span = tracing::trace_span!("create mod boot script file"); + let _enter = span.enter(); + + // TODO: Build actual boot script + let lua = CString::new( + r#" +print("dtmm says hello!") +require("scripts/main") +"#, + ) + .expect("invalid C string"); + let f = lua::compile(MOD_BOOT_SCRIPT.to_string(), &lua) + .wrap_err("failed to compile mod boot script")?; + + // TODO: + bundle.add_file(f); + } + + db.add_bundle(&bundle); + + try_join!( + async { + let bin = bundle + .to_binary() + .wrap_err("failed to serialize boot bundle")?; + fs::write(&bundle_path, bin) + .await + .wrap_err_with(|| format!("failed to write main bundle: {}", bundle_path.display())) + } + .instrument(tracing::trace_span!("write boot bundle")), + async { + let bin = db + .to_binary() + .wrap_err("failed to serialize bundle database")?; + fs::write(&database_path, bin).await.wrap_err_with(|| { + format!( + "failed to write bundle database to '{}'", + database_path.display() + ) + }) + } + .instrument(tracing::trace_span!("write bundle database")) + )?; + + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + game_dir = %state.get_game_dir().display(), + mods = state.get_mods().len() +))] +pub(crate) async fn deploy_mods(state: State) -> Result<()> { + let state = Arc::new(state); + + tracing::info!( + "Deploying {} mods to {}", + state.get_mods().len(), + state.get_game_dir().join("bundle").display() + ); + + tracing::info!("Build mod bundles"); + build_bundles(state.clone()) + .await + .wrap_err("failed to build mod bundles")?; + + tracing::info!("Patch boot bundle"); + patch_boot_bundle(state.clone()) + .await + .wrap_err("failed to patch boot bundle")?; + + tracing::info!("Patch game settings"); + patch_game_settings(state.clone()) + .await + .wrap_err("failed to patch game settings")?; + + // TODO: Build mod order data + // TODO: Handle DMF + + tracing::info!("Finished deploying mods"); + Ok(()) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index e3e8b29..300c6cd 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,24 +1,73 @@ +#![recursion_limit = "256"] +#![feature(let_chains)] + +use std::sync::Arc; + use clap::command; +use clap::Arg; use color_eyre::Report; use color_eyre::Result; use druid::AppLauncher; +use druid::ExtEventSink; +use druid::Target; +use tokio::runtime::Runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; -use crate::state::State; +use crate::engine::deploy_mods; +use crate::state::{AsyncAction, Delegate, State, COMMAND_FINISH_DEPLOY}; +mod controller; +mod engine; mod main_window; mod state; mod theme; mod widget; +fn work_thread( + event_sink: Arc>, + action_queue: Arc>>, +) -> Result<()> { + let rt = Runtime::new()?; + + rt.block_on(async { + while let Some(action) = action_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + match action { + AsyncAction::DeployMods(state) => tokio::spawn(async move { + if let Err(err) = deploy_mods(state).await { + tracing::error!("Failed to deploy mods: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(COMMAND_FINISH_DEPLOY, (), Target::Auto) + .expect("failed to send command"); + }), + }; + } + }); + + Ok(()) +} + #[tracing::instrument] #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; - let _matches = command!().get_matches(); + let matches = command!() + .arg(Arg::new("oodle").long("oodle").help( + "The oodle library to load. This may either be:\n\ + - A library name that will be searched for in the system's default paths.\n\ + - A file path relative to the current working directory.\n\ + - An absolute file path.", + )) + .get_matches(); { let filter_layer = @@ -47,9 +96,27 @@ async fn main() -> Result<()> { } } + unsafe { + oodle_sys::init(matches.get_one::("oodle")); + } + let initial_state = State::new(); - AppLauncher::with_window(main_window::new()) - .launch(initial_state) - .map_err(Report::new) + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let delegate = Delegate::new(sender); + + let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); + + let event_sink = launcher.get_external_handle(); + std::thread::spawn(move || { + let event_sink = Arc::new(RwLock::new(event_sink)); + let receiver = Arc::new(RwLock::new(receiver)); + loop { + if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) { + tracing::error!("Work thread failed, restarting: {:?}", err); + } + } + }); + + launcher.launch(initial_state).map_err(Report::new) } diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index b8bf488..3dd37f0 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -7,7 +7,7 @@ use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; use crate::state::{ ModInfo, PathBufFormatter, State, StateController, View, ACTION_DELETE_SELECTED_MOD, - ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, + ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, COMMAND_START_DEPLOY, }; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -48,11 +48,13 @@ fn build_top_bar() -> impl Widget { ) .with_child( Flex::row() - .with_child(Button::new("Deploy Mods").on_click( - |_ctx, _state: &mut State, _env| { - todo!(); - }, - )) + .with_child( + Button::new("Deploy Mods") + .on_click(|ctx, _state: &mut State, _env| { + ctx.submit_command(COMMAND_START_DEPLOY); + }) + .disabled_if(|data, _| !data.can_deploy_mods()), + ) .with_default_spacer() .with_child( Button::new("Run Game").on_click(|_ctx, _state: &mut State, _env| { diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index c22bdd7..c963515 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -5,7 +5,7 @@ use druid::im::Vector; use druid::text::Formatter; use druid::widget::Controller; use druid::{ - AppDelegate, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, + AppDelegate, Command, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, Widget, }; use tokio::sync::mpsc::UnboundedSender; @@ -15,6 +15,9 @@ pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); +pub const COMMAND_FINISH_DEPLOY: Selector = Selector::new("dtmm.command.finish-deploy"); +pub const COMMAND_START_DEPLOY: Selector = Selector::new("dtmm.command.start-deploy"); + #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { Mods, @@ -147,6 +150,10 @@ impl State { self.selected_mod_index.map(|i| i > 0).unwrap_or(false) } + pub fn can_deploy_mods(&self) -> bool { + !self.is_deployment_in_progress + } + pub(crate) fn get_game_dir(&self) -> &PathBuf { &self.game_dir } @@ -298,6 +305,52 @@ impl> Controller for StateController { } } +pub(crate) enum AsyncAction { + DeployMods(State), +} + +pub(crate) struct Delegate { + sender: UnboundedSender, +} + +impl Delegate { + pub fn new(sender: UnboundedSender) -> Self { + Self { sender } + } +} + +impl AppDelegate for Delegate { + #[tracing::instrument(name = "Delegate", skip_all)] + fn command( + &mut self, + _ctx: &mut DelegateCtx, + _target: Target, + cmd: &Command, + data: &mut State, + _env: &Env, + ) -> Handled { + if cmd.is(COMMAND_START_DEPLOY) { + if self + .sender + .send(AsyncAction::DeployMods(data.clone())) + .is_ok() + { + data.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } else if cmd.is(COMMAND_FINISH_DEPLOY) { + data.is_deployment_in_progress = false; + Handled::Yes + } else { + tracing::debug!("Unknown command: {:?}", cmd); + Handled::No + } + } +} + pub(crate) struct PathBufFormatter; impl PathBufFormatter { diff --git a/crates/dtmm/src/widget/table_select.rs b/crates/dtmm/src/widget/table_select.rs new file mode 100644 index 0000000..00321f8 --- /dev/null +++ b/crates/dtmm/src/widget/table_select.rs @@ -0,0 +1,73 @@ +use druid::widget::{Controller, Flex}; +use druid::{Data, Widget}; + +pub struct TableSelect { + widget: Flex, + controller: TableSelectController, +} + +impl TableSelect { + pub fn new(values: impl IntoIterator + 'static)>) -> Self { + todo!(); + } +} + +impl Widget for TableSelect { + fn event( + &mut self, + ctx: &mut druid::EventCtx, + event: &druid::Event, + data: &mut T, + env: &druid::Env, + ) { + todo!() + } + + fn lifecycle( + &mut self, + ctx: &mut druid::LifeCycleCtx, + event: &druid::LifeCycle, + data: &T, + env: &druid::Env, + ) { + todo!() + } + + fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, env: &druid::Env) { + todo!() + } + + fn layout( + &mut self, + ctx: &mut druid::LayoutCtx, + bc: &druid::BoxConstraints, + data: &T, + env: &druid::Env, + ) -> druid::Size { + todo!() + } + + fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, env: &druid::Env) { + todo!() + } +} + +struct TableSelectController { + inner: T, +} + +impl TableSelectController {} + +impl Controller> for TableSelectController {} + +pub struct TableItem { + inner: dyn Widget, +} + +impl TableItem { + pub fn new(inner: impl Widget) -> Self { + todo!(); + } +} + +impl Widget for TableItem {} diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index b3d9296..8438b40 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -50,14 +50,15 @@ impl BundleDatabase { self.stored_files.entry(hash).or_default().push(file); - // TODO: Resource hashes - for f in bundle.files() { let file_name = FileName { extension: f.file_type(), name: Murmur64::hash(f.name(false, None).as_bytes()), }; + // TODO: Compute actual resource hash + self.resource_hashes.insert(hash, 0); + self.bundle_contents .entry(hash) .or_default() diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index 2efa9b2..a5ed380 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -12,8 +12,6 @@ use crate::binary::sync::*; use crate::filetype::*; use crate::murmur::{HashGroup, IdString64, Murmur64}; -use super::EntryHeader; - #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub enum BundleFileType { Animation, @@ -801,6 +799,12 @@ impl BundleFile { } } +impl PartialEq for BundleFile { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.file_type == other.file_type + } +} + pub struct UserFile { // TODO: Might be able to avoid some allocations with a Cow here data: Vec, diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 9e1a699..ce3a23f 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -91,10 +91,6 @@ impl Bundle { } } - pub fn get_file>(&self, name: S) -> Option<&BundleFile> { - self.files.iter().find(|f| f.base_name().eq(name.as_ref())) - } - #[tracing::instrument(skip(ctx, binary), fields(len_binary = binary.as_ref().len()))] pub fn from_binary(ctx: &crate::Context, name: String, binary: B) -> Result where diff --git a/lib/sdk/src/filetype/package.rs b/lib/sdk/src/filetype/package.rs index 8b42116..2dc9c3c 100644 --- a/lib/sdk/src/filetype/package.rs +++ b/lib/sdk/src/filetype/package.rs @@ -104,6 +104,14 @@ impl DerefMut for Package { } impl Package { + pub fn new(name: String, root: PathBuf) -> Self { + Self { + _name: name, + _root: root, + inner: Default::default(), + } + } + fn len(&self) -> usize { self.values().fold(0, |total, files| total + files.len()) } diff --git a/lib/sdk/src/murmur/mod.rs b/lib/sdk/src/murmur/mod.rs index d054b48..9ea432d 100644 --- a/lib/sdk/src/murmur/mod.rs +++ b/lib/sdk/src/murmur/mod.rs @@ -66,6 +66,12 @@ impl fmt::UpperHex for Murmur64 { } } +impl fmt::LowerHex for Murmur64 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + impl fmt::Display for Murmur64 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) -- 2.45.3 From b8804409a375947ad8ce3291d192d940e1dc59af Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 10:46:05 +0100 Subject: [PATCH 15/90] chore: Update crates --- Cargo.lock | 384 ++++++++++++++++++++++++----------------- crates/dtmm/Cargo.toml | 1 - 2 files changed, 222 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e9ca57..646f90e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "arrayvec" @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" +checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832" dependencies = [ "memchr", "once_cell", @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytecount" @@ -169,9 +169,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "bzip2" @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -254,9 +254,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.32" +version = "4.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" +checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" dependencies = [ "bitflags", "clap_derive", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.21" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" dependencies = [ "heck", "proc-macro-error", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "4.4.2" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", "str-buf", @@ -591,8 +591,8 @@ dependencies = [ [[package]] name = "druid" -version = "0.7.0" -source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +version = "0.8.2" +source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" dependencies = [ "console_error_panic_hook", "druid-derive", @@ -613,8 +613,8 @@ dependencies = [ [[package]] name = "druid-derive" -version = "0.4.0" -source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +version = "0.5.0" +source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" dependencies = [ "proc-macro2", "quote", @@ -623,8 +623,8 @@ dependencies = [ [[package]] name = "druid-shell" -version = "0.7.0" -source = "git+https://github.com/linebender/druid.git#23963d38fdb85761f3a5d1918246958dee3375ad" +version = "0.8.0" +source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" dependencies = [ "anyhow", "bitflags", @@ -641,7 +641,6 @@ dependencies = [ "instant", "js-sys", "keyboard-types", - "kurbo", "objc", "once_cell", "piet-common", @@ -669,7 +668,6 @@ dependencies = [ "serde", "serde_sjson", "tokio", - "toml", "tracing", "tracing-error", "tracing-subscriber", @@ -766,22 +764,22 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fd-lock" -version = "3.0.8" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb21c69b9fea5e15dbc1049e4b77145dd0ba1c84019c488102de0dc4ea4b0a27" +checksum = "8ef1a30ae415c3a691a4f41afddc2dbcd6d70baf338368d85ebc1e8ed92cedb9" dependencies = [ "cfg-if", "rustix", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -867,9 +865,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" dependencies = [ "futures-channel", "futures-core", @@ -882,9 +880,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" dependencies = [ "futures-core", "futures-sink", @@ -892,15 +890,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" dependencies = [ "futures-core", "futures-task", @@ -909,15 +907,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", @@ -926,21 +924,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ "futures-channel", "futures-core", @@ -1036,9 +1034,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "gio" @@ -1193,10 +1191,16 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.4.0" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -1207,6 +1211,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hmac" version = "0.12.1" @@ -1236,6 +1246,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -1269,24 +1289,24 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] name = "is-terminal" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -1306,9 +1326,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -1399,9 +1419,9 @@ dependencies = [ [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" @@ -1435,14 +1455,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -1475,19 +1495,28 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.2" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] -name = "nom_locate" -version = "4.0.0" +name = "nom8" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom_locate" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e299bf5ea7b212e811e71174c5d1a5d065c4c0ad0c8691ecb1f97e3e66025e" dependencies = [ "bytecount", "memchr", @@ -1510,7 +1539,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1525,18 +1554,18 @@ dependencies = [ [[package]] name = "object" -version = "0.30.1" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d864c91689fdc196779b98dba0aceac6118594c2df6ee5d943eb6a8df4d107a" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "oodle-sys" @@ -1649,9 +1678,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.5.3" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" +checksum = "028accff104c4e513bad663bbcd2ad7cfd5304144404c31ed0a77ac103d00660" dependencies = [ "thiserror", "ucd-trie", @@ -1659,9 +1688,9 @@ dependencies = [ [[package]] name = "piet" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd2a0027c9ea84b51f42ea3148864e6fc58970ad927e78390d5f9cf7cfa2c78" +checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1" dependencies = [ "kurbo", "unic-bidi", @@ -1669,9 +1698,9 @@ dependencies = [ [[package]] name = "piet-cairo" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b98e21ec69eefb09d34e34050beb700749547dfc5da7a6cff27089dd59af1b3" +checksum = "12dc0b38ac300c79deb9bfc8c7f91a08f2b080338648f7202981094b22321bb9" dependencies = [ "cairo-rs", "pango", @@ -1683,9 +1712,9 @@ dependencies = [ [[package]] name = "piet-common" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7285cc595907be29db4d7f3fb331ae273250365cebf536a4a7b1dafc62978e" +checksum = "9dd8497cc0bcfecb1e14e027428c5e3eaf9af6e14761176e1212006d8bdba387" dependencies = [ "cairo-rs", "cairo-sys-rs", @@ -1702,9 +1731,9 @@ dependencies = [ [[package]] name = "piet-coregraphics" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32553fa0566ed2f23dadec1078a1e6a5930672f350db54d4267e9358f578ab4f" +checksum = "a819b41d2ddb1d8abf3e45e49422f866cba281b4abb5e2fb948bba06e2c3d3f7" dependencies = [ "associative-cache", "core-foundation", @@ -1717,9 +1746,9 @@ dependencies = [ [[package]] name = "piet-direct2d" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d65edc9fa5d1710daa2e55e9cc7b65e65794304ffa870a565fb7128f3110ac5" +checksum = "dd00e91df4f987be40eb13042afe6ee9e54468466bdb7486390b40d4fef0993e" dependencies = [ "associative-cache", "dwrote", @@ -1731,9 +1760,9 @@ dependencies = [ [[package]] name = "piet-web" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303452b9813560d4e4ea37b5792b18578b12f1e47111f1aea1a4cb9392f6fa95" +checksum = "3a560232a94e535979923d49062d1c6d5407b3804bcd0d0b4cb9e25a9b41db1e" dependencies = [ "js-sys", "piet", @@ -1763,13 +1792,12 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] @@ -1798,9 +1826,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] @@ -1870,9 +1898,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -1926,16 +1954,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.6" +version = "0.36.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -2085,9 +2113,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -2184,9 +2212,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2213,23 +2241,22 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" dependencies = [ - "itoa", "serde", "time-core", - "time-macros", ] [[package]] @@ -2238,29 +2265,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" -[[package]] -name = "time-macros" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" -dependencies = [ - "time-core", -] - [[package]] name = "tinystr" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aeafdfd935e4a7fe16a91ab711fa52d54df84f9c8f7ca5837a9d1d902ef4c2" +checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" dependencies = [ "displaydoc", ] [[package]] name = "tokio" -version = "1.24.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" dependencies = [ "autocfg", "bytes", @@ -2272,7 +2290,7 @@ dependencies = [ "signal-hook-registry", "tokio-macros", "tracing", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2299,13 +2317,30 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + [[package]] name = "tracing" version = "0.1.37" @@ -2496,9 +2531,9 @@ checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" @@ -2544,9 +2579,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2554,9 +2589,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", @@ -2569,9 +2604,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2579,9 +2614,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", @@ -2592,15 +2627,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", @@ -2653,46 +2688,70 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "wio" @@ -2711,9 +2770,9 @@ checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "zip" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" dependencies = [ "aes", "byteorder", @@ -2750,10 +2809,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.4+zstd.1.5.2" +version = "2.0.7+zstd.1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" +checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 006aa9e..c554118 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -17,7 +17,6 @@ serde = { version = "1.0.152", features = ["derive"] } serde_sjson = { path = "../../lib/serde_sjson", version = "*" } oodle-sys = { path = "../../lib/oodle-sys", version = "*" } tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } -toml = "0.5.10" tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } -- 2.45.3 From 4b7f12e487d75055338b645363904fc1a4491cde Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 10:56:20 +0100 Subject: [PATCH 16/90] refactor(dtmm): Use commands instead of notifications --- crates/dtmm/src/main.rs | 4 +- crates/dtmm/src/main_window.rs | 23 ++--- crates/dtmm/src/state.rs | 173 +++++++++++++++------------------ 3 files changed, 90 insertions(+), 110 deletions(-) diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 300c6cd..ba3548a 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -18,7 +18,7 @@ use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; use crate::engine::deploy_mods; -use crate::state::{AsyncAction, Delegate, State, COMMAND_FINISH_DEPLOY}; +use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY}; mod controller; mod engine; @@ -45,7 +45,7 @@ fn work_thread( event_sink .write() .await - .submit_command(COMMAND_FINISH_DEPLOY, (), Target::Auto) + .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .expect("failed to send command"); }), }; diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 3dd37f0..040d796 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -5,9 +5,10 @@ use druid::widget::{ }; use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; +use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_ADD_MOD}; use crate::state::{ - ModInfo, PathBufFormatter, State, StateController, View, ACTION_DELETE_SELECTED_MOD, - ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, COMMAND_START_DEPLOY, + ACTION_DELETE_SELECTED_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, + ACTION_SELECT_MOD, ACTION_START_DEPLOY, }; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -51,7 +52,7 @@ fn build_top_bar() -> impl Widget { .with_child( Button::new("Deploy Mods") .on_click(|ctx, _state: &mut State, _env| { - ctx.submit_command(COMMAND_START_DEPLOY); + ctx.submit_command(ACTION_START_DEPLOY); }) .disabled_if(|data, _| !data.can_deploy_mods()), ) @@ -83,7 +84,7 @@ fn build_mod_list() -> impl Widget { .lens(lens!((usize, ModInfo), 1).then(ModInfo::enabled)), ) .with_child(Label::raw().lens(lens!((usize, ModInfo), 1).then(ModInfo::name))) - .on_click(|ctx, (i, _info), _env| ctx.submit_notification(ACTION_SELECT_MOD.with(*i))) + .on_click(|ctx, (i, _info), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i))) }); let scroll = Scroll::new(list) @@ -123,11 +124,11 @@ fn build_mod_details() -> impl Widget { .lens(State::selected_mod); let button_move_up = Button::new("Move Up") - .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_UP)) + .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_SELECTED_MOD_UP)) .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_up()); let button_move_down = Button::new("Move Down") - .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_DOWN)) + .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_SELECTED_MOD_DOWN)) .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_down()); let button_toggle_mod = Maybe::new( @@ -150,14 +151,11 @@ fn build_mod_details() -> impl Widget { .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); - let button_add_mod = Button::new("Add Mod").on_click(|_ctx, state: &mut State, _env| { - // TODO: Implement properly - let info = ModInfo::new(); - state.add_mod(info); - }); + let button_add_mod = Button::new("Add Mod") + .on_click(|ctx, _state: &mut State, _env| ctx.submit_command(ACTION_ADD_MOD)); let button_delete_mod = Button::new("Delete Mod") - .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_DELETE_SELECTED_MOD)) + .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_DELETE_SELECTED_MOD)) .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); @@ -250,5 +248,4 @@ fn build_window() -> impl Widget { .must_fill_main_axis(true) .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) - .controller(StateController::new()) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index c963515..e3eb35b 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -3,20 +3,18 @@ use std::sync::Arc; use druid::im::Vector; use druid::text::Formatter; -use druid::widget::Controller; -use druid::{ - AppDelegate, Command, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target, - Widget, -}; +use druid::{AppDelegate, Command, Data, DelegateCtx, Env, Handled, Lens, Selector, Target}; use tokio::sync::mpsc::UnboundedSender; -pub const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action..select-mod"); +pub const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); -pub const COMMAND_FINISH_DEPLOY: Selector = Selector::new("dtmm.command.finish-deploy"); -pub const COMMAND_START_DEPLOY: Selector = Selector::new("dtmm.command.start-deploy"); +pub const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); +pub const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); + +pub const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { @@ -236,75 +234,6 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { } } -pub struct StateController {} - -impl StateController { - pub fn new() -> Self { - Self {} - } -} - -// TODO: Turn notifications into commands on the AppDelegate -impl> Controller for StateController { - #[tracing::instrument(name = "StateController::event", skip_all)] - fn event( - &mut self, - child: &mut W, - ctx: &mut EventCtx, - event: &Event, - state: &mut State, - env: &Env, - ) { - match event { - Event::Notification(notif) if notif.is(ACTION_SELECT_MOD) => { - ctx.set_handled(); - let index = notif - .get(ACTION_SELECT_MOD) - .expect("notification type didn't match after check"); - - state.select_mod(*index); - } - Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_UP) => { - ctx.set_handled(); - let Some(i) = state.selected_mod_index else { - return; - }; - - let len = state.mods.len(); - if len == 0 || i == 0 { - return; - } - - state.mods.swap(i, i - 1); - state.selected_mod_index = Some(i - 1); - } - Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_DOWN) => { - ctx.set_handled(); - let Some(i) = state.selected_mod_index else { - return; - }; - - let len = state.mods.len(); - if len == 0 || i == usize::MAX || i >= len - 1 { - return; - } - - state.mods.swap(i, i + 1); - state.selected_mod_index = Some(i + 1); - } - Event::Notification(notif) if notif.is(ACTION_DELETE_SELECTED_MOD) => { - ctx.set_handled(); - let Some(index) = state.selected_mod_index else { - return; - }; - - state.mods.remove(index); - } - _ => child.event(ctx, event, state, env), - } - } -} - pub(crate) enum AsyncAction { DeployMods(State), } @@ -326,27 +255,81 @@ impl AppDelegate for Delegate { _ctx: &mut DelegateCtx, _target: Target, cmd: &Command, - data: &mut State, + state: &mut State, _env: &Env, ) -> Handled { - if cmd.is(COMMAND_START_DEPLOY) { - if self - .sender - .send(AsyncAction::DeployMods(data.clone())) - .is_ok() - { - data.is_deployment_in_progress = true; - } else { - tracing::error!("Failed to queue action to deploy mods"); - } + match cmd { + cmd if cmd.is(ACTION_START_DEPLOY) => { + if self + .sender + .send(AsyncAction::DeployMods(state.clone())) + .is_ok() + { + state.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } - Handled::Yes - } else if cmd.is(COMMAND_FINISH_DEPLOY) { - data.is_deployment_in_progress = false; - Handled::Yes - } else { - tracing::debug!("Unknown command: {:?}", cmd); - Handled::No + Handled::Yes + } + cmd if cmd.is(ACTION_START_DEPLOY) => { + state.is_deployment_in_progress = false; + Handled::Yes + } + cmd if cmd.is(ACTION_SELECT_MOD) => { + let index = cmd + .get(ACTION_SELECT_MOD) + .expect("command type matched but didn't contain the expected value"); + + state.select_mod(*index); + Handled::Yes + } + cmd if cmd.is(ACTION_SELECTED_MOD_UP) => { + let Some(i) = state.selected_mod_index else { + return Handled::No; + }; + + let len = state.mods.len(); + if len == 0 || i == 0 { + return Handled::No; + } + + state.mods.swap(i, i - 1); + state.selected_mod_index = Some(i - 1); + Handled::Yes + } + cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => { + let Some(i) = state.selected_mod_index else { + return Handled::No; + }; + + let len = state.mods.len(); + if len == 0 || i == usize::MAX || i >= len - 1 { + return Handled::No; + } + + state.mods.swap(i, i + 1); + state.selected_mod_index = Some(i + 1); + Handled::Yes + } + cmd if cmd.is(ACTION_DELETE_SELECTED_MOD) => { + let Some(index) = state.selected_mod_index else { + return Handled::No; + }; + + state.mods.remove(index); + Handled::Yes + } + cmd if cmd.is(ACTION_ADD_MOD) => { + // TODO: Implement properly + let info = ModInfo::new(); + state.add_mod(info); + Handled::Yes + } + cmd => { + tracing::debug!("Unknown command: {:?}", cmd); + Handled::No + } } } } -- 2.45.3 From a81fb7c021a59ee76e6ec8110e3502247c118683 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 18:38:21 +0100 Subject: [PATCH 17/90] chore: Update lib/serde_sjson --- Cargo.lock | 2 +- lib/serde_sjson | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 646f90e..02408f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2073,7 +2073,7 @@ dependencies = [ [[package]] name = "serde_sjson" -version = "0.2.1" +version = "0.2.2" dependencies = [ "nom", "nom_locate", diff --git a/lib/serde_sjson b/lib/serde_sjson index 39486e8..fc5d8b2 160000 --- a/lib/serde_sjson +++ b/lib/serde_sjson @@ -1 +1 @@ -Subproject commit 39486e8503488a92b3e84af7cbf93fb67988e13f +Subproject commit fc5d8b25fb8f2b9c180d3d0c7cd361715596bb79 -- 2.45.3 From 560e5496bc3d17ce5249a0a83095eca49fc3b27d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 18 Feb 2023 11:27:25 +0100 Subject: [PATCH 18/90] feat(dtmm): Implement importing mod archives --- Cargo.lock | 1 + crates/dtmm/Cargo.toml | 1 + crates/dtmm/src/engine.rs | 81 ++++++++++++++++++++++++++++++++- crates/dtmm/src/main.rs | 21 +++++++++ crates/dtmm/src/main_window.rs | 14 ++++-- crates/dtmm/src/state.rs | 59 +++++++++++++++++------- crates/dtmt/src/cmd/build.rs | 3 ++ crates/dtmt/src/mods/archive.rs | 19 +++++++- 8 files changed, 177 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02408f0..908f449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,6 +671,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "zip", ] [[package]] diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index c554118..b6ae5e3 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -20,3 +20,4 @@ tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +zip = "0.6.4" diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 818bd8f..bc676c2 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -1,11 +1,13 @@ +use std::collections::HashMap; use std::ffi::CString; -use std::io::{Cursor, ErrorKind}; +use std::io::{Cursor, ErrorKind, Read}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use color_eyre::eyre::Context; use color_eyre::{eyre, Result}; +use druid::FileInfo; use futures::stream; use futures::StreamExt; use sdk::filetype::lua; @@ -14,11 +16,13 @@ use sdk::murmur::Murmur64; use sdk::{ Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; +use serde::Deserialize; use tokio::io::AsyncWriteExt; use tokio::{fs, try_join}; use tracing::Instrument; +use zip::ZipArchive; -use crate::state::{PackageInfo, State}; +use crate::state::{ModInfo, PackageInfo, State}; const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; @@ -371,3 +375,76 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { tracing::info!("Finished deploying mods"); Ok(()) } + +#[derive(Debug, Default, Deserialize)] +struct ModConfig { + name: String, + #[serde(default)] + description: String, +} + +#[tracing::instrument(skip(state))] +pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { + let data = fs::read(&info.path) + .await + .wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; + let data = Cursor::new(data); + + let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?; + + for f in archive.file_names() { + tracing::debug!("{}", f); + } + + let dir_name = { + let f = archive.by_index(0).wrap_err("archive is empty")?; + + if !f.is_dir() { + eyre::bail!("archive does not have a top-level directory"); + } + + let name = f.name(); + // The directory name is returned with a trailing slash, which we don't want + name[..(name.len().saturating_sub(1))].to_string() + }; + + let mod_cfg: ModConfig = { + let mut f = archive + .by_name(&format!("{}/{}", dir_name, "dtmt.cfg")) + .wrap_err("failed to read mod config from archive")?; + let mut buf = Vec::with_capacity(f.size() as usize); + f.read_to_end(&mut buf) + .wrap_err("failed to read mod config from archive")?; + + let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?; + + serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")? + }; + + let files: HashMap> = { + let mut f = archive + .by_name(&format!("{}/{}", dir_name, "files.sjson")) + .wrap_err("failed to read file index from archive")?; + let mut buf = Vec::with_capacity(f.size() as usize); + f.read_to_end(&mut buf) + .wrap_err("failed to read file index from archive")?; + + let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?; + + serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")? + }; + + let mod_dir = state.get_game_dir().join(&mod_cfg.name); + + archive + .extract(&mod_dir) + .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; + + let packages = files + .into_iter() + .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) + .collect(); + let info = ModInfo::new(mod_cfg.name, mod_cfg.description, packages); + + Ok(info) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index ba3548a..0ee5ffa 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -9,7 +9,10 @@ use color_eyre::Report; use color_eyre::Result; use druid::AppLauncher; use druid::ExtEventSink; +use druid::SingleUse; use druid::Target; +use engine::import_mod; +use state::ACTION_FINISH_ADD_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; @@ -48,6 +51,24 @@ fn work_thread( .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) .expect("failed to send command"); }), + AsyncAction::AddMod((state, info)) => tokio::spawn(async move { + match import_mod(state, info).await { + Ok(mod_info) => { + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_ADD_MOD, + SingleUse::new(mod_info), + Target::Auto, + ) + .expect("failed to send command"); + } + Err(err) => { + tracing::error!("Failed to import mod: {:?}", err); + } + } + }), }; } }); diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 040d796..ecf6c2e 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -3,7 +3,7 @@ use druid::widget::{ Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, TextBox, ViewSwitcher, }; -use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; +use druid::{lens, FileDialogOptions, FileSpec, Insets, LensExt, Widget, WidgetExt, WindowDesc}; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_ADD_MOD}; use crate::state::{ @@ -151,8 +151,16 @@ fn build_mod_details() -> impl Widget { .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); - let button_add_mod = Button::new("Add Mod") - .on_click(|ctx, _state: &mut State, _env| ctx.submit_command(ACTION_ADD_MOD)); + let button_add_mod = Button::new("Add Mod").on_click(|ctx, _state: &mut State, _env| { + let zip = FileSpec::new("Zip file", &["zip"]); + let opts = FileDialogOptions::new() + .allowed_types(vec![zip]) + .default_type(zip) + .name_label("Mod Archive") + .title("Choose a mod to add") + .accept_command(ACTION_ADD_MOD); + ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(opts)) + }); let button_delete_mod = Button::new("Delete Mod") .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_DELETE_SELECTED_MOD)) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index e3eb35b..a59ffd3 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -3,18 +3,25 @@ use std::sync::Arc; use druid::im::Vector; use druid::text::Formatter; -use druid::{AppDelegate, Command, Data, DelegateCtx, Env, Handled, Lens, Selector, Target}; +use druid::{ + AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse, + Target, +}; use tokio::sync::mpsc::UnboundedSender; -pub const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); -pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); -pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); -pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod"); +pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); +pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); +pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = + Selector::new("dtmm.action.selected-mod-down"); +pub(crate) const ACTION_DELETE_SELECTED_MOD: Selector = + Selector::new("dtmm.action.delete-selected-mod"); -pub const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); -pub const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); +pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); +pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); -pub const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); +pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); +pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = + Selector::new("dtmm.action.finish-add-mod"); #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { @@ -36,6 +43,10 @@ pub struct PackageInfo { } impl PackageInfo { + pub fn new(name: String, files: Vector) -> Self { + Self { name, files } + } + pub fn get_name(&self) -> &String { &self.name } @@ -55,12 +66,12 @@ pub(crate) struct ModInfo { } impl ModInfo { - pub fn new() -> Self { + pub fn new(name: String, description: String, packages: Vector) -> Self { Self { - name: format!("Test Mod: {:?}", std::time::SystemTime::now()), - description: Arc::new(String::from("A test dummy")), + name, + description: Arc::new(description), + packages, enabled: false, - packages: Vector::new(), } } @@ -236,6 +247,7 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { pub(crate) enum AsyncAction { DeployMods(State), + AddMod((State, FileInfo)), } pub(crate) struct Delegate { @@ -321,13 +333,28 @@ impl AppDelegate for Delegate { Handled::Yes } cmd if cmd.is(ACTION_ADD_MOD) => { - // TODO: Implement properly - let info = ModInfo::new(); - state.add_mod(info); + let info = cmd + .get(ACTION_ADD_MOD) + .expect("command type matched but didn't contain the expected value"); + if let Err(err) = self + .sender + .send(AsyncAction::AddMod((state.clone(), info.clone()))) + { + tracing::error!("Failed to add mod: {}", err); + } + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_ADD_MOD) => { + let info = cmd + .get(ACTION_FINISH_ADD_MOD) + .expect("command type matched but didn't contain the expected value"); + if let Some(info) = info.take() { + state.add_mod(info); + } Handled::Yes } cmd => { - tracing::debug!("Unknown command: {:?}", cmd); + tracing::warn!("Unknown command: {:?}", cmd); Handled::No } } diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index 618d9d2..c2a2045 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -218,6 +218,8 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> fs::read(path).await? }; + let config_file = fs::read(cfg.dir.join("dtmt.cfg")).await?; + { let dest = dest.clone(); let name = cfg.name.clone(); @@ -225,6 +227,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> let mut archive = Archive::new(name); archive.add_mod_file(mod_file); + archive.add_config(config_file); for bundle in bundles { archive.add_bundle(bundle); diff --git a/crates/dtmt/src/mods/archive.rs b/crates/dtmt/src/mods/archive.rs index 16d74d8..683314e 100644 --- a/crates/dtmt/src/mods/archive.rs +++ b/crates/dtmt/src/mods/archive.rs @@ -13,6 +13,7 @@ pub struct Archive { name: String, bundles: Vec, mod_file: Option>, + config_file: Option>, } impl Archive { @@ -21,6 +22,7 @@ impl Archive { name, bundles: Vec::new(), mod_file: None, + config_file: None, } } @@ -32,6 +34,10 @@ impl Archive { self.mod_file = Some(content); } + pub fn add_config(&mut self, content: Vec) { + self.config_file = Some(content); + } + pub fn write

(&self, path: P) -> Result<()> where P: AsRef, @@ -39,7 +45,12 @@ impl Archive { let mod_file = self .mod_file .as_ref() - .ok_or_else(|| eyre::eyre!("Mod file is missing from mod archive"))?; + .ok_or_else(|| eyre::eyre!("Mod file is missing in mod archive"))?; + + let config_file = self + .config_file + .as_ref() + .ok_or_else(|| eyre::eyre!("Config file is missing in mod archive"))?; let f = File::create(path.as_ref()).wrap_err_with(|| { format!( @@ -60,6 +71,12 @@ impl Archive { zip.write_all(mod_file)?; } + { + let name = base_path.join("dtmt.cfg"); + zip.start_file(name.to_string_lossy(), Default::default())?; + zip.write_all(config_file)?; + } + let mut file_map = HashMap::new(); for bundle in self.bundles.iter() { -- 2.45.3 From 0ac3f84dba8a9561beb1aa6b529a763fab8fb3da Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sun, 19 Feb 2023 15:33:29 +0100 Subject: [PATCH 19/90] fix(dtmm): Fix deploy button staying disabled --- crates/dtmm/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index a59ffd3..40db069 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -284,7 +284,7 @@ impl AppDelegate for Delegate { Handled::Yes } - cmd if cmd.is(ACTION_START_DEPLOY) => { + cmd if cmd.is(ACTION_FINISH_DEPLOY) => { state.is_deployment_in_progress = false; Handled::Yes } -- 2.45.3 From 09a6a969a6e5ad63aff2946fe57772efb2c36d57 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:35:32 +0100 Subject: [PATCH 20/90] feat(dtmm): Add config file --- crates/dtmm/Cargo.toml | 2 +- crates/dtmm/src/main.rs | 110 +++++++++++++++++++++++++++++++++++++-- crates/dtmm/src/state.rs | 17 ++---- 3 files changed, 113 insertions(+), 16 deletions(-) diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index b6ae5e3..9eaec24 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] bitflags = "1.3.2" -clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } +clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] } color-eyre = "0.6.2" confy = "0.5.1" druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 0ee5ffa..28143c5 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,10 +1,16 @@ #![recursion_limit = "256"] #![feature(let_chains)] +use std::fs; +use std::io::ErrorKind; +use std::path::PathBuf; use std::sync::Arc; use clap::command; +use clap::parser::ValueSource; +use clap::value_parser; use clap::Arg; +use color_eyre::eyre::Context; use color_eyre::Report; use color_eyre::Result; use druid::AppLauncher; @@ -12,6 +18,8 @@ use druid::ExtEventSink; use druid::SingleUse; use druid::Target; use engine::import_mod; +use serde::Deserialize; +use serde::Serialize; use state::ACTION_FINISH_ADD_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; @@ -30,6 +38,12 @@ mod state; mod theme; mod widget; +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Config { + data_dir: Option, + game_dir: Option, +} + fn work_thread( event_sink: Arc>, action_queue: Arc>>, @@ -76,11 +90,52 @@ fn work_thread( Ok(()) } +#[cfg(not(arget_os = "windows"))] +fn get_default_config_path() -> String { + let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.config") + }); + + format!("{config_dir}/dtmm/dtmm.cfg") +} + +#[cfg(target_os = "windows")] +fn get_default_config_path() -> String { + let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); + format!("{config_dir}\\dtmm\\dtmm.cfg") +} + +#[cfg(not(arget_os = "windows"))] +fn get_default_data_dir() -> String { + let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.local/share") + }); + + format!("{data_dir}/dtmm") +} + +#[cfg(target_os = "windows")] +fn get_default_data_dir() -> String { + let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); + format!("{data_dir}\\dtmm") +} + #[tracing::instrument] -#[tokio::main] -async fn main() -> Result<()> { +fn main() -> Result<()> { color_eyre::install()?; + let default_config_path = get_default_config_path(); + + tracing::trace!(default_config_path); + let matches = command!() .arg(Arg::new("oodle").long("oodle").help( "The oodle library to load. This may either be:\n\ @@ -88,6 +143,14 @@ async fn main() -> Result<()> { - A file path relative to the current working directory.\n\ - An absolute file path.", )) + .arg( + Arg::new("config") + .long("config") + .short('c') + .help("Path to the config file") + .value_parser(value_parser!(PathBuf)) + .default_value(&default_config_path), + ) .get_matches(); { @@ -121,7 +184,48 @@ async fn main() -> Result<()> { oodle_sys::init(matches.get_one::("oodle")); } - let initial_state = State::new(); + let config: Config = { + let path = matches + .get_one::("config") + .expect("argument missing despite default"); + match fs::read(path) { + Ok(data) => { + let data = String::from_utf8(data).wrap_err_with(|| { + format!("config file {} contains invalid UTF-8", path.display()) + })?; + serde_sjson::from_str(&data) + .wrap_err_with(|| format!("invalid config file {}", path.display()))? + } + Err(err) if err.kind() == ErrorKind::NotFound => { + if matches.value_source("config") != Some(ValueSource::DefaultValue) { + return Err(err).wrap_err_with(|| { + format!("failed to read config file {}", path.display()) + })?; + } + + let config = Config { + data_dir: Some(PathBuf::from(get_default_data_dir())), + game_dir: None, + }; + + { + let data = serde_sjson::to_string(&config) + .wrap_err("failed to serialize default config value")?; + fs::write(&default_config_path, data).wrap_err_with(|| { + format!("failed to write default config to {default_config_path}") + })?; + } + + config + } + Err(err) => { + return Err(err) + .wrap_err_with(|| format!("failed to read config file {}", path.display()))?; + } + } + }; + + let initial_state = State::new(config); let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); let delegate = Delegate::new(sender); diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 40db069..ec6530d 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -9,6 +9,8 @@ use druid::{ }; use tokio::sync::mpsc::UnboundedSender; +use crate::Config; + pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = @@ -105,26 +107,17 @@ impl State { #[allow(non_upper_case_globals)] pub const selected_mod: SelectedModLens = SelectedModLens; - pub fn new() -> Self { + pub fn new(config: Config) -> Self { let ctx = sdk::Context::new(); - let (game_dir, data_dir) = if cfg!(debug_assertions) { - ( - std::env::current_dir().expect("PWD is borked").join("data"), - PathBuf::from("/tmp/dtmm"), - ) - } else { - (PathBuf::new(), PathBuf::new()) - }; - Self { ctx: Arc::new(ctx), current_view: View::default(), mods: Vector::new(), selected_mod_index: None, is_deployment_in_progress: false, - game_dir: Arc::new(game_dir), - data_dir: Arc::new(data_dir), + game_dir: Arc::new(config.game_dir.unwrap_or_default()), + data_dir: Arc::new(config.data_dir.unwrap_or_default()), } } -- 2.45.3 From f90247710e3a0a25272c46963add1bf0347ca38d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:37:57 +0100 Subject: [PATCH 21/90] fix(dtmm): Use correct directory to extract to --- crates/dtmm/src/engine.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index bc676c2..76d25f5 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -434,8 +434,16 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")? }; - let mod_dir = state.get_game_dir().join(&mod_cfg.name); + tracing::trace!(?files); + let mod_dir = state.get_mod_dir(); + + tracing::trace!("Creating mods directory {}", mod_dir.display()); + fs::create_dir_all(&mod_dir) + .await + .wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; + + tracing::trace!("Extracting mod archive to {}", mod_dir.display()); archive .extract(&mod_dir) .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; -- 2.45.3 From aa05c5bd4a581aa0adfe79c4b40360f61b2d9150 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:39:08 +0100 Subject: [PATCH 22/90] chore: Improve debug logs --- crates/dtmm/src/engine.rs | 16 +++++++++++++--- lib/sdk/src/bundle/file.rs | 11 ++++++----- lib/sdk/src/bundle/mod.rs | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 76d25f5..0ec0566 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -392,15 +392,21 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?; - for f in archive.file_names() { - tracing::debug!("{}", f); + if tracing::enabled!(tracing::Level::DEBUG) { + let names = archive.file_names().fold(String::new(), |mut s, name| { + s.push('\n'); + s.push_str(name); + s + }); + tracing::debug!("Archive contents:{}", names); } let dir_name = { let f = archive.by_index(0).wrap_err("archive is empty")?; if !f.is_dir() { - eyre::bail!("archive does not have a top-level directory"); + let err = eyre::eyre!("archive does not have a top-level directory"); + return Err(err).with_suggestion(|| "Use 'dtmt build' to create the mod archive."); } let name = f.name(); @@ -408,6 +414,8 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result name[..(name.len().saturating_sub(1))].to_string() }; + tracing::info!("Importing mod {}", dir_name); + let mod_cfg: ModConfig = { let mut f = archive .by_name(&format!("{}/{}", dir_name, "dtmt.cfg")) @@ -421,6 +429,8 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")? }; + tracing::debug!(?mod_cfg); + let files: HashMap> = { let mut f = archive .by_name(&format!("{}/{}", dir_name, "files.sjson")) diff --git a/lib/sdk/src/bundle/file.rs b/lib/sdk/src/bundle/file.rs index a5ed380..872b48b 100644 --- a/lib/sdk/src/bundle/file.rs +++ b/lib/sdk/src/bundle/file.rs @@ -544,22 +544,23 @@ impl BundleFile { r.skip_u32(0)?; for i in 0..header_count { - let span = tracing::info_span!("Read file header", i); + let span = tracing::debug_span!("Read file header", i); let _enter = span.enter(); let header = BundleFileVariant::read_header(r) .wrap_err_with(|| format!("failed to read header {i}"))?; - if props.contains(Properties::DATA) { - tracing::debug!("props: {props:?} | unknown_1: {}", header.unknown_1) - } + // TODO: Figure out how `header.unknown_1` correlates to `properties::DATA` + // if props.contains(Properties::DATA) { + // tracing::debug!("props: {props:?} | unknown_1: {}", header.unknown_1) + // } headers.push(header); } let mut variants = Vec::with_capacity(header_count); for (i, header) in headers.into_iter().enumerate() { - let span = tracing::info_span!( + let span = tracing::debug_span!( "Read file data {}", i, size = header.size, diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index ce3a23f..18a4f52 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -192,7 +192,7 @@ impl Bundle { let mut files = Vec::with_capacity(num_entries); tracing::trace!(num_files = num_entries); for (i, props) in file_props.iter().enumerate() { - let span = tracing::trace_span!("Read file {}", i); + let span = tracing::debug_span!("Read file {}", i); let _enter = span.enter(); let file = BundleFile::from_reader(ctx, &mut r, *props) -- 2.45.3 From ebcbdaeec0e5d21d58aa8a10488bc3c829ed8552 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:48:43 +0100 Subject: [PATCH 23/90] feat(dtmm): Rework mod template Ditch the `.mod` file and move its data into the config file. The `run` function was the only thing that could have been dynamic, but the vast majority of mods in VT2 never made use of that. Infact, VMF was probably the only mod that had a different content for that. --- crates/dtmm/src/engine.rs | 20 ++++++--------- crates/dtmm/src/state.rs | 45 ++++++++++++++++++++++++++++++--- crates/dtmt/src/cmd/build.rs | 31 ++++++++--------------- crates/dtmt/src/cmd/new.rs | 32 ++++++++--------------- crates/dtmt/src/mods/archive.rs | 18 ------------- lib/sdk/src/lib.rs | 20 +++++++++++++++ 6 files changed, 91 insertions(+), 75 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 0ec0566..3526cb5 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -14,9 +14,9 @@ use sdk::filetype::lua; use sdk::filetype::package::Package; use sdk::murmur::Murmur64; use sdk::{ - Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, + Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ModConfig, + ToBinary, }; -use serde::Deserialize; use tokio::io::AsyncWriteExt; use tokio::{fs, try_join}; use tracing::Instrument; @@ -176,7 +176,10 @@ async fn build_bundles(state: Arc) -> Result<()> { bundle.add_file(file); - let src = mod_dir.join(pkg_info.get_name()); + let bundle_name = Murmur64::hash(pkg_info.get_name()) + .to_string() + .to_ascii_lowercase(); + let src = mod_dir.join(&bundle_name); let dest = bundle_dir.clone(); let pkg_name = pkg_info.get_name().clone(); let mod_name = mod_info.get_name().clone(); @@ -200,7 +203,7 @@ async fn build_bundles(state: Arc) -> Result<()> { dest.display() ); fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| { - format!("failed to hard link bundle {pkg_name} for mod {mod_name}") + format!("failed to hard link bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", src.display(), dest.display()) }) } .instrument(span); @@ -376,13 +379,6 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { Ok(()) } -#[derive(Debug, Default, Deserialize)] -struct ModConfig { - name: String, - #[serde(default)] - description: String, -} - #[tracing::instrument(skip(state))] pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { let data = fs::read(&info.path) @@ -462,7 +458,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result .into_iter() .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) .collect(); - let info = ModInfo::new(mod_cfg.name, mod_cfg.description, packages); + let info = ModInfo::new(mod_cfg, packages); Ok(info) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index ec6530d..3241585 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -7,6 +7,7 @@ use druid::{ AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse, Target, }; +use sdk::ModConfig; use tokio::sync::mpsc::UnboundedSender; use crate::Config; @@ -58,6 +59,27 @@ impl PackageInfo { } } +#[derive(Clone, Data, Debug)] +pub(crate) struct ModResourceInfo { + init: String, + data: String, + localization: String, +} + +impl ModResourceInfo { + pub(crate) fn get_init(&self) -> &String { + &self.init + } + + pub(crate) fn get_data(&self) -> &String { + &self.data + } + + pub(crate) fn get_localization(&self) -> &String { + &self.localization + } +} + #[derive(Clone, Data, Debug, Lens)] pub(crate) struct ModInfo { name: String, @@ -65,15 +87,22 @@ pub(crate) struct ModInfo { enabled: bool, #[lens(ignore)] packages: Vector, + #[lens(ignore)] + resources: ModResourceInfo, } impl ModInfo { - pub fn new(name: String, description: String, packages: Vector) -> Self { + pub fn new(cfg: ModConfig, packages: Vector) -> Self { Self { - name, - description: Arc::new(description), - packages, + name: cfg.name, + description: Arc::new(cfg.description), enabled: false, + packages, + resources: ModResourceInfo { + init: cfg.resources.init, + data: cfg.resources.data, + localization: cfg.resources.localization, + }, } } @@ -84,6 +113,14 @@ impl ModInfo { pub(crate) fn get_name(&self) -> &String { &self.name } + + pub(crate) fn get_enabled(&self) -> bool { + self.enabled + } + + pub(crate) fn get_resources(&self) -> &ModResourceInfo { + &self.resources + } } impl PartialEq for ModInfo { diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index c2a2045..0c34a11 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -7,8 +7,7 @@ use color_eyre::{Help, Report}; use futures::future::try_join_all; use futures::StreamExt; use sdk::filetype::package::Package; -use sdk::{Bundle, BundleFile}; -use serde::Deserialize; +use sdk::{Bundle, BundleFile, ModConfig}; use tokio::fs::{self, File}; use tokio::io::AsyncReadExt; @@ -36,16 +35,8 @@ pub(crate) fn command_definition() -> Command { )) } -#[derive(Debug, Default, Deserialize)] -struct ProjectConfig { - #[serde(skip)] - dir: PathBuf, - name: String, - packages: Vec, -} - #[tracing::instrument] -async fn find_project_config(dir: Option) -> Result { +async fn find_project_config(dir: Option) -> Result { let (path, mut file) = if let Some(path) = dir { let file = File::open(&path.join(PROJECT_CONFIG_NAME)) .await @@ -83,7 +74,7 @@ async fn find_project_config(dir: Option) -> Result { let mut buf = String::new(); file.read_to_string(&mut buf).await?; - let mut cfg: ProjectConfig = serde_sjson::from_str(&buf)?; + let mut cfg: ModConfig = serde_sjson::from_str(&buf)?; cfg.dir = path; Ok(cfg) } @@ -210,23 +201,23 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> }) }); - let bundles = try_join_all(tasks).await?; + let bundles = try_join_all(tasks) + .await + .wrap_err("failed to build mod bundles")?; - let mod_file = { - let mut path = cfg.dir.join(&cfg.name); - path.set_extension("mod"); - fs::read(path).await? + let config_file = { + let path = cfg.dir.join("dtmt.cfg"); + fs::read(&path) + .await + .wrap_err_with(|| format!("failed to read mod config at {}", path.display()))? }; - let config_file = fs::read(cfg.dir.join("dtmt.cfg")).await?; - { let dest = dest.clone(); let name = cfg.name.clone(); tokio::task::spawn_blocking(move || { let mut archive = Archive::new(name); - archive.add_mod_file(mod_file); archive.add_config(config_file); for bundle in bundles { diff --git a/crates/dtmt/src/cmd/new.rs b/crates/dtmt/src/cmd/new.rs index a7a66ca..e757f49 100644 --- a/crates/dtmt/src/cmd/new.rs +++ b/crates/dtmt/src/cmd/new.rs @@ -8,13 +8,19 @@ use futures::{StreamExt, TryStreamExt}; use string_template::Template; use tokio::fs::{self, DirBuilder}; -const TEMPLATES: [(&str, &str); 6] = [ +const TEMPLATES: [(&str, &str); 5] = [ ( "dtmt.cfg", r#"name = "{{name}}" description = "An elaborate description of my cool game mod!" version = "0.1.0" +resources = { + script = "scripts/mods/{{name}}/init" + data = "scripts/mods/{{name}}/data" + localization = "scripts/mods/{{name}}/locationzation" +} + packages = [ "packages/{{name}}" ] @@ -23,21 +29,6 @@ depends = [ "dmf" ] "#, - ), - ( - "{{name}}.mod", - r#"return { - run = function() - fassert(rawget(_G, "new_mod"), "`{{title}}` encountered an error loading the Darktide Mod Framework.") - - new_mod("{{name}}", { - mod_script = "scripts/mods/{{name}}/{{name}}", - mod_data = "scripts/mods/{{name}}/{{name}}_data", - mod_localization = "scripts/mods/{{name}}/{{name}}_localization", - }) - end, - packages = {}, -}"#, ), ( "packages/{{name}}.package", @@ -47,7 +38,7 @@ depends = [ "#, ), ( - "scripts/mods/{{name}}/{{name}}.lua", + "scripts/mods/{{name}}/init.lua", r#"local mod = get_mod("{{name}}") -- Your mod code goes here. @@ -55,7 +46,7 @@ depends = [ "#, ), ( - "scripts/mods/{{name}}/{{name}}_data.lua", + "scripts/mods/{{name}}/data.lua", r#"local mod = get_mod("{{name}}") return { @@ -65,7 +56,7 @@ return { }"#, ), ( - "scripts/mods/{{name}}/{{name}}_localization.lua", + "scripts/mods/{{name}}/localization.lua", r#"return { mod_description = { en = "An elaborate description of my cool game mod!", @@ -127,8 +118,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> promptly::prompt_default("The mod identifier name", default)? }; - tracing::debug!(root = %root.display()); - tracing::debug!(title, name); + tracing::debug!(root = %root.display(), title, name); let mut data = HashMap::new(); data.insert("name", name.as_str()); diff --git a/crates/dtmt/src/mods/archive.rs b/crates/dtmt/src/mods/archive.rs index 683314e..9f9eaa1 100644 --- a/crates/dtmt/src/mods/archive.rs +++ b/crates/dtmt/src/mods/archive.rs @@ -12,7 +12,6 @@ use zip::ZipWriter; pub struct Archive { name: String, bundles: Vec, - mod_file: Option>, config_file: Option>, } @@ -21,7 +20,6 @@ impl Archive { Self { name, bundles: Vec::new(), - mod_file: None, config_file: None, } } @@ -30,10 +28,6 @@ impl Archive { self.bundles.push(bundle) } - pub fn add_mod_file(&mut self, content: Vec) { - self.mod_file = Some(content); - } - pub fn add_config(&mut self, content: Vec) { self.config_file = Some(content); } @@ -42,11 +36,6 @@ impl Archive { where P: AsRef, { - let mod_file = self - .mod_file - .as_ref() - .ok_or_else(|| eyre::eyre!("Mod file is missing in mod archive"))?; - let config_file = self .config_file .as_ref() @@ -64,13 +53,6 @@ impl Archive { let base_path = PathBuf::from(&self.name); - { - let mut name = base_path.join(&self.name); - name.set_extension("mod"); - zip.start_file(name.to_string_lossy(), Default::default())?; - zip.write_all(mod_file)?; - } - { let name = base_path.join("dtmt.cfg"); zip.start_file(name.to_string_lossy(), Default::default())?; diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index e229e28..1ce68d6 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -9,3 +9,23 @@ pub use bundle::database::BundleDatabase; pub use bundle::decompress; pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant}; pub use context::Context; + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub struct ModConfigResources { + pub init: String, + pub data: String, + pub localization: String, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub struct ModConfig { + #[serde(skip)] + pub dir: std::path::PathBuf, + pub name: String, + pub description: String, + pub version: String, + pub packages: Vec, + pub resources: ModConfigResources, + #[serde(default)] + pub depends: Vec, +} -- 2.45.3 From 8ebc94825294512e695ca11e506d8932cc79327a Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:53:21 +0100 Subject: [PATCH 24/90] refactor: Make function parameters more generic --- lib/sdk/src/filetype/lua.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/sdk/src/filetype/lua.rs b/lib/sdk/src/filetype/lua.rs index ea072b6..f2f4ef5 100644 --- a/lib/sdk/src/filetype/lua.rs +++ b/lib/sdk/src/filetype/lua.rs @@ -20,15 +20,19 @@ where } #[tracing::instrument(skip_all)] -pub fn compile(name: String, code: S) -> Result +pub fn compile(name: S, code: C) -> Result where - S: AsRef, + S: ToString, + C: AsRef, { + let name = name.to_string(); + let code = code.as_ref(); + let bytecode = unsafe { let state = lua::luaL_newstate(); lua::luaL_openlibs(state); - lua::lua_pushstring(state, code.as_ref().as_ptr() as _); + lua::lua_pushstring(state, code.as_ptr() as _); lua::lua_setglobal(state, b"code\0".as_ptr() as _); lua::lua_pushstring(state, name.as_ptr() as _); -- 2.45.3 From 31d45a1cb407d71390e83b7c20b6b949a9e0a2b2 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:54:07 +0100 Subject: [PATCH 25/90] fix(dtmm): Only deploy mods that are enabled --- crates/dtmm/src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 3526cb5..58c6753 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -156,7 +156,7 @@ async fn build_bundles(state: Arc) -> Result<()> { db }; - for mod_info in state.get_mods() { + for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); let _enter = span.enter(); -- 2.45.3 From bc4d2fcd8adfce3964eb8f7aa743806df178e334 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 16:03:14 +0100 Subject: [PATCH 26/90] feat(dtmt): Add unique mod ID --- crates/dtmm/src/state.rs | 6 +++++ crates/dtmt/src/cmd/build.rs | 8 +++--- crates/dtmt/src/cmd/new.rs | 49 ++++++++++++++++++------------------ lib/sdk/src/lib.rs | 1 + 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 3241585..8775aff 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -82,6 +82,7 @@ impl ModResourceInfo { #[derive(Clone, Data, Debug, Lens)] pub(crate) struct ModInfo { + id: String, name: String, description: Arc, enabled: bool, @@ -94,6 +95,7 @@ pub(crate) struct ModInfo { impl ModInfo { pub fn new(cfg: ModConfig, packages: Vector) -> Self { Self { + id: cfg.id, name: cfg.name, description: Arc::new(cfg.description), enabled: false, @@ -114,6 +116,10 @@ impl ModInfo { &self.name } + pub(crate) fn get_id(&self) -> &String { + &self.id + } + pub(crate) fn get_enabled(&self) -> bool { self.enabled } diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index 0c34a11..a5a471f 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -24,7 +24,7 @@ pub(crate) fn command_definition() -> Command { .value_parser(value_parser!(PathBuf)) .help( "The path to the project to build. \ - If omitted, dtmt will search from the current working directory upward.", + If omitted, dtmt will search from the current working directory upward.", ), ) .arg(Arg::new("oodle").long("oodle").help( @@ -172,7 +172,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> }; let dest = { - let mut path = PathBuf::from(&cfg.name); + let mut path = PathBuf::from(&cfg.id); path.set_extension("zip"); Arc::new(path) }; @@ -214,9 +214,9 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { let dest = dest.clone(); - let name = cfg.name.clone(); + let id = cfg.id.clone(); tokio::task::spawn_blocking(move || { - let mut archive = Archive::new(name); + let mut archive = Archive::new(id); archive.add_config(config_file); diff --git a/crates/dtmt/src/cmd/new.rs b/crates/dtmt/src/cmd/new.rs index e757f49..dd42172 100644 --- a/crates/dtmt/src/cmd/new.rs +++ b/crates/dtmt/src/cmd/new.rs @@ -11,18 +11,19 @@ use tokio::fs::{self, DirBuilder}; const TEMPLATES: [(&str, &str); 5] = [ ( "dtmt.cfg", - r#"name = "{{name}}" + r#"id = "{{id}}" +name = "{{name}}" description = "An elaborate description of my cool game mod!" version = "0.1.0" resources = { - script = "scripts/mods/{{name}}/init" - data = "scripts/mods/{{name}}/data" - localization = "scripts/mods/{{name}}/locationzation" + script = "scripts/mods/{{id}}/init" + data = "scripts/mods/{{id}}/data" + localization = "scripts/mods/{{id}}/localization" } packages = [ - "packages/{{name}}" + "packages/{{id}}" ] depends = [ @@ -31,32 +32,32 @@ depends = [ "#, ), ( - "packages/{{name}}.package", + "packages/{{id}}.package", r#"lua = [ - "scripts/mods/{{name}}/*" + "scripts/mods/{{id}}/*" ] "#, ), ( - "scripts/mods/{{name}}/init.lua", - r#"local mod = get_mod("{{name}}") + "scripts/mods/{{id}}/init.lua", + r#"local mod = get_mod("{{id}}") -- Your mod code goes here. -- https://vmf-docs.verminti.de "#, ), ( - "scripts/mods/{{name}}/data.lua", - r#"local mod = get_mod("{{name}}") + "scripts/mods/{{id}}/data.lua", + r#"local mod = get_mod("{{id}}") return { - name = "{{title}}", + name = "{{name}}", description = mod:localize("mod_description"), is_togglable = true, }"#, ), ( - "scripts/mods/{{name}}/localization.lua", + "scripts/mods/{{id}}/localization.lua", r#"return { mod_description = { en = "An elaborate description of my cool game mod!", @@ -69,8 +70,8 @@ pub(crate) fn command_definition() -> Command { Command::new("new") .about("Create a new project") .arg( - Arg::new("title") - .long("title") + Arg::new("name") + .long("name") .help("The display name of the new mod."), ) .arg(Arg::new("root").help( @@ -98,14 +99,14 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> } }; - let title = if let Some(title) = matches.get_one::("title") { - title.clone() + let name = if let Some(name) = matches.get_one::("name") { + name.clone() } else { - promptly::prompt("The mod display name")? + promptly::prompt("The unique mod ID")? }; - let name = { - let default = title + let id = { + let default = name .chars() .map(|c| { if c.is_ascii_alphanumeric() { @@ -115,14 +116,14 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> } }) .collect::(); - promptly::prompt_default("The mod identifier name", default)? + promptly::prompt_default("The unique mod ID", default)? }; - tracing::debug!(root = %root.display(), title, name); + tracing::debug!(root = %root.display(), name, id); let mut data = HashMap::new(); data.insert("name", name.as_str()); - data.insert("title", title.as_str()); + data.insert("id", id.as_str()); let templates = TEMPLATES .iter() @@ -158,7 +159,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> tracing::info!( "Created {} files for mod '{}' in '{}'.", TEMPLATES.len(), - title, + name, root.display() ); diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 1ce68d6..003e400 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -21,6 +21,7 @@ pub struct ModConfigResources { pub struct ModConfig { #[serde(skip)] pub dir: std::path::PathBuf, + pub id: String, pub name: String, pub description: String, pub version: String, -- 2.45.3 From 0cf29089049155d708548a2ed1bae9d421484e13 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 16:03:59 +0100 Subject: [PATCH 27/90] fix(sdk): Fix resolving package wildcards Directories were ignored as invalid extension type. Now they are recursed into. --- Cargo.lock | 12 +++++++++ lib/sdk/Cargo.toml | 1 + lib/sdk/src/filetype/package.rs | 44 ++++++++++++++++++++------------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 908f449..dbfa6fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46016233fc1bb55c23b856fe556b7db6ccd05119a0a392e04f0b3b7c79058f16" +[[package]] +name = "async-recursion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atk" version = "0.16.0" @@ -2007,6 +2018,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" name = "sdk" version = "0.2.0" dependencies = [ + "async-recursion", "bitflags", "byteorder", "color-eyre", diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index 5c2ef1a..0844518 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -23,3 +23,4 @@ tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } tracing = { version = "0.1.37", features = ["async-await"] } tracing-error = "0.2.0" luajit2-sys = "0.0.2" +async-recursion = "1.0.2" diff --git a/lib/sdk/src/filetype/package.rs b/lib/sdk/src/filetype/package.rs index 2dc9c3c..fc2e8f6 100644 --- a/lib/sdk/src/filetype/package.rs +++ b/lib/sdk/src/filetype/package.rs @@ -4,6 +4,7 @@ use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::str::FromStr; +use async_recursion::async_recursion; use color_eyre::eyre::{self, Context}; use color_eyre::Result; use tokio::fs; @@ -13,14 +14,15 @@ use crate::bundle::file::{BundleFileType, UserFile}; use crate::murmur::{HashGroup, Murmur64}; #[tracing::instrument] +#[async_recursion] async fn resolve_wildcard( wildcard: P1, root: P2, t: Option, ) -> Result> where - P1: AsRef + std::fmt::Debug, - P2: AsRef + std::fmt::Debug, + P1: AsRef + std::fmt::Debug + std::marker::Send, + P2: AsRef + std::fmt::Debug + std::marker::Send + std::marker::Copy, { let wildcard = wildcard.as_ref(); @@ -56,24 +58,32 @@ where path.to_path_buf() }; - // Skip file if there is a desired extension `t`, but the file's - // extension name doesn't match - if t.is_some() { - let ext = file_path - .extension() - .and_then(|ext| ext.to_str()) - .and_then(|ext| BundleFileType::from_str(ext).ok()); + let meta = entry.metadata().await?; + if meta.is_dir() { + let wildcard = file_path.join("*"); + let inner_paths = resolve_wildcard(wildcard, root, t).await?; + paths.extend_from_slice(&inner_paths); + } else { + // Skip file if there is a desired extension `t`, but the file's + // extension name doesn't match + if t.is_some() { + let ext = file_path + .extension() + .and_then(|ext| ext.to_str()) + .and_then(|ext| BundleFileType::from_str(ext).ok()); - if ext != t { - tracing::debug!( - "Skipping wildcard result with invalid extension: {}", - file_path.display(), - ); - continue; + if ext != t { + tracing::warn!( + "Skipping wildcard result with invalid extension: {}", + file_path.display(), + ); + continue; + } } - } - paths.push(file_path); + tracing::debug!("Found file {}", file_path.display()); + paths.push(file_path); + } } Ok(paths) -- 2.45.3 From 571ae9605aa10165a4adaced15aedaff1993a46e Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:54:57 +0100 Subject: [PATCH 28/90] wip: Add preliminary mod loading injection --- crates/dtmm/assets/mod_main.lua | 71 ++++++ crates/dtmm/assets/mod_manager.lua | 357 +++++++++++++++++++++++++++++ crates/dtmm/src/engine.rs | 94 ++++++-- 3 files changed, 507 insertions(+), 15 deletions(-) create mode 100644 crates/dtmm/assets/mod_main.lua create mode 100644 crates/dtmm/assets/mod_manager.lua diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua new file mode 100644 index 0000000..e8a83e3 --- /dev/null +++ b/crates/dtmm/assets/mod_main.lua @@ -0,0 +1,71 @@ +Mods = { + -- Keep a backup of certain system libraries before + -- Fatshark's code scrubs them. + -- The metatable setup prevents mods from overwriting them. + lua = setmetatable({}, { + __index = { io = io, debug = debug, ffi = ffi, os = os }, + }), +} + +require("scripts/game_states/boot/state_boot_sub_state_base") +local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") + +StateBootLoadMods.on_enter = function (self, parent, params) + StateBootLoadMods.super.on_enter(self, parent, params) + + local state_params = self:_state_params() + local package_manager = state_params.package_manager + self._package_manager = package_manager + self._package_handles = { + ["packages/mods"] = package_manager:load("packages/mods", "StateBootLoadMods", nil), + ["packages/dmf"] = package_manager:load("packages/dmf", "StateBootLoadMods", nil), + } +end + +StateBootLoadMods._state_update = function (self, dt) + local state = self._state + local package_manager = self._package_manager + + if state == "load_package" and package_manager:update() then + self._state = "load_mods" + local dmf_loader = require("scripts/mods/dmf/dmf_loader") + self._dmf_loader = dmf_loader + + local mod_data = require("scripts/mods/mod_data") + dmf_loader:init(self._parent.gui, mod_data) + elseif state == "load_mods" and self._dmf_loader:update(dt) then + return true, false + end + + return false, false +end + +require("scripts/main") + +-- Patch `GameStateMachine.init` to add our own state for loading mods. +-- In the future, Fatshark might provide us with a dedicated way to do this. +local function patch_mod_loading_state() + local GameStateMachine = require("scripts/foundations/utilities/game_state_machine") + + local GameStateMachine_init = GameStateMachine.init + GameStateMachine.init = function(self, parent, start_state, params, ...) + -- Hardcoded position after `StateRequireScripts`. + -- We do want to wait until then, so that most of the game's core + -- systems are at least loaded and can be hooked, even if they aren't + -- running, yet. + local pos = 4 + table.insert(params.state, pos, { + StateBootLoadMods, + { + package_manager = params.package_manager, + }, + }) + + return GameStateMachine_init(self, parent, start_state, params, ...) + end +end + +function init() + Main.init() + patch_mod_loading_state() +end diff --git a/crates/dtmm/assets/mod_manager.lua b/crates/dtmm/assets/mod_manager.lua new file mode 100644 index 0000000..549a9b6 --- /dev/null +++ b/crates/dtmm/assets/mod_manager.lua @@ -0,0 +1,357 @@ +-- Copyright on this file is owned by Fatshark. +-- It is extracted, used and modified with permission only for +-- the purpose of loading mods within Warhammer 40,000: Darktide. +local ModManager = class("ModManager") + +local MOD_DATA = require("scripts/mods/mod_data") + +local LOG_LEVELS = { + spew = 4, + info = 3, + warning = 2, + error = 1 +} +local DEFAULT_SETTINGS = { + log_level = LOG_LEVELS.error, + developer_mode = false +} + +local Keyboard = Keyboard +local BUTTON_INDEX_R = Keyboard.button_index("r") +local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") +local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") + +ModManager.init = function(self, boot_gui) + self._mods = {} + self._num_mods = nil + self._state = "not_loaded" + self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS + + self._chat_print_buffer = {} + self._reload_data = {} + self._gui = boot_gui + self._ui_time = 0 + self._network_callbacks = {} + + Crashify.print_property("realm", "modded") + + self._state = "scanning" +end + +ModManager.developer_mode_enabled = function(self) + return self._settings.developer_mode +end + +ModManager._draw_state_to_gui = function(self, gui, dt) + local state = self._state + local t = self._ui_time + dt + self._ui_time = t + local status_str = "Loading mods" + + if state == "scanning" then + status_str = "Scanning for mods" + elseif state == "loading" then + local mod = self._mods[self._mod_load_index] + status_str = string.format("Loading mod %q", mod.name) + end + + Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) +end + +ModManager.remove_gui = function(self) + self._gui = nil +end + +ModManager._has_enabled_mods = function() + return true +end + +ModManager._check_reload = function() + return Keyboard.pressed(BUTTON_INDEX_R) and + Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + + Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 +end + +ModManager.update = function(self, dt) + local chat_print_buffer = self._chat_print_buffer + local num_delayed_prints = #chat_print_buffer + + if num_delayed_prints > 0 and Managers.chat then + for i = 1, num_delayed_prints, 1 do + -- TODO: Use new chat system + -- Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) + + chat_print_buffer[i] = nil + end + end + + local old_state = self._state + + if self._settings.developer_mode and self:_check_reload() then + self._reload_requested = true + end + + if self._reload_requested and self._state == "done" then + self:_reload_mods() + end + + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "update", dt) + end + end + elseif self._state == "scanning" then + self:_build_mod_table() + + self._state = self:_load_mod(1) + self._ui_time = 0 + elseif self._state == "loading" then + local handle = self._loading_resource_handle + + if ResourcePackage.has_loaded(handle) then + ResourcePackage.flush(handle) + + local mod = self._mods[self._mod_load_index] + local next_index = mod.package_index + 1 + local mod_data = mod.data + + if next_index > #mod_data.packages then + mod.state = "running" + local ok, object = pcall(mod_data.run) + + if not ok then self:print("error", "%s", object) end + + local name = mod.name + mod.object = object or {} + + self:_run_callback(mod, "init", self._reload_data[mod.id]) + self:print("info", "%s loaded.", name) + + self._state = self:_load_mod(self._mod_load_index + 1) + else + self:_load_package(mod, next_index) + end + end + end + + local gui = self._gui + + if gui then self:_draw_state_to_gui(gui, dt) end + + if old_state ~= self._state then + self:print("info", "%s -> %s", old_state, self._state) + end +end + +ModManager.all_mods_loaded = function(self) + return self._state == "done" +end + +ModManager.destroy = function(self) + self:unload_all_mods() +end + +ModManager._run_callback = function(self, mod, callback_name, ...) + local object = mod.object + local cb = object[callback_name] + + if not cb then + return + end + + local success, val = pcall(cb, object, ...) + + if success then + return val + else + self:print("error", "%s", val or "[unknown error]") + self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", + callback_name, mod.name, mod.id) + + mod.callbacks_disabled = true + end +end + +ModManager._start_scan = function(self) + self:print("info", "Starting mod scan") + self._state = "scanning" +end + +ModManager._build_mod_table = function(self) + fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") + + for i, mod_data in ipairs(MOD_DATA) do + printf("[ModManager] mods[%d] = name=%q", i, mod_data.name) + + self._mods[i] = { + id = i, + state = "not_loaded", + callbacks_disabled = false, + name = mod_data.name, + loaded_packages = {}, + packages = mod_data.packages, + } + end + + self._num_mods = #self._mods + + self:print("info", "Found %i mods", #self._mods) +end + +ModManager._load_mod = function(self, index) + self._ui_time = 0 + local mods = self._mods + local mod = mods[index] + + if not mod then + table.clear(self._reload_data) + + return "done" + end + + self:print("info", "loading mod %i", mod.id) + Crashify.print_property("modded", true) + + mod.state = "loading" + + Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) + + self._mod_load_index = index + + self:_load_package(mod, 1) + + return "loading" +end + +ModManager._load_package = function(self, mod, index) + mod.package_index = index + local package_name = mod.packages[index] + + if not package_name then + return + end + + self:print("info", "loading package %q", package_name) + + local resource_handle = Application.resource_package(package_name) + self._loading_resource_handle = resource_handle + + ResourcePackage.load(resource_handle) + + mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle +end + +ModManager.unload_all_mods = function(self) + if self._state ~= "done" then + self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) + + return + end + + self:print("info", "Unload all mod packages") + + for i = self._num_mods, 1, -1 do + local mod = self._mods[i] + + if mod then + self:unload_mod(i) + end + + self._mods[i] = nil + end + + self._num_mods = nil + self._state = "unloaded" +end + +ModManager.unload_mod = function(self, index) + local mod = self._mods[index] + + if mod then + self:print("info", "Unloading %q.", mod.name) + self:_run_callback(mod, "on_unload") + + for _, handle in ipairs(mod.loaded_packages) do + ResourcePackage.unload(handle) + Application.release_resource_package(handle) + end + + mod.state = "not_loaded" + else + self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) + end +end + +ModManager._reload_mods = function(self) + self:print("info", "reloading mods") + + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and mod.state == "running" then + self:print("info", "reloading %s", mod.name) + + self._reload_data[i] = self:_run_callback(mod, "on_reload") + else + self:print("info", "not reloading mod, state: %s", mod.state) + end + end + + self:unload_all_mods() + self:_start_scan() + + self._reload_requested = false +end + +ModManager.on_game_state_changed = function(self, status, state_name, state_object) + if self._state == "done" then + for i = 1, self._num_mods, 1 do + local mod = self._mods[i] + + if mod and not mod.callbacks_disabled then + self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) + end + end + else + self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) + end +end + +ModManager.print = function(self, level, str, ...) + local message = string.format("[ModManager][" .. level .. "] " .. str, ...) + local log_level = LOG_LEVELS[level] or 99 + + if log_level <= 2 then + print(message) + end + + if log_level <= self._settings.log_level then + self._chat_print_buffer[#self._chat_print_buffer + 1] = message + end +end + +local function noop() +end + +ModManager.network_bind = noop + +ModManager.network_unbind = noop + +ModManager.network_is_occupied = function() + return false +end + +ModManager.network_send = noop + +ModManager.rpc_mod_user_data = noop + +ModManager.register_network_event_delegate = noop + +ModManager.unregister_network_event_delegate = noop + +ModManager.network_context_created = noop + +return ModManager diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 58c6753..472e878 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use std::sync::Arc; use color_eyre::eyre::Context; -use color_eyre::{eyre, Result}; +use color_eyre::{eyre, Help, Result}; use druid::FileInfo; use futures::stream; use futures::StreamExt; @@ -28,6 +28,7 @@ const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; const BUNDLE_DATABASE_NAME: &str = "bundle_database.data"; const MOD_BOOT_SCRIPT: &str = "scripts/mod_main"; +const MOD_DATA_SCRIPT: &str = "scripts/mods/mod_data"; #[tracing::instrument] async fn read_file_with_backup

(path: P) -> Result> @@ -138,6 +139,52 @@ fn make_package(info: &PackageInfo) -> Result { Ok(pkg) } +fn build_mod_data_lua(state: Arc) -> String { + let mut lua = String::from("return {\n"); + + for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { + lua.push_str(" {\n name = \""); + lua.push_str(mod_info.get_name()); + + lua.push_str("\",\n id = \""); + lua.push_str(mod_info.get_id()); + + lua.push_str("\",\n run = function()\n"); + + if mod_info.get_name() == "dmf" { + lua.push_str(" return dofile(\""); + lua.push_str(mod_info.get_resources().get_init()); + lua.push_str("\")\n"); + } else { + lua.push_str(" return new_mod(\""); + lua.push_str(mod_info.get_name()); + lua.push_str("\", {\n init = \""); + lua.push_str(mod_info.get_resources().get_init()); + lua.push_str("\",\n data = \""); + lua.push_str(mod_info.get_resources().get_data()); + lua.push_str("\",\n localization = \""); + lua.push_str(mod_info.get_resources().get_localization()); + lua.push_str("\",\n })\n"); + } + + lua.push_str(" end,\n packages = [\n"); + + for pkg_info in mod_info.get_packages() { + lua.push_str(" \""); + lua.push_str(pkg_info.get_name()); + lua.push_str("\",\n"); + } + + lua.push_str(" ]\n }\n"); + } + + lua.push('}'); + + tracing::debug!("mod_data_lua:\n{}", lua); + + lua +} + #[tracing::instrument(skip_all)] async fn build_bundles(state: Arc) -> Result<()> { let mut bundle = Bundle::new(MOD_BUNDLE_NAME.into()); @@ -156,6 +203,31 @@ async fn build_bundles(state: Arc) -> Result<()> { db }; + { + let span = tracing::debug_span!("Building mod data script"); + let _enter = span.enter(); + + let lua = build_mod_data_lua(state.clone()); + let lua = CString::new(lua).wrap_err("failed to build CString from mod data Lua string")?; + let file = + lua::compile(MOD_DATA_SCRIPT, &lua).wrap_err("failed to compile mod data Lua file")?; + + bundle.add_file(file); + } + + { + let span = tracing::debug_span!("Importing mod manager script"); + let _enter = span.enter(); + + let lua = include_str!("../assets/mod_manager.lua"); + let lua = + CString::new(lua).wrap_err("failed to build CString from mod manager Lua string")?; + let file = lua::compile(MOD_MANAGER_SCRIPT, &lua) + .wrap_err("failed to compile mod manager Lua file")?; + + bundle.add_file(file); + } + for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); let _enter = span.enter(); @@ -296,23 +368,15 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { } { - tracing::trace!("Adding main mod Lua file to boot bundle"); - let span = tracing::trace_span!("create mod boot script file"); + let span = tracing::debug_span!("Importing mod main script"); let _enter = span.enter(); - // TODO: Build actual boot script - let lua = CString::new( - r#" -print("dtmm says hello!") -require("scripts/main") -"#, - ) - .expect("invalid C string"); - let f = lua::compile(MOD_BOOT_SCRIPT.to_string(), &lua) - .wrap_err("failed to compile mod boot script")?; + let lua = include_str!("../assets/mod_main.lua"); + let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?; + let file = + lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?; - // TODO: - bundle.add_file(f); + bundle.add_file(file); } db.add_bundle(&bundle); -- 2.45.3 From 659ef1ce71bf06915c5d30f022cd524d59579a74 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 21 Feb 2023 21:06:29 +0100 Subject: [PATCH 29/90] feat: Handle DMF specially for mod loading DMF is part of the mod loading process and can therefore not be treated like a regular mod. --- crates/dtmm/src/engine.rs | 58 ++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 472e878..f711503 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -142,7 +142,13 @@ fn make_package(info: &PackageInfo) -> Result { fn build_mod_data_lua(state: Arc) -> String { let mut lua = String::from("return {\n"); - for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { + // DMF is handled explicitely by the loading procedures, as it actually drives most of that + // and should therefore not show up in the load order. + for mod_info in state + .get_mods() + .iter() + .filter(|m| m.get_id() != "dmf" && m.get_enabled()) + { lua.push_str(" {\n name = \""); lua.push_str(mod_info.get_name()); @@ -150,23 +156,15 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str(mod_info.get_id()); lua.push_str("\",\n run = function()\n"); - - if mod_info.get_name() == "dmf" { - lua.push_str(" return dofile(\""); - lua.push_str(mod_info.get_resources().get_init()); - lua.push_str("\")\n"); - } else { - lua.push_str(" return new_mod(\""); - lua.push_str(mod_info.get_name()); - lua.push_str("\", {\n init = \""); - lua.push_str(mod_info.get_resources().get_init()); - lua.push_str("\",\n data = \""); - lua.push_str(mod_info.get_resources().get_data()); - lua.push_str("\",\n localization = \""); - lua.push_str(mod_info.get_resources().get_localization()); - lua.push_str("\",\n })\n"); - } - + lua.push_str(" return new_mod(\""); + lua.push_str(mod_info.get_name()); + lua.push_str("\", {\n init = \""); + lua.push_str(mod_info.get_resources().get_init()); + lua.push_str("\",\n data = \""); + lua.push_str(mod_info.get_resources().get_data()); + lua.push_str("\",\n localization = \""); + lua.push_str(mod_info.get_resources().get_localization()); + lua.push_str("\",\n })\n"); lua.push_str(" end,\n packages = [\n"); for pkg_info in mod_info.get_packages() { @@ -215,19 +213,6 @@ async fn build_bundles(state: Arc) -> Result<()> { bundle.add_file(file); } - { - let span = tracing::debug_span!("Importing mod manager script"); - let _enter = span.enter(); - - let lua = include_str!("../assets/mod_manager.lua"); - let lua = - CString::new(lua).wrap_err("failed to build CString from mod manager Lua string")?; - let file = lua::compile(MOD_MANAGER_SCRIPT, &lua) - .wrap_err("failed to compile mod manager Lua file")?; - - bundle.add_file(file); - } - for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); let _enter = span.enter(); @@ -415,6 +400,14 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { pub(crate) async fn deploy_mods(state: State) -> Result<()> { let state = Arc::new(state); + { + let mods = state.get_mods(); + let first = mods.get(0); + if first.is_none() || !(first.unwrap().get_id() == "dmf" && first.unwrap().get_enabled()) { + eyre::bail!("'Darktide Mod Framework' needs to be installed, enabled and at the top of the load order"); + } + } + tracing::info!( "Deploying {} mods to {}", state.get_mods().len(), @@ -436,9 +429,6 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { .await .wrap_err("failed to patch game settings")?; - // TODO: Build mod order data - // TODO: Handle DMF - tracing::info!("Finished deploying mods"); Ok(()) } -- 2.45.3 From 6c9d5dabd4db604ea97d078dd815dce8bf9fbc47 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 21 Feb 2023 21:48:15 +0100 Subject: [PATCH 30/90] fix(dtmm): Fix creating default config file Create parent directories if necessary. --- crates/dtmm/src/main.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 28143c5..c640350 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -91,7 +91,7 @@ fn work_thread( } #[cfg(not(arget_os = "windows"))] -fn get_default_config_path() -> String { +fn get_default_config_path() -> PathBuf { let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { let home = std::env::var("HOME").unwrap_or_else(|_| { let user = std::env::var("USER").expect("user env variable not set"); @@ -100,17 +100,17 @@ fn get_default_config_path() -> String { format!("{home}/.config") }); - format!("{config_dir}/dtmm/dtmm.cfg") + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") } #[cfg(target_os = "windows")] -fn get_default_config_path() -> String { +fn get_default_config_path() -> PathBuf { let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); - format!("{config_dir}\\dtmm\\dtmm.cfg") + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") } #[cfg(not(arget_os = "windows"))] -fn get_default_data_dir() -> String { +fn get_default_data_dir() -> PathBuf { let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { let home = std::env::var("HOME").unwrap_or_else(|_| { let user = std::env::var("USER").expect("user env variable not set"); @@ -119,13 +119,13 @@ fn get_default_data_dir() -> String { format!("{home}/.local/share") }); - format!("{data_dir}/dtmm") + PathBuf::from(data_dir).join("dtmm") } #[cfg(target_os = "windows")] -fn get_default_data_dir() -> String { +fn get_default_data_dir() -> PathBuf { let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); - format!("{data_dir}\\dtmm") + PathBuf::from(data_dir).join("dtmm") } #[tracing::instrument] @@ -134,7 +134,7 @@ fn main() -> Result<()> { let default_config_path = get_default_config_path(); - tracing::trace!(default_config_path); + tracing::trace!(default_config_path = %default_config_path.display()); let matches = command!() .arg(Arg::new("oodle").long("oodle").help( @@ -149,7 +149,7 @@ fn main() -> Result<()> { .short('c') .help("Path to the config file") .value_parser(value_parser!(PathBuf)) - .default_value(&default_config_path), + .default_value(default_config_path.to_string_lossy().to_string()), ) .get_matches(); @@ -203,8 +203,17 @@ fn main() -> Result<()> { })?; } + { + let parent = default_config_path + .parent() + .expect("a file path always has a parent directory"); + fs::create_dir_all(parent).wrap_err_with(|| { + format!("failed to create directories {}", parent.display()) + })?; + } + let config = Config { - data_dir: Some(PathBuf::from(get_default_data_dir())), + data_dir: Some(get_default_data_dir()), game_dir: None, }; @@ -212,7 +221,10 @@ fn main() -> Result<()> { let data = serde_sjson::to_string(&config) .wrap_err("failed to serialize default config value")?; fs::write(&default_config_path, data).wrap_err_with(|| { - format!("failed to write default config to {default_config_path}") + format!( + "failed to write default config to {}", + default_config_path.display() + ) })?; } -- 2.45.3 From c73b8d2cc90940feab6404a44df08576db86cc30 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 21 Feb 2023 21:48:59 +0100 Subject: [PATCH 31/90] refactor: Only show unknown commands in debug There are a bunch of optional commands provided by druid that trigger this, but we only really need it to check for custom ones that weren't implemented, yet. --- crates/dtmm/src/state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 8775aff..18a7735 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -390,7 +390,9 @@ impl AppDelegate for Delegate { Handled::Yes } cmd => { - tracing::warn!("Unknown command: {:?}", cmd); + if cfg!(debug_assertions) { + tracing::warn!("Unknown command: {:?}", cmd); + } Handled::No } } -- 2.45.3 From 1decd1ce3af57c0e9566642daf7207ad5f27ddab Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:04:13 +0100 Subject: [PATCH 32/90] fix(dtmt): Fix duplicate prompts --- crates/dtmt/src/cmd/new.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/dtmt/src/cmd/new.rs b/crates/dtmt/src/cmd/new.rs index dd42172..67e35da 100644 --- a/crates/dtmt/src/cmd/new.rs +++ b/crates/dtmt/src/cmd/new.rs @@ -13,7 +13,7 @@ const TEMPLATES: [(&str, &str); 5] = [ "dtmt.cfg", r#"id = "{{id}}" name = "{{name}}" -description = "An elaborate description of my cool game mod!" +description = "This is my new mod '{{name}}'!" version = "0.1.0" resources = { @@ -60,7 +60,7 @@ return { "scripts/mods/{{id}}/localization.lua", r#"return { mod_description = { - en = "An elaborate description of my cool game mod!", + en = "This is my new mod '{{name}}'!", }, }"#, ), @@ -102,7 +102,7 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> let name = if let Some(name) = matches.get_one::("name") { name.clone() } else { - promptly::prompt("The unique mod ID")? + promptly::prompt("The display name")? }; let id = { -- 2.45.3 From 58a3df2d4015c2e7dcc0d8f51f26fa260f7ab5de Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:23:11 +0100 Subject: [PATCH 33/90] fix(dtmt): Fix incorrect field in config template --- crates/dtmt/src/cmd/new.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmt/src/cmd/new.rs b/crates/dtmt/src/cmd/new.rs index 67e35da..187706c 100644 --- a/crates/dtmt/src/cmd/new.rs +++ b/crates/dtmt/src/cmd/new.rs @@ -17,7 +17,7 @@ description = "This is my new mod '{{name}}'!" version = "0.1.0" resources = { - script = "scripts/mods/{{id}}/init" + init = "scripts/mods/{{id}}/init" data = "scripts/mods/{{id}}/data" localization = "scripts/mods/{{id}}/localization" } -- 2.45.3 From 701516aa7cfdac58412393ff7cbbd70c106fd02c Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 09:23:40 +0100 Subject: [PATCH 34/90] feat: Make mod resource scripts optional With splitting DMF and DML, there is now more than one case where this is needed, so it may well be made proper now. The template still defines them, and, as with VT2 most creators will probably stick with it, but they do have the option to make a non-DMF mod now. --- crates/dtmm/src/engine.rs | 34 +++++++++++++++++++++++++--------- crates/dtmm/src/state.rs | 12 ++++++------ crates/dtmt/src/cmd/build.rs | 7 +++++-- lib/sdk/src/lib.rs | 6 ++++-- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index f711503..c994091 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -156,15 +156,31 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str(mod_info.get_id()); lua.push_str("\",\n run = function()\n"); - lua.push_str(" return new_mod(\""); - lua.push_str(mod_info.get_name()); - lua.push_str("\", {\n init = \""); - lua.push_str(mod_info.get_resources().get_init()); - lua.push_str("\",\n data = \""); - lua.push_str(mod_info.get_resources().get_data()); - lua.push_str("\",\n localization = \""); - lua.push_str(mod_info.get_resources().get_localization()); - lua.push_str("\",\n })\n"); + + let resources = mod_info.get_resources(); + if resources.get_data().is_some() || resources.get_localization().is_some() { + lua.push_str(" new_mod(\""); + lua.push_str(mod_info.get_id()); + lua.push_str("\", {\n init = \""); + lua.push_str(resources.get_init()); + + if let Some(data) = resources.get_data() { + lua.push_str("\",\n data = \""); + lua.push_str(data); + } + + if let Some(localization) = resources.get_localization() { + lua.push_str("\",\n localization = \""); + lua.push_str(localization); + } + + lua.push_str("\",\n })\n"); + } else { + lua.push_str(" return dofile(\""); + lua.push_str(resources.get_init()); + lua.push_str("\")"); + } + lua.push_str(" end,\n packages = [\n"); for pkg_info in mod_info.get_packages() { diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 18a7735..f90b1d8 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -62,8 +62,8 @@ impl PackageInfo { #[derive(Clone, Data, Debug)] pub(crate) struct ModResourceInfo { init: String, - data: String, - localization: String, + data: Option, + localization: Option, } impl ModResourceInfo { @@ -71,12 +71,12 @@ impl ModResourceInfo { &self.init } - pub(crate) fn get_data(&self) -> &String { - &self.data + pub(crate) fn get_data(&self) -> Option<&String> { + self.data.as_ref() } - pub(crate) fn get_localization(&self) -> &String { - &self.localization + pub(crate) fn get_localization(&self) -> Option<&String> { + self.localization.as_ref() } } diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index a5a471f..55acad1 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -72,9 +72,12 @@ async fn find_project_config(dir: Option) -> Result { }; let mut buf = String::new(); - file.read_to_string(&mut buf).await?; + file.read_to_string(&mut buf) + .await + .wrap_err("invalid UTF-8")?; - let mut cfg: ModConfig = serde_sjson::from_str(&buf)?; + let mut cfg: ModConfig = + serde_sjson::from_str(&buf).wrap_err("failed to deserialize mod config")?; cfg.dir = path; Ok(cfg) } diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 003e400..1e2f5bb 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -13,8 +13,10 @@ pub use context::Context; #[derive(Clone, Debug, Default, serde::Deserialize)] pub struct ModConfigResources { pub init: String, - pub data: String, - pub localization: String, + #[serde(default)] + pub data: Option, + #[serde(default)] + pub localization: Option, } #[derive(Clone, Debug, Default, serde::Deserialize)] -- 2.45.3 From 0705430b957e1e473a022bcaf96311847f8c85ca Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 11:58:39 +0100 Subject: [PATCH 35/90] refactor: Move mod loading to separate base mod --- crates/dtmm/assets/mod_main.lua | 22 +- crates/dtmm/assets/mod_manager.lua | 357 ----------------------------- crates/dtmm/src/engine.rs | 7 +- 3 files changed, 13 insertions(+), 373 deletions(-) delete mode 100644 crates/dtmm/assets/mod_manager.lua diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index e8a83e3..4422b4a 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -1,11 +1,7 @@ -Mods = { - -- Keep a backup of certain system libraries before - -- Fatshark's code scrubs them. - -- The metatable setup prevents mods from overwriting them. - lua = setmetatable({}, { - __index = { io = io, debug = debug, ffi = ffi, os = os }, - }), -} +-- Keep a backup of certain system libraries before +-- Fatshark's code scrubs them. +-- The loader can then decide to pass them on to mods, or ignore them +local libs = { io = io, debug = debug, ffi = ffi, os = os } require("scripts/game_states/boot/state_boot_sub_state_base") local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") @@ -18,7 +14,7 @@ StateBootLoadMods.on_enter = function (self, parent, params) self._package_manager = package_manager self._package_handles = { ["packages/mods"] = package_manager:load("packages/mods", "StateBootLoadMods", nil), - ["packages/dmf"] = package_manager:load("packages/dmf", "StateBootLoadMods", nil), + ["packages/dml"] = package_manager:load("packages/dml", "StateBootLoadMods", nil), } end @@ -28,12 +24,12 @@ StateBootLoadMods._state_update = function (self, dt) if state == "load_package" and package_manager:update() then self._state = "load_mods" - local dmf_loader = require("scripts/mods/dmf/dmf_loader") - self._dmf_loader = dmf_loader + local mod_loader = require("scripts/mods/dml/init") + self._mod_loader = mod_loader local mod_data = require("scripts/mods/mod_data") - dmf_loader:init(self._parent.gui, mod_data) - elseif state == "load_mods" and self._dmf_loader:update(dt) then + mod_loader:init(mod_data, libs, self._parent.gui) + elseif state == "load_mods" and self._mod_loader:update(dt) then return true, false end diff --git a/crates/dtmm/assets/mod_manager.lua b/crates/dtmm/assets/mod_manager.lua deleted file mode 100644 index 549a9b6..0000000 --- a/crates/dtmm/assets/mod_manager.lua +++ /dev/null @@ -1,357 +0,0 @@ --- Copyright on this file is owned by Fatshark. --- It is extracted, used and modified with permission only for --- the purpose of loading mods within Warhammer 40,000: Darktide. -local ModManager = class("ModManager") - -local MOD_DATA = require("scripts/mods/mod_data") - -local LOG_LEVELS = { - spew = 4, - info = 3, - warning = 2, - error = 1 -} -local DEFAULT_SETTINGS = { - log_level = LOG_LEVELS.error, - developer_mode = false -} - -local Keyboard = Keyboard -local BUTTON_INDEX_R = Keyboard.button_index("r") -local BUTTON_INDEX_LEFT_SHIFT = Keyboard.button_index("left shift") -local BUTTON_INDEX_LEFT_CTRL = Keyboard.button_index("left ctrl") - -ModManager.init = function(self, boot_gui) - self._mods = {} - self._num_mods = nil - self._state = "not_loaded" - self._settings = Application.user_setting("mod_settings") or DEFAULT_SETTINGS - - self._chat_print_buffer = {} - self._reload_data = {} - self._gui = boot_gui - self._ui_time = 0 - self._network_callbacks = {} - - Crashify.print_property("realm", "modded") - - self._state = "scanning" -end - -ModManager.developer_mode_enabled = function(self) - return self._settings.developer_mode -end - -ModManager._draw_state_to_gui = function(self, gui, dt) - local state = self._state - local t = self._ui_time + dt - self._ui_time = t - local status_str = "Loading mods" - - if state == "scanning" then - status_str = "Scanning for mods" - elseif state == "loading" then - local mod = self._mods[self._mod_load_index] - status_str = string.format("Loading mod %q", mod.name) - end - - Gui.text(gui, status_str .. string.rep(".", (2 * t) % 4), "materials/fonts/arial", 16, nil, Vector3(5, 10, 1)) -end - -ModManager.remove_gui = function(self) - self._gui = nil -end - -ModManager._has_enabled_mods = function() - return true -end - -ModManager._check_reload = function() - return Keyboard.pressed(BUTTON_INDEX_R) and - Keyboard.button(BUTTON_INDEX_LEFT_SHIFT) + - Keyboard.button(BUTTON_INDEX_LEFT_CTRL) == 2 -end - -ModManager.update = function(self, dt) - local chat_print_buffer = self._chat_print_buffer - local num_delayed_prints = #chat_print_buffer - - if num_delayed_prints > 0 and Managers.chat then - for i = 1, num_delayed_prints, 1 do - -- TODO: Use new chat system - -- Managers.chat:add_local_system_message(1, chat_print_buffer[i], true) - - chat_print_buffer[i] = nil - end - end - - local old_state = self._state - - if self._settings.developer_mode and self:_check_reload() then - self._reload_requested = true - end - - if self._reload_requested and self._state == "done" then - self:_reload_mods() - end - - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "update", dt) - end - end - elseif self._state == "scanning" then - self:_build_mod_table() - - self._state = self:_load_mod(1) - self._ui_time = 0 - elseif self._state == "loading" then - local handle = self._loading_resource_handle - - if ResourcePackage.has_loaded(handle) then - ResourcePackage.flush(handle) - - local mod = self._mods[self._mod_load_index] - local next_index = mod.package_index + 1 - local mod_data = mod.data - - if next_index > #mod_data.packages then - mod.state = "running" - local ok, object = pcall(mod_data.run) - - if not ok then self:print("error", "%s", object) end - - local name = mod.name - mod.object = object or {} - - self:_run_callback(mod, "init", self._reload_data[mod.id]) - self:print("info", "%s loaded.", name) - - self._state = self:_load_mod(self._mod_load_index + 1) - else - self:_load_package(mod, next_index) - end - end - end - - local gui = self._gui - - if gui then self:_draw_state_to_gui(gui, dt) end - - if old_state ~= self._state then - self:print("info", "%s -> %s", old_state, self._state) - end -end - -ModManager.all_mods_loaded = function(self) - return self._state == "done" -end - -ModManager.destroy = function(self) - self:unload_all_mods() -end - -ModManager._run_callback = function(self, mod, callback_name, ...) - local object = mod.object - local cb = object[callback_name] - - if not cb then - return - end - - local success, val = pcall(cb, object, ...) - - if success then - return val - else - self:print("error", "%s", val or "[unknown error]") - self:print("error", "Failed to run callback %q for mod %q with id %d. Disabling callbacks until reload.", - callback_name, mod.name, mod.id) - - mod.callbacks_disabled = true - end -end - -ModManager._start_scan = function(self) - self:print("info", "Starting mod scan") - self._state = "scanning" -end - -ModManager._build_mod_table = function(self) - fassert(table.is_empty(self._mods), "Trying to add mods to non-empty mod table") - - for i, mod_data in ipairs(MOD_DATA) do - printf("[ModManager] mods[%d] = name=%q", i, mod_data.name) - - self._mods[i] = { - id = i, - state = "not_loaded", - callbacks_disabled = false, - name = mod_data.name, - loaded_packages = {}, - packages = mod_data.packages, - } - end - - self._num_mods = #self._mods - - self:print("info", "Found %i mods", #self._mods) -end - -ModManager._load_mod = function(self, index) - self._ui_time = 0 - local mods = self._mods - local mod = mods[index] - - if not mod then - table.clear(self._reload_data) - - return "done" - end - - self:print("info", "loading mod %i", mod.id) - Crashify.print_property("modded", true) - - mod.state = "loading" - - Crashify.print_property(string.format("Mod:%i:%s", mod.id, mod.name), true) - - self._mod_load_index = index - - self:_load_package(mod, 1) - - return "loading" -end - -ModManager._load_package = function(self, mod, index) - mod.package_index = index - local package_name = mod.packages[index] - - if not package_name then - return - end - - self:print("info", "loading package %q", package_name) - - local resource_handle = Application.resource_package(package_name) - self._loading_resource_handle = resource_handle - - ResourcePackage.load(resource_handle) - - mod.loaded_packages[#mod.loaded_packages + 1] = resource_handle -end - -ModManager.unload_all_mods = function(self) - if self._state ~= "done" then - self:print("error", "Mods can't be unloaded, mod state is not \"done\". current: %q", self._state) - - return - end - - self:print("info", "Unload all mod packages") - - for i = self._num_mods, 1, -1 do - local mod = self._mods[i] - - if mod then - self:unload_mod(i) - end - - self._mods[i] = nil - end - - self._num_mods = nil - self._state = "unloaded" -end - -ModManager.unload_mod = function(self, index) - local mod = self._mods[index] - - if mod then - self:print("info", "Unloading %q.", mod.name) - self:_run_callback(mod, "on_unload") - - for _, handle in ipairs(mod.loaded_packages) do - ResourcePackage.unload(handle) - Application.release_resource_package(handle) - end - - mod.state = "not_loaded" - else - self:print("error", "Mod index %i can't be unloaded, has not been loaded", index) - end -end - -ModManager._reload_mods = function(self) - self:print("info", "reloading mods") - - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and mod.state == "running" then - self:print("info", "reloading %s", mod.name) - - self._reload_data[i] = self:_run_callback(mod, "on_reload") - else - self:print("info", "not reloading mod, state: %s", mod.state) - end - end - - self:unload_all_mods() - self:_start_scan() - - self._reload_requested = false -end - -ModManager.on_game_state_changed = function(self, status, state_name, state_object) - if self._state == "done" then - for i = 1, self._num_mods, 1 do - local mod = self._mods[i] - - if mod and not mod.callbacks_disabled then - self:_run_callback(mod, "on_game_state_changed", status, state_name, state_object) - end - end - else - self:print("warning", "Ignored on_game_state_changed call due to being in state %q", self._state) - end -end - -ModManager.print = function(self, level, str, ...) - local message = string.format("[ModManager][" .. level .. "] " .. str, ...) - local log_level = LOG_LEVELS[level] or 99 - - if log_level <= 2 then - print(message) - end - - if log_level <= self._settings.log_level then - self._chat_print_buffer[#self._chat_print_buffer + 1] = message - end -end - -local function noop() -end - -ModManager.network_bind = noop - -ModManager.network_unbind = noop - -ModManager.network_is_occupied = function() - return false -end - -ModManager.network_send = noop - -ModManager.rpc_mod_user_data = noop - -ModManager.register_network_event_delegate = noop - -ModManager.unregister_network_event_delegate = noop - -ModManager.network_context_created = noop - -return ModManager diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index c994091..a8cab2e 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -147,7 +147,7 @@ fn build_mod_data_lua(state: Arc) -> String { for mod_info in state .get_mods() .iter() - .filter(|m| m.get_id() != "dmf" && m.get_enabled()) + .filter(|m| m.get_id() != "dml" && m.get_enabled()) { lua.push_str(" {\n name = \""); lua.push_str(mod_info.get_name()); @@ -419,8 +419,9 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { { let mods = state.get_mods(); let first = mods.get(0); - if first.is_none() || !(first.unwrap().get_id() == "dmf" && first.unwrap().get_enabled()) { - eyre::bail!("'Darktide Mod Framework' needs to be installed, enabled and at the top of the load order"); + if first.is_none() || !(first.unwrap().get_id() == "dml" && first.unwrap().get_enabled()) { + // TODO: Add a suggestion where to get it, once that's published + eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order"); } } -- 2.45.3 From d9e6cc05a092c10b0dfb70c7c5f620ae5d0ab88f Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 15:28:26 +0100 Subject: [PATCH 36/90] fix(dtmm): Add additional Lua globals to backup --- crates/dtmm/assets/mod_main.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 4422b4a..e825248 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -1,7 +1,15 @@ -- Keep a backup of certain system libraries before -- Fatshark's code scrubs them. -- The loader can then decide to pass them on to mods, or ignore them -local libs = { io = io, debug = debug, ffi = ffi, os = os } +local libs = { + io = io, + debug = debug, + ffi = ffi, + os = os, + load = load, + loadfile = loadfile, + loadstring = loadstring, +} require("scripts/game_states/boot/state_boot_sub_state_base") local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") -- 2.45.3 From 09c0ca777d68ae8b6ce3e58c3c3e4fd9d9d70c1c Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 18:38:53 +0100 Subject: [PATCH 37/90] fix(dtmm): Fix generated mod data Lua code --- crates/dtmm/src/engine.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index a8cab2e..85750c3 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -181,7 +181,7 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str("\")"); } - lua.push_str(" end,\n packages = [\n"); + lua.push_str(" end,\n packages = {\n"); for pkg_info in mod_info.get_packages() { lua.push_str(" \""); @@ -189,7 +189,7 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str("\",\n"); } - lua.push_str(" ]\n }\n"); + lua.push_str(" }\n }\n"); } lua.push('}'); -- 2.45.3 From 2a49b4a5b99892db02955593f56eb655de295913 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 18:41:15 +0100 Subject: [PATCH 38/90] fix(dtmm): Fix deploying mod bundles --- crates/dtmm/src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 85750c3..8642f5b 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -233,7 +233,7 @@ async fn build_bundles(state: Arc) -> Result<()> { let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); let _enter = span.enter(); - let mod_dir = state.get_mod_dir().join(mod_info.get_name()); + let mod_dir = state.get_mod_dir().join(mod_info.get_id()); for pkg_info in mod_info.get_packages() { let span = tracing::trace_span!("building package", name = pkg_info.get_name()); let _enter = span.enter(); -- 2.45.3 From f1375ae88d007fc53b70d5a1bcc88d082072f1f9 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 18:51:58 +0100 Subject: [PATCH 39/90] fix(dtmm): Fix re-deploying the same mod --- crates/dtmm/src/engine.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 8642f5b..05e792e 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -275,6 +275,13 @@ async fn build_bundles(state: Arc) -> Result<()> { src.display(), dest.display() ); + // We attempt to remove any previous file, so that the hard link can be created. + // We can reasonably ignore errors here, as a 'NotFound' is actually fine, the link + // may be possible anyways, or the error will be reported by it anyways. + // TODO: There is a chance that we delete an actual game bundle, but with 64bit + // hashes, it's low enough for now, and the set up required to detect + // "game bundle vs mod bundle" is non-trivial. + let _ = fs::remove_file(dest.as_ref()).await; fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| { format!("failed to hard link bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", src.display(), dest.display()) }) -- 2.45.3 From 0c071b5b0a377c71269e696419b004ef7382e494 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 19:02:59 +0100 Subject: [PATCH 40/90] fix(dtmm): Copy mod bundle Hard linking doesn't work across devices/mount points, and I'm not keen on reworking the data directory to be placed accordingly, right now. --- crates/dtmm/src/engine.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 05e792e..e481214 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -253,7 +253,7 @@ async fn build_bundles(state: Arc) -> Result<()> { .to_string() .to_ascii_lowercase(); let src = mod_dir.join(&bundle_name); - let dest = bundle_dir.clone(); + let dest = bundle_dir.join(&bundle_name); let pkg_name = pkg_info.get_name().clone(); let mod_name = mod_info.get_name().clone(); @@ -276,14 +276,18 @@ async fn build_bundles(state: Arc) -> Result<()> { dest.display() ); // We attempt to remove any previous file, so that the hard link can be created. - // We can reasonably ignore errors here, as a 'NotFound' is actually fine, the link - // may be possible anyways, or the error will be reported by it anyways. + // We can reasonably ignore errors here, as a 'NotFound' is actually fine, the copy + // may be possible despite an error here, or the error will be reported by it anyways. // TODO: There is a chance that we delete an actual game bundle, but with 64bit - // hashes, it's low enough for now, and the set up required to detect + // hashes, it's low enough for now, and the setup required to detect // "game bundle vs mod bundle" is non-trivial. - let _ = fs::remove_file(dest.as_ref()).await; - fs::hard_link(&src, dest.as_ref()).await.wrap_err_with(|| { - format!("failed to hard link bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", src.display(), dest.display()) + let _ = fs::remove_file(&dest).await; + fs::copy(&src, &dest).await.wrap_err_with(|| { + format!( + "failed to copy bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", + src.display(), + dest.display() + ) }) } .instrument(span); -- 2.45.3 From 41344f022d1ab686786d3698a353a42ebc04bea0 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 23 Feb 2023 13:30:39 +0100 Subject: [PATCH 41/90] feat(dtmm): Delete mod files Only files in `data_dir` will be deleted, deployed bundles will stay for now. See #29 for the rational. Closes #24. --- crates/dtmm/src/engine.rs | 10 +++++++++ crates/dtmm/src/main.rs | 22 ++++++++++++++++++++ crates/dtmm/src/main_window.rs | 18 ++++++++++++----- crates/dtmm/src/state.rs | 37 ++++++++++++++++++++++++++++++---- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index e481214..06c8c5c 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -544,3 +544,13 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result Ok(info) } + +#[tracing::instrument(skip(state))] +pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { + let mod_dir = state.get_mod_dir().join(info.get_id()); + fs::remove_dir_all(&mod_dir) + .await + .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; + + Ok(()) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index c640350..a738fef 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -17,10 +17,12 @@ use druid::AppLauncher; use druid::ExtEventSink; use druid::SingleUse; use druid::Target; +use engine::delete_mod; use engine::import_mod; use serde::Deserialize; use serde::Serialize; use state::ACTION_FINISH_ADD_MOD; +use state::ACTION_FINISH_DELETE_SELECTED_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; @@ -83,6 +85,26 @@ fn work_thread( } } }), + AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { + if let Err(err) = delete_mod(state, &info).await { + tracing::error!( + "Failed to delete mod files. \ + You might want to clean up the data directory manually. \ + Reason: {:?}", + err + ); + } + + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_DELETE_SELECTED_MOD, + SingleUse::new(info), + Target::Auto, + ) + .expect("failed to send command"); + }), }; } }); diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index ecf6c2e..107b3aa 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -3,12 +3,14 @@ use druid::widget::{ Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, TextBox, ViewSwitcher, }; -use druid::{lens, FileDialogOptions, FileSpec, Insets, LensExt, Widget, WidgetExt, WindowDesc}; +use druid::{ + lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc, +}; -use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_ADD_MOD}; +use crate::state::{ModInfo, PathBufFormatter, State, View}; use crate::state::{ - ACTION_DELETE_SELECTED_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, - ACTION_SELECT_MOD, ACTION_START_DEPLOY, + ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, + ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, }; use crate::theme; use crate::widget::ExtraWidgetExt; @@ -163,7 +165,13 @@ fn build_mod_details() -> impl Widget { }); let button_delete_mod = Button::new("Delete Mod") - .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_DELETE_SELECTED_MOD)) + .on_click(|ctx, data: &mut Option, _env| { + if let Some(info) = data { + ctx.submit_command( + ACTION_START_DELETE_SELECTED_MOD.with(SingleUse::new(info.clone())), + ); + } + }) .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index f90b1d8..93afd61 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -16,8 +16,10 @@ pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down"); -pub(crate) const ACTION_DELETE_SELECTED_MOD: Selector = - Selector::new("dtmm.action.delete-selected-mod"); +pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.srart-delete-selected-mod"); +pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.finish-delete-selected-mod"); pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); @@ -284,6 +286,7 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { pub(crate) enum AsyncAction { DeployMods(State), AddMod((State, FileInfo)), + DeleteMod((State, ModInfo)), } pub(crate) struct Delegate { @@ -360,8 +363,34 @@ impl AppDelegate for Delegate { state.selected_mod_index = Some(i + 1); Handled::Yes } - cmd if cmd.is(ACTION_DELETE_SELECTED_MOD) => { - let Some(index) = state.selected_mod_index else { + cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + if self + .sender + .send(AsyncAction::DeleteMod((state.clone(), info))) + .is_ok() + { + state.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + let mods = state.get_mods(); + let found = mods + .iter() + .enumerate() + .find(|(_, i)| i.get_id() == info.get_id()); + let Some((index, _)) = found else { return Handled::No; }; -- 2.45.3 From 45e0d79fa7578af8003f547ccbe2a441d448b791 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 23 Feb 2023 15:18:16 +0100 Subject: [PATCH 42/90] refactor: Add crate for shared functionality Adds an additional crate for shared functionality between dtmt and dtmm that doesn't fit in the sdk. --- Cargo.lock | 9 +++++++++ crates/dtmm/Cargo.toml | 7 ++++--- crates/dtmm/src/engine.rs | 4 ++-- crates/dtmm/src/state.rs | 2 +- crates/dtmt/Cargo.toml | 19 ++++++++++--------- crates/dtmt/src/cmd/build.rs | 3 ++- lib/dtmt-shared/Cargo.toml | 9 +++++++++ lib/dtmt-shared/README.adoc | 13 +++++++++++++ lib/dtmt-shared/src/lib.rs | 22 ++++++++++++++++++++++ lib/dtmt-shared/src/log.rs | 6 ++++++ lib/sdk/src/lib.rs | 23 ----------------------- 11 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 lib/dtmt-shared/Cargo.toml create mode 100644 lib/dtmt-shared/README.adoc create mode 100644 lib/dtmt-shared/src/lib.rs create mode 100644 lib/dtmt-shared/src/log.rs diff --git a/Cargo.lock b/Cargo.lock index dbfa6fe..55dd12d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -673,6 +673,7 @@ dependencies = [ "color-eyre", "confy", "druid", + "dtmt-shared", "futures", "oodle-sys", "sdk", @@ -694,6 +695,7 @@ dependencies = [ "color-eyre", "confy", "csv-async", + "dtmt-shared", "futures", "futures-util", "glob", @@ -715,6 +717,13 @@ dependencies = [ "zip", ] +[[package]] +name = "dtmt-shared" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "dwrote" version = "0.11.0" diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 9eaec24..65b1bca 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -11,11 +11,12 @@ clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "str color-eyre = "0.6.2" confy = "0.5.1" druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } +dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } futures = "0.3.25" -sdk = { path = "../../lib/sdk", version = "0.2.0" } -serde = { version = "1.0.152", features = ["derive"] } -serde_sjson = { path = "../../lib/serde_sjson", version = "*" } oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +sdk = { path = "../../lib/sdk", version = "0.2.0" } +serde_sjson = { path = "../../lib/serde_sjson", version = "*" } +serde = { version = "1.0.152", features = ["derive"] } tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tracing = "0.1.37" tracing-error = "0.2.0" diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 06c8c5c..3509b0b 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -8,14 +8,14 @@ use std::sync::Arc; use color_eyre::eyre::Context; use color_eyre::{eyre, Help, Result}; use druid::FileInfo; +use dtmt_shared::ModConfig; use futures::stream; use futures::StreamExt; use sdk::filetype::lua; use sdk::filetype::package::Package; use sdk::murmur::Murmur64; use sdk::{ - Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ModConfig, - ToBinary, + Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; use tokio::io::AsyncWriteExt; use tokio::{fs, try_join}; diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 93afd61..315cd2e 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -7,7 +7,7 @@ use druid::{ AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse, Target, }; -use sdk::ModConfig; +use dtmt_shared::ModConfig; use tokio::sync::mpsc::UnboundedSender; use crate::Config; diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index cb8f646..3b8cb36 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -5,28 +5,29 @@ edition = "2021" [dependencies] clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "unicode"] } +cli-table = { version = "0.4.7", default-features = false, features = ["derive"] } color-eyre = "0.6.2" +confy = "0.5.1" csv-async = { version = "1.2.4", features = ["tokio", "serde"] } -sdk = { path = "../../lib/sdk", version = "0.2.0" } +dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } futures = "0.3.25" futures-util = "0.3.24" glob = "0.3.0" libloading = "0.7.4" nanorand = "0.7.0" -pin-project-lite = "0.2.9" -serde = { version = "1.0.147", features = ["derive"] } oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +pin-project-lite = "0.2.9" +promptly = "0.3.1" +sdk = { path = "../../lib/sdk", version = "0.2.0" } serde_sjson = { path = "../../lib/serde_sjson", version = "*" } -tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } +serde = { version = "1.0.147", features = ["derive"] } +string_template = "0.2.1" tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } -tracing = { version = "0.1.37", features = ["async-await"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } -confy = "0.5.1" +tracing = { version = "0.1.37", features = ["async-await"] } zip = "0.6.3" -string_template = "0.2.1" -promptly = "0.3.1" -cli-table = { version = "0.4.7", default-features = false, features = ["derive"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index 55acad1..f470d97 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -4,10 +4,11 @@ use std::sync::Arc; use clap::{value_parser, Arg, ArgMatches, Command}; use color_eyre::eyre::{self, Context, Result}; use color_eyre::{Help, Report}; +use dtmt_shared::ModConfig; use futures::future::try_join_all; use futures::StreamExt; use sdk::filetype::package::Package; -use sdk::{Bundle, BundleFile, ModConfig}; +use sdk::{Bundle, BundleFile}; use tokio::fs::{self, File}; use tokio::io::AsyncReadExt; diff --git a/lib/dtmt-shared/Cargo.toml b/lib/dtmt-shared/Cargo.toml new file mode 100644 index 0000000..30daa24 --- /dev/null +++ b/lib/dtmt-shared/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dtmt-shared" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "1.0.152" diff --git a/lib/dtmt-shared/README.adoc b/lib/dtmt-shared/README.adoc new file mode 100644 index 0000000..01b26ec --- /dev/null +++ b/lib/dtmt-shared/README.adoc @@ -0,0 +1,13 @@ += dtmt-shared +:idprefix: +:idseparator: +:toc: macro +:toclevels: 1 +:!toc-title: +:caution-caption: :fire: +:important-caption: :exclamtion: +:note-caption: :paperclip: +:tip-caption: :bulb: +:warning-caption: :warning: + +A set of types and functions shared between multiple crates within _Darktide Mod Tools_ that don't fit into the engine SDK. diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs new file mode 100644 index 0000000..592d779 --- /dev/null +++ b/lib/dtmt-shared/src/lib.rs @@ -0,0 +1,22 @@ +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub struct ModConfigResources { + pub init: String, + #[serde(default)] + pub data: Option, + #[serde(default)] + pub localization: Option, +} + +#[derive(Clone, Debug, Default, serde::Deserialize)] +pub struct ModConfig { + #[serde(skip)] + pub dir: std::path::PathBuf, + pub id: String, + pub name: String, + pub description: String, + pub version: String, + pub packages: Vec, + pub resources: ModConfigResources, + #[serde(default)] + pub depends: Vec, +} diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs new file mode 100644 index 0000000..6b48375 --- /dev/null +++ b/lib/dtmt-shared/src/log.rs @@ -0,0 +1,6 @@ +use tracing_subscriber::fmt::format::Writer; + +fn format_time(w: &mut Writer) -> std::fmt::Result { + let time = now_local(); + write!(w, ""); +} diff --git a/lib/sdk/src/lib.rs b/lib/sdk/src/lib.rs index 1e2f5bb..e229e28 100644 --- a/lib/sdk/src/lib.rs +++ b/lib/sdk/src/lib.rs @@ -9,26 +9,3 @@ pub use bundle::database::BundleDatabase; pub use bundle::decompress; pub use bundle::{Bundle, BundleFile, BundleFileType, BundleFileVariant}; pub use context::Context; - -#[derive(Clone, Debug, Default, serde::Deserialize)] -pub struct ModConfigResources { - pub init: String, - #[serde(default)] - pub data: Option, - #[serde(default)] - pub localization: Option, -} - -#[derive(Clone, Debug, Default, serde::Deserialize)] -pub struct ModConfig { - #[serde(skip)] - pub dir: std::path::PathBuf, - pub id: String, - pub name: String, - pub description: String, - pub version: String, - pub packages: Vec, - pub resources: ModConfigResources, - #[serde(default)] - pub depends: Vec, -} -- 2.45.3 From 44a0b9a6516b06542d466150f2893d0329353cc1 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 23 Feb 2023 21:05:53 +0100 Subject: [PATCH 43/90] feat: Reduce clutter in release mode logging Ref #7. --- Cargo.lock | 26 +++++++++ crates/dtmm/src/main.rs | 30 +--------- crates/dtmt/src/main.rs | 17 +----- lib/dtmt-shared/Cargo.toml | 4 ++ lib/dtmt-shared/src/lib.rs | 4 ++ lib/dtmt-shared/src/log.rs | 115 +++++++++++++++++++++++++++++++++++-- 6 files changed, 147 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55dd12d..ef1fbb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,6 +722,10 @@ name = "dtmt-shared" version = "0.1.0" dependencies = [ "serde", + "time", + "tracing", + "tracing-error", + "tracing-subscriber", ] [[package]] @@ -1564,6 +1568,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -2277,8 +2290,12 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" dependencies = [ + "itoa", + "libc", + "num_threads", "serde", "time-core", + "time-macros", ] [[package]] @@ -2287,6 +2304,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" +dependencies = [ + "time-core", +] + [[package]] name = "tinystr" version = "0.7.1" diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index a738fef..32c15f8 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -26,9 +26,6 @@ use state::ACTION_FINISH_DELETE_SELECTED_MOD; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; -use tracing_error::ErrorLayer; -use tracing_subscriber::prelude::*; -use tracing_subscriber::EnvFilter; use crate::engine::deploy_mods; use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY}; @@ -175,32 +172,7 @@ fn main() -> Result<()> { ) .get_matches(); - { - let filter_layer = - EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; - - if cfg!(debug_assertions) { - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } else { - let fmt_layer = tracing_subscriber::fmt::layer().compact(); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } - } + dtmt_shared::create_tracing_subscriber(); unsafe { oodle_sys::init(matches.get_one::("oodle")); diff --git a/crates/dtmt/src/main.rs b/crates/dtmt/src/main.rs index 830550d..dc4853e 100644 --- a/crates/dtmt/src/main.rs +++ b/crates/dtmt/src/main.rs @@ -13,9 +13,6 @@ use serde::{Deserialize, Serialize}; use tokio::fs::File; use tokio::io::BufReader; use tokio::sync::RwLock; -use tracing_error::ErrorLayer; -use tracing_subscriber::prelude::*; -use tracing_subscriber::EnvFilter; mod cmd { pub mod build; @@ -62,19 +59,7 @@ async fn main() -> Result<()> { // .subcommand(cmd::watch::command_definition()) .get_matches(); - { - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); - let filter_layer = - EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } + dtmt_shared::create_tracing_subscriber(); // TODO: Move this into a `Context::init` method? let ctx = sdk::Context::new(); diff --git a/lib/dtmt-shared/Cargo.toml b/lib/dtmt-shared/Cargo.toml index 30daa24..0f8ed63 100644 --- a/lib/dtmt-shared/Cargo.toml +++ b/lib/dtmt-shared/Cargo.toml @@ -7,3 +7,7 @@ edition = "2021" [dependencies] serde = "1.0.152" +time = { version = "0.3.19", features = ["formatting", "local-offset", "macros"] } +tracing = "0.1.37" +tracing-error = "0.2.0" +tracing-subscriber = "0.3.16" diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs index 592d779..6625c5b 100644 --- a/lib/dtmt-shared/src/lib.rs +++ b/lib/dtmt-shared/src/lib.rs @@ -1,3 +1,7 @@ +mod log; + +pub use log::*; + #[derive(Clone, Debug, Default, serde::Deserialize)] pub struct ModConfigResources { pub init: String, diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs index 6b48375..41b015d 100644 --- a/lib/dtmt-shared/src/log.rs +++ b/lib/dtmt-shared/src/log.rs @@ -1,6 +1,113 @@ -use tracing_subscriber::fmt::format::Writer; +// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code +// unused. These sections should be small enough that no truly dead code slips in. -fn format_time(w: &mut Writer) -> std::fmt::Result { - let time = now_local(); - write!(w, ""); +#[allow(dead_code)] +mod prod { + use std::fmt::Result; + + use time::format_description::FormatItem; + use time::macros::format_description; + use time::OffsetDateTime; + use tracing::field::Field; + use tracing::{Event, Metadata, Subscriber}; + use tracing_error::ErrorLayer; + use tracing_subscriber::filter::FilterFn; + use tracing_subscriber::fmt::format::{debug_fn, Writer}; + use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; + use tracing_subscriber::prelude::*; + use tracing_subscriber::registry::LookupSpan; + use tracing_subscriber::EnvFilter; + + const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); + + fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { + if field.name() == "message" { + write!(w, "{:?}", val) + } else { + Ok(()) + } + } + + fn filter(metadata: &Metadata<'_>) -> bool { + metadata + .fields() + .iter() + .any(|field| field.name() == "message") + } + + struct Formatter; + + impl FormatEvent for Formatter + where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, + { + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> Result { + let meta = event.metadata(); + + let time = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let time = time.format(TIME_FORMAT).map_err(|_| std::fmt::Error)?; + + write!(writer, "[{}] [{:>5}] ", time, meta.level())?; + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } + } + + /// Creates a subscriber that + /// - only prints events that contain a message + /// - does not print fields + /// - does not print spans/targets + /// - only prints time, not date + pub fn create_tracing_subscriber() { + let filter_layer = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); + + let fmt_layer = tracing_subscriber::fmt::layer() + .event_format(Formatter) + .fmt_fields(debug_fn(format_field)); + + tracing_subscriber::registry() + .with(FilterFn::new(filter)) + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::new( + tracing_subscriber::fmt::format::Pretty::default(), + )) + .init(); + } } + +#[allow(dead_code)] +mod dev { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + pub fn create_tracing_subscriber() { + let filter_layer = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); + let fmt_layer = tracing_subscriber::fmt::layer().pretty(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::new( + tracing_subscriber::fmt::format::Pretty::default(), + )) + .init(); + } +} + +#[cfg(debug_assertions)] +pub use dev::create_tracing_subscriber; + +#[cfg(not(debug_assertions))] +pub use prod::create_tracing_subscriber; -- 2.45.3 From b20b3c4e66d8f1f1eb2d1bbfa089f67a31100f8a Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 22 Feb 2023 22:06:16 +0100 Subject: [PATCH 44/90] WIP: mod_main logging and more DML specialication DML's package needs to be added to the boot bundle as well. Current issue: Adding any package file to the bundle breaks the game's loading. But I don't know where this messes things up. --- crates/dtmm/assets/mod_main.lua | 19 +++++++++++----- crates/dtmm/src/engine.rs | 40 ++++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index e825248..f4eae1b 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -1,3 +1,4 @@ +print("[mod_main] Initializing mods...") -- Keep a backup of certain system libraries before -- Fatshark's code scrubs them. -- The loader can then decide to pass them on to mods, or ignore them @@ -11,10 +12,14 @@ local libs = { loadstring = loadstring, } +require("scripts/main") +print("[mod_main] 'scripts/main' loaded") + require("scripts/game_states/boot/state_boot_sub_state_base") local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") StateBootLoadMods.on_enter = function (self, parent, params) + print("[mod_main][StateBootLoadMods] Entered") StateBootLoadMods.super.on_enter(self, parent, params) local state_params = self:_state_params() @@ -31,6 +36,7 @@ StateBootLoadMods._state_update = function (self, dt) local package_manager = self._package_manager if state == "load_package" and package_manager:update() then + print("[mod_main][StateBootLoadMods] Packages loaded, loading mods") self._state = "load_mods" local mod_loader = require("scripts/mods/dml/init") self._mod_loader = mod_loader @@ -38,18 +44,18 @@ StateBootLoadMods._state_update = function (self, dt) local mod_data = require("scripts/mods/mod_data") mod_loader:init(mod_data, libs, self._parent.gui) elseif state == "load_mods" and self._mod_loader:update(dt) then + print("[mod_main][StateBootLoadMods] Mods loaded, exiting") return true, false end return false, false end -require("scripts/main") - -- Patch `GameStateMachine.init` to add our own state for loading mods. -- In the future, Fatshark might provide us with a dedicated way to do this. local function patch_mod_loading_state() - local GameStateMachine = require("scripts/foundations/utilities/game_state_machine") +print("[mod_main] Adding mod loading state") + local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") local GameStateMachine_init = GameStateMachine.init GameStateMachine.init = function(self, parent, start_state, params, ...) @@ -58,18 +64,21 @@ local function patch_mod_loading_state() -- systems are at least loaded and can be hooked, even if they aren't -- running, yet. local pos = 4 - table.insert(params.state, pos, { + table.insert(params.states, pos, { StateBootLoadMods, { package_manager = params.package_manager, }, }) + -- Clean up after us + GameStateMachine.init = GameStateMachine_init + return GameStateMachine_init(self, parent, start_state, params, ...) end end function init() - Main.init() patch_mod_loading_state() + Main:init() end diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 3509b0b..7b61361 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -26,6 +26,7 @@ use crate::state::{ModInfo, PackageInfo, State}; const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; +const DML_BUNDLE_NAME: &str = "packages/dml"; const BUNDLE_DATABASE_NAME: &str = "bundle_database.data"; const MOD_BOOT_SCRIPT: &str = "scripts/mod_main"; const MOD_DATA_SCRIPT: &str = "scripts/mods/mod_data"; @@ -229,7 +230,11 @@ async fn build_bundles(state: Arc) -> Result<()> { bundle.add_file(file); } - for mod_info in state.get_mods().iter().filter(|m| m.get_enabled()) { + for mod_info in state + .get_mods() + .iter() + .filter(|m| m.get_id() != "dml" && m.get_enabled()) + { let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); let _enter = span.enter(); @@ -371,6 +376,8 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { } } + pkg.add_file(BundleFileType::Lua, MOD_DATA_SCRIPT); + let mut variant = BundleFileVariant::new(); variant.set_data(pkg.to_binary()?); let mut f = BundleFile::new(MOD_BUNDLE_NAME.to_string(), BundleFileType::Package); @@ -379,6 +386,29 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { bundle.add_file(f); } + { + tracing::trace!("Adding dml package file to boot bundle"); + let span = tracing::trace_span!("create dml package file"); + let _enter = span.enter(); + + let mut variant = BundleFileVariant::new(); + + let mods = state.get_mods(); + let pkg_info = mods + .iter() + .find(|m| m.get_id() == "dml") + .and_then(|info| info.get_packages().get(0)); + if let Some(pkg_info) = &pkg_info { + let pkg = make_package(pkg_info).wrap_err("failed to create package file for dml")?; + variant.set_data(pkg.to_binary()?); + } + + let mut f = BundleFile::new(DML_BUNDLE_NAME.to_string(), BundleFileType::Package); + f.add_variant(variant); + + bundle.add_file(f); + } + { let span = tracing::debug_span!("Importing mod main script"); let _enter = span.enter(); @@ -442,10 +472,10 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { state.get_game_dir().join("bundle").display() ); - tracing::info!("Build mod bundles"); - build_bundles(state.clone()) - .await - .wrap_err("failed to build mod bundles")?; + // tracing::info!("Build mod bundles"); + // build_bundles(state.clone()) + // .await + // .wrap_err("failed to build mod bundles")?; tracing::info!("Patch boot bundle"); patch_boot_bundle(state.clone()) -- 2.45.3 From 79729cad0274bb27402902cb26476b955d33ab40 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 24 Feb 2023 11:45:55 +0100 Subject: [PATCH 45/90] chore: Update serde_sjson --- Cargo.lock | 2 +- lib/serde_sjson | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef1fbb5..9781122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2108,7 +2108,7 @@ dependencies = [ [[package]] name = "serde_sjson" -version = "0.2.2" +version = "0.2.3" dependencies = [ "nom", "nom_locate", diff --git a/lib/serde_sjson b/lib/serde_sjson index fc5d8b2..a6ef5a9 160000 --- a/lib/serde_sjson +++ b/lib/serde_sjson @@ -1 +1 @@ -Subproject commit fc5d8b25fb8f2b9c180d3d0c7cd361715596bb79 +Subproject commit a6ef5a914e15f22d3ebcc475969b65182475139f -- 2.45.3 From 55a1fc9723f0b646a887166e64798fefbaf350ef Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 10:12:23 +0100 Subject: [PATCH 46/90] feat(dtmt): Validate path values in dtmt.cfg Closes #34. --- Cargo.lock | 7 ++++ crates/dtmm/src/engine.rs | 8 ++--- crates/dtmm/src/state.rs | 16 +++++---- crates/dtmt/Cargo.toml | 1 + crates/dtmt/src/cmd/build.rs | 66 +++++++++++++++++++++++++++++++++++- lib/dtmt-shared/src/lib.rs | 8 +++-- 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9781122..db80a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,6 +702,7 @@ dependencies = [ "libloading", "nanorand", "oodle-sys", + "path-clean", "pin-project-lite", "promptly", "sdk", @@ -1698,6 +1699,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 7b61361..a0e07a2 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -163,22 +163,22 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str(" new_mod(\""); lua.push_str(mod_info.get_id()); lua.push_str("\", {\n init = \""); - lua.push_str(resources.get_init()); + lua.push_str(&resources.get_init().to_string_lossy()); if let Some(data) = resources.get_data() { lua.push_str("\",\n data = \""); - lua.push_str(data); + lua.push_str(&data.to_string_lossy()); } if let Some(localization) = resources.get_localization() { lua.push_str("\",\n localization = \""); - lua.push_str(localization); + lua.push_str(&localization.to_string_lossy()); } lua.push_str("\",\n })\n"); } else { lua.push_str(" return dofile(\""); - lua.push_str(resources.get_init()); + lua.push_str(&resources.get_init().to_string_lossy()); lua.push_str("\")"); } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 315cd2e..e320b0e 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -61,23 +61,23 @@ impl PackageInfo { } } -#[derive(Clone, Data, Debug)] +#[derive(Clone, Debug)] pub(crate) struct ModResourceInfo { - init: String, - data: Option, - localization: Option, + init: PathBuf, + data: Option, + localization: Option, } impl ModResourceInfo { - pub(crate) fn get_init(&self) -> &String { + pub(crate) fn get_init(&self) -> &PathBuf { &self.init } - pub(crate) fn get_data(&self) -> Option<&String> { + pub(crate) fn get_data(&self) -> Option<&PathBuf> { self.data.as_ref() } - pub(crate) fn get_localization(&self) -> Option<&String> { + pub(crate) fn get_localization(&self) -> Option<&PathBuf> { self.localization.as_ref() } } @@ -89,8 +89,10 @@ pub(crate) struct ModInfo { description: Arc, enabled: bool, #[lens(ignore)] + #[data(ignore)] packages: Vector, #[lens(ignore)] + #[data(ignore)] resources: ModResourceInfo, } diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index 3b8cb36..83015db 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -28,6 +28,7 @@ tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing = { version = "0.1.37", features = ["async-await"] } zip = "0.6.3" +path-clean = "1.0.1" [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index f470d97..46462e1 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -164,6 +164,23 @@ where .wrap_err("failed to build bundle") } +fn normalize_file_path>(path: P) -> Result { + let path = path.as_ref(); + + if path.is_absolute() || path.has_root() { + let err = eyre::eyre!("path is absolute: {}", path.display()); + return Err(err).with_suggestion(|| "Specify a relative file path.".to_string()); + } + + let path = path_clean::clean(path); + + if path.starts_with("..") { + eyre::bail!("path starts with a parent component: {}", path.display()); + } + + Ok(path) +} + #[tracing::instrument(skip_all)] pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { unsafe { @@ -172,7 +189,54 @@ pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> let cfg = { let dir = matches.get_one::("directory").cloned(); - find_project_config(dir).await? + let mut cfg = find_project_config(dir).await?; + + cfg.resources.init = normalize_file_path(cfg.resources.init) + .wrap_err("invalid config field 'resources.init'") + .with_suggestion(|| { + "Specify a file path relative to and child path of the \ + directory where 'dtmt.cfg' is." + .to_string() + }) + .with_suggestion(|| { + "Use 'dtmt new' in a separate directory to generate \ + a valid mod template." + .to_string() + })?; + + if let Some(path) = cfg.resources.data { + let path = normalize_file_path(path) + .wrap_err("invalid config field 'resources.data'") + .with_suggestion(|| { + "Specify a file path relative to and child path of the \ + directory where 'dtmt.cfg' is." + .to_string() + }) + .with_suggestion(|| { + "Use 'dtmt new' in a separate directory to generate \ + a valid mod template." + .to_string() + })?; + cfg.resources.data = Some(path); + } + + if let Some(path) = cfg.resources.localization { + let path = normalize_file_path(path) + .wrap_err("invalid config field 'resources.localization'") + .with_suggestion(|| { + "Specify a file path relative to and child path of the \ + directory where 'dtmt.cfg' is." + .to_string() + }) + .with_suggestion(|| { + "Use 'dtmt new' in a separate directory to generate \ + a valid mod template." + .to_string() + })?; + cfg.resources.localization = Some(path); + } + + cfg }; let dest = { diff --git a/lib/dtmt-shared/src/lib.rs b/lib/dtmt-shared/src/lib.rs index 6625c5b..3c8690d 100644 --- a/lib/dtmt-shared/src/lib.rs +++ b/lib/dtmt-shared/src/lib.rs @@ -1,14 +1,16 @@ mod log; +use std::path::PathBuf; + pub use log::*; #[derive(Clone, Debug, Default, serde::Deserialize)] pub struct ModConfigResources { - pub init: String, + pub init: PathBuf, #[serde(default)] - pub data: Option, + pub data: Option, #[serde(default)] - pub localization: Option, + pub localization: Option, } #[derive(Clone, Debug, Default, serde::Deserialize)] -- 2.45.3 From 09f357d72dc81fbcd4dc4b213a28b4e5771e8ddc Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 11:28:37 +0100 Subject: [PATCH 47/90] fix(sdk): Prevent duplicates in bundle database bundles Ref #28. --- crates/dtmm/src/engine.rs | 4 +- crates/dtmt/src/cmd/bundle/list.rs | 4 +- crates/dtmt/src/mods/archive.rs | 3 +- lib/sdk/src/bundle/database.rs | 26 +++++++++---- lib/sdk/src/bundle/mod.rs | 12 +++--- lib/sdk/src/murmur/mod.rs | 61 ++++++++++++++++++++++++++++-- 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index a0e07a2..1981ef4 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -202,7 +202,7 @@ fn build_mod_data_lua(state: Arc) -> String { #[tracing::instrument(skip_all)] async fn build_bundles(state: Arc) -> Result<()> { - let mut bundle = Bundle::new(MOD_BUNDLE_NAME.into()); + let mut bundle = Bundle::new(MOD_BUNDLE_NAME); let mut tasks = Vec::new(); let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); @@ -312,7 +312,7 @@ async fn build_bundles(state: Arc) -> Result<()> { db.add_bundle(&bundle); { - let path = bundle_dir.join(format!("{:x}", Murmur64::hash(bundle.name()))); + let path = bundle_dir.join(format!("{:x}", bundle.name().to_murmur64())); tracing::trace!("Writing mod bundle to '{}'", path.display()); fs::write(&path, bundle.to_binary()?) .await diff --git a/crates/dtmt/src/cmd/bundle/list.rs b/crates/dtmt/src/cmd/bundle/list.rs index a206af3..b985ad2 100644 --- a/crates/dtmt/src/cmd/bundle/list.rs +++ b/crates/dtmt/src/cmd/bundle/list.rs @@ -50,13 +50,13 @@ where match fmt { OutputFormat::Text => { - println!("Bundle: {}", bundle.name()); + println!("Bundle: {}", bundle.name().display()); for f in bundle.files().iter() { if f.variants().len() != 1 { let err = eyre::eyre!("Expected exactly one version for this file.") .with_section(|| f.variants().len().to_string().header("Bundle:")) - .with_section(|| bundle.name().clone().header("Bundle:")); + .with_section(|| bundle.name().display().header("Bundle:")); tracing::error!("{:#}", err); } diff --git a/crates/dtmt/src/mods/archive.rs b/crates/dtmt/src/mods/archive.rs index 9f9eaa1..2947640 100644 --- a/crates/dtmt/src/mods/archive.rs +++ b/crates/dtmt/src/mods/archive.rs @@ -5,7 +5,6 @@ use std::path::{Path, PathBuf}; use color_eyre::eyre::{self, Context}; use color_eyre::Result; -use sdk::murmur::Murmur64; use sdk::Bundle; use zip::ZipWriter; @@ -70,7 +69,7 @@ impl Archive { map_entry.insert(file.name(false, None)); } - let name = Murmur64::hash(bundle.name().as_bytes()); + let name = bundle.name().to_murmur64(); let path = base_path.join(name.to_string().to_ascii_lowercase()); zip.start_file(path.to_string_lossy(), Default::default())?; diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index 8438b40..20d8501 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -38,17 +38,27 @@ pub struct BundleDatabase { impl BundleDatabase { pub fn add_bundle(&mut self, bundle: &Bundle) { - let hash = Murmur64::hash(bundle.name().as_bytes()); + let hash = bundle.name().to_murmur64(); let name = hash.to_string(); let stream = format!("{}.stream", &name); - let file = BundleFile { - name, - stream, - file_time: 0, - platform_specific: false, - }; - self.stored_files.entry(hash).or_default().push(file); + { + let entry = self.stored_files.entry(hash).or_default(); + let existing = entry.iter().position(|f| f.name == name); + + let file = BundleFile { + name, + stream, + file_time: 0, + platform_specific: false, + }; + + entry.push(file); + + if let Some(pos) = existing { + entry.swap_remove(pos); + } + } for f in bundle.files() { let file_name = FileName { diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 18a4f52..cfc0d06 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -8,7 +8,7 @@ use oodle_sys::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; use crate::binary::sync::*; use crate::bundle::file::Properties; -use crate::murmur::{HashGroup, Murmur64}; +use crate::murmur::{HashGroup, IdString64, Murmur64}; pub(crate) mod database; pub(crate) mod file; @@ -46,13 +46,13 @@ pub struct Bundle { format: BundleFormat, properties: [Murmur64; 32], files: Vec, - name: String, + name: IdString64, } impl Bundle { - pub fn new(name: String) -> Self { + pub fn new>(name: S) -> Self { Self { - name, + name: name.into(), format: BundleFormat::F8, properties: [0.into(); 32], files: Vec::new(), @@ -201,7 +201,7 @@ impl Bundle { } Ok(Self { - name: bundle_name, + name: bundle_name.into(), format, files, properties, @@ -281,7 +281,7 @@ impl Bundle { Ok(w.into_inner()) } - pub fn name(&self) -> &String { + pub fn name(&self) -> &IdString64 { &self.name } diff --git a/lib/sdk/src/murmur/mod.rs b/lib/sdk/src/murmur/mod.rs index 9ea432d..7ede170 100644 --- a/lib/sdk/src/murmur/mod.rs +++ b/lib/sdk/src/murmur/mod.rs @@ -289,9 +289,9 @@ impl IdString64 { } } -impl From for IdString64 { - fn from(value: String) -> Self { - Self::String(value) +impl> From for IdString64 { + fn from(value: S) -> Self { + Self::String(value.into()) } } @@ -313,6 +313,61 @@ impl PartialEq for IdString64 { } } +impl std::hash::Hash for IdString64 { + fn hash(&self, state: &mut H) { + state.write_u64(self.to_murmur64().into()); + } +} + +impl serde::Serialize for IdString64 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(self.to_murmur64().into()) + } +} + +struct IdString64Visitor; + +impl<'de> serde::de::Visitor<'de> for IdString64Visitor { + type Value = IdString64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an u64 or a string") + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + Ok(IdString64::Hash(value.into())) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(IdString64::String(v.to_string())) + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + Ok(IdString64::String(v)) + } +} + +impl<'de> serde::Deserialize<'de> for IdString64 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_u64(IdString64Visitor) + } +} + pub struct IdString64Display(String); impl std::fmt::Display for IdString64Display { -- 2.45.3 From b9cd9ed5de3d430a404d838afa8d3435c22b8033 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 12:52:05 +0100 Subject: [PATCH 48/90] chore(sdk): Add debugging for unknown value in bundle database Ref #28. --- lib/sdk/src/bundle/database.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index 20d8501..51018ad 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -129,6 +129,10 @@ impl FromBinary for BundleDatabase { let mut buffer = [0; 20]; r.read_exact(&mut buffer)?; + if cfg!(debug_assertions) && buffer.iter().any(|b| *b != 0) { + tracing::warn!("Unknown value in 20-byte buffer: {:?}", buffer); + } + let file_time = r.read_u64()?; let file = BundleFile { -- 2.45.3 From 93c8f4fe9cbb68066784df4b249b2fa08f889e3c Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 13:45:20 +0100 Subject: [PATCH 49/90] fix(sdk): Add missing value in Package binary format After digging through the VT2 SDK `.exe`, I found that `.package` files (`stingray::ResourcePackageResource`) actually have more data than I originally knew about. Most notably, there is a 1 byte `flags` value that is written at the end of every package file. Depending on what value those flags have, more data could come after it, but in most cases, it's just that one byte, which I must have missed in the binary. Ref: #28. Ref: #36. --- lib/sdk/src/filetype/package.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/sdk/src/filetype/package.rs b/lib/sdk/src/filetype/package.rs index fc2e8f6..338cc8e 100644 --- a/lib/sdk/src/filetype/package.rs +++ b/lib/sdk/src/filetype/package.rs @@ -97,6 +97,7 @@ pub struct Package { _name: String, _root: PathBuf, inner: PackageType, + flags: u8, } impl Deref for Package { @@ -119,6 +120,7 @@ impl Package { _name: name, _root: root, inner: Default::default(), + flags: 1, } } @@ -179,6 +181,7 @@ impl Package { inner, _name: name, _root: root.to_path_buf(), + flags: 1, }; Ok(pkg) @@ -225,10 +228,19 @@ impl Package { .insert(PathBuf::from(path.display().to_string())); } + let flags = r.read_u8()?; + + if cfg!(debug_assertions) && flags != 1 { + tracing::warn!("Unexpected value for package flags: {:0x}", flags); + } else if (flags & 0xFE) >= 2 { + tracing::warn!("Resource Package has common packages. Ignoring."); + } + let pkg = Self { inner, _name: name, _root: PathBuf::new(), + flags, }; Ok(pkg) @@ -251,6 +263,8 @@ impl Package { } } + w.write_u8(self.flags)?; + Ok(w.into_inner()) } } -- 2.45.3 From 0b17e8edf545807e98db34c07a9063bfe55e0be1 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 16:07:16 +0100 Subject: [PATCH 50/90] fix(sdk): Fix generating bundle name from file path --- lib/sdk/src/bundle/mod.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index cfc0d06..8b8d7c5 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -59,7 +59,7 @@ impl Bundle { } } - pub fn get_name_from_path

(ctx: &crate::Context, path: P) -> String + pub fn get_name_from_path

(ctx: &crate::Context, path: P) -> IdString64 where P: AsRef, { @@ -67,12 +67,8 @@ impl Bundle { path.file_name() .and_then(|name| name.to_str()) .and_then(|name| Murmur64::try_from(name).ok()) - .map(|hash| { - ctx.lookup_hash(hash, HashGroup::Filename) - .display() - .to_string() - }) - .unwrap_or_else(|| path.display().to_string()) + .map(|hash| ctx.lookup_hash(hash, HashGroup::Filename)) + .unwrap_or_else(|| path.display().to_string().into()) } pub fn add_file(&mut self, file: BundleFile) { @@ -92,11 +88,11 @@ impl Bundle { } #[tracing::instrument(skip(ctx, binary), fields(len_binary = binary.as_ref().len()))] - pub fn from_binary(ctx: &crate::Context, name: String, binary: B) -> Result + pub fn from_binary(ctx: &crate::Context, name: S, binary: B) -> Result where B: AsRef<[u8]>, + S: Into + std::fmt::Debug, { - let bundle_name = name; let mut r = BufReader::new(Cursor::new(binary)); let format = r.read_u32().and_then(BundleFormat::try_from)?; @@ -201,7 +197,7 @@ impl Bundle { } Ok(Self { - name: bundle_name.into(), + name: name.into(), format, files, properties, -- 2.45.3 From cd9554fbe3e0ab79dbba23a193ece527e1d83226 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 16:07:47 +0100 Subject: [PATCH 51/90] fix(dtmm): Fix deploying mod bundles Two different functions were each reading the bundle database from the backup, so their changes would overwrite each other. Additionally, mod bundles were missing from the database. Ref: #28. --- crates/dtmm/src/engine.rs | 266 ++++++++++++++++++++------------- lib/sdk/src/bundle/database.rs | 11 +- 2 files changed, 171 insertions(+), 106 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index 1981ef4..edf5cfb 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -17,8 +17,8 @@ use sdk::murmur::Murmur64; use sdk::{ Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; +use tokio::fs; use tokio::io::AsyncWriteExt; -use tokio::{fs, try_join}; use tracing::Instrument; use zip::ZipArchive; @@ -118,7 +118,6 @@ async fn patch_game_settings(state: Arc) -> Result<()> { f.write_all(settings[(i + j)..].as_bytes()).await?; - tracing::info!("Patched game settings"); Ok(()) } @@ -179,7 +178,7 @@ fn build_mod_data_lua(state: Arc) -> String { } else { lua.push_str(" return dofile(\""); lua.push_str(&resources.get_init().to_string_lossy()); - lua.push_str("\")"); + lua.push_str("\")\n"); } lua.push_str(" end,\n packages = {\n"); @@ -201,22 +200,13 @@ fn build_mod_data_lua(state: Arc) -> String { } #[tracing::instrument(skip_all)] -async fn build_bundles(state: Arc) -> Result<()> { - let mut bundle = Bundle::new(MOD_BUNDLE_NAME); +async fn build_bundles(state: Arc) -> Result> { + let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME); let mut tasks = Vec::new(); let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); - let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); - let mut db = { - let bin = read_file_with_backup(&database_path) - .await - .wrap_err("failed to read bundle database")?; - let mut r = Cursor::new(bin); - let db = BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database")?; - tracing::trace!("Finished parsing bundle database"); - db - }; + let mut bundles = Vec::new(); { let span = tracing::debug_span!("Building mod data script"); @@ -227,7 +217,7 @@ async fn build_bundles(state: Arc) -> Result<()> { let file = lua::compile(MOD_DATA_SCRIPT, &lua).wrap_err("failed to compile mod data Lua file")?; - bundle.add_file(file); + mod_bundle.add_file(file); } for mod_info in state @@ -252,7 +242,7 @@ async fn build_bundles(state: Arc) -> Result<()> { let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package); file.add_variant(variant); - bundle.add_file(file); + mod_bundle.add_file(file); let bundle_name = Murmur64::hash(pkg_info.get_name()) .to_string() @@ -262,23 +252,28 @@ async fn build_bundles(state: Arc) -> Result<()> { let pkg_name = pkg_info.get_name().clone(); let mod_name = mod_info.get_name().clone(); - tracing::trace!( - "Adding package {} for mod {} to bundle database", - pkg_info.get_name(), - mod_info.get_name() - ); - // Explicitely drop the guard, so that we can move the span // into the async operation drop(_enter); + let ctx = state.get_ctx().clone(); + let task = async move { + let bundle = { + let bin = fs::read(&src).await.wrap_err_with(|| { + format!("failed to read bundle file '{}'", src.display()) + })?; + let name = Bundle::get_name_from_path(&ctx, &src); + Bundle::from_binary(&ctx, name, bin) + .wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))? + }; + tracing::debug!( - "Copying bundle {} for mod {}: {} -> {}", + src = %src.display(), + dest = %dest.display(), + "Copying bundle '{}' for mod '{}'", pkg_name, mod_name, - src.display(), - dest.display() ); // We attempt to remove any previous file, so that the hard link can be created. // We can reasonably ignore errors here, as a 'NotFound' is actually fine, the copy @@ -293,7 +288,9 @@ async fn build_bundles(state: Arc) -> Result<()> { src.display(), dest.display() ) - }) + })?; + + Ok::(bundle) } .instrument(span); @@ -306,62 +303,41 @@ async fn build_bundles(state: Arc) -> Result<()> { let mut tasks = stream::iter(tasks).buffer_unordered(10); while let Some(res) = tasks.next().await { - res?; + let bundle = res?; + bundles.push(bundle); } - db.add_bundle(&bundle); - { - let path = bundle_dir.join(format!("{:x}", bundle.name().to_murmur64())); + let path = bundle_dir.join(format!("{:x}", mod_bundle.name().to_murmur64())); tracing::trace!("Writing mod bundle to '{}'", path.display()); - fs::write(&path, bundle.to_binary()?) + fs::write(&path, mod_bundle.to_binary()?) .await .wrap_err_with(|| format!("failed to write bundle to '{}'", path.display()))?; } - { - tracing::trace!("Writing bundle database to '{}'", database_path.display()); - let bin = db - .to_binary() - .wrap_err("failed to serialize bundle database")?; - fs::write(&database_path, bin).await.wrap_err_with(|| { - format!( - "failed to write bundle database to '{}'", - database_path.display() - ) - })?; - } + bundles.push(mod_bundle); - Ok(()) + Ok(bundles) } #[tracing::instrument(skip_all)] -async fn patch_boot_bundle(state: Arc) -> Result<()> { +async fn patch_boot_bundle(state: Arc) -> Result> { let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); - let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); - let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); - let (mut db, mut bundle) = try_join!( - async { - let bin = read_file_with_backup(&database_path) - .await - .wrap_err("failed to read bundle database")?; - let mut r = Cursor::new(bin); + let mut bundles = Vec::with_capacity(2); - BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database") - } - .instrument(tracing::trace_span!("read bundle database")), - async { - let bin = read_file_with_backup(&bundle_path) - .await - .wrap_err("failed to read boot bundle")?; + let mut boot_bundle = async { + let bin = read_file_with_backup(&bundle_path) + .await + .wrap_err("failed to read boot bundle")?; - Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin) - .wrap_err("failed to parse boot bundle") - } - .instrument(tracing::trace_span!("read boot bundle")) - )?; + Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin) + .wrap_err("failed to parse boot bundle") + } + .instrument(tracing::trace_span!("read boot bundle")) + .await + .wrap_err_with(|| format!("failed to read bundle '{}'", BOOT_BUNDLE_NAME))?; { tracing::trace!("Adding mod package file to boot bundle"); @@ -383,30 +359,82 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { let mut f = BundleFile::new(MOD_BUNDLE_NAME.to_string(), BundleFileType::Package); f.add_variant(variant); - bundle.add_file(f); + boot_bundle.add_file(f); } { - tracing::trace!("Adding dml package file to boot bundle"); - let span = tracing::trace_span!("create dml package file"); + tracing::trace!("Handling DML packages and bundle"); + let span = tracing::trace_span!("handle DML"); let _enter = span.enter(); let mut variant = BundleFileVariant::new(); let mods = state.get_mods(); - let pkg_info = mods + let mod_info = mods .iter() .find(|m| m.get_id() == "dml") - .and_then(|info| info.get_packages().get(0)); - if let Some(pkg_info) = &pkg_info { - let pkg = make_package(pkg_info).wrap_err("failed to create package file for dml")?; - variant.set_data(pkg.to_binary()?); + .ok_or_else(|| eyre::eyre!("DML not found in mod list"))?; + let pkg_info = mod_info + .get_packages() + .get(0) + .ok_or_else(|| eyre::eyre!("invalid mod package for DML")) + .with_suggestion(|| "Re-download and import the newest version.".to_string())?; + let bundle_name = Murmur64::hash(pkg_info.get_name()) + .to_string() + .to_ascii_lowercase(); + let src = state + .get_mod_dir() + .join(mod_info.get_id()) + .join(&bundle_name); + + { + let ctx = state.get_ctx(); + let bin = fs::read(&src) + .await + .wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?; + let name = Bundle::get_name_from_path(&ctx, &src); + + let dml_bundle = Bundle::from_binary(&ctx, name, bin) + .wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?; + + bundles.push(dml_bundle); + }; + + { + let dest = bundle_dir.join(&bundle_name); + let pkg_name = pkg_info.get_name().clone(); + let mod_name = mod_info.get_name().clone(); + + tracing::debug!( + "Copying bundle {} for mod {}: {} -> {}", + pkg_name, + mod_name, + src.display(), + dest.display() + ); + // We attempt to remove any previous file, so that the hard link can be created. + // We can reasonably ignore errors here, as a 'NotFound' is actually fine, the copy + // may be possible despite an error here, or the error will be reported by it anyways. + // TODO: There is a chance that we delete an actual game bundle, but with 64bit + // hashes, it's low enough for now, and the setup required to detect + // "game bundle vs mod bundle" is non-trivial. + let _ = fs::remove_file(&dest).await; + fs::copy(&src, &dest).await.wrap_err_with(|| { + format!( + "failed to copy bundle {pkg_name} for mod {mod_name}. src: {}, dest: {}", + src.display(), + dest.display() + ) + })?; } + let pkg = make_package(pkg_info).wrap_err("failed to create package file for dml")?; + variant.set_data(pkg.to_binary()?); + let mut f = BundleFile::new(DML_BUNDLE_NAME.to_string(), BundleFileType::Package); f.add_variant(variant); - bundle.add_file(f); + boot_bundle.add_file(f); } { @@ -418,34 +446,56 @@ async fn patch_boot_bundle(state: Arc) -> Result<()> { let file = lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?; - bundle.add_file(file); + boot_bundle.add_file(file); } - db.add_bundle(&bundle); + async { + let bin = boot_bundle + .to_binary() + .wrap_err("failed to serialize boot bundle")?; + fs::write(&bundle_path, bin) + .await + .wrap_err_with(|| format!("failed to write main bundle: {}", bundle_path.display())) + } + .instrument(tracing::trace_span!("write boot bundle")) + .await?; - try_join!( - async { - let bin = bundle - .to_binary() - .wrap_err("failed to serialize boot bundle")?; - fs::write(&bundle_path, bin) - .await - .wrap_err_with(|| format!("failed to write main bundle: {}", bundle_path.display())) - } - .instrument(tracing::trace_span!("write boot bundle")), - async { - let bin = db - .to_binary() - .wrap_err("failed to serialize bundle database")?; - fs::write(&database_path, bin).await.wrap_err_with(|| { - format!( - "failed to write bundle database to '{}'", - database_path.display() - ) - }) - } - .instrument(tracing::trace_span!("write bundle database")) - )?; + bundles.push(boot_bundle); + + Ok(bundles) +} + +#[tracing::instrument(skip_all, fields(bundles = bundles.len()))] +async fn patch_bundle_database(state: Arc, bundles: Vec) -> Result<()> { + let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); + + let mut db = { + let bin = read_file_with_backup(&database_path) + .await + .wrap_err("failed to read bundle database")?; + let mut r = Cursor::new(bin); + let db = BundleDatabase::from_binary(&mut r).wrap_err("failed to parse bundle database")?; + tracing::trace!("Finished parsing bundle database"); + db + }; + + for bundle in bundles { + tracing::trace!("Adding '{}' to bundle database", bundle.name().display()); + db.add_bundle(&bundle); + } + + { + let bin = db + .to_binary() + .wrap_err("failed to serialize bundle database")?; + fs::write(&database_path, bin).await.wrap_err_with(|| { + format!( + "failed to write bundle database to '{}'", + database_path.display() + ) + })?; + } Ok(()) } @@ -472,21 +522,27 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { state.get_game_dir().join("bundle").display() ); - // tracing::info!("Build mod bundles"); - // build_bundles(state.clone()) - // .await - // .wrap_err("failed to build mod bundles")?; + tracing::info!("Build mod bundles"); + let mut bundles = build_bundles(state.clone()) + .await + .wrap_err("failed to build mod bundles")?; tracing::info!("Patch boot bundle"); - patch_boot_bundle(state.clone()) + let mut more_bundles = patch_boot_bundle(state.clone()) .await .wrap_err("failed to patch boot bundle")?; + bundles.append(&mut more_bundles); tracing::info!("Patch game settings"); patch_game_settings(state.clone()) .await .wrap_err("failed to patch game settings")?; + tracing::info!("Patching bundle database"); + patch_bundle_database(state.clone(), bundles) + .await + .wrap_err("failed to patch bundle database")?; + tracing::info!("Finished deploying mods"); Ok(()) } diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index 51018ad..6152ede 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -42,6 +42,14 @@ impl BundleDatabase { let name = hash.to_string(); let stream = format!("{}.stream", &name); + tracing::trace!( + "Adding bundle '{} ({:?} | {:016X})' to database. Hash exists: {}", + bundle.name().display(), + bundle.name(), + hash, + self.stored_files.contains_key(&hash) + ); + { let entry = self.stored_files.entry(hash).or_default(); let existing = entry.iter().position(|f| f.name == name); @@ -56,6 +64,7 @@ impl BundleDatabase { entry.push(file); if let Some(pos) = existing { + tracing::debug!("Found bundle '{}' at {}. Replacing.", hash.to_string(), pos); entry.swap_remove(pos); } } @@ -63,7 +72,7 @@ impl BundleDatabase { for f in bundle.files() { let file_name = FileName { extension: f.file_type(), - name: Murmur64::hash(f.name(false, None).as_bytes()), + name: f.base_name().to_murmur64(), }; // TODO: Compute actual resource hash -- 2.45.3 From 1d26a5c1138813efac3d5b9db1e925b8a1eefd13 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 16:28:21 +0100 Subject: [PATCH 52/90] fix(sdk): Fix generating Lua file names during compilation --- lib/sdk/src/filetype/lua.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/sdk/src/filetype/lua.rs b/lib/sdk/src/filetype/lua.rs index f2f4ef5..68b95e3 100644 --- a/lib/sdk/src/filetype/lua.rs +++ b/lib/sdk/src/filetype/lua.rs @@ -1,8 +1,10 @@ use std::ffi::CStr; +use std::ffi::CString; use std::io::Cursor; use std::io::Write; use color_eyre::eyre; +use color_eyre::eyre::Context; use color_eyre::Result; use luajit2_sys as lua; @@ -22,10 +24,10 @@ where #[tracing::instrument(skip_all)] pub fn compile(name: S, code: C) -> Result where - S: ToString, + S: Into, C: AsRef, { - let name = name.to_string(); + let name = name.into(); let code = code.as_ref(); let bytecode = unsafe { @@ -35,6 +37,8 @@ where lua::lua_pushstring(state, code.as_ptr() as _); lua::lua_setglobal(state, b"code\0".as_ptr() as _); + let name = CString::new(name.as_bytes()) + .wrap_err_with(|| format!("cannot convert name into CString: {}", name))?; lua::lua_pushstring(state, name.as_ptr() as _); lua::lua_setglobal(state, b"name\0".as_ptr() as _); -- 2.45.3 From 5a3c19fb3ead3a340e562e53074743544e9be325 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 16:36:59 +0100 Subject: [PATCH 53/90] fix(dtmm): Fix generating mod data It would generate a syntax error when there was more than one entry in the table. --- crates/dtmm/src/engine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index edf5cfb..dea3d53 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -189,7 +189,7 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str("\",\n"); } - lua.push_str(" }\n }\n"); + lua.push_str(" },\n },\n"); } lua.push('}'); -- 2.45.3 From 7c3629947773ccdf8e64c75ec1796e80d16ff65d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 16:41:42 +0100 Subject: [PATCH 54/90] fix(sdk): Fix bundle names in archive file index --- crates/dtmt/src/mods/archive.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/dtmt/src/mods/archive.rs b/crates/dtmt/src/mods/archive.rs index 2947640..37fec19 100644 --- a/crates/dtmt/src/mods/archive.rs +++ b/crates/dtmt/src/mods/archive.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use color_eyre::eyre::{self, Context}; use color_eyre::Result; +use sdk::murmur::IdString64; use sdk::Bundle; use zip::ZipWriter; @@ -61,7 +62,10 @@ impl Archive { let mut file_map = HashMap::new(); for bundle in self.bundles.iter() { - let bundle_name = bundle.name().clone(); + let bundle_name = match bundle.name() { + IdString64::Hash(_) => eyre::bail!("bundle name must be known as string. got hash"), + IdString64::String(s) => s, + }; let map_entry: &mut HashSet<_> = file_map.entry(bundle_name).or_default(); -- 2.45.3 From 974641b2ea9fc90425d14708641b45288224e8d6 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 17:25:55 +0100 Subject: [PATCH 55/90] fix(dtmm): Fix infinite loop on game load --- crates/dtmm/assets/mod_main.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index f4eae1b..6564769 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -24,6 +24,8 @@ StateBootLoadMods.on_enter = function (self, parent, params) local state_params = self:_state_params() local package_manager = state_params.package_manager + + self._state = "load_package" self._package_manager = package_manager self._package_handles = { ["packages/mods"] = package_manager:load("packages/mods", "StateBootLoadMods", nil), -- 2.45.3 From d6ee6e9a1040eeaa341cc676de6eef0f947ac356 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 17:26:24 +0100 Subject: [PATCH 56/90] fix(dtmm): Fix patching GameStateMachine The monkey-pacthed function was still called multiple times. --- crates/dtmm/assets/mod_main.lua | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 6564769..02940fb 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -56,28 +56,32 @@ end -- Patch `GameStateMachine.init` to add our own state for loading mods. -- In the future, Fatshark might provide us with a dedicated way to do this. local function patch_mod_loading_state() -print("[mod_main] Adding mod loading state") + print("[mod_main] Adding mod loading state") local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") + local patched = false + local GameStateMachine_init = GameStateMachine.init GameStateMachine.init = function(self, parent, start_state, params, ...) - -- Hardcoded position after `StateRequireScripts`. - -- We do want to wait until then, so that most of the game's core - -- systems are at least loaded and can be hooked, even if they aren't - -- running, yet. - local pos = 4 - table.insert(params.states, pos, { - StateBootLoadMods, - { - package_manager = params.package_manager, - }, - }) + if not patched then + patched = true - -- Clean up after us - GameStateMachine.init = GameStateMachine_init + -- Hardcoded position after `StateRequireScripts`. + -- We do want to wait until then, so that most of the game's core + -- systems are at least loaded and can be hooked, even if they aren't + -- running, yet. + local pos = 4 + table.insert(params.states, pos, { + StateBootLoadMods, + { + package_manager = params.package_manager, + }, + }) + end - return GameStateMachine_init(self, parent, start_state, params, ...) + GameStateMachine_init(self, parent, start_state, params, ...) end + print("[mod_main] Mod patching complete") end function init() -- 2.45.3 From 8e9f26ed0c43738e78d39e0f90207058a0473bcf Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 17:27:06 +0100 Subject: [PATCH 57/90] feat(dtmm): Overwrite Crashify property As requested by Fatshark. --- crates/dtmm/assets/mod_main.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 02940fb..c2dfe4a 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -85,6 +85,9 @@ local function patch_mod_loading_state() end function init() + local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts") + StateRequireScripts._get_is_modded = function() return true end + patch_mod_loading_state() Main:init() end -- 2.45.3 From 86c4c200ff161f0d97356396b97130669c82e01f Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 17:29:25 +0100 Subject: [PATCH 58/90] fix(dtmm): Fix deleting mods --- crates/dtmm/src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index e320b0e..d42d9ac 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -367,7 +367,7 @@ impl AppDelegate for Delegate { } cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { let info = cmd - .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .get(ACTION_START_DELETE_SELECTED_MOD) .and_then(|info| info.take()) .expect("command type matched but didn't contain the expected value"); if self -- 2.45.3 From 1d1209944813f23e0e8818673e5b3a9faf4b7a77 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 17:33:46 +0100 Subject: [PATCH 59/90] feat(dtmm): Replace existing mods on import Closes #19. --- crates/dtmm/src/state.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index d42d9ac..42f2b41 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -185,8 +185,13 @@ impl State { } pub fn add_mod(&mut self, info: ModInfo) { - self.mods.push_back(info); - self.selected_mod_index = Some(self.mods.len() - 1); + if let Some(pos) = self.mods.index_of(&info) { + self.mods.set(pos, info); + self.selected_mod_index = Some(pos); + } else { + self.mods.push_back(info); + self.selected_mod_index = Some(self.mods.len() - 1); + } } pub fn can_move_mod_down(&self) -> bool { -- 2.45.3 From 22d8ab05ab0d3fe1877b5b27e4c06ad9e17add09 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 18:27:32 +0100 Subject: [PATCH 60/90] feat(dtmm): Provide dt for loading state Apparently Fatshark does not proxy `dt` in their boot loading sub states. But we do need that for the mod manager. --- crates/dtmm/assets/mod_main.lua | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index c2dfe4a..1a0a31a 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -15,7 +15,31 @@ local libs = { require("scripts/main") print("[mod_main] 'scripts/main' loaded") -require("scripts/game_states/boot/state_boot_sub_state_base") +local StateBootSubStateBase = require("scripts/game_states/boot/state_boot_sub_state_base") + +-- A necessary override. +-- The original does not proxy `dt` to `_state_update`, but we need that. +StateBootSubStateBase.update = function (self, dt) + local done, error = self:_state_update(dt) + local params = self._params + + if error then + return StateError, { + error + } + elseif done then + local next_index = params.sub_state_index + 1 + params.sub_state_index = next_index + local next_state_data = params.states[next_index] + + if next_state_data then + return next_state_data[1], self._params + else + self._parent:sub_states_done() + end + end +end + local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") StateBootLoadMods.on_enter = function (self, parent, params) -- 2.45.3 From c9cfb7d4156d4c21c24f1952f7ced8583378abe5 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 18:29:28 +0100 Subject: [PATCH 61/90] fix(dtmm): Fix mod loader GUI The final puzzle piece for mod loading, at least on DTMM's side. Closes #28. --- crates/dtmm/assets/mod_main.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 1a0a31a..91ec1cd 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -68,7 +68,7 @@ StateBootLoadMods._state_update = function (self, dt) self._mod_loader = mod_loader local mod_data = require("scripts/mods/mod_data") - mod_loader:init(mod_data, libs, self._parent.gui) + mod_loader:init(mod_data, libs, self._parent:gui()) elseif state == "load_mods" and self._mod_loader:update(dt) then print("[mod_main][StateBootLoadMods] Mods loaded, exiting") return true, false -- 2.45.3 From c5b2e136fa005a52f28e8b7e9a821233ac35b58d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 25 Feb 2023 18:37:15 +0100 Subject: [PATCH 62/90] feat(dtmm): Improve mod main logging --- crates/dtmm/assets/mod_main.lua | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 91ec1cd..285d15e 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -1,4 +1,13 @@ -print("[mod_main] Initializing mods...") +local log = function(category, format, ...) + local Log = rawget(_G, "Log") + if Log then + Log.info(category, format, ...) + else + print(string.format("[%s] %s", category or "", string.format(format or "", ...))) + end +end + +log("mod_main", " Initializing mods...") -- Keep a backup of certain system libraries before -- Fatshark's code scrubs them. -- The loader can then decide to pass them on to mods, or ignore them @@ -13,7 +22,7 @@ local libs = { } require("scripts/main") -print("[mod_main] 'scripts/main' loaded") +log("mod_main", "'scripts/main' loaded") local StateBootSubStateBase = require("scripts/game_states/boot/state_boot_sub_state_base") @@ -24,9 +33,7 @@ StateBootSubStateBase.update = function (self, dt) local params = self._params if error then - return StateError, { - error - } + return StateError, { error } elseif done then local next_index = params.sub_state_index + 1 params.sub_state_index = next_index @@ -43,7 +50,7 @@ end local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") StateBootLoadMods.on_enter = function (self, parent, params) - print("[mod_main][StateBootLoadMods] Entered") + log("StateBootLoadMods", "Entered") StateBootLoadMods.super.on_enter(self, parent, params) local state_params = self:_state_params() @@ -62,7 +69,7 @@ StateBootLoadMods._state_update = function (self, dt) local package_manager = self._package_manager if state == "load_package" and package_manager:update() then - print("[mod_main][StateBootLoadMods] Packages loaded, loading mods") + log("StateBootLoadMods", "Packages loaded, loading mods") self._state = "load_mods" local mod_loader = require("scripts/mods/dml/init") self._mod_loader = mod_loader @@ -70,7 +77,7 @@ StateBootLoadMods._state_update = function (self, dt) local mod_data = require("scripts/mods/mod_data") mod_loader:init(mod_data, libs, self._parent:gui()) elseif state == "load_mods" and self._mod_loader:update(dt) then - print("[mod_main][StateBootLoadMods] Mods loaded, exiting") + log("StateBootLoadMods", "Mods loaded, exiting") return true, false end @@ -80,7 +87,7 @@ end -- Patch `GameStateMachine.init` to add our own state for loading mods. -- In the future, Fatshark might provide us with a dedicated way to do this. local function patch_mod_loading_state() - print("[mod_main] Adding mod loading state") + log("mod_main", "Adding mod loading state") local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") local patched = false @@ -105,7 +112,7 @@ local function patch_mod_loading_state() GameStateMachine_init(self, parent, start_state, params, ...) end - print("[mod_main] Mod patching complete") + log("mod_main", "Mod patching complete") end function init() -- 2.45.3 From bb671c5fd2bd149cc23609ec61abee509dbf1481 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 11:10:14 +0100 Subject: [PATCH 63/90] feat: Add button to reset mod deployment For now this merely recovers backed-up game files and leaves mod bundles in-tact. The game doesn't care about those anyways. Closes #8. --- crates/dtmm/src/engine.rs | 31 +++++++++++++++++++++++++++++++ crates/dtmm/src/main.rs | 13 +++++++++++++ crates/dtmm/src/main_window.rs | 10 ++++++---- crates/dtmm/src/state.rs | 29 +++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/engine.rs index dea3d53..891778a 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/engine.rs @@ -547,6 +547,37 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { Ok(()) } +#[tracing::instrument(skip(state))] +pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { + let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME]; + let bundle_dir = state.get_game_dir().join("bundle"); + + tracing::info!("Resetting mod deployment in {}", bundle_dir.display()); + + for p in paths { + let path = bundle_dir.join(p); + let backup = bundle_dir.join(&format!("{}.bak", p)); + + tracing::debug!( + "Copying from backup: {} -> {}", + backup.display(), + path.display() + ); + + fs::copy(&backup, &path) + .await + .wrap_err_with(|| format!("failed to '{}' restore from backup", p))?; + + tracing::debug!("Deleting backup: {}", backup.display(),); + + fs::remove_file(&backup) + .await + .wrap_err_with(|| format!("failed to remove backup '{}'", p))?; + } + + Ok(()) +} + #[tracing::instrument(skip(state))] pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { let data = fs::read(&info.path) diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 32c15f8..352aeb0 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -19,10 +19,12 @@ use druid::SingleUse; use druid::Target; use engine::delete_mod; use engine::import_mod; +use engine::reset_mod_deployment; use serde::Deserialize; use serde::Serialize; use state::ACTION_FINISH_ADD_MOD; use state::ACTION_FINISH_DELETE_SELECTED_MOD; +use state::ACTION_FINISH_RESET_DEPLOYMENT; use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; @@ -102,6 +104,17 @@ fn work_thread( ) .expect("failed to send command"); }), + AsyncAction::ResetDeployment(state) => tokio::spawn(async move { + if let Err(err) = reset_mod_deployment(state).await { + tracing::error!("Failed to reset mod deployment: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) + .expect("failed to send command"); + }), }; } }); diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 107b3aa..1b06c95 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -7,7 +7,7 @@ use druid::{ lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc, }; -use crate::state::{ModInfo, PathBufFormatter, State, View}; +use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; use crate::state::{ ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, @@ -60,9 +60,11 @@ fn build_top_bar() -> impl Widget { ) .with_default_spacer() .with_child( - Button::new("Run Game").on_click(|_ctx, _state: &mut State, _env| { - todo!(); - }), + Button::new("Reset Mods") + .on_click(|ctx, _state: &mut State, _env| { + ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); + }) + .disabled_if(|data, _| !data.can_reset_deployment()), ), ) .padding(theme::TOP_BAR_INSETS) diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 42f2b41..76f1c8a 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -24,6 +24,11 @@ pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); +pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector = + Selector::new("dtmm.action.start-reset-deployment"); +pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector = + Selector::new("dtmm.action.finish-reset-deployment"); + pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = Selector::new("dtmm.action.finish-add-mod"); @@ -145,6 +150,7 @@ pub(crate) struct State { mods: Vector, selected_mod_index: Option, is_deployment_in_progress: bool, + is_reset_in_progress: bool, game_dir: Arc, data_dir: Arc, ctx: Arc, @@ -163,6 +169,7 @@ impl State { mods: Vector::new(), selected_mod_index: None, is_deployment_in_progress: false, + is_reset_in_progress: false, game_dir: Arc::new(config.game_dir.unwrap_or_default()), data_dir: Arc::new(config.data_dir.unwrap_or_default()), } @@ -208,6 +215,10 @@ impl State { !self.is_deployment_in_progress } + pub fn can_reset_deployment(&self) -> bool { + !self.is_reset_in_progress + } + pub(crate) fn get_game_dir(&self) -> &PathBuf { &self.game_dir } @@ -292,6 +303,7 @@ impl Lens, Vector<(usize, T)>> for IndexedVectorLens { pub(crate) enum AsyncAction { DeployMods(State), + ResetDeployment(State), AddMod((State, FileInfo)), DeleteMod((State, ModInfo)), } @@ -334,6 +346,23 @@ impl AppDelegate for Delegate { state.is_deployment_in_progress = false; Handled::Yes } + cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { + if self + .sender + .send(AsyncAction::ResetDeployment(state.clone())) + .is_ok() + { + state.is_reset_in_progress = true; + } else { + tracing::error!("Failed to queue action to reset mod deployment"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => { + state.is_reset_in_progress = false; + Handled::Yes + } cmd if cmd.is(ACTION_SELECT_MOD) => { let index = cmd .get(ACTION_SELECT_MOD) -- 2.45.3 From 3895ab12d6d761fb671cc117af7a793356c3b6d7 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 27 Feb 2023 16:32:29 +0100 Subject: [PATCH 64/90] feat(dtmm): Implement log view Ref: #7. --- Cargo.lock | 8 +- crates/dtmm/src/log.rs | 80 ++++++++++++ crates/dtmm/src/main.rs | 215 +++------------------------------ crates/dtmm/src/main_window.rs | 23 +++- crates/dtmm/src/state.rs | 24 +++- crates/dtmm/src/util.rs | 117 ++++++++++++++++++ crates/dtmm/src/worker.rs | 114 +++++++++++++++++ lib/dtmt-shared/src/log.rs | 176 ++++++++++++--------------- 8 files changed, 447 insertions(+), 310 deletions(-) create mode 100644 crates/dtmm/src/log.rs create mode 100644 crates/dtmm/src/util.rs create mode 100644 crates/dtmm/src/worker.rs diff --git a/Cargo.lock b/Cargo.lock index db80a10..6439337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,9 +2293,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "libc", @@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] diff --git a/crates/dtmm/src/log.rs b/crates/dtmm/src/log.rs new file mode 100644 index 0000000..12aeeff --- /dev/null +++ b/crates/dtmm/src/log.rs @@ -0,0 +1,80 @@ +use tokio::sync::mpsc::UnboundedSender; +use tracing_error::ErrorLayer; +use tracing_subscriber::filter::FilterFn; +use tracing_subscriber::fmt; +use tracing_subscriber::fmt::format::debug_fn; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::prelude::*; +use tracing_subscriber::EnvFilter; + +// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber` +// that would allow me to pass an extra `Layer` to that function. So, for now, +// its code has to be duplicated here. + +pub struct ChannelWriter { + tx: UnboundedSender, +} + +impl ChannelWriter { + pub fn new(tx: UnboundedSender) -> Self { + Self { tx } + } +} + +impl std::io::Write for ChannelWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let tx = self.tx.clone(); + let string = String::from_utf8_lossy(buf).to_string(); + + // The `send` errors when the receiving end has closed. + // But there's not much we can do at that point, so we just ignore it. + let _ = tx.send(string); + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +pub fn create_tracing_subscriber(tx: UnboundedSender) { + let env_layer = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); + + let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { + let fmt_layer = fmt::layer().pretty(); + (Some(fmt_layer), None, None) + } else { + // Creates a layer that + // - only prints events that contain a message + // - does not print fields + // - does not print spans/targets + // - only prints time, not date + let fmt_layer = fmt::layer() + .event_format(dtmt_shared::Formatter) + .fmt_fields(debug_fn(dtmt_shared::format_field)); + + ( + None, + Some(fmt_layer), + Some(FilterFn::new(dtmt_shared::filter)), + ) + }; + + let channel_layer = fmt::layer() + // TODO: Re-enable and implement a formatter for the Druid widget + .with_ansi(false) + .event_format(dtmt_shared::Formatter) + .fmt_fields(debug_fn(dtmt_shared::format_field)) + .with_writer(move || ChannelWriter::new(tx.clone())); + + tracing_subscriber::registry() + .with(channel_layer) + .with(filter_layer) + .with(env_layer) + .with(dev_stdout_layer) + .with(prod_stdout_layer) + .with(ErrorLayer::new(fmt::format::Pretty::default())) + .init(); +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 352aeb0..6a3252a 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,170 +1,35 @@ #![recursion_limit = "256"] #![feature(let_chains)] -use std::fs; -use std::io::ErrorKind; use std::path::PathBuf; use std::sync::Arc; use clap::command; -use clap::parser::ValueSource; use clap::value_parser; use clap::Arg; use color_eyre::eyre::Context; -use color_eyre::Report; -use color_eyre::Result; +use color_eyre::{Report, Result}; use druid::AppLauncher; -use druid::ExtEventSink; -use druid::SingleUse; -use druid::Target; -use engine::delete_mod; -use engine::import_mod; -use engine::reset_mod_deployment; -use serde::Deserialize; -use serde::Serialize; -use state::ACTION_FINISH_ADD_MOD; -use state::ACTION_FINISH_DELETE_SELECTED_MOD; -use state::ACTION_FINISH_RESET_DEPLOYMENT; -use tokio::runtime::Runtime; -use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; -use crate::engine::deploy_mods; -use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY}; +use crate::state::{Delegate, State}; +use crate::worker::work_thread; mod controller; mod engine; +mod log; mod main_window; mod state; mod theme; +mod util; mod widget; - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct Config { - data_dir: Option, - game_dir: Option, -} - -fn work_thread( - event_sink: Arc>, - action_queue: Arc>>, -) -> Result<()> { - let rt = Runtime::new()?; - - rt.block_on(async { - while let Some(action) = action_queue.write().await.recv().await { - let event_sink = event_sink.clone(); - match action { - AsyncAction::DeployMods(state) => tokio::spawn(async move { - if let Err(err) = deploy_mods(state).await { - tracing::error!("Failed to deploy mods: {:?}", err); - } - - event_sink - .write() - .await - .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) - .expect("failed to send command"); - }), - AsyncAction::AddMod((state, info)) => tokio::spawn(async move { - match import_mod(state, info).await { - Ok(mod_info) => { - event_sink - .write() - .await - .submit_command( - ACTION_FINISH_ADD_MOD, - SingleUse::new(mod_info), - Target::Auto, - ) - .expect("failed to send command"); - } - Err(err) => { - tracing::error!("Failed to import mod: {:?}", err); - } - } - }), - AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { - if let Err(err) = delete_mod(state, &info).await { - tracing::error!( - "Failed to delete mod files. \ - You might want to clean up the data directory manually. \ - Reason: {:?}", - err - ); - } - - event_sink - .write() - .await - .submit_command( - ACTION_FINISH_DELETE_SELECTED_MOD, - SingleUse::new(info), - Target::Auto, - ) - .expect("failed to send command"); - }), - AsyncAction::ResetDeployment(state) => tokio::spawn(async move { - if let Err(err) = reset_mod_deployment(state).await { - tracing::error!("Failed to reset mod deployment: {:?}", err); - } - - event_sink - .write() - .await - .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) - .expect("failed to send command"); - }), - }; - } - }); - - Ok(()) -} - -#[cfg(not(arget_os = "windows"))] -fn get_default_config_path() -> PathBuf { - let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| { - let user = std::env::var("USER").expect("user env variable not set"); - format!("/home/{user}") - }); - format!("{home}/.config") - }); - - PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") -} - -#[cfg(target_os = "windows")] -fn get_default_config_path() -> PathBuf { - let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); - PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") -} - -#[cfg(not(arget_os = "windows"))] -fn get_default_data_dir() -> PathBuf { - let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| { - let user = std::env::var("USER").expect("user env variable not set"); - format!("/home/{user}") - }); - format!("{home}/.local/share") - }); - - PathBuf::from(data_dir).join("dtmm") -} - -#[cfg(target_os = "windows")] -fn get_default_data_dir() -> PathBuf { - let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); - PathBuf::from(data_dir).join("dtmm") -} +mod worker; #[tracing::instrument] fn main() -> Result<()> { color_eyre::install()?; - let default_config_path = get_default_config_path(); + let default_config_path = util::get_default_config_path(); tracing::trace!(default_config_path = %default_config_path.display()); @@ -185,78 +50,30 @@ fn main() -> Result<()> { ) .get_matches(); - dtmt_shared::create_tracing_subscriber(); + let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel(); + log::create_tracing_subscriber(log_tx); unsafe { oodle_sys::init(matches.get_one::("oodle")); } - let config: Config = { - let path = matches - .get_one::("config") - .expect("argument missing despite default"); - match fs::read(path) { - Ok(data) => { - let data = String::from_utf8(data).wrap_err_with(|| { - format!("config file {} contains invalid UTF-8", path.display()) - })?; - serde_sjson::from_str(&data) - .wrap_err_with(|| format!("invalid config file {}", path.display()))? - } - Err(err) if err.kind() == ErrorKind::NotFound => { - if matches.value_source("config") != Some(ValueSource::DefaultValue) { - return Err(err).wrap_err_with(|| { - format!("failed to read config file {}", path.display()) - })?; - } - - { - let parent = default_config_path - .parent() - .expect("a file path always has a parent directory"); - fs::create_dir_all(parent).wrap_err_with(|| { - format!("failed to create directories {}", parent.display()) - })?; - } - - let config = Config { - data_dir: Some(get_default_data_dir()), - game_dir: None, - }; - - { - let data = serde_sjson::to_string(&config) - .wrap_err("failed to serialize default config value")?; - fs::write(&default_config_path, data).wrap_err_with(|| { - format!( - "failed to write default config to {}", - default_config_path.display() - ) - })?; - } - - config - } - Err(err) => { - return Err(err) - .wrap_err_with(|| format!("failed to read config file {}", path.display()))?; - } - } - }; + let config = + util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?; let initial_state = State::new(config); - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); - let delegate = Delegate::new(sender); + let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); + let delegate = Delegate::new(action_tx); let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); let event_sink = launcher.get_external_handle(); std::thread::spawn(move || { let event_sink = Arc::new(RwLock::new(event_sink)); - let receiver = Arc::new(RwLock::new(receiver)); + let action_rx = Arc::new(RwLock::new(action_rx)); + let log_rx = Arc::new(RwLock::new(log_rx)); loop { - if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) { + if let Err(err) = work_thread(event_sink.clone(), action_rx.clone(), log_rx.clone()) { tracing::error!("Work thread failed, restarting: {:?}", err); } } diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index 1b06c95..6fcbc87 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -1,10 +1,11 @@ use druid::im::Vector; use druid::widget::{ - Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, - TextBox, ViewSwitcher, + Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe, + Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ - lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc, + lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse, + Widget, WidgetExt, WindowDesc, }; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; @@ -261,9 +262,25 @@ fn build_main() -> impl Widget { ) } +fn build_log_view() -> impl Widget { + let font = FontDescriptor::new(FontFamily::MONOSPACE); + let label = Label::raw() + .with_font(font) + .with_line_break_mode(LineBreaking::WordWrap) + .lens(State::log); + + SizedBox::new(label) + .expand_width() + .height(128.0) + .scroll() + .vertical() +} + fn build_window() -> impl Widget { + // TODO: Add borders between the sections Flex::column() .must_fill_main_axis(true) .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) + .with_child(build_log_view()) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 76f1c8a..71f66c2 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -10,7 +10,7 @@ use druid::{ use dtmt_shared::ModConfig; use tokio::sync::mpsc::UnboundedSender; -use crate::Config; +use crate::util::Config; pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); @@ -33,6 +33,8 @@ pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = Selector::new("dtmm.action.finish-add-mod"); +pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); + #[derive(Copy, Clone, Data, Debug, PartialEq)] pub(crate) enum View { Mods, @@ -154,6 +156,7 @@ pub(crate) struct State { game_dir: Arc, data_dir: Arc, ctx: Arc, + log: Arc, } impl State { @@ -170,8 +173,9 @@ impl State { selected_mod_index: None, is_deployment_in_progress: false, is_reset_in_progress: false, - game_dir: Arc::new(config.game_dir.unwrap_or_default()), - data_dir: Arc::new(config.data_dir.unwrap_or_default()), + game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()), + data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()), + log: Arc::new(String::new()), } } @@ -230,6 +234,11 @@ impl State { pub(crate) fn get_ctx(&self) -> Arc { self.ctx.clone() } + + pub(crate) fn add_log_line(&mut self, line: String) { + let log = Arc::make_mut(&mut self.log); + log.push_str(&line); + } } pub(crate) struct SelectedModLens; @@ -454,6 +463,15 @@ impl AppDelegate for Delegate { } Handled::Yes } + cmd if cmd.is(ACTION_LOG) => { + let line = cmd + .get(ACTION_LOG) + .expect("command type matched but didn't contain the expected value"); + if let Some(line) = line.take() { + state.add_log_line(line); + } + Handled::Yes + } cmd => { if cfg!(debug_assertions) { tracing::warn!("Unknown command: {:?}", cmd); diff --git a/crates/dtmm/src/util.rs b/crates/dtmm/src/util.rs new file mode 100644 index 0000000..4483c53 --- /dev/null +++ b/crates/dtmm/src/util.rs @@ -0,0 +1,117 @@ +use std::fs; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +use clap::{parser::ValueSource, ArgMatches}; +use color_eyre::{eyre::Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct Config { + data_dir: Option, + game_dir: Option, +} + +impl Config { + pub fn game_dir(&self) -> Option<&PathBuf> { + self.game_dir.as_ref() + } + + pub fn data_dir(&self) -> Option<&PathBuf> { + self.data_dir.as_ref() + } +} + +#[cfg(not(arget_os = "windows"))] +pub fn get_default_config_path() -> PathBuf { + let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.config") + }); + + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") +} + +#[cfg(target_os = "windows")] +pub fn get_default_config_path() -> PathBuf { + let config_dir = std::env::var("APPDATA").expect("appdata env var not set"); + PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg") +} + +#[cfg(not(arget_os = "windows"))] +pub fn get_default_data_dir() -> PathBuf { + let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| { + let user = std::env::var("USER").expect("user env variable not set"); + format!("/home/{user}") + }); + format!("{home}/.local/share") + }); + + PathBuf::from(data_dir).join("dtmm") +} + +#[cfg(target_os = "windows")] +pub fn get_default_data_dir() -> PathBuf { + let data_dir = std::env::var("APPDATA").expect("appdata env var not set"); + PathBuf::from(data_dir).join("dtmm") +} + +pub(crate) fn read_config>( + default_config_path: P, + matches: &ArgMatches, +) -> Result { + let path = matches + .get_one::("config") + .expect("argument missing despite default"); + let default_config_path = default_config_path.as_ref(); + + match fs::read(path) { + Ok(data) => { + let data = String::from_utf8(data).wrap_err_with(|| { + format!("config file {} contains invalid UTF-8", path.display()) + })?; + serde_sjson::from_str(&data) + .wrap_err_with(|| format!("invalid config file {}", path.display())) + } + Err(err) if err.kind() == ErrorKind::NotFound => { + if matches.value_source("config") != Some(ValueSource::DefaultValue) { + return Err(err) + .wrap_err_with(|| format!("failed to read config file {}", path.display()))?; + } + + { + let parent = default_config_path + .parent() + .expect("a file path always has a parent directory"); + fs::create_dir_all(parent).wrap_err_with(|| { + format!("failed to create directories {}", parent.display()) + })?; + } + + let config = Config { + data_dir: Some(get_default_data_dir()), + game_dir: None, + }; + + { + let data = serde_sjson::to_string(&config) + .wrap_err("failed to serialize default config value")?; + fs::write(default_config_path, data).wrap_err_with(|| { + format!( + "failed to write default config to {}", + default_config_path.display() + ) + })?; + } + + Ok(config) + } + Err(err) => { + Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display())) + } + } +} diff --git a/crates/dtmm/src/worker.rs b/crates/dtmm/src/worker.rs new file mode 100644 index 0000000..4715480 --- /dev/null +++ b/crates/dtmm/src/worker.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use color_eyre::Result; +use druid::{ExtEventSink, SingleUse, Target}; +use tokio::runtime::Runtime; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::RwLock; + +use crate::engine::*; +use crate::state::*; + +async fn handle_action( + event_sink: Arc>, + action_queue: Arc>>, +) { + while let Some(action) = action_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + match action { + AsyncAction::DeployMods(state) => tokio::spawn(async move { + if let Err(err) = deploy_mods(state).await { + tracing::error!("Failed to deploy mods: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto) + .expect("failed to send command"); + }), + AsyncAction::AddMod((state, info)) => tokio::spawn(async move { + match import_mod(state, info).await { + Ok(mod_info) => { + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_ADD_MOD, + SingleUse::new(mod_info), + Target::Auto, + ) + .expect("failed to send command"); + } + Err(err) => { + tracing::error!("Failed to import mod: {:?}", err); + } + } + }), + AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move { + if let Err(err) = delete_mod(state, &info).await { + tracing::error!( + "Failed to delete mod files. \ + You might want to clean up the data directory manually. \ + Reason: {:?}", + err + ); + } + + event_sink + .write() + .await + .submit_command( + ACTION_FINISH_DELETE_SELECTED_MOD, + SingleUse::new(info), + Target::Auto, + ) + .expect("failed to send command"); + }), + AsyncAction::ResetDeployment(state) => tokio::spawn(async move { + if let Err(err) = reset_mod_deployment(state).await { + tracing::error!("Failed to reset mod deployment: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) + .expect("failed to send command"); + }), + }; + } +} + +async fn handle_log( + event_sink: Arc>, + log_queue: Arc>>, +) { + while let Some(line) = log_queue.write().await.recv().await { + let event_sink = event_sink.clone(); + event_sink + .write() + .await + .submit_command(ACTION_LOG, SingleUse::new(line), Target::Auto) + .expect("failed to send command"); + } +} + +pub(crate) fn work_thread( + event_sink: Arc>, + action_queue: Arc>>, + log_queue: Arc>>, +) -> Result<()> { + let rt = Runtime::new()?; + + rt.block_on(async { + loop { + tokio::select! { + _ = handle_action(event_sink.clone(), action_queue.clone()) => {}, + _ = handle_log(event_sink.clone(), log_queue.clone()) => {}, + } + } + }); + + Ok(()) +} diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs index 41b015d..15a26b1 100644 --- a/lib/dtmt-shared/src/log.rs +++ b/lib/dtmt-shared/src/log.rs @@ -1,113 +1,87 @@ -// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code -// unused. These sections should be small enough that no truly dead code slips in. +use std::fmt::Result; -#[allow(dead_code)] -mod prod { - use std::fmt::Result; +use time::format_description::FormatItem; +use time::macros::format_description; +use time::OffsetDateTime; +use tracing::field::Field; +use tracing::{Event, Metadata, Subscriber}; +use tracing_error::ErrorLayer; +use tracing_subscriber::filter::FilterFn; +use tracing_subscriber::fmt::format::{debug_fn, Writer}; +use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::prelude::*; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::EnvFilter; - use time::format_description::FormatItem; - use time::macros::format_description; - use time::OffsetDateTime; - use tracing::field::Field; - use tracing::{Event, Metadata, Subscriber}; - use tracing_error::ErrorLayer; - use tracing_subscriber::filter::FilterFn; - use tracing_subscriber::fmt::format::{debug_fn, Writer}; - use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; - use tracing_subscriber::prelude::*; - use tracing_subscriber::registry::LookupSpan; - use tracing_subscriber::EnvFilter; +pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); - const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); - - fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { - if field.name() == "message" { - write!(w, "{:?}", val) - } else { - Ok(()) - } +pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { + if field.name() == "message" { + write!(w, "{:?}", val) + } else { + Ok(()) } +} - fn filter(metadata: &Metadata<'_>) -> bool { - metadata - .fields() - .iter() - .any(|field| field.name() == "message") +pub fn filter(metadata: &Metadata<'_>) -> bool { + metadata + .fields() + .iter() + .any(|field| field.name() == "message") +} + +pub struct Formatter; + +impl FormatEvent for Formatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> Result { + let meta = event.metadata(); + + let time = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + let time = time.format(TIME_FORMAT).map_err(|_| std::fmt::Error)?; + + write!(writer, "[{}] [{:>5}] ", time, meta.level())?; + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) } +} - struct Formatter; +pub fn create_tracing_subscriber() { + let env_layer = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - impl FormatEvent for Formatter - where - S: Subscriber + for<'a> LookupSpan<'a>, - N: for<'a> FormatFields<'a> + 'static, - { - fn format_event( - &self, - ctx: &FmtContext<'_, S, N>, - mut writer: Writer<'_>, - event: &Event<'_>, - ) -> Result { - let meta = event.metadata(); - - let time = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); - let time = time.format(TIME_FORMAT).map_err(|_| std::fmt::Error)?; - - write!(writer, "[{}] [{:>5}] ", time, meta.level())?; - - ctx.field_format().format_fields(writer.by_ref(), event)?; - - writeln!(writer) - } - } - - /// Creates a subscriber that - /// - only prints events that contain a message - /// - does not print fields - /// - does not print spans/targets - /// - only prints time, not date - pub fn create_tracing_subscriber() { - let filter_layer = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - - let fmt_layer = tracing_subscriber::fmt::layer() + let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { + let fmt_layer = fmt::layer().pretty(); + (Some(fmt_layer), None, None) + } else { + // Creates a layer that + // - only prints events that contain a message + // - does not print fields + // - does not print spans/targets + // - only prints time, not date + let fmt_layer = fmt::layer() .event_format(Formatter) .fmt_fields(debug_fn(format_field)); - tracing_subscriber::registry() - .with(FilterFn::new(filter)) - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } + (None, Some(fmt_layer), Some(FilterFn::new(filter))) + }; + + tracing_subscriber::registry() + .with(filter_layer) + .with(env_layer) + .with(dev_stdout_layer) + .with(prod_stdout_layer) + .with(ErrorLayer::new(fmt::format::Pretty::default())) + .init(); } - -#[allow(dead_code)] -mod dev { - use tracing_error::ErrorLayer; - use tracing_subscriber::prelude::*; - use tracing_subscriber::EnvFilter; - - pub fn create_tracing_subscriber() { - let filter_layer = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .with(ErrorLayer::new( - tracing_subscriber::fmt::format::Pretty::default(), - )) - .init(); - } -} - -#[cfg(debug_assertions)] -pub use dev::create_tracing_subscriber; - -#[cfg(not(debug_assertions))] -pub use prod::create_tracing_subscriber; -- 2.45.3 From 7a063d070da1e6708a3d32736cbe8d26a6bbbd18 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 09:07:08 +0100 Subject: [PATCH 65/90] chore: Update crates --- Cargo.lock | 92 ++++++++++++++++++++++++------------------------------ 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6439337..c761a21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f0778972c64420fdedc63f09919c8a88bda7b25135357fd25a5d9f3257e832" +checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" dependencies = [ "memchr", "once_cell", @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.6" +version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" +checksum = "2f3061d6db6d8fcbbd4b05e057f2acace52e64e96b498c08c2d7a4e65addd340" dependencies = [ "bitflags", "clap_derive", @@ -282,9 +282,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "34d122164198950ba84a918270a3bb3f7ededd25e15f7451673d986f55bd2667" dependencies = [ "heck", "proc-macro-error", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" dependencies = [ "os_str_bytes", ] @@ -603,7 +603,7 @@ dependencies = [ [[package]] name = "druid" version = "0.8.2" -source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" +source = "git+https://github.com/linebender/druid.git#5fa4ce51ed3d74640388de6385f135c50d346c8d" dependencies = [ "console_error_panic_hook", "druid-derive", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "druid-derive" version = "0.5.0" -source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" +source = "git+https://github.com/linebender/druid.git#5fa4ce51ed3d74640388de6385f135c50d346c8d" dependencies = [ "proc-macro2", "quote", @@ -635,7 +635,7 @@ dependencies = [ [[package]] name = "druid-shell" version = "0.8.0" -source = "git+https://github.com/linebender/druid.git#115e46c5fcb9f6f08aeae406bc60f4493aff716e" +source = "git+https://github.com/linebender/druid.git#5fa4ce51ed3d74640388de6385f135c50d346c8d" dependencies = [ "anyhow", "bitflags", @@ -1325,9 +1325,9 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", @@ -1529,15 +1529,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - [[package]] name = "nom_locate" version = "4.1.0" @@ -1833,9 +1824,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "proc-macro-crate" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit", @@ -1963,15 +1954,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2174,9 +2156,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -2216,9 +2198,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2240,16 +2222,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.42.0", ] [[package]] @@ -2361,9 +2342,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" dependencies = [ "futures-core", "pin-project-lite", @@ -2381,19 +2362,19 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" [[package]] name = "toml_edit" -version = "0.18.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" dependencies = [ "indexmap", - "nom8", "toml_datetime", + "winnow", ] [[package]] @@ -2808,6 +2789,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winnow" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" +dependencies = [ + "memchr", +] + [[package]] name = "wio" version = "0.2.2" -- 2.45.3 From e5a72731ddd125cc97fdb9bd7fc4c147d7ea3d69 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 10:03:56 +0100 Subject: [PATCH 66/90] refactor(dtmm): Split files into smaller modules --- crates/dtmm/src/{ => controller}/engine.rs | 119 ++-- crates/dtmm/src/{ => controller}/worker.rs | 2 +- crates/dtmm/src/main.rs | 29 +- crates/dtmm/src/state.rs | 510 ------------------ crates/dtmm/src/state/data.rs | 144 +++++ crates/dtmm/src/state/delegate.rs | 197 +++++++ crates/dtmm/src/state/lens.rs | 73 +++ crates/dtmm/src/state/mod.rs | 9 + crates/dtmm/src/state/util.rs | 31 ++ crates/dtmm/src/ui/mod.rs | 5 + crates/dtmm/src/{ => ui}/theme.rs | 0 crates/dtmm/src/{ => ui}/widget/container.rs | 0 crates/dtmm/src/{ => ui/widget}/controller.rs | 0 .../src/{ => ui}/widget/fill_container.rs | 0 crates/dtmm/src/{ => ui}/widget/mod.rs | 1 + .../dtmm/src/{ => ui}/widget/table_select.rs | 0 .../src/{main_window.rs => ui/window/main.rs} | 22 +- crates/dtmm/src/{util.rs => util/config.rs} | 0 crates/dtmm/src/{ => util}/log.rs | 0 19 files changed, 539 insertions(+), 603 deletions(-) rename crates/dtmm/src/{ => controller}/engine.rs (86%) rename crates/dtmm/src/{ => controller}/worker.rs (99%) delete mode 100644 crates/dtmm/src/state.rs create mode 100644 crates/dtmm/src/state/data.rs create mode 100644 crates/dtmm/src/state/delegate.rs create mode 100644 crates/dtmm/src/state/lens.rs create mode 100644 crates/dtmm/src/state/mod.rs create mode 100644 crates/dtmm/src/state/util.rs create mode 100644 crates/dtmm/src/ui/mod.rs rename crates/dtmm/src/{ => ui}/theme.rs (100%) rename crates/dtmm/src/{ => ui}/widget/container.rs (100%) rename crates/dtmm/src/{ => ui/widget}/controller.rs (100%) rename crates/dtmm/src/{ => ui}/widget/fill_container.rs (100%) rename crates/dtmm/src/{ => ui}/widget/mod.rs (94%) rename crates/dtmm/src/{ => ui}/widget/table_select.rs (100%) rename crates/dtmm/src/{main_window.rs => ui/window/main.rs} (93%) rename crates/dtmm/src/{util.rs => util/config.rs} (100%) rename crates/dtmm/src/{ => util}/log.rs (100%) diff --git a/crates/dtmm/src/engine.rs b/crates/dtmm/src/controller/engine.rs similarity index 86% rename from crates/dtmm/src/engine.rs rename to crates/dtmm/src/controller/engine.rs index 891778a..9956df8 100644 --- a/crates/dtmm/src/engine.rs +++ b/crates/dtmm/src/controller/engine.rs @@ -93,7 +93,7 @@ where #[tracing::instrument(skip_all)] async fn patch_game_settings(state: Arc) -> Result<()> { let settings_path = state - .get_game_dir() + .game_dir .join("bundle/application_settings/settings_common.ini"); let settings = read_file_with_backup(&settings_path) @@ -121,11 +121,11 @@ async fn patch_game_settings(state: Arc) -> Result<()> { Ok(()) } -#[tracing::instrument(skip_all, fields(package = info.get_name()))] +#[tracing::instrument(skip_all, fields(package = info.name))] fn make_package(info: &PackageInfo) -> Result { - let mut pkg = Package::new(info.get_name().clone(), PathBuf::new()); + let mut pkg = Package::new(info.name.clone(), PathBuf::new()); - for f in info.get_files().iter() { + for f in &info.files { let mut it = f.rsplit('.'); let file_type = it .next() @@ -144,32 +144,28 @@ fn build_mod_data_lua(state: Arc) -> String { // DMF is handled explicitely by the loading procedures, as it actually drives most of that // and should therefore not show up in the load order. - for mod_info in state - .get_mods() - .iter() - .filter(|m| m.get_id() != "dml" && m.get_enabled()) - { + for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) { lua.push_str(" {\n name = \""); - lua.push_str(mod_info.get_name()); + lua.push_str(&mod_info.name); lua.push_str("\",\n id = \""); - lua.push_str(mod_info.get_id()); + lua.push_str(&mod_info.id); lua.push_str("\",\n run = function()\n"); - let resources = mod_info.get_resources(); - if resources.get_data().is_some() || resources.get_localization().is_some() { + let resources = &mod_info.resources; + if resources.data.is_some() || resources.localization.is_some() { lua.push_str(" new_mod(\""); - lua.push_str(mod_info.get_id()); + lua.push_str(&mod_info.id); lua.push_str("\", {\n init = \""); - lua.push_str(&resources.get_init().to_string_lossy()); + lua.push_str(&resources.init.to_string_lossy()); - if let Some(data) = resources.get_data() { + if let Some(data) = resources.data.as_ref() { lua.push_str("\",\n data = \""); lua.push_str(&data.to_string_lossy()); } - if let Some(localization) = resources.get_localization() { + if let Some(localization) = &resources.localization { lua.push_str("\",\n localization = \""); lua.push_str(&localization.to_string_lossy()); } @@ -177,15 +173,15 @@ fn build_mod_data_lua(state: Arc) -> String { lua.push_str("\",\n })\n"); } else { lua.push_str(" return dofile(\""); - lua.push_str(&resources.get_init().to_string_lossy()); + lua.push_str(&resources.init.to_string_lossy()); lua.push_str("\")\n"); } lua.push_str(" end,\n packages = {\n"); - for pkg_info in mod_info.get_packages() { + for pkg_info in &mod_info.packages { lua.push_str(" \""); - lua.push_str(pkg_info.get_name()); + lua.push_str(&pkg_info.name); lua.push_str("\",\n"); } @@ -201,10 +197,10 @@ fn build_mod_data_lua(state: Arc) -> String { #[tracing::instrument(skip_all)] async fn build_bundles(state: Arc) -> Result> { - let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME); + let mut mod_bundle = Bundle::new(MOD_BUNDLE_NAME.to_string()); let mut tasks = Vec::new(); - let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let bundle_dir = Arc::new(state.game_dir.join("bundle")); let mut bundles = Vec::new(); @@ -220,17 +216,13 @@ async fn build_bundles(state: Arc) -> Result> { mod_bundle.add_file(file); } - for mod_info in state - .get_mods() - .iter() - .filter(|m| m.get_id() != "dml" && m.get_enabled()) - { - let span = tracing::trace_span!("building mod packages", name = mod_info.get_name()); + for mod_info in state.mods.iter().filter(|m| m.id != "dml" && m.enabled) { + let span = tracing::trace_span!("building mod packages", name = mod_info.name); let _enter = span.enter(); - let mod_dir = state.get_mod_dir().join(mod_info.get_id()); - for pkg_info in mod_info.get_packages() { - let span = tracing::trace_span!("building package", name = pkg_info.get_name()); + let mod_dir = state.get_mod_dir().join(&mod_info.id); + for pkg_info in &mod_info.packages { + let span = tracing::trace_span!("building package", name = pkg_info.name); let _enter = span.enter(); let pkg = make_package(pkg_info).wrap_err("failed to make package")?; @@ -239,24 +231,24 @@ async fn build_bundles(state: Arc) -> Result> { .to_binary() .wrap_err("failed to serialize package to binary")?; variant.set_data(bin); - let mut file = BundleFile::new(pkg_info.get_name().clone(), BundleFileType::Package); + let mut file = BundleFile::new(pkg_info.name.clone(), BundleFileType::Package); file.add_variant(variant); mod_bundle.add_file(file); - let bundle_name = Murmur64::hash(pkg_info.get_name()) + let bundle_name = Murmur64::hash(&pkg_info.name) .to_string() .to_ascii_lowercase(); let src = mod_dir.join(&bundle_name); let dest = bundle_dir.join(&bundle_name); - let pkg_name = pkg_info.get_name().clone(); - let mod_name = mod_info.get_name().clone(); + let pkg_name = pkg_info.name.clone(); + let mod_name = mod_info.name.clone(); // Explicitely drop the guard, so that we can move the span // into the async operation drop(_enter); - let ctx = state.get_ctx().clone(); + let ctx = state.ctx.clone(); let task = async move { let bundle = { @@ -322,7 +314,7 @@ async fn build_bundles(state: Arc) -> Result> { #[tracing::instrument(skip_all)] async fn patch_boot_bundle(state: Arc) -> Result> { - let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let bundle_dir = Arc::new(state.game_dir.join("bundle")); let bundle_path = bundle_dir.join(format!("{:x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes()))); let mut bundles = Vec::with_capacity(2); @@ -332,7 +324,7 @@ async fn patch_boot_bundle(state: Arc) -> Result> { .await .wrap_err("failed to read boot bundle")?; - Bundle::from_binary(&state.get_ctx(), BOOT_BUNDLE_NAME.to_string(), bin) + Bundle::from_binary(&state.ctx, BOOT_BUNDLE_NAME.to_string(), bin) .wrap_err("failed to parse boot bundle") } .instrument(tracing::trace_span!("read boot bundle")) @@ -346,9 +338,9 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let mut pkg = Package::new(MOD_BUNDLE_NAME.to_string(), PathBuf::new()); - for mod_info in state.get_mods() { - for pkg_info in mod_info.get_packages() { - pkg.add_file(BundleFileType::Package, pkg_info.get_name()); + for mod_info in &state.mods { + for pkg_info in &mod_info.packages { + pkg.add_file(BundleFileType::Package, &pkg_info.name); } } @@ -369,32 +361,28 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let mut variant = BundleFileVariant::new(); - let mods = state.get_mods(); - let mod_info = mods + let mod_info = state + .mods .iter() - .find(|m| m.get_id() == "dml") + .find(|m| m.id == "dml") .ok_or_else(|| eyre::eyre!("DML not found in mod list"))?; let pkg_info = mod_info - .get_packages() + .packages .get(0) .ok_or_else(|| eyre::eyre!("invalid mod package for DML")) .with_suggestion(|| "Re-download and import the newest version.".to_string())?; - let bundle_name = Murmur64::hash(pkg_info.get_name()) + let bundle_name = Murmur64::hash(&pkg_info.name) .to_string() .to_ascii_lowercase(); - let src = state - .get_mod_dir() - .join(mod_info.get_id()) - .join(&bundle_name); + let src = state.get_mod_dir().join(&mod_info.id).join(&bundle_name); { - let ctx = state.get_ctx(); let bin = fs::read(&src) .await .wrap_err_with(|| format!("failed to read bundle file '{}'", src.display()))?; - let name = Bundle::get_name_from_path(&ctx, &src); + let name = Bundle::get_name_from_path(&state.ctx, &src); - let dml_bundle = Bundle::from_binary(&ctx, name, bin) + let dml_bundle = Bundle::from_binary(&state.ctx, name, bin) .wrap_err_with(|| format!("failed to parse bundle '{}'", src.display()))?; bundles.push(dml_bundle); @@ -402,8 +390,8 @@ async fn patch_boot_bundle(state: Arc) -> Result> { { let dest = bundle_dir.join(&bundle_name); - let pkg_name = pkg_info.get_name().clone(); - let mod_name = mod_info.get_name().clone(); + let pkg_name = pkg_info.name.clone(); + let mod_name = mod_info.name.clone(); tracing::debug!( "Copying bundle {} for mod {}: {} -> {}", @@ -441,7 +429,7 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let span = tracing::debug_span!("Importing mod main script"); let _enter = span.enter(); - let lua = include_str!("../assets/mod_main.lua"); + let lua = include_str!("../../assets/mod_main.lua"); let lua = CString::new(lua).wrap_err("failed to build CString from mod main Lua string")?; let file = lua::compile(MOD_BOOT_SCRIPT, &lua).wrap_err("failed to compile mod main Lua file")?; @@ -467,7 +455,7 @@ async fn patch_boot_bundle(state: Arc) -> Result> { #[tracing::instrument(skip_all, fields(bundles = bundles.len()))] async fn patch_bundle_database(state: Arc, bundles: Vec) -> Result<()> { - let bundle_dir = Arc::new(state.get_game_dir().join("bundle")); + let bundle_dir = Arc::new(state.game_dir.join("bundle")); let database_path = bundle_dir.join(BUNDLE_DATABASE_NAME); let mut db = { @@ -501,16 +489,15 @@ async fn patch_bundle_database(state: Arc, bundles: Vec) -> Resul } #[tracing::instrument(skip_all, fields( - game_dir = %state.get_game_dir().display(), - mods = state.get_mods().len() + game_dir = %state.game_dir.display(), + mods = state.mods.len() ))] pub(crate) async fn deploy_mods(state: State) -> Result<()> { let state = Arc::new(state); { - let mods = state.get_mods(); - let first = mods.get(0); - if first.is_none() || !(first.unwrap().get_id() == "dml" && first.unwrap().get_enabled()) { + let first = state.mods.get(0); + if first.is_none() || !(first.unwrap().id == "dml" && first.unwrap().enabled) { // TODO: Add a suggestion where to get it, once that's published eyre::bail!("'Darktide Mod Loader' needs to be installed, enabled and at the top of the load order"); } @@ -518,8 +505,8 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { tracing::info!( "Deploying {} mods to {}", - state.get_mods().len(), - state.get_game_dir().join("bundle").display() + state.mods.len(), + state.game_dir.join("bundle").display() ); tracing::info!("Build mod bundles"); @@ -550,7 +537,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { #[tracing::instrument(skip(state))] pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME]; - let bundle_dir = state.get_game_dir().join("bundle"); + let bundle_dir = state.game_dir.join("bundle"); tracing::info!("Resetting mod deployment in {}", bundle_dir.display()); @@ -664,7 +651,7 @@ pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result #[tracing::instrument(skip(state))] pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { - let mod_dir = state.get_mod_dir().join(info.get_id()); + let mod_dir = state.get_mod_dir().join(&info.id); fs::remove_dir_all(&mod_dir) .await .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; diff --git a/crates/dtmm/src/worker.rs b/crates/dtmm/src/controller/worker.rs similarity index 99% rename from crates/dtmm/src/worker.rs rename to crates/dtmm/src/controller/worker.rs index 4715480..58b3827 100644 --- a/crates/dtmm/src/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -6,7 +6,7 @@ use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; -use crate::engine::*; +use crate::controller::engine::*; use crate::state::*; async fn handle_action( diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 6a3252a..bf38c65 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -12,24 +12,25 @@ use color_eyre::{Report, Result}; use druid::AppLauncher; use tokio::sync::RwLock; +use crate::controller::worker::work_thread; use crate::state::{Delegate, State}; -use crate::worker::work_thread; -mod controller; -mod engine; -mod log; -mod main_window; +mod controller { + pub mod engine; + pub mod worker; +} mod state; -mod theme; -mod util; -mod widget; -mod worker; +mod util { + pub mod config; + pub mod log; +} +mod ui; #[tracing::instrument] fn main() -> Result<()> { color_eyre::install()?; - let default_config_path = util::get_default_config_path(); + let default_config_path = util::config::get_default_config_path(); tracing::trace!(default_config_path = %default_config_path.display()); @@ -51,21 +52,21 @@ fn main() -> Result<()> { .get_matches(); let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel(); - log::create_tracing_subscriber(log_tx); + util::log::create_tracing_subscriber(log_tx); unsafe { oodle_sys::init(matches.get_one::("oodle")); } - let config = - util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?; + let config = util::config::read_config(&default_config_path, &matches) + .wrap_err("failed to read config file")?; let initial_state = State::new(config); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); let delegate = Delegate::new(action_tx); - let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); + let launcher = AppLauncher::with_window(ui::window::main::new()).delegate(delegate); let event_sink = launcher.get_external_handle(); std::thread::spawn(move || { diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs deleted file mode 100644 index 71f66c2..0000000 --- a/crates/dtmm/src/state.rs +++ /dev/null @@ -1,510 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use druid::im::Vector; -use druid::text::Formatter; -use druid::{ - AppDelegate, Command, Data, DelegateCtx, Env, FileInfo, Handled, Lens, Selector, SingleUse, - Target, -}; -use dtmt_shared::ModConfig; -use tokio::sync::mpsc::UnboundedSender; - -use crate::util::Config; - -pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); -pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); -pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = - Selector::new("dtmm.action.selected-mod-down"); -pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector> = - Selector::new("dtmm.action.srart-delete-selected-mod"); -pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> = - Selector::new("dtmm.action.finish-delete-selected-mod"); - -pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); -pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); - -pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector = - Selector::new("dtmm.action.start-reset-deployment"); -pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector = - Selector::new("dtmm.action.finish-reset-deployment"); - -pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); -pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = - Selector::new("dtmm.action.finish-add-mod"); - -pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); - -#[derive(Copy, Clone, Data, Debug, PartialEq)] -pub(crate) enum View { - Mods, - Settings, - About, -} - -impl Default for View { - fn default() -> Self { - Self::Mods - } -} - -#[derive(Clone, Data, Debug)] -pub struct PackageInfo { - name: String, - files: Vector, -} - -impl PackageInfo { - pub fn new(name: String, files: Vector) -> Self { - Self { name, files } - } - - pub fn get_name(&self) -> &String { - &self.name - } - - pub fn get_files(&self) -> &Vector { - &self.files - } -} - -#[derive(Clone, Debug)] -pub(crate) struct ModResourceInfo { - init: PathBuf, - data: Option, - localization: Option, -} - -impl ModResourceInfo { - pub(crate) fn get_init(&self) -> &PathBuf { - &self.init - } - - pub(crate) fn get_data(&self) -> Option<&PathBuf> { - self.data.as_ref() - } - - pub(crate) fn get_localization(&self) -> Option<&PathBuf> { - self.localization.as_ref() - } -} - -#[derive(Clone, Data, Debug, Lens)] -pub(crate) struct ModInfo { - id: String, - name: String, - description: Arc, - enabled: bool, - #[lens(ignore)] - #[data(ignore)] - packages: Vector, - #[lens(ignore)] - #[data(ignore)] - resources: ModResourceInfo, -} - -impl ModInfo { - pub fn new(cfg: ModConfig, packages: Vector) -> Self { - Self { - id: cfg.id, - name: cfg.name, - description: Arc::new(cfg.description), - enabled: false, - packages, - resources: ModResourceInfo { - init: cfg.resources.init, - data: cfg.resources.data, - localization: cfg.resources.localization, - }, - } - } - - pub fn get_packages(&self) -> &Vector { - &self.packages - } - - pub(crate) fn get_name(&self) -> &String { - &self.name - } - - pub(crate) fn get_id(&self) -> &String { - &self.id - } - - pub(crate) fn get_enabled(&self) -> bool { - self.enabled - } - - pub(crate) fn get_resources(&self) -> &ModResourceInfo { - &self.resources - } -} - -impl PartialEq for ModInfo { - fn eq(&self, other: &Self) -> bool { - self.name.eq(&other.name) - } -} - -#[derive(Clone, Data, Lens)] -pub(crate) struct State { - current_view: View, - mods: Vector, - selected_mod_index: Option, - is_deployment_in_progress: bool, - is_reset_in_progress: bool, - game_dir: Arc, - data_dir: Arc, - ctx: Arc, - log: Arc, -} - -impl State { - #[allow(non_upper_case_globals)] - pub const selected_mod: SelectedModLens = SelectedModLens; - - pub fn new(config: Config) -> Self { - let ctx = sdk::Context::new(); - - Self { - ctx: Arc::new(ctx), - current_view: View::default(), - mods: Vector::new(), - selected_mod_index: None, - is_deployment_in_progress: false, - is_reset_in_progress: false, - game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()), - data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()), - log: Arc::new(String::new()), - } - } - - pub fn get_current_view(&self) -> View { - self.current_view - } - - pub fn set_current_view(&mut self, view: View) { - self.current_view = view; - } - - pub fn get_mods(&self) -> Vector { - self.mods.clone() - } - - pub fn select_mod(&mut self, index: usize) { - self.selected_mod_index = Some(index); - } - - pub fn add_mod(&mut self, info: ModInfo) { - if let Some(pos) = self.mods.index_of(&info) { - self.mods.set(pos, info); - self.selected_mod_index = Some(pos); - } else { - self.mods.push_back(info); - self.selected_mod_index = Some(self.mods.len() - 1); - } - } - - pub fn can_move_mod_down(&self) -> bool { - self.selected_mod_index - .map(|i| i < (self.mods.len().saturating_sub(1))) - .unwrap_or(false) - } - - pub fn can_move_mod_up(&self) -> bool { - self.selected_mod_index.map(|i| i > 0).unwrap_or(false) - } - - pub fn can_deploy_mods(&self) -> bool { - !self.is_deployment_in_progress - } - - pub fn can_reset_deployment(&self) -> bool { - !self.is_reset_in_progress - } - - pub(crate) fn get_game_dir(&self) -> &PathBuf { - &self.game_dir - } - - pub(crate) fn get_mod_dir(&self) -> PathBuf { - self.data_dir.join("mods") - } - - pub(crate) fn get_ctx(&self) -> Arc { - self.ctx.clone() - } - - pub(crate) fn add_log_line(&mut self, line: String) { - let log = Arc::make_mut(&mut self.log); - log.push_str(&line); - } -} - -pub(crate) struct SelectedModLens; - -impl Lens> for SelectedModLens { - #[tracing::instrument(name = "SelectedModLens::with", skip_all)] - fn with) -> V>(&self, data: &State, f: F) -> V { - let info = data - .selected_mod_index - .and_then(|i| data.mods.get(i).cloned()); - - f(&info) - } - - #[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)] - fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { - match data.selected_mod_index { - Some(i) => { - let mut info = data.mods.get_mut(i).cloned(); - let ret = f(&mut info); - - if let Some(info) = info { - // TODO: Figure out a way to check for equality and - // only update when needed - data.mods.set(i, info); - } else { - data.selected_mod_index = None; - } - - ret - } - None => f(&mut None), - } - } -} - -/// A Lens that maps an `im::Vector` to `im::Vector<(usize, T)>`, -/// where each element in the destination vector includes its index in the -/// source vector. -pub(crate) struct IndexedVectorLens; - -impl Lens, Vector<(usize, T)>> for IndexedVectorLens { - #[tracing::instrument(name = "IndexedVectorLens::with", skip_all)] - fn with) -> V>(&self, values: &Vector, f: F) -> V { - let indexed = values - .iter() - .enumerate() - .map(|(i, val)| (i, val.clone())) - .collect(); - f(&indexed) - } - - #[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)] - fn with_mut) -> V>( - &self, - values: &mut Vector, - f: F, - ) -> V { - let mut indexed = values - .iter() - .enumerate() - .map(|(i, val)| (i, val.clone())) - .collect(); - let ret = f(&mut indexed); - - *values = indexed.into_iter().map(|(_i, val)| val).collect(); - - ret - } -} - -pub(crate) enum AsyncAction { - DeployMods(State), - ResetDeployment(State), - AddMod((State, FileInfo)), - DeleteMod((State, ModInfo)), -} - -pub(crate) struct Delegate { - sender: UnboundedSender, -} - -impl Delegate { - pub fn new(sender: UnboundedSender) -> Self { - Self { sender } - } -} - -impl AppDelegate for Delegate { - #[tracing::instrument(name = "Delegate", skip_all)] - fn command( - &mut self, - _ctx: &mut DelegateCtx, - _target: Target, - cmd: &Command, - state: &mut State, - _env: &Env, - ) -> Handled { - match cmd { - cmd if cmd.is(ACTION_START_DEPLOY) => { - if self - .sender - .send(AsyncAction::DeployMods(state.clone())) - .is_ok() - { - state.is_deployment_in_progress = true; - } else { - tracing::error!("Failed to queue action to deploy mods"); - } - - Handled::Yes - } - cmd if cmd.is(ACTION_FINISH_DEPLOY) => { - state.is_deployment_in_progress = false; - Handled::Yes - } - cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { - if self - .sender - .send(AsyncAction::ResetDeployment(state.clone())) - .is_ok() - { - state.is_reset_in_progress = true; - } else { - tracing::error!("Failed to queue action to reset mod deployment"); - } - - Handled::Yes - } - cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => { - state.is_reset_in_progress = false; - Handled::Yes - } - cmd if cmd.is(ACTION_SELECT_MOD) => { - let index = cmd - .get(ACTION_SELECT_MOD) - .expect("command type matched but didn't contain the expected value"); - - state.select_mod(*index); - Handled::Yes - } - cmd if cmd.is(ACTION_SELECTED_MOD_UP) => { - let Some(i) = state.selected_mod_index else { - return Handled::No; - }; - - let len = state.mods.len(); - if len == 0 || i == 0 { - return Handled::No; - } - - state.mods.swap(i, i - 1); - state.selected_mod_index = Some(i - 1); - Handled::Yes - } - cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => { - let Some(i) = state.selected_mod_index else { - return Handled::No; - }; - - let len = state.mods.len(); - if len == 0 || i == usize::MAX || i >= len - 1 { - return Handled::No; - } - - state.mods.swap(i, i + 1); - state.selected_mod_index = Some(i + 1); - Handled::Yes - } - cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { - let info = cmd - .get(ACTION_START_DELETE_SELECTED_MOD) - .and_then(|info| info.take()) - .expect("command type matched but didn't contain the expected value"); - if self - .sender - .send(AsyncAction::DeleteMod((state.clone(), info))) - .is_ok() - { - state.is_deployment_in_progress = true; - } else { - tracing::error!("Failed to queue action to deploy mods"); - } - - Handled::Yes - } - cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { - let info = cmd - .get(ACTION_FINISH_DELETE_SELECTED_MOD) - .and_then(|info| info.take()) - .expect("command type matched but didn't contain the expected value"); - let mods = state.get_mods(); - let found = mods - .iter() - .enumerate() - .find(|(_, i)| i.get_id() == info.get_id()); - let Some((index, _)) = found else { - return Handled::No; - }; - - state.mods.remove(index); - Handled::Yes - } - cmd if cmd.is(ACTION_ADD_MOD) => { - let info = cmd - .get(ACTION_ADD_MOD) - .expect("command type matched but didn't contain the expected value"); - if let Err(err) = self - .sender - .send(AsyncAction::AddMod((state.clone(), info.clone()))) - { - tracing::error!("Failed to add mod: {}", err); - } - Handled::Yes - } - cmd if cmd.is(ACTION_FINISH_ADD_MOD) => { - let info = cmd - .get(ACTION_FINISH_ADD_MOD) - .expect("command type matched but didn't contain the expected value"); - if let Some(info) = info.take() { - state.add_mod(info); - } - Handled::Yes - } - cmd if cmd.is(ACTION_LOG) => { - let line = cmd - .get(ACTION_LOG) - .expect("command type matched but didn't contain the expected value"); - if let Some(line) = line.take() { - state.add_log_line(line); - } - Handled::Yes - } - cmd => { - if cfg!(debug_assertions) { - tracing::warn!("Unknown command: {:?}", cmd); - } - Handled::No - } - } - } -} - -pub(crate) struct PathBufFormatter; - -impl PathBufFormatter { - pub fn new() -> Self { - Self {} - } -} - -impl Formatter> for PathBufFormatter { - fn format(&self, value: &Arc) -> String { - value.display().to_string() - } - - fn validate_partial_input( - &self, - _input: &str, - _sel: &druid::text::Selection, - ) -> druid::text::Validation { - druid::text::Validation::success() - } - - fn value(&self, input: &str) -> Result, druid::text::ValidationError> { - let p = PathBuf::from(input); - Ok(Arc::new(p)) - } -} diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs new file mode 100644 index 0000000..b016b5a --- /dev/null +++ b/crates/dtmm/src/state/data.rs @@ -0,0 +1,144 @@ +use std::{path::PathBuf, sync::Arc}; + +use druid::{im::Vector, Data, Lens}; +use dtmt_shared::ModConfig; + +use crate::util::config::Config; + +use super::SelectedModLens; + +#[derive(Copy, Clone, Data, Debug, PartialEq)] +pub(crate) enum View { + Mods, + Settings, + About, +} + +impl Default for View { + fn default() -> Self { + Self::Mods + } +} + +#[derive(Clone, Data, Debug)] +pub struct PackageInfo { + pub name: String, + pub files: Vector, +} + +impl PackageInfo { + pub fn new(name: String, files: Vector) -> Self { + Self { name, files } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct ModResourceInfo { + pub init: PathBuf, + pub data: Option, + pub localization: Option, +} + +#[derive(Clone, Data, Debug, Lens)] +pub(crate) struct ModInfo { + pub id: String, + pub name: String, + pub description: Arc, + pub enabled: bool, + #[lens(ignore)] + #[data(ignore)] + pub packages: Vector, + #[lens(ignore)] + #[data(ignore)] + pub resources: ModResourceInfo, +} + +impl ModInfo { + pub fn new(cfg: ModConfig, packages: Vector) -> Self { + Self { + id: cfg.id, + name: cfg.name, + description: Arc::new(cfg.description), + enabled: false, + packages, + resources: ModResourceInfo { + init: cfg.resources.init, + data: cfg.resources.data, + localization: cfg.resources.localization, + }, + } + } +} + +impl PartialEq for ModInfo { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +#[derive(Clone, Data, Lens)] +pub(crate) struct State { + pub current_view: View, + pub mods: Vector, + pub selected_mod_index: Option, + pub is_deployment_in_progress: bool, + pub is_reset_in_progress: bool, + pub game_dir: Arc, + pub data_dir: Arc, + pub ctx: Arc, + pub log: Arc, +} + +impl State { + #[allow(non_upper_case_globals)] + pub const selected_mod: SelectedModLens = SelectedModLens; + + pub fn new(config: Config) -> Self { + let ctx = sdk::Context::new(); + + Self { + ctx: Arc::new(ctx), + current_view: View::default(), + mods: Vector::new(), + selected_mod_index: None, + is_deployment_in_progress: false, + is_reset_in_progress: false, + game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()), + data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()), + log: Arc::new(String::new()), + } + } + + pub fn select_mod(&mut self, index: usize) { + self.selected_mod_index = Some(index); + } + + pub fn add_mod(&mut self, info: ModInfo) { + if let Some(pos) = self.mods.index_of(&info) { + self.mods.set(pos, info); + self.selected_mod_index = Some(pos); + } else { + self.mods.push_back(info); + self.selected_mod_index = Some(self.mods.len() - 1); + } + } + + pub fn can_move_mod_down(&self) -> bool { + self.selected_mod_index + .map(|i| i < (self.mods.len().saturating_sub(1))) + .unwrap_or(false) + } + + pub fn can_move_mod_up(&self) -> bool { + self.selected_mod_index.map(|i| i > 0).unwrap_or(false) + } + + pub(crate) fn get_mod_dir(&self) -> PathBuf { + self.data_dir.join("mods") + } + + pub(crate) fn add_log_line(&mut self, line: String) { + let log = Arc::make_mut(&mut self.log); + log.push_str(&line); + } +} diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs new file mode 100644 index 0000000..9a2a1ef --- /dev/null +++ b/crates/dtmm/src/state/delegate.rs @@ -0,0 +1,197 @@ +use druid::{ + AppDelegate, Command, DelegateCtx, Env, FileInfo, Handled, Selector, SingleUse, Target, +}; +use tokio::sync::mpsc::UnboundedSender; + +use super::{ModInfo, State}; + +pub(crate) const ACTION_SELECT_MOD: Selector = Selector::new("dtmm.action.select-mod"); +pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up"); +pub(crate) const ACTION_SELECTED_MOD_DOWN: Selector = + Selector::new("dtmm.action.selected-mod-down"); +pub(crate) const ACTION_START_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.srart-delete-selected-mod"); +pub(crate) const ACTION_FINISH_DELETE_SELECTED_MOD: Selector> = + Selector::new("dtmm.action.finish-delete-selected-mod"); + +pub(crate) const ACTION_START_DEPLOY: Selector = Selector::new("dtmm.action.start-deploy"); +pub(crate) const ACTION_FINISH_DEPLOY: Selector = Selector::new("dtmm.action.finish-deploy"); + +pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector = + Selector::new("dtmm.action.start-reset-deployment"); +pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector = + Selector::new("dtmm.action.finish-reset-deployment"); + +pub(crate) const ACTION_ADD_MOD: Selector = Selector::new("dtmm.action.add-mod"); +pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = + Selector::new("dtmm.action.finish-add-mod"); + +pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); + +pub(crate) enum AsyncAction { + DeployMods(State), + ResetDeployment(State), + AddMod((State, FileInfo)), + DeleteMod((State, ModInfo)), +} + +pub(crate) struct Delegate { + sender: UnboundedSender, +} + +impl Delegate { + pub fn new(sender: UnboundedSender) -> Self { + Self { sender } + } +} + +impl AppDelegate for Delegate { + #[tracing::instrument(name = "Delegate", skip_all)] + fn command( + &mut self, + _ctx: &mut DelegateCtx, + _target: Target, + cmd: &Command, + state: &mut State, + _env: &Env, + ) -> Handled { + match cmd { + cmd if cmd.is(ACTION_START_DEPLOY) => { + if self + .sender + .send(AsyncAction::DeployMods(state.clone())) + .is_ok() + { + state.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_DEPLOY) => { + state.is_deployment_in_progress = false; + Handled::Yes + } + cmd if cmd.is(ACTION_START_RESET_DEPLOYMENT) => { + if self + .sender + .send(AsyncAction::ResetDeployment(state.clone())) + .is_ok() + { + state.is_reset_in_progress = true; + } else { + tracing::error!("Failed to queue action to reset mod deployment"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_RESET_DEPLOYMENT) => { + state.is_reset_in_progress = false; + Handled::Yes + } + cmd if cmd.is(ACTION_SELECT_MOD) => { + let index = cmd + .get(ACTION_SELECT_MOD) + .expect("command type matched but didn't contain the expected value"); + + state.select_mod(*index); + Handled::Yes + } + cmd if cmd.is(ACTION_SELECTED_MOD_UP) => { + let Some(i) = state.selected_mod_index else { + return Handled::No; + }; + + let len = state.mods.len(); + if len == 0 || i == 0 { + return Handled::No; + } + + state.mods.swap(i, i - 1); + state.selected_mod_index = Some(i - 1); + Handled::Yes + } + cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => { + let Some(i) = state.selected_mod_index else { + return Handled::No; + }; + + let len = state.mods.len(); + if len == 0 || i == usize::MAX || i >= len - 1 { + return Handled::No; + } + + state.mods.swap(i, i + 1); + state.selected_mod_index = Some(i + 1); + Handled::Yes + } + cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_START_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + if self + .sender + .send(AsyncAction::DeleteMod((state.clone(), info))) + .is_ok() + { + state.is_deployment_in_progress = true; + } else { + tracing::error!("Failed to queue action to deploy mods"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_DELETE_SELECTED_MOD) => { + let info = cmd + .get(ACTION_FINISH_DELETE_SELECTED_MOD) + .and_then(|info| info.take()) + .expect("command type matched but didn't contain the expected value"); + let found = state.mods.iter().enumerate().find(|(_, i)| i.id == info.id); + let Some((index, _)) = found else { + return Handled::No; + }; + + state.mods.remove(index); + Handled::Yes + } + cmd if cmd.is(ACTION_ADD_MOD) => { + let info = cmd + .get(ACTION_ADD_MOD) + .expect("command type matched but didn't contain the expected value"); + if let Err(err) = self + .sender + .send(AsyncAction::AddMod((state.clone(), info.clone()))) + { + tracing::error!("Failed to add mod: {}", err); + } + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_ADD_MOD) => { + let info = cmd + .get(ACTION_FINISH_ADD_MOD) + .expect("command type matched but didn't contain the expected value"); + if let Some(info) = info.take() { + state.add_mod(info); + } + Handled::Yes + } + cmd if cmd.is(ACTION_LOG) => { + let line = cmd + .get(ACTION_LOG) + .expect("command type matched but didn't contain the expected value"); + if let Some(line) = line.take() { + state.add_log_line(line); + } + Handled::Yes + } + cmd => { + if cfg!(debug_assertions) { + tracing::warn!("Unknown command: {:?}", cmd); + } + Handled::No + } + } + } +} diff --git a/crates/dtmm/src/state/lens.rs b/crates/dtmm/src/state/lens.rs new file mode 100644 index 0000000..6c457a4 --- /dev/null +++ b/crates/dtmm/src/state/lens.rs @@ -0,0 +1,73 @@ +use druid::im::Vector; +use druid::{Data, Lens}; + +use super::{ModInfo, State}; + +pub(crate) struct SelectedModLens; + +impl Lens> for SelectedModLens { + #[tracing::instrument(name = "SelectedModLens::with", skip_all)] + fn with) -> V>(&self, data: &State, f: F) -> V { + let info = data + .selected_mod_index + .and_then(|i| data.mods.get(i).cloned()); + + f(&info) + } + + #[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)] + fn with_mut) -> V>(&self, data: &mut State, f: F) -> V { + match data.selected_mod_index { + Some(i) => { + let mut info = data.mods.get_mut(i).cloned(); + let ret = f(&mut info); + + if let Some(info) = info { + // TODO: Figure out a way to check for equality and + // only update when needed + data.mods.set(i, info); + } else { + data.selected_mod_index = None; + } + + ret + } + None => f(&mut None), + } + } +} + +/// A Lens that maps an `im::Vector` to `im::Vector<(usize, T)>`, +/// where each element in the destination vector includes its index in the +/// source vector. +pub(crate) struct IndexedVectorLens; + +impl Lens, Vector<(usize, T)>> for IndexedVectorLens { + #[tracing::instrument(name = "IndexedVectorLens::with", skip_all)] + fn with) -> V>(&self, values: &Vector, f: F) -> V { + let indexed = values + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect(); + f(&indexed) + } + + #[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)] + fn with_mut) -> V>( + &self, + values: &mut Vector, + f: F, + ) -> V { + let mut indexed = values + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone())) + .collect(); + let ret = f(&mut indexed); + + *values = indexed.into_iter().map(|(_i, val)| val).collect(); + + ret + } +} diff --git a/crates/dtmm/src/state/mod.rs b/crates/dtmm/src/state/mod.rs new file mode 100644 index 0000000..1586e3c --- /dev/null +++ b/crates/dtmm/src/state/mod.rs @@ -0,0 +1,9 @@ +mod data; +mod delegate; +mod lens; +mod util; + +pub(crate) use data::*; +pub(crate) use delegate::*; +pub(crate) use lens::*; +pub(crate) use util::*; diff --git a/crates/dtmm/src/state/util.rs b/crates/dtmm/src/state/util.rs new file mode 100644 index 0000000..804b751 --- /dev/null +++ b/crates/dtmm/src/state/util.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use druid::text::Formatter; + +pub(crate) struct PathBufFormatter; + +impl PathBufFormatter { + pub fn new() -> Self { + Self {} + } +} + +impl Formatter> for PathBufFormatter { + fn format(&self, value: &Arc) -> String { + value.display().to_string() + } + + fn validate_partial_input( + &self, + _input: &str, + _sel: &druid::text::Selection, + ) -> druid::text::Validation { + druid::text::Validation::success() + } + + fn value(&self, input: &str) -> Result, druid::text::ValidationError> { + let p = PathBuf::from(input); + Ok(Arc::new(p)) + } +} diff --git a/crates/dtmm/src/ui/mod.rs b/crates/dtmm/src/ui/mod.rs new file mode 100644 index 0000000..cf8554f --- /dev/null +++ b/crates/dtmm/src/ui/mod.rs @@ -0,0 +1,5 @@ +pub mod theme; +pub mod widget; +pub mod window { + pub mod main; +} diff --git a/crates/dtmm/src/theme.rs b/crates/dtmm/src/ui/theme.rs similarity index 100% rename from crates/dtmm/src/theme.rs rename to crates/dtmm/src/ui/theme.rs diff --git a/crates/dtmm/src/widget/container.rs b/crates/dtmm/src/ui/widget/container.rs similarity index 100% rename from crates/dtmm/src/widget/container.rs rename to crates/dtmm/src/ui/widget/container.rs diff --git a/crates/dtmm/src/controller.rs b/crates/dtmm/src/ui/widget/controller.rs similarity index 100% rename from crates/dtmm/src/controller.rs rename to crates/dtmm/src/ui/widget/controller.rs diff --git a/crates/dtmm/src/widget/fill_container.rs b/crates/dtmm/src/ui/widget/fill_container.rs similarity index 100% rename from crates/dtmm/src/widget/fill_container.rs rename to crates/dtmm/src/ui/widget/fill_container.rs diff --git a/crates/dtmm/src/widget/mod.rs b/crates/dtmm/src/ui/widget/mod.rs similarity index 94% rename from crates/dtmm/src/widget/mod.rs rename to crates/dtmm/src/ui/widget/mod.rs index 9262d0a..8561c31 100644 --- a/crates/dtmm/src/widget/mod.rs +++ b/crates/dtmm/src/ui/widget/mod.rs @@ -3,6 +3,7 @@ use druid::{Data, Widget}; use self::fill_container::FillContainer; pub mod container; +pub mod controller; pub mod fill_container; pub trait ExtraWidgetExt: Widget + Sized + 'static { diff --git a/crates/dtmm/src/widget/table_select.rs b/crates/dtmm/src/ui/widget/table_select.rs similarity index 100% rename from crates/dtmm/src/widget/table_select.rs rename to crates/dtmm/src/ui/widget/table_select.rs diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/ui/window/main.rs similarity index 93% rename from crates/dtmm/src/main_window.rs rename to crates/dtmm/src/ui/window/main.rs index 6fcbc87..1853d2e 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -13,8 +13,8 @@ use crate::state::{ ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, }; -use crate::theme; -use crate::widget::ExtraWidgetExt; +use crate::ui::theme; +use crate::ui::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; const WINDOW_SIZE: (f64, f64) = (800.0, 600.0); @@ -33,21 +33,19 @@ fn build_top_bar() -> impl Widget { .with_child( Flex::row() .with_child( - Button::new("Mods").on_click(|_ctx, state: &mut State, _env| { - state.set_current_view(View::Mods) - }), + Button::new("Mods") + .on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods), ) .with_default_spacer() .with_child( Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { - state.set_current_view(View::Settings) + state.current_view = View::Settings; }), ) .with_default_spacer() .with_child( - Button::new("About").on_click(|_ctx, state: &mut State, _env| { - state.set_current_view(View::About) - }), + Button::new("About") + .on_click(|_ctx, state: &mut State, _env| state.current_view = View::About), ), ) .with_child( @@ -57,7 +55,7 @@ fn build_top_bar() -> impl Widget { .on_click(|ctx, _state: &mut State, _env| { ctx.submit_command(ACTION_START_DEPLOY); }) - .disabled_if(|data, _| !data.can_deploy_mods()), + .disabled_if(|data, _| !data.is_deployment_in_progress), ) .with_default_spacer() .with_child( @@ -65,7 +63,7 @@ fn build_top_bar() -> impl Widget { .on_click(|ctx, _state: &mut State, _env| { ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); }) - .disabled_if(|data, _| !data.can_reset_deployment()), + .disabled_if(|data, _| !data.is_reset_in_progress), ), ) .padding(theme::TOP_BAR_INSETS) @@ -253,7 +251,7 @@ fn build_view_about() -> impl Widget { fn build_main() -> impl Widget { ViewSwitcher::new( - |state: &State, _env| state.get_current_view(), + |state: &State, _env| state.current_view, |selector, _state, _env| match selector { View::Mods => Box::new(build_view_mods()), View::Settings => Box::new(build_view_settings()), diff --git a/crates/dtmm/src/util.rs b/crates/dtmm/src/util/config.rs similarity index 100% rename from crates/dtmm/src/util.rs rename to crates/dtmm/src/util/config.rs diff --git a/crates/dtmm/src/log.rs b/crates/dtmm/src/util/log.rs similarity index 100% rename from crates/dtmm/src/log.rs rename to crates/dtmm/src/util/log.rs -- 2.45.3 From f7627c091b2132d5ca7ac3a690e8f5796ee7e6d3 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 11:05:15 +0100 Subject: [PATCH 67/90] fix(dtmm): Fix scrolling log view --- crates/dtmm/src/ui/window/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 1853d2e..f7897f0 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -265,13 +265,11 @@ fn build_log_view() -> impl Widget { let label = Label::raw() .with_font(font) .with_line_break_mode(LineBreaking::WordWrap) - .lens(State::log); - - SizedBox::new(label) - .expand_width() - .height(128.0) + .lens(State::log) .scroll() - .vertical() + .vertical(); + + SizedBox::new(label).expand_width().height(128.0) } fn build_window() -> impl Widget { -- 2.45.3 From d208c51cb487fa8bef7a9290e502b858969f8835 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 11:28:53 +0100 Subject: [PATCH 68/90] feat(dtmm): Automatically scroll log view --- crates/dtmm/src/ui/widget/controller.rs | 24 ++++++++++++++++++++++-- crates/dtmm/src/ui/window/main.rs | 4 +++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/dtmm/src/ui/widget/controller.rs b/crates/dtmm/src/ui/widget/controller.rs index a2bf429..f7f71ef 100644 --- a/crates/dtmm/src/ui/widget/controller.rs +++ b/crates/dtmm/src/ui/widget/controller.rs @@ -1,5 +1,5 @@ -use druid::widget::{Button, Controller}; -use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, UpdateCtx, Widget}; +use druid::widget::{Button, Controller, Scroll}; +use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, Rect, UpdateCtx, Widget}; pub struct DisabledButtonController; @@ -45,3 +45,23 @@ impl Controller> for DisabledButtonController { child.update(ctx, old_data, data, env) } } + +pub struct AutoScrollController; + +impl> Controller> for AutoScrollController { + fn update( + &mut self, + child: &mut Scroll, + ctx: &mut UpdateCtx, + old_data: &T, + data: &T, + env: &Env, + ) { + if !ctx.is_disabled() { + let size = child.child_size(); + let end_region = Rect::new(size.width - 1., size.height - 1., size.width, size.height); + child.scroll_to(ctx, end_region); + } + child.update(ctx, old_data, data, env) + } +} diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index f7897f0..fa66c1f 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -14,6 +14,7 @@ use crate::state::{ ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, }; use crate::ui::theme; +use crate::ui::widget::controller::AutoScrollController; use crate::ui::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; @@ -267,7 +268,8 @@ fn build_log_view() -> impl Widget { .with_line_break_mode(LineBreaking::WordWrap) .lens(State::log) .scroll() - .vertical(); + .vertical() + .controller(AutoScrollController); SizedBox::new(label).expand_width().height(128.0) } -- 2.45.3 From e88bc7fb9bc4ae2570eb826b1d61310a3034391b Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 14:21:58 +0100 Subject: [PATCH 69/90] feat(dtmm): Improve mod list visuals - add checkbox for enable/disable - highlight the selected mod - adding visual spacing - add alternating row background color Ref: #15. --- crates/dtmm/src/ui/window/main.rs | 56 ++++++++++++++++++------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index fa66c1f..d7ee41e 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -1,11 +1,11 @@ use druid::im::Vector; use druid::widget::{ - Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe, - Scroll, SizedBox, Split, TextBox, ViewSwitcher, + Align, Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, + MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ - lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse, - Widget, WidgetExt, WindowDesc, + lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, Key, LensExt, + SingleUse, Widget, WidgetExt, WindowDesc, }; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; @@ -21,6 +21,8 @@ const TITLE: &str = "Darktide Mod Manager"; const WINDOW_SIZE: (f64, f64) = (800.0, 600.0); const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; +const KEY_MOD_LIST_ITEM_BG_COLOR: Key = Key::new("dtmm.mod-list.item.background-color"); + pub(crate) fn new() -> WindowDesc { WindowDesc::new(build_window()) .title(TITLE) @@ -75,34 +77,42 @@ fn build_top_bar() -> impl Widget { fn build_mod_list() -> impl Widget { let list = List::new(|| { + let checkbox = + Checkbox::new("").lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::enabled)); + let name = Label::raw().lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::name)); + Flex::row() .must_fill_main_axis(true) - .with_child( - Label::dynamic(|enabled, _env| { - if *enabled { - "Enabled".into() - } else { - "Disabled".into() - } - }) - .lens(lens!((usize, ModInfo), 1).then(ModInfo::enabled)), - ) - .with_child(Label::raw().lens(lens!((usize, ModInfo), 1).then(ModInfo::name))) - .on_click(|ctx, (i, _info), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i))) + .with_child(checkbox) + .with_child(name) + .padding((5.0, 4.0)) + .background(KEY_MOD_LIST_ITEM_BG_COLOR) + .on_click(|ctx, (i, _, _), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i))) + .env_scope(|env, (i, _, selected)| { + if *selected { + env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::NAVY); + } else if (i % 2) == 1 { + env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::WHITE.with_alpha(0.05)); + } else { + env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::TRANSPARENT); + } + }) }); let scroll = Scroll::new(list) .vertical() - .lens(State::mods.map( - |mods| { - mods.iter() + .lens(lens::Identity.map( + |state: &State| { + state + .mods + .iter() .enumerate() - .map(|(i, val)| (i, val.clone())) + .map(|(i, val)| (i, val.clone(), Some(i) == state.selected_mod_index)) .collect::>() }, - |mods, infos| { - infos.into_iter().for_each(|(i, info)| { - mods.set(i, info); + |state, infos| { + infos.into_iter().for_each(|(i, info, _)| { + state.mods.set(i, info); }); }, )) -- 2.45.3 From 91020c02e1fb0b93417bcffaa3e0ae0cc61d484f Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 15:39:54 +0100 Subject: [PATCH 70/90] feat(dtmm): Improve mod details pane UI Closes: #14. --- crates/dtmm/src/ui/window/main.rs | 66 ++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index d7ee41e..6b98d85 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -5,7 +5,7 @@ use druid::widget::{ }; use druid::{ lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, Key, LensExt, - SingleUse, Widget, WidgetExt, WindowDesc, + SingleUse, TextAlignment, Widget, WidgetExt, WindowDesc, }; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; @@ -18,8 +18,8 @@ use crate::ui::widget::controller::AutoScrollController; use crate::ui::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; -const WINDOW_SIZE: (f64, f64) = (800.0, 600.0); -const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; +const WINDOW_SIZE: (f64, f64) = (1080., 720.); +const MOD_DETAILS_MIN_WIDTH: f64 = 325.; const KEY_MOD_LIST_ITEM_BG_COLOR: Key = Key::new("dtmm.mod-list.item.background-color"); @@ -124,19 +124,7 @@ fn build_mod_list() -> impl Widget { .with_flex_child(scroll, 1.0) } -fn build_mod_details() -> impl Widget { - let details_container = Maybe::new( - || { - Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(Label::raw().lens(ModInfo::name)) - .with_flex_child(Label::raw().lens(ModInfo::description), 1.0) - }, - Flex::column, - ) - .padding(Insets::uniform_xy(5.0, 1.0)) - .lens(State::selected_mod); - +fn build_mod_details_buttons() -> impl Widget { let button_move_up = Button::new("Move Up") .on_click(|ctx, _state, _env| ctx.submit_command(ACTION_SELECTED_MOD_UP)) .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_up()); @@ -187,15 +175,16 @@ fn build_mod_details() -> impl Widget { .disabled_if(|info: &Option, _env: &druid::Env| info.is_none()) .lens(State::selected_mod); - let buttons = Flex::column() + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Flex::row() .main_axis_alignment(MainAxisAlignment::End) .with_child(button_move_up) .with_default_spacer() - .with_child(button_move_down) - .padding(Insets::uniform_xy(5.0, 2.0)), + .with_child(button_move_down), ) + .with_default_spacer() .with_child( Flex::row() .main_axis_alignment(MainAxisAlignment::End) @@ -203,16 +192,45 @@ fn build_mod_details() -> impl Widget { .with_default_spacer() .with_child(button_add_mod) .with_default_spacer() - .with_child(button_delete_mod) - .padding(Insets::uniform_xy(5.0, 2.0)), + .with_child(button_delete_mod), ) - .with_default_spacer(); + .expand_width() +} +fn build_mod_details_info() -> impl Widget { + Maybe::new( + || { + let name = Label::raw() + .with_text_alignment(TextAlignment::Center) + .with_text_size(24.) + // Force the label to take up the entire details' pane width, + // so that we can center-align it. + .expand_width() + .lens(ModInfo::name); + let description = Label::raw() + .with_line_break_mode(LineBreaking::WordWrap) + .lens(ModInfo::description); + + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(name) + .with_spacer(4.) + .with_child(description) + }, + Flex::column, + ) + .padding((4., 4.)) + .lens(State::selected_mod) +} + +fn build_mod_details() -> impl Widget { Flex::column() .must_fill_main_axis(true) + .cross_axis_alignment(CrossAxisAlignment::Start) .main_axis_alignment(MainAxisAlignment::SpaceBetween) - .with_flex_child(details_container, 1.0) - .with_child(buttons) + .with_flex_child(build_mod_details_info(), 1.0) + .with_child(build_mod_details_buttons().padding(4.)) } fn build_view_mods() -> impl Widget { -- 2.45.3 From a1a7e9a26ea14f7a86aec368f33730394d19152b Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 16:09:47 +0100 Subject: [PATCH 71/90] fix(dtmm): Fix disabled state of deployment buttons --- crates/dtmm/src/ui/window/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 6b98d85..d42143c 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -58,7 +58,9 @@ fn build_top_bar() -> impl Widget { .on_click(|ctx, _state: &mut State, _env| { ctx.submit_command(ACTION_START_DEPLOY); }) - .disabled_if(|data, _| !data.is_deployment_in_progress), + .disabled_if(|data, _| { + data.is_deployment_in_progress || data.is_reset_in_progress + }), ) .with_default_spacer() .with_child( @@ -66,7 +68,9 @@ fn build_top_bar() -> impl Widget { .on_click(|ctx, _state: &mut State, _env| { ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); }) - .disabled_if(|data, _| !data.is_reset_in_progress), + .disabled_if(|data, _| { + data.is_deployment_in_progress || data.is_reset_in_progress + }), ), ) .padding(theme::TOP_BAR_INSETS) -- 2.45.3 From 783e0b8de17c1914ca9d695f810e8b16861ad699 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 18:29:13 +0100 Subject: [PATCH 72/90] feat(dtmm): Remove about tab There's not much info to show here, really. Ref: #16. --- crates/dtmm/src/state/data.rs | 1 - crates/dtmm/src/ui/window/main.rs | 24 ++++-------------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index b016b5a..17916a5 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -11,7 +11,6 @@ use super::SelectedModLens; pub(crate) enum View { Mods, Settings, - About, } impl Default for View { diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index d42143c..5470342 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -1,7 +1,7 @@ use druid::im::Vector; use druid::widget::{ - Align, Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, - MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, + Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, + Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, Key, LensExt, @@ -44,11 +44,6 @@ fn build_top_bar() -> impl Widget { Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { state.current_view = View::Settings; }), - ) - .with_default_spacer() - .with_child( - Button::new("About") - .on_click(|_ctx, state: &mut State, _env| state.current_view = View::About), ), ) .with_child( @@ -272,23 +267,12 @@ fn build_view_settings() -> impl Widget { .padding(Insets::uniform(5.0)) } -fn build_view_about() -> impl Widget { - Align::centered( - Flex::column() - .with_child(Label::new("Darktide Mod Manager")) - .with_child(Label::new( - "Website: https://git.sclu1034.dev/bitsquid_dt/dtmt", - )), - ) -} - fn build_main() -> impl Widget { ViewSwitcher::new( - |state: &State, _env| state.current_view, - |selector, _state, _env| match selector { + |state: &State, _| state.current_view, + |selector, _, _| match selector { View::Mods => Box::new(build_view_mods()), View::Settings => Box::new(build_view_settings()), - View::About => Box::new(build_view_about()), }, ) } -- 2.45.3 From 02fd4009124389b17e4b4403936e732f1e8419ed Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 19:24:19 +0100 Subject: [PATCH 73/90] feat(dtmm): Increase textbox size in settings view Ref: #16. --- crates/dtmm/src/ui/widget/mod.rs | 9 +--- crates/dtmm/src/ui/window/main.rs | 82 ++++++++++++++++++------------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/crates/dtmm/src/ui/widget/mod.rs b/crates/dtmm/src/ui/widget/mod.rs index 8561c31..84a57a2 100644 --- a/crates/dtmm/src/ui/widget/mod.rs +++ b/crates/dtmm/src/ui/widget/mod.rs @@ -1,15 +1,8 @@ use druid::{Data, Widget}; -use self::fill_container::FillContainer; - pub mod container; pub mod controller; -pub mod fill_container; -pub trait ExtraWidgetExt: Widget + Sized + 'static { - fn content_must_fill(self) -> FillContainer { - FillContainer::new(self) - } -} +pub trait ExtraWidgetExt: Widget + Sized + 'static {} impl + 'static> ExtraWidgetExt for W {} diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 5470342..c9815a1 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -4,8 +4,8 @@ use druid::widget::{ Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ - lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, Key, LensExt, - SingleUse, TextAlignment, Widget, WidgetExt, WindowDesc, + lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, + TextAlignment, Widget, WidgetExt, WindowDesc, }; use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; @@ -15,7 +15,6 @@ use crate::state::{ }; use crate::ui::theme; use crate::ui::widget::controller::AutoScrollController; -use crate::ui::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; const WINDOW_SIZE: (f64, f64) = (1080., 720.); @@ -98,24 +97,21 @@ fn build_mod_list() -> impl Widget { }) }); - let scroll = Scroll::new(list) - .vertical() - .lens(lens::Identity.map( - |state: &State| { - state - .mods - .iter() - .enumerate() - .map(|(i, val)| (i, val.clone(), Some(i) == state.selected_mod_index)) - .collect::>() - }, - |state, infos| { - infos.into_iter().for_each(|(i, info, _)| { - state.mods.set(i, info); - }); - }, - )) - .content_must_fill(); + let scroll = Scroll::new(list).vertical().lens(lens::Identity.map( + |state: &State| { + state + .mods + .iter() + .enumerate() + .map(|(i, val)| (i, val.clone(), Some(i) == state.selected_mod_index)) + .collect::>() + }, + |state, infos| { + infos.into_iter().for_each(|(i, info, _)| { + state.mods.set(i, info); + }); + }, + )); Flex::column() .must_fill_main_axis(true) @@ -242,29 +238,45 @@ fn build_view_mods() -> impl Widget { } fn build_view_settings() -> impl Widget { - let game_dir_setting = Flex::row() - .main_axis_alignment(MainAxisAlignment::Start) - .with_child(Label::new("Game Directory:")) - .with_default_spacer() - .with_child( - TextBox::new() - .with_formatter(PathBufFormatter::new()) - .lens(State::game_dir), - ); let data_dir_setting = Flex::row() + .must_fill_main_axis(true) .main_axis_alignment(MainAxisAlignment::Start) .with_child(Label::new("Data Directory:")) .with_default_spacer() - .with_child( + .with_flex_child( TextBox::new() .with_formatter(PathBufFormatter::new()) + .expand_width() .lens(State::data_dir), - ); + 1., + ) + .expand_width(); - Flex::column() + let game_dir_setting = Flex::row() + .must_fill_main_axis(true) + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(Label::new("Game Directory:")) + .with_default_spacer() + .with_flex_child( + TextBox::new() + .with_formatter(PathBufFormatter::new()) + .expand_width() + .lens(State::game_dir), + 1., + ) + .expand_width(); + + let content = Flex::column() + .must_fill_main_axis(true) + .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(data_dir_setting) - .with_child(game_dir_setting) - .padding(Insets::uniform(5.0)) + .with_default_spacer() + .with_child(game_dir_setting); + + SizedBox::new(content) + .width(800.) + .expand_height() + .padding(5.) } fn build_main() -> impl Widget { -- 2.45.3 From d5687ccae4326fc0b657aa3bcc510783fbd4e2a5 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 19:41:38 +0100 Subject: [PATCH 74/90] feat(dtmm): Add padding to log view --- crates/dtmm/src/ui/window/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index c9815a1..fc64315 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -295,6 +295,7 @@ fn build_log_view() -> impl Widget { .with_font(font) .with_line_break_mode(LineBreaking::WordWrap) .lens(State::log) + .padding(4.) .scroll() .vertical() .controller(AutoScrollController); -- 2.45.3 From 7c7b9b5890a66fa4041f13c0b179099e39dfa3dc Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 21:18:32 +0100 Subject: [PATCH 75/90] fix(dtmm): Fix resetting mods --- crates/dtmm/src/controller/engine.rs | 40 +++++++++++++++++++--------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/crates/dtmm/src/controller/engine.rs b/crates/dtmm/src/controller/engine.rs index 9956df8..4769223 100644 --- a/crates/dtmm/src/controller/engine.rs +++ b/crates/dtmm/src/controller/engine.rs @@ -536,7 +536,8 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { #[tracing::instrument(skip(state))] pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { - let paths = [BUNDLE_DATABASE_NAME, BOOT_BUNDLE_NAME]; + let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); + let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path]; let bundle_dir = state.game_dir.join("bundle"); tracing::info!("Resetting mod deployment in {}", bundle_dir.display()); @@ -545,23 +546,36 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { let path = bundle_dir.join(p); let backup = bundle_dir.join(&format!("{}.bak", p)); - tracing::debug!( - "Copying from backup: {} -> {}", - backup.display(), - path.display() - ); + let res = async { + tracing::debug!( + "Copying from backup: {} -> {}", + backup.display(), + path.display() + ); - fs::copy(&backup, &path) - .await - .wrap_err_with(|| format!("failed to '{}' restore from backup", p))?; + fs::copy(&backup, &path) + .await + .wrap_err_with(|| format!("failed to copy from '{}'", backup.display()))?; - tracing::debug!("Deleting backup: {}", backup.display(),); + tracing::debug!("Deleting backup: {}", backup.display()); - fs::remove_file(&backup) - .await - .wrap_err_with(|| format!("failed to remove backup '{}'", p))?; + fs::remove_file(&backup) + .await + .wrap_err_with(|| format!("failed to remove '{}'", backup.display())) + } + .await; + + if let Err(err) = res { + tracing::error!( + "Failed to restore '{}' from backup. You may need to verify game files. Error: {:?}", + &p, + err + ); + } } + tracing::info!("Reset finished"); + Ok(()) } -- 2.45.3 From 464face3a81d2491fb725447d14a33f4966ccd9d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 21:28:14 +0100 Subject: [PATCH 76/90] fix(dtmm): Reset settings file --- crates/dtmm/src/controller/engine.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/dtmm/src/controller/engine.rs b/crates/dtmm/src/controller/engine.rs index 4769223..c89eedf 100644 --- a/crates/dtmm/src/controller/engine.rs +++ b/crates/dtmm/src/controller/engine.rs @@ -30,6 +30,7 @@ const DML_BUNDLE_NAME: &str = "packages/dml"; const BUNDLE_DATABASE_NAME: &str = "bundle_database.data"; const MOD_BOOT_SCRIPT: &str = "scripts/mod_main"; const MOD_DATA_SCRIPT: &str = "scripts/mods/mod_data"; +const SETTINGS_FILE_PATH: &str = "application_settings/settings_common.ini"; #[tracing::instrument] async fn read_file_with_backup

(path: P) -> Result> @@ -92,9 +93,7 @@ where #[tracing::instrument(skip_all)] async fn patch_game_settings(state: Arc) -> Result<()> { - let settings_path = state - .game_dir - .join("bundle/application_settings/settings_common.ini"); + let settings_path = state.game_dir.join("bundle").join(SETTINGS_FILE_PATH); let settings = read_file_with_backup(&settings_path) .await @@ -537,7 +536,7 @@ pub(crate) async fn deploy_mods(state: State) -> Result<()> { #[tracing::instrument(skip(state))] pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { let boot_bundle_path = format!("{:016x}", Murmur64::hash(BOOT_BUNDLE_NAME.as_bytes())); - let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path]; + let paths = [BUNDLE_DATABASE_NAME, &boot_bundle_path, SETTINGS_FILE_PATH]; let bundle_dir = state.game_dir.join("bundle"); tracing::info!("Resetting mod deployment in {}", bundle_dir.display()); -- 2.45.3 From e2043aa502327929373dc310e8523988c72411a4 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 23:30:27 +0100 Subject: [PATCH 77/90] fix(dtmm): Fix incorrect DMF resource names --- crates/dtmm/src/controller/engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/dtmm/src/controller/engine.rs b/crates/dtmm/src/controller/engine.rs index c89eedf..7d202e2 100644 --- a/crates/dtmm/src/controller/engine.rs +++ b/crates/dtmm/src/controller/engine.rs @@ -156,16 +156,16 @@ fn build_mod_data_lua(state: Arc) -> String { if resources.data.is_some() || resources.localization.is_some() { lua.push_str(" new_mod(\""); lua.push_str(&mod_info.id); - lua.push_str("\", {\n init = \""); + lua.push_str("\", {\n mod_script = \""); lua.push_str(&resources.init.to_string_lossy()); if let Some(data) = resources.data.as_ref() { - lua.push_str("\",\n data = \""); + lua.push_str("\",\n mod_data = \""); lua.push_str(&data.to_string_lossy()); } if let Some(localization) = &resources.localization { - lua.push_str("\",\n localization = \""); + lua.push_str("\",\n mod_localization = \""); lua.push_str(&localization.to_string_lossy()); } -- 2.45.3 From f0450285ad3dffe00ce1b9a646c0d34192d3c87d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 28 Feb 2023 23:31:00 +0100 Subject: [PATCH 78/90] fix(dtmm): Fix deleting mods preventing deployment --- crates/dtmm/src/state/delegate.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 9a2a1ef..4f44a68 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -134,10 +134,8 @@ impl AppDelegate for Delegate { if self .sender .send(AsyncAction::DeleteMod((state.clone(), info))) - .is_ok() + .is_err() { - state.is_deployment_in_progress = true; - } else { tracing::error!("Failed to queue action to deploy mods"); } @@ -160,11 +158,12 @@ impl AppDelegate for Delegate { let info = cmd .get(ACTION_ADD_MOD) .expect("command type matched but didn't contain the expected value"); - if let Err(err) = self + if self .sender .send(AsyncAction::AddMod((state.clone(), info.clone()))) + .is_err() { - tracing::error!("Failed to add mod: {}", err); + tracing::error!("Failed to queue action to add mod"); } Handled::Yes } -- 2.45.3 From be1cff9f3cbc5eaf4be36e991ed5afb919526b50 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 00:20:45 +0100 Subject: [PATCH 79/90] feat(dtmm): Move class and require hooks into early loading These need to be executed as early as possible if they're supposed to capture all of their respective calls. --- crates/dtmm/assets/mod_main.lua | 226 +++++++++++++++++++++----------- 1 file changed, 147 insertions(+), 79 deletions(-) diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 285d15e..715397f 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -1,3 +1,7 @@ +local _G = _G +local rawget = rawget +local rawset = rawset + local log = function(category, format, ...) local Log = rawget(_G, "Log") if Log then @@ -7,87 +11,69 @@ local log = function(category, format, ...) end end -log("mod_main", " Initializing mods...") --- Keep a backup of certain system libraries before --- Fatshark's code scrubs them. --- The loader can then decide to pass them on to mods, or ignore them -local libs = { - io = io, - debug = debug, - ffi = ffi, - os = os, - load = load, - loadfile = loadfile, - loadstring = loadstring, -} - -require("scripts/main") -log("mod_main", "'scripts/main' loaded") - -local StateBootSubStateBase = require("scripts/game_states/boot/state_boot_sub_state_base") - --- A necessary override. --- The original does not proxy `dt` to `_state_update`, but we need that. -StateBootSubStateBase.update = function (self, dt) - local done, error = self:_state_update(dt) - local params = self._params - - if error then - return StateError, { error } - elseif done then - local next_index = params.sub_state_index + 1 - params.sub_state_index = next_index - local next_state_data = params.states[next_index] - - if next_state_data then - return next_state_data[1], self._params - else - self._parent:sub_states_done() - end - end -end - -local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") - -StateBootLoadMods.on_enter = function (self, parent, params) - log("StateBootLoadMods", "Entered") - StateBootLoadMods.super.on_enter(self, parent, params) - - local state_params = self:_state_params() - local package_manager = state_params.package_manager - - self._state = "load_package" - self._package_manager = package_manager - self._package_handles = { - ["packages/mods"] = package_manager:load("packages/mods", "StateBootLoadMods", nil), - ["packages/dml"] = package_manager:load("packages/dml", "StateBootLoadMods", nil), - } -end - -StateBootLoadMods._state_update = function (self, dt) - local state = self._state - local package_manager = self._package_manager - - if state == "load_package" and package_manager:update() then - log("StateBootLoadMods", "Packages loaded, loading mods") - self._state = "load_mods" - local mod_loader = require("scripts/mods/dml/init") - self._mod_loader = mod_loader - - local mod_data = require("scripts/mods/mod_data") - mod_loader:init(mod_data, libs, self._parent:gui()) - elseif state == "load_mods" and self._mod_loader:update(dt) then - log("StateBootLoadMods", "Mods loaded, exiting") - return true, false - end - - return false, false -end - -- Patch `GameStateMachine.init` to add our own state for loading mods. -- In the future, Fatshark might provide us with a dedicated way to do this. local function patch_mod_loading_state() - log("mod_main", "Adding mod loading state") + local StateBootSubStateBase = require("scripts/game_states/boot/state_boot_sub_state_base") + + -- A necessary override. + -- The original does not proxy `dt` to `_state_update`, but we need that. + StateBootSubStateBase.update = function (self, dt) + local done, error = self:_state_update(dt) + local params = self._params + + if error then + return StateError, { error } + elseif done then + local next_index = params.sub_state_index + 1 + params.sub_state_index = next_index + local next_state_data = params.states[next_index] + + if next_state_data then + return next_state_data[1], self._params + else + self._parent:sub_states_done() + end + end + end + + local StateBootLoadMods = class("StateBootLoadMods", "StateBootSubStateBase") + + StateBootLoadMods.on_enter = function (self, parent, params) + log("StateBootLoadMods", "Entered") + StateBootLoadMods.super.on_enter(self, parent, params) + + local state_params = self:_state_params() + local package_manager = state_params.package_manager + + self._state = "load_package" + self._package_manager = package_manager + self._package_handles = { + ["packages/mods"] = package_manager:load("packages/mods", "StateBootLoadMods", nil), + ["packages/dml"] = package_manager:load("packages/dml", "StateBootLoadMods", nil), + } + end + + StateBootLoadMods._state_update = function (self, dt) + local state = self._state + local package_manager = self._package_manager + + if state == "load_package" and package_manager:update() then + log("StateBootLoadMods", "Packages loaded, loading mods") + self._state = "load_mods" + local mod_loader = require("scripts/mods/dml/init") + self._mod_loader = mod_loader + + local mod_data = require("scripts/mods/mod_data") + mod_loader:init(mod_data, self._parent:gui()) + elseif state == "load_mods" and self._mod_loader:update(dt) then + log("StateBootLoadMods", "Mods loaded, exiting") + return true, false + end + + return false, false + end + local GameStateMachine = require("scripts/foundation/utilities/game_state_machine") local patched = false @@ -95,6 +81,7 @@ local function patch_mod_loading_state() local GameStateMachine_init = GameStateMachine.init GameStateMachine.init = function(self, parent, start_state, params, ...) if not patched then + log("mod_main", "Injecting mod loading state") patched = true -- Hardcoded position after `StateRequireScripts`. @@ -112,13 +99,94 @@ local function patch_mod_loading_state() GameStateMachine_init(self, parent, start_state, params, ...) end + log("mod_main", "Mod patching complete") end +log("mod_main", "Initializing mods...") + +local require_store = {} + +Mods = { + -- Keep a backup of certain system libraries before + -- Fatshark's code scrubs them. + -- The loader can then decide to pass them on to mods, or ignore them + lua = setmetatable({}, { + io = io, + debug = debug, + ffi = ffi, + os = os, + load = load, + loadfile = loadfile, + loadstring = loadstring, + }), + require_store = require_store +} + +local can_insert = function(filepath, new_result) + local store = require_store[filepath] + if not store or #store then + return true + end + + if store[#store] ~= new_result then + return true + end +end + +local original_require = require +require = function(filepath, ...) + local result = original_require(filepath, ...) + if result and type(result) == "table" then + if can_insert(filepath, result) then + require_store[filepath] = require_store[filepath] or {} + local store = require_store[filepath] + + table.insert(store, result) + + if Mods.hook then + Mods.hook.enable_by_file(filepath, #store) + end + end + end + + return result +end + +require("scripts/boot_init") +require("scripts/foundation/utilities/class") + +-- The `__index` metamethod maps a proper identifier `CLASS.MyClassName` to the +-- stringified version of the key: `"MyClassName"`. +-- This allows using LuaCheck for the stringified class names in hook parameters. +_G.CLASS = setmetatable({}, { + __index = function(_, key) + return key + end +}) + +local original_class = class +class = function(class_name, super_name, ...) + local result = original_class(class_name, super_name, ...) + if not rawget(_G, class_name) then + rawset(_G, class_name, result) + end + if not rawget(_G.CLASS, class_name) then + rawset(_G.CLASS, class_name, result) + end + return result +end + +require("scripts/main") +log("mod_main", "'scripts/main' loaded") + +-- Override `init` to run our injection function init() + patch_mod_loading_state() + + -- As requested by Fatshark local StateRequireScripts = require("scripts/game_states/boot/state_require_scripts") StateRequireScripts._get_is_modded = function() return true end - patch_mod_loading_state() Main:init() end -- 2.45.3 From 55335c0fdc2bad2ea22c4bb8b0cf1a65f1ceac14 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 10:40:34 +0100 Subject: [PATCH 80/90] refactor(dtmm): Split controller files --- crates/dtmm/src/controller/app.rs | 105 ++++++++++++++++++ .../src/controller/{engine.rs => game.rs} | 102 +---------------- crates/dtmm/src/controller/worker.rs | 9 +- crates/dtmm/src/main.rs | 3 +- 4 files changed, 116 insertions(+), 103 deletions(-) create mode 100644 crates/dtmm/src/controller/app.rs rename crates/dtmm/src/controller/{engine.rs => game.rs} (85%) diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs new file mode 100644 index 0000000..01dc22c --- /dev/null +++ b/crates/dtmm/src/controller/app.rs @@ -0,0 +1,105 @@ +use std::collections::HashMap; +use std::io::{Cursor, Read}; + +use color_eyre::eyre::{self, Context}; +use color_eyre::{Help, Result}; +use druid::FileInfo; +use dtmt_shared::ModConfig; +use tokio::fs; +use zip::ZipArchive; + +use crate::state::{ModInfo, PackageInfo, State}; + +#[tracing::instrument(skip(state))] +pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { + let data = fs::read(&info.path) + .await + .wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; + let data = Cursor::new(data); + + let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?; + + if tracing::enabled!(tracing::Level::DEBUG) { + let names = archive.file_names().fold(String::new(), |mut s, name| { + s.push('\n'); + s.push_str(name); + s + }); + tracing::debug!("Archive contents:{}", names); + } + + let dir_name = { + let f = archive.by_index(0).wrap_err("archive is empty")?; + + if !f.is_dir() { + let err = eyre::eyre!("archive does not have a top-level directory"); + return Err(err).with_suggestion(|| "Use 'dtmt build' to create the mod archive."); + } + + let name = f.name(); + // The directory name is returned with a trailing slash, which we don't want + name[..(name.len().saturating_sub(1))].to_string() + }; + + tracing::info!("Importing mod {}", dir_name); + + let mod_cfg: ModConfig = { + let mut f = archive + .by_name(&format!("{}/{}", dir_name, "dtmt.cfg")) + .wrap_err("failed to read mod config from archive")?; + let mut buf = Vec::with_capacity(f.size() as usize); + f.read_to_end(&mut buf) + .wrap_err("failed to read mod config from archive")?; + + let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?; + + serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")? + }; + + tracing::debug!(?mod_cfg); + + let files: HashMap> = { + let mut f = archive + .by_name(&format!("{}/{}", dir_name, "files.sjson")) + .wrap_err("failed to read file index from archive")?; + let mut buf = Vec::with_capacity(f.size() as usize); + f.read_to_end(&mut buf) + .wrap_err("failed to read file index from archive")?; + + let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?; + + serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")? + }; + + tracing::trace!(?files); + + let mod_dir = state.get_mod_dir(); + + tracing::trace!("Creating mods directory {}", mod_dir.display()); + fs::create_dir_all(&mod_dir) + .await + .wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; + + tracing::trace!("Extracting mod archive to {}", mod_dir.display()); + archive + .extract(&mod_dir) + .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; + + let packages = files + .into_iter() + .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) + .collect(); + let info = ModInfo::new(mod_cfg, packages); + + Ok(info) +} + +#[tracing::instrument(skip(state))] +pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { + let mod_dir = state.get_mod_dir().join(&info.id); + fs::remove_dir_all(&mod_dir) + .await + .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; + + Ok(()) +} diff --git a/crates/dtmm/src/controller/engine.rs b/crates/dtmm/src/controller/game.rs similarity index 85% rename from crates/dtmm/src/controller/engine.rs rename to crates/dtmm/src/controller/game.rs index 7d202e2..508e84a 100644 --- a/crates/dtmm/src/controller/engine.rs +++ b/crates/dtmm/src/controller/game.rs @@ -1,14 +1,11 @@ -use std::collections::HashMap; use std::ffi::CString; -use std::io::{Cursor, ErrorKind, Read}; +use std::io::{Cursor, ErrorKind}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use color_eyre::eyre::Context; use color_eyre::{eyre, Help, Result}; -use druid::FileInfo; -use dtmt_shared::ModConfig; use futures::stream; use futures::StreamExt; use sdk::filetype::lua; @@ -20,9 +17,8 @@ use sdk::{ use tokio::fs; use tokio::io::AsyncWriteExt; use tracing::Instrument; -use zip::ZipArchive; -use crate::state::{ModInfo, PackageInfo, State}; +use crate::state::{PackageInfo, State}; const MOD_BUNDLE_NAME: &str = "packages/mods"; const BOOT_BUNDLE_NAME: &str = "packages/boot"; @@ -577,97 +573,3 @@ pub(crate) async fn reset_mod_deployment(state: State) -> Result<()> { Ok(()) } - -#[tracing::instrument(skip(state))] -pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { - let data = fs::read(&info.path) - .await - .wrap_err_with(|| format!("failed to read file {}", info.path.display()))?; - let data = Cursor::new(data); - - let mut archive = ZipArchive::new(data).wrap_err("failed to open ZIP archive")?; - - if tracing::enabled!(tracing::Level::DEBUG) { - let names = archive.file_names().fold(String::new(), |mut s, name| { - s.push('\n'); - s.push_str(name); - s - }); - tracing::debug!("Archive contents:{}", names); - } - - let dir_name = { - let f = archive.by_index(0).wrap_err("archive is empty")?; - - if !f.is_dir() { - let err = eyre::eyre!("archive does not have a top-level directory"); - return Err(err).with_suggestion(|| "Use 'dtmt build' to create the mod archive."); - } - - let name = f.name(); - // The directory name is returned with a trailing slash, which we don't want - name[..(name.len().saturating_sub(1))].to_string() - }; - - tracing::info!("Importing mod {}", dir_name); - - let mod_cfg: ModConfig = { - let mut f = archive - .by_name(&format!("{}/{}", dir_name, "dtmt.cfg")) - .wrap_err("failed to read mod config from archive")?; - let mut buf = Vec::with_capacity(f.size() as usize); - f.read_to_end(&mut buf) - .wrap_err("failed to read mod config from archive")?; - - let data = String::from_utf8(buf).wrap_err("mod config is not valid UTF-8")?; - - serde_sjson::from_str(&data).wrap_err("failed to deserialize mod config")? - }; - - tracing::debug!(?mod_cfg); - - let files: HashMap> = { - let mut f = archive - .by_name(&format!("{}/{}", dir_name, "files.sjson")) - .wrap_err("failed to read file index from archive")?; - let mut buf = Vec::with_capacity(f.size() as usize); - f.read_to_end(&mut buf) - .wrap_err("failed to read file index from archive")?; - - let data = String::from_utf8(buf).wrap_err("file index is not valid UTF-8")?; - - serde_sjson::from_str(&data).wrap_err("failed to deserialize file index")? - }; - - tracing::trace!(?files); - - let mod_dir = state.get_mod_dir(); - - tracing::trace!("Creating mods directory {}", mod_dir.display()); - fs::create_dir_all(&mod_dir) - .await - .wrap_err_with(|| format!("failed to create data directory {}", mod_dir.display()))?; - - tracing::trace!("Extracting mod archive to {}", mod_dir.display()); - archive - .extract(&mod_dir) - .wrap_err_with(|| format!("failed to extract archive to {}", mod_dir.display()))?; - - let packages = files - .into_iter() - .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) - .collect(); - let info = ModInfo::new(mod_cfg, packages); - - Ok(info) -} - -#[tracing::instrument(skip(state))] -pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { - let mod_dir = state.get_mod_dir().join(&info.id); - fs::remove_dir_all(&mod_dir) - .await - .wrap_err_with(|| format!("failed to remove directory {}", mod_dir.display()))?; - - Ok(()) -} diff --git a/crates/dtmm/src/controller/worker.rs b/crates/dtmm/src/controller/worker.rs index 58b3827..1c11b2a 100644 --- a/crates/dtmm/src/controller/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -6,8 +6,13 @@ use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::RwLock; -use crate::controller::engine::*; -use crate::state::*; +use crate::controller::app::*; +use crate::controller::game::*; +use crate::state::AsyncAction; +use crate::state::{ + ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY, + ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG, +}; async fn handle_action( event_sink: Arc>, diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index bf38c65..885cfcf 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -16,7 +16,8 @@ use crate::controller::worker::work_thread; use crate::state::{Delegate, State}; mod controller { - pub mod engine; + pub mod app; + pub mod game; pub mod worker; } mod state; -- 2.45.3 From e6c9fe834c73ab70a68fda83169c008e1dce6add Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 14:13:11 +0100 Subject: [PATCH 81/90] feat(dtmm): Save settings to config file Closes #18. --- crates/dtmm/src/controller/app.rs | 21 ++++++++++++++ crates/dtmm/src/controller/worker.rs | 12 ++++++++ crates/dtmm/src/state/data.rs | 17 +++++++++-- crates/dtmm/src/state/delegate.rs | 40 +++++++++++++++++++++++++- crates/dtmm/src/state/util.rs | 18 ++++++++++++ crates/dtmm/src/ui/window/main.rs | 11 ++++++- crates/dtmm/src/util/config.rs | 43 +++++++++++++--------------- 7 files changed, 134 insertions(+), 28 deletions(-) diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index 01dc22c..31a79e2 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -9,6 +9,7 @@ use tokio::fs; use zip::ZipArchive; use crate::state::{ModInfo, PackageInfo, State}; +use crate::util::config::Config; #[tracing::instrument(skip(state))] pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { @@ -103,3 +104,23 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { Ok(()) } + +#[tracing::instrument(skip(state))] +pub(crate) async fn save_settings(state: State) -> Result<()> { + // TODO: Avoid allocations, especially once the config grows, by + // creating a separate struct with only borrowed data to serialize from. + let cfg = Config { + path: state.config_path.as_ref().clone(), + game_dir: Some(state.game_dir.as_ref().clone()), + data_dir: Some(state.data_dir.as_ref().clone()), + }; + + tracing::info!("Saving settings to '{}'", state.config_path.display()); + tracing::debug!(?cfg); + + let data = serde_sjson::to_string(&cfg).wrap_err("failed to serialize config")?; + + fs::write(&cfg.path, &data) + .await + .wrap_err_with(|| format!("failed to write config to '{}'", cfg.path.display())) +} diff --git a/crates/dtmm/src/controller/worker.rs b/crates/dtmm/src/controller/worker.rs index 1c11b2a..80abf53 100644 --- a/crates/dtmm/src/controller/worker.rs +++ b/crates/dtmm/src/controller/worker.rs @@ -9,6 +9,7 @@ use tokio::sync::RwLock; use crate::controller::app::*; use crate::controller::game::*; use crate::state::AsyncAction; +use crate::state::ACTION_FINISH_SAVE_SETTINGS; use crate::state::{ ACTION_FINISH_ADD_MOD, ACTION_FINISH_DELETE_SELECTED_MOD, ACTION_FINISH_DEPLOY, ACTION_FINISH_RESET_DEPLOYMENT, ACTION_LOG, @@ -81,6 +82,17 @@ async fn handle_action( .submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto) .expect("failed to send command"); }), + AsyncAction::SaveSettings(state) => tokio::spawn(async move { + if let Err(err) = save_settings(state).await { + tracing::error!("Failed to save settings: {:?}", err); + } + + event_sink + .write() + .await + .submit_command(ACTION_FINISH_SAVE_SETTINGS, (), Target::Auto) + .expect("failed to send command"); + }), }; } } diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index 17916a5..63bfe41 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -82,10 +82,18 @@ pub(crate) struct State { pub selected_mod_index: Option, pub is_deployment_in_progress: bool, pub is_reset_in_progress: bool, + pub is_save_in_progress: bool, + pub is_next_save_pending: bool, pub game_dir: Arc, pub data_dir: Arc, - pub ctx: Arc, pub log: Arc, + + #[lens(ignore)] + #[data(ignore)] + pub config_path: Arc, + #[lens(ignore)] + #[data(ignore)] + pub ctx: Arc, } impl State { @@ -102,8 +110,11 @@ impl State { selected_mod_index: None, is_deployment_in_progress: false, is_reset_in_progress: false, - game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()), - data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()), + is_save_in_progress: false, + is_next_save_pending: false, + config_path: Arc::new(config.path), + game_dir: Arc::new(config.game_dir.unwrap_or_default()), + data_dir: Arc::new(config.data_dir.unwrap_or_default()), log: Arc::new(String::new()), } } diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 4f44a68..6aab016 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -28,11 +28,17 @@ pub(crate) const ACTION_FINISH_ADD_MOD: Selector> = pub(crate) const ACTION_LOG: Selector> = Selector::new("dtmm.action.log"); +pub(crate) const ACTION_START_SAVE_SETTINGS: Selector = + Selector::new("dtmm.action.start-save-settings"); +pub(crate) const ACTION_FINISH_SAVE_SETTINGS: Selector = + Selector::new("dtmm.action.finish-save-settings"); + pub(crate) enum AsyncAction { DeployMods(State), ResetDeployment(State), AddMod((State, FileInfo)), DeleteMod((State, ModInfo)), + SaveSettings(State), } pub(crate) struct Delegate { @@ -49,12 +55,16 @@ impl AppDelegate for Delegate { #[tracing::instrument(name = "Delegate", skip_all)] fn command( &mut self, - _ctx: &mut DelegateCtx, + ctx: &mut DelegateCtx, _target: Target, cmd: &Command, state: &mut State, _env: &Env, ) -> Handled { + if cfg!(debug_assertions) && !cmd.is(ACTION_LOG) { + tracing::trace!(?cmd); + } + match cmd { cmd if cmd.is(ACTION_START_DEPLOY) => { if self @@ -152,6 +162,8 @@ impl AppDelegate for Delegate { }; state.mods.remove(index); + ctx.submit_command(ACTION_START_SAVE_SETTINGS); + Handled::Yes } cmd if cmd.is(ACTION_ADD_MOD) => { @@ -173,6 +185,7 @@ impl AppDelegate for Delegate { .expect("command type matched but didn't contain the expected value"); if let Some(info) = info.take() { state.add_mod(info); + ctx.submit_command(ACTION_START_SAVE_SETTINGS); } Handled::Yes } @@ -185,6 +198,31 @@ impl AppDelegate for Delegate { } Handled::Yes } + cmd if cmd.is(ACTION_START_SAVE_SETTINGS) => { + if state.is_save_in_progress { + state.is_next_save_pending = true; + } else if self + .sender + .send(AsyncAction::SaveSettings(state.clone())) + .is_ok() + { + state.is_save_in_progress = true; + } else { + tracing::error!("Failed to queue action to save settings"); + } + + Handled::Yes + } + cmd if cmd.is(ACTION_FINISH_SAVE_SETTINGS) => { + state.is_save_in_progress = false; + + if state.is_next_save_pending { + state.is_next_save_pending = false; + ctx.submit_command(ACTION_START_SAVE_SETTINGS); + } + + Handled::Yes + } cmd => { if cfg!(debug_assertions) { tracing::warn!("Unknown command: {:?}", cmd); diff --git a/crates/dtmm/src/state/util.rs b/crates/dtmm/src/state/util.rs index 804b751..1776d19 100644 --- a/crates/dtmm/src/state/util.rs +++ b/crates/dtmm/src/state/util.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use druid::text::Formatter; +use druid::widget::{TextBoxEvent, ValidationDelegate}; +use druid::EventCtx; pub(crate) struct PathBufFormatter; @@ -29,3 +31,19 @@ impl Formatter> for PathBufFormatter { Ok(Arc::new(p)) } } + +pub struct TextBoxOnChanged(F); + +impl TextBoxOnChanged { + pub fn new(f: F) -> Self { + Self(f) + } +} + +impl ValidationDelegate for TextBoxOnChanged { + fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) { + if let TextBoxEvent::Complete = event { + (self.0)(ctx, current_text) + } + } +} diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index fc64315..d6d4f9c 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -8,7 +8,10 @@ use druid::{ TextAlignment, Widget, WidgetExt, WindowDesc, }; -use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT}; +use crate::state::{ + ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_START_RESET_DEPLOYMENT, + ACTION_START_SAVE_SETTINGS, +}; use crate::state::{ ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, @@ -246,6 +249,9 @@ fn build_view_settings() -> impl Widget { .with_flex_child( TextBox::new() .with_formatter(PathBufFormatter::new()) + .delegate(TextBoxOnChanged::new(|ctx, _| { + ctx.submit_command(ACTION_START_SAVE_SETTINGS) + })) .expand_width() .lens(State::data_dir), 1., @@ -260,6 +266,9 @@ fn build_view_settings() -> impl Widget { .with_flex_child( TextBox::new() .with_formatter(PathBufFormatter::new()) + .delegate(TextBoxOnChanged::new(|ctx, _| { + ctx.submit_command(ACTION_START_SAVE_SETTINGS) + })) .expand_width() .lens(State::game_dir), 1., diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 4483c53..0008edf 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -1,6 +1,6 @@ use std::fs; use std::io::ErrorKind; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use clap::{parser::ValueSource, ArgMatches}; use color_eyre::{eyre::Context, Result}; @@ -8,18 +8,10 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub(crate) struct Config { - data_dir: Option, - game_dir: Option, -} - -impl Config { - pub fn game_dir(&self) -> Option<&PathBuf> { - self.game_dir.as_ref() - } - - pub fn data_dir(&self) -> Option<&PathBuf> { - self.data_dir.as_ref() - } + #[serde(skip)] + pub path: PathBuf, + pub data_dir: Option, + pub game_dir: Option, } #[cfg(not(arget_os = "windows"))] @@ -60,22 +52,26 @@ pub fn get_default_data_dir() -> PathBuf { PathBuf::from(data_dir).join("dtmm") } -pub(crate) fn read_config>( - default_config_path: P, - matches: &ArgMatches, -) -> Result { +#[tracing::instrument(skip(matches),fields(path = ?matches.get_one::("config")))] +pub(crate) fn read_config

(default: P, matches: &ArgMatches) -> Result +where + P: Into + std::fmt::Debug, +{ let path = matches .get_one::("config") .expect("argument missing despite default"); - let default_config_path = default_config_path.as_ref(); + let default_path = default.into(); match fs::read(path) { Ok(data) => { let data = String::from_utf8(data).wrap_err_with(|| { format!("config file {} contains invalid UTF-8", path.display()) })?; - serde_sjson::from_str(&data) - .wrap_err_with(|| format!("invalid config file {}", path.display())) + let mut cfg: Config = serde_sjson::from_str(&data) + .wrap_err_with(|| format!("invalid config file {}", path.display()))?; + + cfg.path = path.clone(); + Ok(cfg) } Err(err) if err.kind() == ErrorKind::NotFound => { if matches.value_source("config") != Some(ValueSource::DefaultValue) { @@ -84,7 +80,7 @@ pub(crate) fn read_config>( } { - let parent = default_config_path + let parent = default_path .parent() .expect("a file path always has a parent directory"); fs::create_dir_all(parent).wrap_err_with(|| { @@ -93,6 +89,7 @@ pub(crate) fn read_config>( } let config = Config { + path: default_path, data_dir: Some(get_default_data_dir()), game_dir: None, }; @@ -100,10 +97,10 @@ pub(crate) fn read_config>( { let data = serde_sjson::to_string(&config) .wrap_err("failed to serialize default config value")?; - fs::write(default_config_path, data).wrap_err_with(|| { + fs::write(&config.path, data).wrap_err_with(|| { format!( "failed to write default config to {}", - default_config_path.display() + config.path.display() ) })?; } -- 2.45.3 From 4d632d9119b06d2bdf38bca99b0fe40f5ebe2e4d Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 14:16:36 +0100 Subject: [PATCH 82/90] feat(dtmm): Rename reset button --- crates/dtmm/src/ui/window/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index d6d4f9c..ec93d0d 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -61,7 +61,7 @@ fn build_top_bar() -> impl Widget { ) .with_default_spacer() .with_child( - Button::new("Reset Mods") + Button::new("Reset Game") .on_click(|ctx, _state: &mut State, _env| { ctx.submit_command(ACTION_START_RESET_DEPLOYMENT); }) -- 2.45.3 From 14385d56e1a51beaea3df62a551d4b0ae69ef7c0 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 14:19:53 +0100 Subject: [PATCH 83/90] refactor(dtmm): Reorder imports --- crates/dtmm/src/ui/window/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index ec93d0d..5544367 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -1,21 +1,20 @@ use druid::im::Vector; +use druid::lens; use druid::widget::{ Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher, }; use druid::{ - lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, + Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Key, LensExt, SingleUse, TextAlignment, Widget, WidgetExt, WindowDesc, }; use crate::state::{ - ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_START_RESET_DEPLOYMENT, + ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_ADD_MOD, + ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, + ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT, ACTION_START_SAVE_SETTINGS, }; -use crate::state::{ - ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, - ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, -}; use crate::ui::theme; use crate::ui::widget::controller::AutoScrollController; -- 2.45.3 From 16a785dc5b4db51c444f2960219bde66be7fc22f Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 14:38:04 +0100 Subject: [PATCH 84/90] feat(dtmm): Avoid allocations in settings --- crates/dtmm/src/controller/app.rs | 34 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index 31a79e2..0171ed6 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::io::{Cursor, Read}; +use std::path::Path; use color_eyre::eyre::{self, Context}; use color_eyre::{Help, Result}; @@ -9,7 +10,6 @@ use tokio::fs; use zip::ZipArchive; use crate::state::{ModInfo, PackageInfo, State}; -use crate::util::config::Config; #[tracing::instrument(skip(state))] pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { @@ -105,22 +105,36 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { Ok(()) } +#[derive(Debug, serde::Serialize)] +struct Config<'a> { + game_dir: &'a Path, + data_dir: &'a Path, +} + +impl<'a> From<&'a State> for Config<'a> { + fn from(value: &'a State) -> Self { + Self { + game_dir: &value.game_dir, + data_dir: &value.data_dir, + } + } +} + #[tracing::instrument(skip(state))] pub(crate) async fn save_settings(state: State) -> Result<()> { - // TODO: Avoid allocations, especially once the config grows, by - // creating a separate struct with only borrowed data to serialize from. - let cfg = Config { - path: state.config_path.as_ref().clone(), - game_dir: Some(state.game_dir.as_ref().clone()), - data_dir: Some(state.data_dir.as_ref().clone()), - }; + let cfg = Config::from(&state); tracing::info!("Saving settings to '{}'", state.config_path.display()); tracing::debug!(?cfg); let data = serde_sjson::to_string(&cfg).wrap_err("failed to serialize config")?; - fs::write(&cfg.path, &data) + fs::write(state.config_path.as_ref(), &data) .await - .wrap_err_with(|| format!("failed to write config to '{}'", cfg.path.display())) + .wrap_err_with(|| { + format!( + "failed to write config to '{}'", + state.config_path.display() + ) + }) } -- 2.45.3 From baf4bed3bd11b086ae4d80eb655e34becc5be919 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 19:49:05 +0100 Subject: [PATCH 85/90] chore: Update serde_sjson --- Cargo.lock | 2 +- lib/serde_sjson | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c761a21..1ebc2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2097,7 +2097,7 @@ dependencies = [ [[package]] name = "serde_sjson" -version = "0.2.3" +version = "0.2.4" dependencies = [ "nom", "nom_locate", diff --git a/lib/serde_sjson b/lib/serde_sjson index a6ef5a9..e94218d 160000 --- a/lib/serde_sjson +++ b/lib/serde_sjson @@ -1 +1 @@ -Subproject commit a6ef5a914e15f22d3ebcc475969b65182475139f +Subproject commit e94218d8f52a51529c83af33a99cc17f66caae2e -- 2.45.3 From de072fd0c4d03c949d9a406e50b8a3e7b4351ef6 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 19:51:28 +0100 Subject: [PATCH 86/90] feat(dtmm): Save the mod list Closes #9. --- Cargo.lock | 3 + crates/dtmm/Cargo.toml | 5 +- crates/dtmm/src/controller/app.rs | 109 ++++++++++++++++++++---- crates/dtmm/src/main.rs | 12 ++- crates/dtmm/src/state/data.rs | 10 +-- crates/dtmm/src/state/delegate.rs | 7 +- crates/dtmm/src/state/mod.rs | 2 - crates/dtmm/src/state/util.rs | 49 ----------- crates/dtmm/src/ui/widget/controller.rs | 39 ++++++--- crates/dtmm/src/ui/widget/mod.rs | 49 +++++++++++ crates/dtmm/src/ui/window/main.rs | 17 ++-- crates/dtmm/src/util/config.rs | 49 ++++++++++- 12 files changed, 247 insertions(+), 104 deletions(-) delete mode 100644 crates/dtmm/src/state/util.rs diff --git a/Cargo.lock b/Cargo.lock index 1ebc2cc..d10dfcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,7 @@ dependencies = [ "serde", "serde_sjson", "tokio", + "tokio-stream", "tracing", "tracing-error", "tracing-subscriber", @@ -1261,6 +1262,7 @@ dependencies = [ "bitmaps", "rand_core", "rand_xoshiro", + "serde", "sized-chunks", "typenum", "version_check", @@ -1375,6 +1377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e119590a03caff1f7a582e8ee8c2164ddcc975791701188132fd1d1b518d3871" dependencies = [ "arrayvec", + "serde", ] [[package]] diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 65b1bca..6f42d88 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -10,15 +10,16 @@ bitflags = "1.3.2" clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] } color-eyre = "0.6.2" confy = "0.5.1" -druid = { git = "https://github.com/linebender/druid.git", features = ["im"] } +druid = { git = "https://github.com/linebender/druid.git", features = ["im", "serde"] } dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } futures = "0.3.25" oodle-sys = { path = "../../lib/oodle-sys", version = "*" } sdk = { path = "../../lib/sdk", version = "0.2.0" } serde_sjson = { path = "../../lib/serde_sjson", version = "*" } -serde = { version = "1.0.152", features = ["derive"] } +serde = { version = "1.0.152", features = ["derive", "rc"] } tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] } tracing = "0.1.37" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } zip = "0.6.4" +tokio-stream = { version = "0.1.12", features = ["fs"] } diff --git a/crates/dtmm/src/controller/app.rs b/crates/dtmm/src/controller/app.rs index 0171ed6..d8cb619 100644 --- a/crates/dtmm/src/controller/app.rs +++ b/crates/dtmm/src/controller/app.rs @@ -1,15 +1,21 @@ use std::collections::HashMap; -use std::io::{Cursor, Read}; +use std::io::{Cursor, ErrorKind, Read}; use std::path::Path; use color_eyre::eyre::{self, Context}; use color_eyre::{Help, Result}; +use druid::im::Vector; use druid::FileInfo; use dtmt_shared::ModConfig; -use tokio::fs; +use serde::Deserialize; +use tokio::fs::{self, DirEntry}; +use tokio::runtime::Runtime; +use tokio_stream::wrappers::ReadDirStream; +use tokio_stream::StreamExt; use zip::ZipArchive; use crate::state::{ModInfo, PackageInfo, State}; +use crate::util::config::{ConfigSerialize, LoadOrderEntry}; #[tracing::instrument(skip(state))] pub(crate) async fn import_mod(state: State, info: FileInfo) -> Result { @@ -105,24 +111,9 @@ pub(crate) async fn delete_mod(state: State, info: &ModInfo) -> Result<()> { Ok(()) } -#[derive(Debug, serde::Serialize)] -struct Config<'a> { - game_dir: &'a Path, - data_dir: &'a Path, -} - -impl<'a> From<&'a State> for Config<'a> { - fn from(value: &'a State) -> Self { - Self { - game_dir: &value.game_dir, - data_dir: &value.data_dir, - } - } -} - #[tracing::instrument(skip(state))] pub(crate) async fn save_settings(state: State) -> Result<()> { - let cfg = Config::from(&state); + let cfg = ConfigSerialize::from(&state); tracing::info!("Saving settings to '{}'", state.config_path.display()); tracing::debug!(?cfg); @@ -138,3 +129,85 @@ pub(crate) async fn save_settings(state: State) -> Result<()> { ) }) } + +async fn read_sjson_file(path: P) -> Result +where + T: for<'a> Deserialize<'a>, + P: AsRef + std::fmt::Debug, +{ + let buf = fs::read(path).await.wrap_err("failed to read file")?; + let data = String::from_utf8(buf).wrap_err("invalid UTF8")?; + serde_sjson::from_str(&data).wrap_err("failed to deserialize") +} + +#[tracing::instrument(skip_all,fields( + name = ?res.as_ref().map(|entry| entry.file_name()) +))] +async fn read_mod_dir_entry(res: Result) -> Result { + let entry = res?; + let config_path = entry.path().join("dtmt.cfg"); + let index_path = entry.path().join("files.sjson"); + + let cfg: ModConfig = read_sjson_file(&config_path) + .await + .wrap_err_with(|| format!("failed to read mod config '{}'", config_path.display()))?; + + let files: HashMap> = read_sjson_file(&index_path) + .await + .wrap_err_with(|| format!("failed to read file index '{}'", index_path.display()))?; + + let packages = files + .into_iter() + .map(|(name, files)| PackageInfo::new(name, files.into_iter().collect())) + .collect(); + let info = ModInfo::new(cfg, packages); + Ok(info) +} + +#[tracing::instrument(skip(mod_order))] +pub(crate) fn load_mods<'a, P, S>(mod_dir: P, mod_order: S) -> Result> +where + S: Iterator, + P: AsRef + std::fmt::Debug, +{ + let rt = Runtime::new()?; + + rt.block_on(async move { + let mod_dir = mod_dir.as_ref(); + let read_dir = match fs::read_dir(mod_dir).await { + Ok(read_dir) => read_dir, + Err(err) if err.kind() == ErrorKind::NotFound => { + return Ok(Vector::new()); + } + Err(err) => { + return Err(err) + .wrap_err_with(|| format!("failed to open directory '{}'", mod_dir.display())); + } + }; + + let stream = ReadDirStream::new(read_dir) + .map(|res| res.wrap_err("failed to read dir entry")) + .then(read_mod_dir_entry); + tokio::pin!(stream); + + let mut mods: HashMap = HashMap::new(); + + while let Some(res) = stream.next().await { + let info = res?; + mods.insert(info.id.clone(), info); + } + + let mods = mod_order + .filter_map(|entry| { + if let Some(mut info) = mods.remove(&entry.id) { + info.enabled = entry.enabled; + Some(info) + } else { + None + } + }) + .collect(); + + Ok::<_, color_eyre::Report>(mods) + }) +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index 885cfcf..fdce5ab 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -12,6 +12,7 @@ use color_eyre::{Report, Result}; use druid::AppLauncher; use tokio::sync::RwLock; +use crate::controller::app::load_mods; use crate::controller::worker::work_thread; use crate::state::{Delegate, State}; @@ -62,7 +63,16 @@ fn main() -> Result<()> { let config = util::config::read_config(&default_config_path, &matches) .wrap_err("failed to read config file")?; - let initial_state = State::new(config); + let initial_state = { + let mut state = State::new( + config.path, + config.game_dir.unwrap_or_default(), + config.data_dir.unwrap_or_default(), + ); + state.mods = load_mods(state.get_mod_dir(), config.mod_order.iter()) + .wrap_err("failed to load mods")?; + state + }; let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel(); let delegate = Delegate::new(action_tx); diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index 63bfe41..c8dc3aa 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -3,8 +3,6 @@ use std::{path::PathBuf, sync::Arc}; use druid::{im::Vector, Data, Lens}; use dtmt_shared::ModConfig; -use crate::util::config::Config; - use super::SelectedModLens; #[derive(Copy, Clone, Data, Debug, PartialEq)] @@ -100,7 +98,7 @@ impl State { #[allow(non_upper_case_globals)] pub const selected_mod: SelectedModLens = SelectedModLens; - pub fn new(config: Config) -> Self { + pub fn new(config_path: PathBuf, game_dir: PathBuf, data_dir: PathBuf) -> Self { let ctx = sdk::Context::new(); Self { @@ -112,9 +110,9 @@ impl State { is_reset_in_progress: false, is_save_in_progress: false, is_next_save_pending: false, - config_path: Arc::new(config.path), - game_dir: Arc::new(config.game_dir.unwrap_or_default()), - data_dir: Arc::new(config.data_dir.unwrap_or_default()), + config_path: Arc::new(config_path), + game_dir: Arc::new(game_dir), + data_dir: Arc::new(data_dir), log: Arc::new(String::new()), } } diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 6aab016..08d17b0 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -106,6 +106,7 @@ impl AppDelegate for Delegate { .expect("command type matched but didn't contain the expected value"); state.select_mod(*index); + // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_SELECTED_MOD_UP) => { @@ -120,6 +121,7 @@ impl AppDelegate for Delegate { state.mods.swap(i, i - 1); state.selected_mod_index = Some(i - 1); + // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_SELECTED_MOD_DOWN) => { @@ -134,6 +136,7 @@ impl AppDelegate for Delegate { state.mods.swap(i, i + 1); state.selected_mod_index = Some(i + 1); + // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } cmd if cmd.is(ACTION_START_DELETE_SELECTED_MOD) => { @@ -162,7 +165,7 @@ impl AppDelegate for Delegate { }; state.mods.remove(index); - ctx.submit_command(ACTION_START_SAVE_SETTINGS); + // ctx.submit_command(ACTION_START_SAVE_SETTINGS); Handled::Yes } @@ -185,7 +188,7 @@ impl AppDelegate for Delegate { .expect("command type matched but didn't contain the expected value"); if let Some(info) = info.take() { state.add_mod(info); - ctx.submit_command(ACTION_START_SAVE_SETTINGS); + // ctx.submit_command(ACTION_START_SAVE_SETTINGS); } Handled::Yes } diff --git a/crates/dtmm/src/state/mod.rs b/crates/dtmm/src/state/mod.rs index 1586e3c..f0eb8c3 100644 --- a/crates/dtmm/src/state/mod.rs +++ b/crates/dtmm/src/state/mod.rs @@ -1,9 +1,7 @@ mod data; mod delegate; mod lens; -mod util; pub(crate) use data::*; pub(crate) use delegate::*; pub(crate) use lens::*; -pub(crate) use util::*; diff --git a/crates/dtmm/src/state/util.rs b/crates/dtmm/src/state/util.rs deleted file mode 100644 index 1776d19..0000000 --- a/crates/dtmm/src/state/util.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use druid::text::Formatter; -use druid::widget::{TextBoxEvent, ValidationDelegate}; -use druid::EventCtx; - -pub(crate) struct PathBufFormatter; - -impl PathBufFormatter { - pub fn new() -> Self { - Self {} - } -} - -impl Formatter> for PathBufFormatter { - fn format(&self, value: &Arc) -> String { - value.display().to_string() - } - - fn validate_partial_input( - &self, - _input: &str, - _sel: &druid::text::Selection, - ) -> druid::text::Validation { - druid::text::Validation::success() - } - - fn value(&self, input: &str) -> Result, druid::text::ValidationError> { - let p = PathBuf::from(input); - Ok(Arc::new(p)) - } -} - -pub struct TextBoxOnChanged(F); - -impl TextBoxOnChanged { - pub fn new(f: F) -> Self { - Self(f) - } -} - -impl ValidationDelegate for TextBoxOnChanged { - fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) { - if let TextBoxEvent::Complete = event { - (self.0)(ctx, current_text) - } - } -} diff --git a/crates/dtmm/src/ui/widget/controller.rs b/crates/dtmm/src/ui/widget/controller.rs index f7f71ef..ce18d5b 100644 --- a/crates/dtmm/src/ui/widget/controller.rs +++ b/crates/dtmm/src/ui/widget/controller.rs @@ -1,5 +1,7 @@ use druid::widget::{Button, Controller, Scroll}; -use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, Rect, UpdateCtx, Widget}; +use druid::{Data, Env, Event, EventCtx, Rect, UpdateCtx, Widget}; + +use crate::state::{State, ACTION_START_SAVE_SETTINGS}; pub struct DisabledButtonController; @@ -19,17 +21,6 @@ impl Controller> for DisabledButtonController { child.event(ctx, event, data, env) } - fn lifecycle( - &mut self, - child: &mut Button, - ctx: &mut LifeCycleCtx, - event: &LifeCycle, - data: &T, - env: &Env, - ) { - child.lifecycle(ctx, event, data, env) - } - fn update( &mut self, child: &mut Button, @@ -65,3 +56,27 @@ impl> Controller> for AutoScrollController child.update(ctx, old_data, data, env) } } + +/// A controller that submits the command to save settings every time its widget's +/// data changes. +pub struct SaveSettingsController; + +impl> Controller for SaveSettingsController { + fn update( + &mut self, + child: &mut W, + ctx: &mut UpdateCtx, + old_data: &State, + data: &State, + env: &Env, + ) { + // Only filter for the values that actually go into the settings file. + if old_data.mods != data.mods + || old_data.game_dir != data.game_dir + || old_data.data_dir != data.data_dir + { + ctx.submit_command(ACTION_START_SAVE_SETTINGS); + } + child.update(ctx, old_data, data, env) + } +} diff --git a/crates/dtmm/src/ui/widget/mod.rs b/crates/dtmm/src/ui/widget/mod.rs index 84a57a2..801ad0a 100644 --- a/crates/dtmm/src/ui/widget/mod.rs +++ b/crates/dtmm/src/ui/widget/mod.rs @@ -1,3 +1,9 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use druid::text::Formatter; +use druid::widget::{TextBoxEvent, ValidationDelegate}; +use druid::EventCtx; use druid::{Data, Widget}; pub mod container; @@ -6,3 +12,46 @@ pub mod controller; pub trait ExtraWidgetExt: Widget + Sized + 'static {} impl + 'static> ExtraWidgetExt for W {} + +pub(crate) struct PathBufFormatter; + +impl PathBufFormatter { + pub fn new() -> Self { + Self {} + } +} + +impl Formatter> for PathBufFormatter { + fn format(&self, value: &Arc) -> String { + value.display().to_string() + } + + fn validate_partial_input( + &self, + _input: &str, + _sel: &druid::text::Selection, + ) -> druid::text::Validation { + druid::text::Validation::success() + } + + fn value(&self, input: &str) -> Result, druid::text::ValidationError> { + let p = PathBuf::from(input); + Ok(Arc::new(p)) + } +} + +pub struct TextBoxOnChanged(F); + +impl TextBoxOnChanged { + pub fn new(f: F) -> Self { + Self(f) + } +} + +impl ValidationDelegate for TextBoxOnChanged { + fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) { + if let TextBoxEvent::Complete = event { + (self.0)(ctx, current_text) + } + } +} diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 5544367..a0ccaa2 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -10,13 +10,13 @@ use druid::{ }; use crate::state::{ - ModInfo, PathBufFormatter, State, TextBoxOnChanged, View, ACTION_ADD_MOD, - ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD, - ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT, - ACTION_START_SAVE_SETTINGS, + ModInfo, State, View, ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, + ACTION_SELECT_MOD, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, + ACTION_START_RESET_DEPLOYMENT, }; use crate::ui::theme; -use crate::ui::widget::controller::AutoScrollController; +use crate::ui::widget::controller::{AutoScrollController, SaveSettingsController}; +use crate::ui::widget::PathBufFormatter; const TITLE: &str = "Darktide Mod Manager"; const WINDOW_SIZE: (f64, f64) = (1080., 720.); @@ -248,9 +248,6 @@ fn build_view_settings() -> impl Widget { .with_flex_child( TextBox::new() .with_formatter(PathBufFormatter::new()) - .delegate(TextBoxOnChanged::new(|ctx, _| { - ctx.submit_command(ACTION_START_SAVE_SETTINGS) - })) .expand_width() .lens(State::data_dir), 1., @@ -265,9 +262,6 @@ fn build_view_settings() -> impl Widget { .with_flex_child( TextBox::new() .with_formatter(PathBufFormatter::new()) - .delegate(TextBoxOnChanged::new(|ctx, _| { - ctx.submit_command(ACTION_START_SAVE_SETTINGS) - })) .expand_width() .lens(State::game_dir), 1., @@ -318,4 +312,5 @@ fn build_window() -> impl Widget { .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) .with_child(build_log_view()) + .controller(SaveSettingsController) } diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 0008edf..d2ae44c 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -1,17 +1,63 @@ -use std::fs; use std::io::ErrorKind; use std::path::PathBuf; +use std::{fs, path::Path}; use clap::{parser::ValueSource, ArgMatches}; use color_eyre::{eyre::Context, Result}; use serde::{Deserialize, Serialize}; +use crate::state::{ModInfo, State}; + +#[derive(Clone, Debug, Serialize)] +pub(crate) struct LoadOrderEntrySerialize<'a> { + pub id: &'a String, + pub enabled: bool, +} + +impl<'a> From<&'a ModInfo> for LoadOrderEntrySerialize<'a> { + fn from(info: &'a ModInfo) -> Self { + Self { + id: &info.id, + enabled: info.enabled, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ConfigSerialize<'a> { + game_dir: &'a Path, + data_dir: &'a Path, + mod_order: Vec>, +} + +impl<'a> From<&'a State> for ConfigSerialize<'a> { + fn from(state: &'a State) -> Self { + Self { + game_dir: &state.game_dir, + data_dir: &state.data_dir, + mod_order: state + .mods + .iter() + .map(LoadOrderEntrySerialize::from) + .collect(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct LoadOrderEntry { + pub id: String, + pub enabled: bool, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub(crate) struct Config { #[serde(skip)] pub path: PathBuf, pub data_dir: Option, pub game_dir: Option, + #[serde(default)] + pub mod_order: Vec, } #[cfg(not(arget_os = "windows"))] @@ -92,6 +138,7 @@ where path: default_path, data_dir: Some(get_default_data_dir()), game_dir: None, + mod_order: Vec::new(), }; { -- 2.45.3 From fa8764984f869a755e3046dddcb51751a8260321 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 20:12:18 +0100 Subject: [PATCH 87/90] feat(dtmm): Rework log levels and filters Keep the flexibility for dev, but in prod drop stdout and restrict the log view to stuff useful to the user. --- crates/dtmm/src/util/log.rs | 43 ++++++++++++------------------------- lib/dtmt-shared/src/log.rs | 8 +++---- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/crates/dtmm/src/util/log.rs b/crates/dtmm/src/util/log.rs index 12aeeff..e6a019e 100644 --- a/crates/dtmm/src/util/log.rs +++ b/crates/dtmm/src/util/log.rs @@ -7,10 +7,6 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; -// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber` -// that would allow me to pass an extra `Layer` to that function. So, for now, -// its code has to be duplicated here. - pub struct ChannelWriter { tx: UnboundedSender, } @@ -39,42 +35,31 @@ impl std::io::Write for ChannelWriter { } pub fn create_tracing_subscriber(tx: UnboundedSender) { - let env_layer = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); - - let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) { - let fmt_layer = fmt::layer().pretty(); - (Some(fmt_layer), None, None) + let env_layer = if cfg!(debug_assertions) { + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")) } else { - // Creates a layer that - // - only prints events that contain a message - // - does not print fields - // - does not print spans/targets - // - only prints time, not date - let fmt_layer = fmt::layer() - .event_format(dtmt_shared::Formatter) - .fmt_fields(debug_fn(dtmt_shared::format_field)); + EnvFilter::new("error,dtmm=info") + }; - ( - None, - Some(fmt_layer), - Some(FilterFn::new(dtmt_shared::filter)), - ) + let stdout_layer = if cfg!(debug_assertions) { + let layer = fmt::layer().pretty(); + Some(layer) + } else { + None }; let channel_layer = fmt::layer() // TODO: Re-enable and implement a formatter for the Druid widget .with_ansi(false) .event_format(dtmt_shared::Formatter) - .fmt_fields(debug_fn(dtmt_shared::format_field)) - .with_writer(move || ChannelWriter::new(tx.clone())); + .fmt_fields(debug_fn(dtmt_shared::format_fields)) + .with_writer(move || ChannelWriter::new(tx.clone())) + .with_filter(FilterFn::new(dtmt_shared::filter_fields)); tracing_subscriber::registry() - .with(channel_layer) - .with(filter_layer) .with(env_layer) - .with(dev_stdout_layer) - .with(prod_stdout_layer) + .with(channel_layer) + .with(stdout_layer) .with(ErrorLayer::new(fmt::format::Pretty::default())) .init(); } diff --git a/lib/dtmt-shared/src/log.rs b/lib/dtmt-shared/src/log.rs index 15a26b1..3c46a4b 100644 --- a/lib/dtmt-shared/src/log.rs +++ b/lib/dtmt-shared/src/log.rs @@ -16,7 +16,7 @@ use tracing_subscriber::EnvFilter; pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); -pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { +pub fn format_fields(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result { if field.name() == "message" { write!(w, "{:?}", val) } else { @@ -24,7 +24,7 @@ pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug } } -pub fn filter(metadata: &Metadata<'_>) -> bool { +pub fn filter_fields(metadata: &Metadata<'_>) -> bool { metadata .fields() .iter() @@ -72,9 +72,9 @@ pub fn create_tracing_subscriber() { // - only prints time, not date let fmt_layer = fmt::layer() .event_format(Formatter) - .fmt_fields(debug_fn(format_field)); + .fmt_fields(debug_fn(format_fields)); - (None, Some(fmt_layer), Some(FilterFn::new(filter))) + (None, Some(fmt_layer), Some(FilterFn::new(filter_fields))) }; tracing_subscriber::registry() -- 2.45.3 From 1b5a9b8159ca90f1948ca3d35e4b08d973c2c72e Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 20:14:40 +0100 Subject: [PATCH 88/90] chore: Remove dead code --- crates/dtmm/src/ui/widget/container.rs | 7 ------- crates/dtmm/src/ui/widget/mod.rs | 19 ------------------- 2 files changed, 26 deletions(-) delete mode 100644 crates/dtmm/src/ui/widget/container.rs diff --git a/crates/dtmm/src/ui/widget/container.rs b/crates/dtmm/src/ui/widget/container.rs deleted file mode 100644 index e58e64c..0000000 --- a/crates/dtmm/src/ui/widget/container.rs +++ /dev/null @@ -1,7 +0,0 @@ -use druid::{Data, Widget, WidgetPod}; - -pub struct Container { - child: WidgetPod>>, -} - -impl Container {} diff --git a/crates/dtmm/src/ui/widget/mod.rs b/crates/dtmm/src/ui/widget/mod.rs index 801ad0a..ebb634e 100644 --- a/crates/dtmm/src/ui/widget/mod.rs +++ b/crates/dtmm/src/ui/widget/mod.rs @@ -2,11 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use druid::text::Formatter; -use druid::widget::{TextBoxEvent, ValidationDelegate}; -use druid::EventCtx; use druid::{Data, Widget}; -pub mod container; pub mod controller; pub trait ExtraWidgetExt: Widget + Sized + 'static {} @@ -39,19 +36,3 @@ impl Formatter> for PathBufFormatter { Ok(Arc::new(p)) } } - -pub struct TextBoxOnChanged(F); - -impl TextBoxOnChanged { - pub fn new(f: F) -> Self { - Self(f) - } -} - -impl ValidationDelegate for TextBoxOnChanged { - fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str) { - if let TextBoxEvent::Complete = event { - (self.0)(ctx, current_text) - } - } -} -- 2.45.3 From 45da42908b0e709ebba6bd0a50fb952572abcb56 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 20:43:44 +0100 Subject: [PATCH 89/90] docs: Add split READMEs Closes #17. --- README.adoc | 21 ++++++++------------- crates/dtmm/README.adoc | 16 ++++++++++++++++ crates/dtmt/README.adoc | 32 ++++++++++++++++++++++++++++++++ docs/screenshots/dtmm.png | Bin 0 -> 58994 bytes 4 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 crates/dtmm/README.adoc create mode 100644 crates/dtmt/README.adoc create mode 100644 docs/screenshots/dtmm.png diff --git a/README.adoc b/README.adoc index bc322fd..34e0ef0 100644 --- a/README.adoc +++ b/README.adoc @@ -10,23 +10,18 @@ :tip-caption: :bulb: :warning-caption: :warning: -A set of tools to develop mods for the newest generation of the Bitsquid game engine that powers the game _Warhammer 40.000: Darktide_. +A set of tools to use and develop mods for the newest generation of the Bitsquid game engine that powers the game _Warhammer 40.000: Darktide_. -== Quickstart +== Darktide Mod Manager (DTMM) -1. Download the latest https://git.sclu1034.dev/bitsquid_dt/dtmt/releases/[release] for your platform. -2. Place the binary for your system and `dictionary.csv` next to each other. -3. Open a command prompt, navigate to the downloaded binary and run `dtmt.exe help`. -4. Use the `help` command (it works for subcommands, too) and the https://git.sclu1034.dev/bitsquid_dt/dtmt/wiki/CLI-Reference[CLI Reference]. +DTMM is a GUI application to install and manage mods for the game. -== Runtime dependencies +image::docs/screenshots/dtmm.png[dtmm main view] -The LuaJit decompiler (short "ljd") is used to decompile Lua files. A version tailored specifically to Bitsquid may be found here: https://github.com/Aussiemon/ljd. +Head to https://git.sclu1034.dev/bitsquid_dt/dtmt/src/branch/master/crates/dtmm[crates/dtmm] for more information or check the https://git.sclu1034.dev/bitsquid_dt/dtmt/wiki[Wiki]. -A custom executable location may be passed via the `--ljd` flag during extraction, otherwise decompilation expects `ljd` to be found via the `PATH` environmental variable. +== Darktide Mod Tools (DTMT) -== Building +DTMT is a CLI application providing various commands that aid in developing mods for the game. -1. Install Rust from https://www.rust-lang.org/learn/get-started[rust-lang.org] or via the preferred means for your system. -2. Download or clone this source code. Make sure to include the submodules in `lib/`. -3. Run `cargo build`. +Head to https://git.sclu1034.dev/bitsquid_dt/dtmt/src/branch/master/crates/dtmt[crates/dtmt] for more information or check the https://git.sclu1034.dev/bitsquid_dt/dtmt/wiki[Wiki]. diff --git a/crates/dtmm/README.adoc b/crates/dtmm/README.adoc new file mode 100644 index 0000000..45130f1 --- /dev/null +++ b/crates/dtmm/README.adoc @@ -0,0 +1,16 @@ += Darktide Mod Manager (DTMM) +:idprefix: +:idseparator: +:toc: macro +:toclevels: 1 +:!toc-title: +:caution-caption: :fire: +:important-caption: :exclamtion: +:note-caption: :paperclip: +:tip-caption: :bulb: +:warning-caption: :warning: + +DTMM is a GUI application to install and manage mods for the game. + +![dtmm main view](../../docs/screenshots/dtmm.png) + diff --git a/crates/dtmt/README.adoc b/crates/dtmt/README.adoc new file mode 100644 index 0000000..4304805 --- /dev/null +++ b/crates/dtmt/README.adoc @@ -0,0 +1,32 @@ += Darktide Mod Tools (DTMT) +:idprefix: +:idseparator: +:toc: macro +:toclevels: 1 +:!toc-title: +:caution-caption: :fire: +:important-caption: :exclamtion: +:note-caption: :paperclip: +:tip-caption: :bulb: +:warning-caption: :warning: + +A set of tools to develop mods for the newest generation of the Bitsquid game engine that powers the game _Warhammer 40.000: Darktide_. + +== Quickstart + +1. Head to the latest https://git.sclu1034.dev/bitsquid_dt/dtmt/releases/[release] and download the `dtmt` binary for your platform. +2. Place the binary and `dictionary.csv` next to each other. +3. Open a command prompt, navigate to the downloaded binary and run `dtmt.exe help`. +4. Use the `help` command (it works for subcommands, too) and the https://git.sclu1034.dev/bitsquid_dt/dtmt/wiki/CLI-Reference[CLI Reference]. + +== Runtime dependencies + +The LuaJit decompiler (short "ljd") is used to decompile Lua files. A version tailored specifically to Bitsquid may be found here: https://github.com/Aussiemon/ljd. + +A custom executable location may be passed via the `--ljd` flag during extraction, otherwise decompilation expects `ljd` to be found via the `PATH` environmental variable. + +== Building + +1. Install Rust from https://www.rust-lang.org/learn/get-started[rust-lang.org] or via the preferred means for your system. +2. Download or clone this source code. Make sure to include the submodules in `lib/`. +3. Run `cargo build`. diff --git a/docs/screenshots/dtmm.png b/docs/screenshots/dtmm.png new file mode 100644 index 0000000000000000000000000000000000000000..af2a9805d643bfc8d38e348d4ad09aa5f10760a1 GIT binary patch literal 58994 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 iwSWmqA#PFVmcT-UE1d8-u5-pUXO@geCyn1PZPI literal 0 HcmV?d00001 -- 2.45.3 From d08321a6455a7721944b0965855f3e6feb757702 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 1 Mar 2023 22:11:42 +0100 Subject: [PATCH 90/90] feat(sdk): Switch to statically linked Oodle This removes another runtime dependency. --- Cargo.lock | 89 +- crates/dtmm/Cargo.toml | 2 +- crates/dtmm/src/main.rs | 4 - crates/dtmt/Cargo.toml | 2 +- crates/dtmt/src/cmd/build.rs | 4 - crates/dtmt/src/cmd/bundle/mod.rs | 4 - lib/oodle-sys/.gitignore | 2 - lib/oodle-sys/src/lib.rs | 77 -- lib/oodle-sys/src/library.rs | 154 --- lib/oodle-sys/src/types.rs | 197 ---- lib/{oodle-sys => oodle}/Cargo.toml | 8 +- lib/oodle/build.rs | 44 + lib/oodle/oodle2.h | 1643 +++++++++++++++++++++++++++ lib/oodle/oodle2base.h | 167 +++ lib/oodle/src/lib.rs | 145 +++ lib/sdk/Cargo.toml | 2 +- lib/sdk/src/bundle/mod.rs | 8 +- 17 files changed, 2094 insertions(+), 458 deletions(-) delete mode 100644 lib/oodle-sys/.gitignore delete mode 100644 lib/oodle-sys/src/lib.rs delete mode 100644 lib/oodle-sys/src/library.rs delete mode 100644 lib/oodle-sys/src/types.rs rename lib/{oodle-sys => oodle}/Cargo.toml (69%) create mode 100644 lib/oodle/build.rs create mode 100644 lib/oodle/oodle2.h create mode 100644 lib/oodle/oodle2base.h create mode 100644 lib/oodle/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d10dfcf..52c1f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,28 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -239,6 +261,15 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-expr" version = "0.11.0" @@ -263,6 +294,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clang-sys" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.1.7" @@ -675,7 +717,7 @@ dependencies = [ "druid", "dtmt-shared", "futures", - "oodle-sys", + "oodle", "sdk", "serde", "serde_sjson", @@ -702,7 +744,7 @@ dependencies = [ "glob", "libloading", "nanorand", - "oodle-sys", + "oodle", "path-clean", "pin-project-lite", "promptly", @@ -742,6 +784,12 @@ dependencies = [ "wio", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "endian-type" version = "0.1.2" @@ -1386,6 +1434,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.139" @@ -1597,11 +1651,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] -name = "oodle-sys" +name = "oodle" version = "0.1.0" dependencies = [ - "libloading", - "thiserror", + "bindgen", + "color-eyre", "tracing", ] @@ -1711,6 +1765,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pest" version = "2.5.5" @@ -2044,7 +2104,7 @@ dependencies = [ "libloading", "luajit2-sys", "nanorand", - "oodle-sys", + "oodle", "pin-project-lite", "serde", "serde_sjson", @@ -2138,6 +2198,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2680,6 +2746,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 6f42d88..3971ee3 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -13,7 +13,7 @@ confy = "0.5.1" druid = { git = "https://github.com/linebender/druid.git", features = ["im", "serde"] } dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" } futures = "0.3.25" -oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +oodle = { path = "../../lib/oodle", version = "*" } sdk = { path = "../../lib/sdk", version = "0.2.0" } serde_sjson = { path = "../../lib/serde_sjson", version = "*" } serde = { version = "1.0.152", features = ["derive", "rc"] } diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index fdce5ab..9ce6192 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -56,10 +56,6 @@ fn main() -> Result<()> { let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel(); util::log::create_tracing_subscriber(log_tx); - unsafe { - oodle_sys::init(matches.get_one::("oodle")); - } - let config = util::config::read_config(&default_config_path, &matches) .wrap_err("failed to read config file")?; diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index 83015db..f20e062 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -15,7 +15,7 @@ futures-util = "0.3.24" glob = "0.3.0" libloading = "0.7.4" nanorand = "0.7.0" -oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +oodle = { path = "../../lib/oodle", version = "*" } pin-project-lite = "0.2.9" promptly = "0.3.1" sdk = { path = "../../lib/sdk", version = "0.2.0" } diff --git a/crates/dtmt/src/cmd/build.rs b/crates/dtmt/src/cmd/build.rs index 46462e1..f077094 100644 --- a/crates/dtmt/src/cmd/build.rs +++ b/crates/dtmt/src/cmd/build.rs @@ -183,10 +183,6 @@ fn normalize_file_path>(path: P) -> Result { #[tracing::instrument(skip_all)] pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { - unsafe { - oodle_sys::init(matches.get_one::("oodle")); - } - let cfg = { let dir = matches.get_one::("directory").cloned(); let mut cfg = find_project_config(dir).await?; diff --git a/crates/dtmt/src/cmd/bundle/mod.rs b/crates/dtmt/src/cmd/bundle/mod.rs index 03ab3f5..6baf860 100644 --- a/crates/dtmt/src/cmd/bundle/mod.rs +++ b/crates/dtmt/src/cmd/bundle/mod.rs @@ -24,10 +24,6 @@ pub(crate) fn command_definition() -> Command { #[tracing::instrument(skip_all)] pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { - unsafe { - oodle_sys::init(matches.get_one::("oodle")); - } - match matches.subcommand() { Some(("decompress", sub_matches)) => decompress::run(ctx, sub_matches).await, Some(("extract", sub_matches)) => extract::run(ctx, sub_matches).await, diff --git a/lib/oodle-sys/.gitignore b/lib/oodle-sys/.gitignore deleted file mode 100644 index 4fffb2f..0000000 --- a/lib/oodle-sys/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -/Cargo.lock diff --git a/lib/oodle-sys/src/lib.rs b/lib/oodle-sys/src/lib.rs deleted file mode 100644 index 8346c5f..0000000 --- a/lib/oodle-sys/src/lib.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![feature(c_size_t)] -#![feature(once_cell)] - -use std::ffi::OsStr; -use std::sync::OnceLock; - -mod library; -mod types; - -pub use library::Library; -pub use library::CHUNK_SIZE; -pub use types::*; - -#[derive(thiserror::Error, Debug)] -pub enum OodleError { - #[error("{0}")] - Oodle(String), - #[error(transparent)] - Library(#[from] libloading::Error), -} - -type Result = std::result::Result; - -static LIB: OnceLock = OnceLock::new(); - -/// Initialize the global library handle that this module's -/// functions operate on. -/// -/// # Safety -/// -/// The safety concerns as described by [`libloading::Library::new`] apply. -pub unsafe fn init>(name: Option

) { - let lib = match name { - Some(name) => Library::with_name(name), - None => Library::new(), - }; - - let lib = lib.expect("Failed to load library."); - if LIB.set(lib).is_err() { - panic!("Library was already initialized. Did you call `init` twice?"); - } -} - -fn get() -> Result<&'static Library> { - match LIB.get() { - Some(lib) => Ok(lib), - None => { - let err = OodleError::Oodle(String::from("Library has not been initialized, yet.")); - Err(err) - } - } -} - -pub fn decompress( - data: I, - fuzz_safe: OodleLZ_FuzzSafe, - check_crc: OodleLZ_CheckCRC, -) -> Result> -where - I: AsRef<[u8]>, -{ - let lib = get()?; - lib.decompress(data, fuzz_safe, check_crc) -} - -pub fn compress(data: I) -> Result> -where - I: AsRef<[u8]>, -{ - let lib = get()?; - lib.compress(data) -} - -pub fn get_decode_buffer_size(raw_size: usize, corruption_possible: bool) -> Result { - let lib = get()?; - lib.get_decode_buffer_size(raw_size, corruption_possible) -} diff --git a/lib/oodle-sys/src/library.rs b/lib/oodle-sys/src/library.rs deleted file mode 100644 index ef773e4..0000000 --- a/lib/oodle-sys/src/library.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::{ffi::OsStr, ptr}; - -use libloading::Symbol; - -use super::Result; -use crate::{types::*, OodleError}; - -// Hardcoded chunk size of Bitsquid's bundle compression -pub const CHUNK_SIZE: usize = 512 * 1024; -pub const COMPRESSOR: OodleLZ_Compressor = OodleLZ_Compressor::Kraken; -pub const LEVEL: OodleLZ_CompressionLevel = OodleLZ_CompressionLevel::Optimal2; - -#[cfg(target_os = "windows")] -const OODLE_LIB_NAME: &str = "oo2core_8_win64"; - -#[cfg(target_os = "linux")] -const OODLE_LIB_NAME: &str = "liboo2corelinux64.so"; - -pub struct Library { - inner: libloading::Library, -} - -impl Library { - /// Load the Oodle library by its default name. - /// - /// The default name is platform-specific: - /// - Windows: `oo2core_8_win64` - /// - Linux: `liboo2corelinux64.so` - /// - /// # Safety - /// - /// The safety concerns as described by [`libloading::Library::new`] apply. - pub unsafe fn new() -> Result { - Self::with_name(OODLE_LIB_NAME) - } - - /// Load the Oodle library by the given name or path. - /// - /// See [`libloading::Library::new`] for how the `name` parameter is handled. - /// - /// # Safety - /// - /// The safety concerns as described by [`libloading::Library::new`] apply. - pub unsafe fn with_name>(name: P) -> Result { - let inner = libloading::Library::new(name)?; - Ok(Self { inner }) - } - - #[tracing::instrument(skip(self, data))] - pub fn decompress( - &self, - data: I, - fuzz_safe: OodleLZ_FuzzSafe, - check_crc: OodleLZ_CheckCRC, - ) -> Result> - where - I: AsRef<[u8]>, - { - let data = data.as_ref(); - let mut out = vec![0; CHUNK_SIZE]; - - let verbosity = if tracing::enabled!(tracing::Level::INFO) { - OodleLZ_Verbosity::Minimal - } else if tracing::enabled!(tracing::Level::DEBUG) { - OodleLZ_Verbosity::Some - } else if tracing::enabled!(tracing::Level::TRACE) { - OodleLZ_Verbosity::Lots - } else { - OodleLZ_Verbosity::None - }; - - let ret = unsafe { - let decompress: Symbol = self.inner.get(b"OodleLZ_Decompress\0")?; - - decompress( - data.as_ptr() as *const _, - data.len(), - out.as_mut_ptr() as *mut _, - out.len(), - fuzz_safe, - check_crc, - verbosity, - ptr::null_mut(), - 0, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - 0, - OodleLZ_Decode_ThreadPhase::UNTHREADED, - ) - }; - - if ret == 0 { - let err = OodleError::Oodle(String::from("Decompression failed.")); - return Err(err); - } - - Ok(out) - } - - #[tracing::instrument(name = "Oodle::compress", skip(self, data))] - pub fn compress(&self, data: I) -> Result> - where - I: AsRef<[u8]>, - { - let mut raw = Vec::from(data.as_ref()); - raw.resize(CHUNK_SIZE, 0); - - // TODO: Query oodle for buffer size - let mut out = vec![0u8; CHUNK_SIZE]; - - let ret = unsafe { - let compress: Symbol = self.inner.get(b"OodleLZ_Compress\0")?; - - compress( - COMPRESSOR, - raw.as_ptr() as *const _, - raw.len(), - out.as_mut_ptr() as *mut _, - LEVEL, - ptr::null_mut(), - 0, - ptr::null_mut(), - ptr::null_mut(), - 0, - ) - }; - - tracing::debug!(compressed_size = ret, "Compressed chunk"); - - if ret == 0 { - let err = OodleError::Oodle(String::from("Compression failed.")); - return Err(err); - } - - out.resize(ret as usize, 0); - - Ok(out) - } - - pub fn get_decode_buffer_size( - &self, - raw_size: usize, - corruption_possible: bool, - ) -> Result { - unsafe { - let f: Symbol = - self.inner.get(b"OodleLZ_GetDecodeBufferSize\0")?; - - let size = f(COMPRESSOR, raw_size, corruption_possible); - Ok(size) - } - } -} diff --git a/lib/oodle-sys/src/types.rs b/lib/oodle-sys/src/types.rs deleted file mode 100644 index 5d306f8..0000000 --- a/lib/oodle-sys/src/types.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![allow(dead_code)] -use core::ffi::{c_char, c_int, c_size_t, c_ulonglong, c_void}; - -// Type definitions taken from Unreal Engine's `oodle2.h` - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_FuzzSafe { - No = 0, - Yes = 1, -} - -impl From for OodleLZ_FuzzSafe { - fn from(value: bool) -> Self { - if value { - Self::Yes - } else { - Self::No - } - } -} - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_CheckCRC { - No = 0, - Yes = 1, - Force32 = 0x40000000, -} - -impl From for OodleLZ_CheckCRC { - fn from(value: bool) -> Self { - if value { - Self::Yes - } else { - Self::No - } - } -} - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_Verbosity { - None = 0, - Minimal = 1, - Some = 2, - Lots = 3, - Force32 = 0x40000000, -} - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_Decode_ThreadPhase { - Phase1 = 1, - Phase2 = 2, - PhaseAll = 3, -} - -impl OodleLZ_Decode_ThreadPhase { - pub const UNTHREADED: Self = OodleLZ_Decode_ThreadPhase::PhaseAll; -} - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_Compressor { - Invalid = -1, - // None = memcpy, pass through uncompressed bytes - None = 3, - - // NEW COMPRESSORS: - // Fast decompression and high compression ratios, amazing! - Kraken = 8, - // Leviathan = Kraken's big brother with higher compression, slightly slower decompression. - Leviathan = 13, - // Mermaid is between Kraken & Selkie - crazy fast, still decent compression. - Mermaid = 9, - // Selkie is a super-fast relative of Mermaid. For maximum decode speed. - Selkie = 11, - // Hydra, the many-headed beast = Leviathan, Kraken, Mermaid, or Selkie (see $OodleLZ_About_Hydra) - Hydra = 12, - BitKnit = 10, - // DEPRECATED but still supported - Lzb16 = 4, - Lzna = 7, - Lzh = 0, - Lzhlw = 1, - Lznib = 2, - Lzblw = 5, - Lza = 6, - Count = 14, - Force32 = 0x40000000, -} - -#[repr(C)] -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug)] -pub enum OodleLZ_CompressionLevel { - // don't compress, just copy raw bytes - None = 0, - // super fast mode, lower compression ratio - SuperFast = 1, - // fastest LZ mode with still decent compression ratio - VeryFast = 2, - // fast - good for daily use - Fast = 3, - // standard medium speed LZ mode - Normal = 4, - // optimal parse level 1 (faster optimal encoder) - Optimal1 = 5, - // optimal parse level 2 (recommended baseline optimal encoder) - Optimal2 = 6, - // optimal parse level 3 (slower optimal encoder) - Optimal3 = 7, - // optimal parse level 4 (very slow optimal encoder) - Optimal4 = 8, - // optimal parse level 5 (don't care about encode speed, maximum compression) - Optimal5 = 9, - // faster than SuperFast, less compression - HyperFast1 = -1, - // faster than HyperFast1, less compression - HyperFast2 = -2, - // faster than HyperFast2, less compression - HyperFast3 = -3, - // fastest, less compression - HyperFast4 = -4, - Force32 = 0x40000000, -} - -impl OodleLZ_CompressionLevel { - // alias hyperfast base level - pub const HYPERFAST: Self = OodleLZ_CompressionLevel::HyperFast1; - // alias optimal standard level - pub const OPTIMAL: Self = OodleLZ_CompressionLevel::Optimal2; - // maximum compression level - pub const MAX: Self = OodleLZ_CompressionLevel::Optimal5; - // fastest compression level - pub const MIN: Self = OodleLZ_CompressionLevel::HyperFast4; - pub const INVALID: Self = OodleLZ_CompressionLevel::Force32; -} - -#[allow(non_camel_case_types)] -pub type t_fp_OodleCore_Plugin_Printf = - extern "C" fn(level: c_int, file: *const c_char, line: c_int, fmt: *const c_char); - -#[allow(non_camel_case_types)] -pub type OodleLZ_Decompress = extern "C" fn( - compressed_buffer: *const c_void, - compressed_length: c_size_t, - raw_buffer: *mut c_void, - raw_length: c_size_t, - fuzz_safe: OodleLZ_FuzzSafe, - check_crc: OodleLZ_CheckCRC, - verbosity: OodleLZ_Verbosity, - decBufBase: *mut c_void, - decBufSize: c_size_t, - callback: *const c_void, - callback_user_data: *const c_void, - decoder_memory: *mut c_void, - decoder_memory_size: c_size_t, - thread_phase: OodleLZ_Decode_ThreadPhase, -) -> c_ulonglong; - -#[allow(non_camel_case_types)] -pub type OodleLZ_Compress = extern "C" fn( - compressor: OodleLZ_Compressor, - raw_buffer: *const c_void, - raw_len: c_size_t, - compressed_buffer: *mut c_void, - level: OodleLZ_CompressionLevel, - options: *const c_void, - dictionary_base: c_size_t, - lrm: *const c_void, - scratch_memory: *mut c_void, - scratch_size: c_size_t, -) -> c_ulonglong; - -#[allow(non_camel_case_types)] -pub type OodleLZ_GetDecodeBufferSize = extern "C" fn( - compressor: OodleLZ_Compressor, - raw_size: c_size_t, - corruption_possible: bool, -) -> c_size_t; - -#[allow(non_camel_case_types)] -pub type OodleCore_Plugins_SetPrintf = - extern "C" fn(f: t_fp_OodleCore_Plugin_Printf) -> t_fp_OodleCore_Plugin_Printf; - -#[allow(non_camel_case_types)] -pub type OodleCore_Plugin_Printf_Verbose = t_fp_OodleCore_Plugin_Printf; - -#[allow(non_camel_case_types)] -pub type OodleCore_Plugin_Printf_Default = t_fp_OodleCore_Plugin_Printf; diff --git a/lib/oodle-sys/Cargo.toml b/lib/oodle/Cargo.toml similarity index 69% rename from lib/oodle-sys/Cargo.toml rename to lib/oodle/Cargo.toml index 539427d..3283592 100644 --- a/lib/oodle-sys/Cargo.toml +++ b/lib/oodle/Cargo.toml @@ -1,11 +1,13 @@ [package] -name = "oodle-sys" +name = "oodle" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libloading = "0.7.4" -thiserror = "1.0.38" +color-eyre = "0.6.2" tracing = "0.1.37" + +[build-dependencies] +bindgen = "0.64.0" diff --git a/lib/oodle/build.rs b/lib/oodle/build.rs new file mode 100644 index 0000000..2a48078 --- /dev/null +++ b/lib/oodle/build.rs @@ -0,0 +1,44 @@ +extern crate bindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + // Tell cargo to look for shared libraries in the specified directory + // println!("cargo:rustc-link-search=/path/to/lib"); + + // Tell cargo to tell rustc to link the system bzip2 + // shared library. + if cfg!(target_os = "windows") { + println!("cargo:rustc-link-lib=oo2core_8_win64"); + } else { + println!("cargo:rustc-link-lib=oo2corelinux64"); + } + + // Tell cargo to invalidate the built crate whenever the wrapper changes + println!("cargo:rerun-if-changed=oodle2.h"); + + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header("oodle2base.h") + .header("oodle2.h") + .blocklist_file("stdint.h") + .blocklist_file("stdlib.h") + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/lib/oodle/oodle2.h b/lib/oodle/oodle2.h new file mode 100644 index 0000000..ffe4152 --- /dev/null +++ b/lib/oodle/oodle2.h @@ -0,0 +1,1643 @@ + +//=================================================== +// Oodle2 Core header +// (C) Copyright 1994-2021 Epic Games Tools LLC +//=================================================== + +#ifndef __OODLE2_H_INCLUDED__ +#define __OODLE2_H_INCLUDED__ + +#ifndef OODLE2_PUBLIC_HEADER +#define OODLE2_PUBLIC_HEADER 1 +#endif + +#ifndef __OODLE2BASE_H_INCLUDED__ +#include "oodle2base.h" +#endif + +#ifdef _MSC_VER +#pragma pack(push, Oodle, 8) + +#pragma warning(push) +#pragma warning(disable : 4127) // conditional is constant +#endif + +// header version : +// the DLL is incompatible when MAJOR is bumped +// MINOR is for internal revs and bug fixes that don't affect API compatibility +#define OODLE2_VERSION_MAJOR 9 +#define OODLE2_VERSION_MINOR 5 + +// OodleVersion string is 1 . MAJOR . MINOR +// don't make it from macros cuz the doc tool has to parse the string literal + +#define OodleVersion "2.9.5" /* +*/ + +//----------------------------------------------------- +// OodleLZ + +#if 0 +#define OODLE_ALLOW_DEPRECATED_COMPRESSORS /* If you need to encode with the deprecated compressors, define this before including oodle2.h + + You may still decode with them without defining this. +*/ +#endif + +// Default verbosity selection of 0 will not even log when it sees corruption +typedef enum OodleLZ_Verbosity +{ + OodleLZ_Verbosity_None = 0, + OodleLZ_Verbosity_Minimal = 1, + OodleLZ_Verbosity_Some = 2, + OodleLZ_Verbosity_Lots = 3, + OodleLZ_Verbosity_Force32 = 0x40000000 +} OodleLZ_Verbosity; +/* Verbosity of LZ functions + LZ functions print information to the function set by $OodleCore_Plugins_SetPrintf + or $OodleXLog_Printf if using OodleX. +*/ + +OO_COMPILER_ASSERT( sizeof(OodleLZ_Verbosity) == 4 ); + +typedef enum OodleLZ_Compressor +{ + OodleLZ_Compressor_Invalid = -1, + OodleLZ_Compressor_None = 3, // None = memcpy, pass through uncompressed bytes + + // NEW COMPRESSORS : + OodleLZ_Compressor_Kraken = 8, // Fast decompression and high compression ratios, amazing! + OodleLZ_Compressor_Leviathan = 13,// Leviathan = Kraken's big brother with higher compression, slightly slower decompression. + OodleLZ_Compressor_Mermaid = 9, // Mermaid is between Kraken & Selkie - crazy fast, still decent compression. + OodleLZ_Compressor_Selkie = 11, // Selkie is a super-fast relative of Mermaid. For maximum decode speed. + OodleLZ_Compressor_Hydra = 12, // Hydra, the many-headed beast = Leviathan, Kraken, Mermaid, or Selkie (see $OodleLZ_About_Hydra) + +#ifdef OODLE_ALLOW_DEPRECATED_COMPRESSORS + OodleLZ_Compressor_BitKnit = 10, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZB16 = 4, // DEPRECATED but still supported + OodleLZ_Compressor_LZNA = 7, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZH = 0, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZHLW = 1, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZNIB = 2, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZBLW = 5, // no longer supported as of Oodle 2.9.0 + OodleLZ_Compressor_LZA = 6, // no longer supported as of Oodle 2.9.0 +#endif + + OodleLZ_Compressor_Count = 14, + OodleLZ_Compressor_Force32 = 0x40000000 +} OodleLZ_Compressor; +/* Selection of compression algorithm. + + Each compressor provides a different balance of speed vs compression ratio. + + New Oodle users should only use the new sea monster family of compressors. + + The OODLE_ALLOW_DEPRECATED_COMPRESSORS set of compressors is no longer supported + as of Oodle 2.9.0 ; see $Oodle_FAQ_deprecated_compressors + + The sea monsters are all fuzz safe and use whole-block quantum (not the 16k quantum) + ($OodleLZ_Compressor_UsesWholeBlockQuantum) + + If you need to encode the deprecated compressors, define $OODLE_ALLOW_DEPRECATED_COMPRESSORS before + including oodle2.h + + See $Oodle_FAQ_WhichLZ for a quick FAQ on which compressor to use + + See $OodleLZ_About for discussion of how to choose a compressor. +*/ + +OO_COMPILER_ASSERT( sizeof(OodleLZ_Compressor) == 4 ); + +typedef enum OodleLZ_PackedRawOverlap +{ + OodleLZ_PackedRawOverlap_No = 0, + OodleLZ_PackedRawOverlap_Yes = 1, + OodleLZ_PackedRawOverlap_Force32 = 0x40000000 +} OodleLZ_PackedRawOverlap; +/* Bool enum +*/ + +typedef enum OodleLZ_CheckCRC +{ + OodleLZ_CheckCRC_No = 0, + OodleLZ_CheckCRC_Yes = 1, + OodleLZ_CheckCRC_Force32 = 0x40000000 +} OodleLZ_CheckCRC; +/* Bool enum for the LZ decoder - should it check CRC before decoding or not? + + NOTE : the CRC's in the LZH decompress checks are the CRC's of the *compressed* bytes. This allows checking the CRc + prior to decompression, so corrupted data cannot be fed to the compressor. + + To use OodleLZ_CheckCRC_Yes, the compressed data must have been made with $(OodleLZ_CompressOptions:sendQuantumCRCs) set to true. + + If you want a CRC of the raw bytes, there is one optionally stored in the $OodleLZ_SeekTable and can be confirmed with + $OodleLZ_CheckSeekTableCRCs +*/ + + +typedef enum OodleLZ_Profile +{ + OodleLZ_Profile_Main=0, // Main profile (all current features allowed) + OodleLZ_Profile_Reduced=1, // Reduced profile (Kraken only, limited feature set) + OodleLZ_Profile_Force32 = 0x40000000 +} OodleLZ_Profile; +/* Decode profile to target */ + +// Not flagged for idoc and done using a #define since it's internal (testing) use only +#define OodleLZ_Profile_Internal_Custom ((OodleLZ_Profile)100) + +OO_COMPILER_ASSERT( sizeof(OodleLZ_Profile) == 4 ); + +typedef enum OodleDecompressCallbackRet +{ + OodleDecompressCallbackRet_Continue=0, + OodleDecompressCallbackRet_Cancel=1, + OodleDecompressCallbackRet_Invalid=2, + OodleDecompressCallbackRet_Force32 = 0x40000000 +} OodleDecompressCallbackRet; +/* Return value for $OodleDecompressCallback + return OodleDecompressCallbackRet_Cancel to abort the in-progress decompression +*/ + +OODEFFUNC typedef OodleDecompressCallbackRet (OODLE_CALLBACK OodleDecompressCallback)(void * userdata, const OO_U8 * rawBuf,OO_SINTa rawLen,const OO_U8 * compBuf,OO_SINTa compBufferSize , OO_SINTa rawDone, OO_SINTa compUsed); +/* User-provided callback for decompression + + $:userdata the data you passed for _pcbData_ + $:rawBuf the decompressed buffer + $:rawLen the total decompressed length + $:compBuf the compressed buffer + $:compBufferSize the total compressed length + $:rawDone number of bytes in rawBuf decompressed so far + $:compUsed number of bytes in compBuf consumed so far + + OodleDecompressCallback is called incrementally during decompression. +*/ + +typedef enum OodleLZ_CompressionLevel +{ + OodleLZ_CompressionLevel_None=0, // don't compress, just copy raw bytes + OodleLZ_CompressionLevel_SuperFast=1, // super fast mode, lower compression ratio + OodleLZ_CompressionLevel_VeryFast=2, // fastest LZ mode with still decent compression ratio + OodleLZ_CompressionLevel_Fast=3, // fast - good for daily use + OodleLZ_CompressionLevel_Normal=4, // standard medium speed LZ mode + + OodleLZ_CompressionLevel_Optimal1=5, // optimal parse level 1 (faster optimal encoder) + OodleLZ_CompressionLevel_Optimal2=6, // optimal parse level 2 (recommended baseline optimal encoder) + OodleLZ_CompressionLevel_Optimal3=7, // optimal parse level 3 (slower optimal encoder) + OodleLZ_CompressionLevel_Optimal4=8, // optimal parse level 4 (very slow optimal encoder) + OodleLZ_CompressionLevel_Optimal5=9, // optimal parse level 5 (don't care about encode speed, maximum compression) + + OodleLZ_CompressionLevel_HyperFast1=-1, // faster than SuperFast, less compression + OodleLZ_CompressionLevel_HyperFast2=-2, // faster than HyperFast1, less compression + OodleLZ_CompressionLevel_HyperFast3=-3, // faster than HyperFast2, less compression + OodleLZ_CompressionLevel_HyperFast4=-4, // fastest, less compression + + // aliases : + OodleLZ_CompressionLevel_HyperFast=OodleLZ_CompressionLevel_HyperFast1, // alias hyperfast base level + OodleLZ_CompressionLevel_Optimal = OodleLZ_CompressionLevel_Optimal2, // alias optimal standard level + OodleLZ_CompressionLevel_Max = OodleLZ_CompressionLevel_Optimal5, // maximum compression level + OodleLZ_CompressionLevel_Min = OodleLZ_CompressionLevel_HyperFast4, // fastest compression level + + OodleLZ_CompressionLevel_Force32 = 0x40000000, + OodleLZ_CompressionLevel_Invalid = OodleLZ_CompressionLevel_Force32 +} OodleLZ_CompressionLevel; +/* Selection of compression encoder complexity + + Higher numerical value of CompressionLevel = slower compression, but smaller compressed data. + + The compressed stream is always decodable with the same decompressors. + CompressionLevel controls the amount of work the encoder does to find the best compressed bit stream. + CompressionLevel does not primary affect decode speed, it trades off encode speed for compressed bit stream quality. + + I recommend starting with OodleLZ_CompressionLevel_Normal, then try up or down if you want + faster encoding or smaller output files. + + The Optimal levels are good for distribution when you compress rarely and decompress often; + they provide very high compression ratios but are slow to encode. Optimal2 is the recommended level + to start with of the optimal levels. + Optimal4 and 5 are not recommended for common use, they are very slow and provide the maximum compression ratio, + but the gain over Optimal3 is usually small. + + The HyperFast levels have negative numeric CompressionLevel values. + They are faster than SuperFast for when you're encoder CPU time constrained or want + something closer to symmetric compression vs. decompression time. + The HyperFast levels are currently only available in Kraken, Mermaid & Selkie. + Higher levels of HyperFast are faster to encode, eg. HyperFast4 is the fastest. + + The CompressionLevel does not affect decode speed much. Higher compression level does not mean + slower to decode. To trade off decode speed vs ratio, use _spaceSpeedTradeoffBytes_ in $OodleLZ_CompressOptions + +*/ + +OO_COMPILER_ASSERT( sizeof(OodleLZ_CompressionLevel) == 4 ); + +typedef enum OodleLZ_Jobify +{ + OodleLZ_Jobify_Default=0, // Use compressor default for level of internal job usage + OodleLZ_Jobify_Disable=1, // Don't use jobs at all + OodleLZ_Jobify_Normal=2, // Try to balance parallelism with increased memory usage + OodleLZ_Jobify_Aggressive=3, // Maximize parallelism even when doing so requires large amounts of memory + OodleLZ_Jobify_Count=4, + + OodleLZ_Jobify_Force32 = 0x40000000, +} OodleLZ_Jobify; +/* Controls the amount of internal threading in $OodleLZ_Compress calls + + Once you install a pluggable job system via $OodleCore_Plugins_SetJobSystem, Oodle can internally break + heavy-weight compression tasks into smaller jobs that can run in parallel. This can speed up + compression of large blocks of data at Optimal1 and higher levels substantially. + + The trade-off is that running more jobs concurrently rather than sequentially can greatly increase + memory requirements when there are multiple outstanding memory-intensive jobs. + + OodleLZ_Jobify_Default lets the compressor decide; typically compressors will default to "Normal" + when a pluggable job system has been installed, and "Disable" otherwise. + + OodleLZ_Jobify_Disable disables use of internal jobs entirely; all compression work is done on + the calling thread. This minimizes the amount of memory used, and is also appropriate when you're + getting parallelism in other ways, e.g. by running OodleLZ_Compress on many threads yourself. + + OodleLZ_Jobify_Normal uses jobs to increase compressor parallelism and speeds up compression of + large blocks of data, but avoids handing out many concurrent jobs for tasks that are memory-intensive. + + OodleLZ_Jobify_Aggressive will use concurrent jobs even for highly memory-intensive tasks. This + can speed up things further, but at a potentially significant increase in the amount of memory used + by Oodle. + +*/ + +#define OODLELZ_LOCALDICTIONARYSIZE_MAX (1<<30) /* Maximum value of maxLocalDictionarySize in OodleLZ_CompressOptions +*/ + +#define OODLELZ_SPACESPEEDTRADEOFFBYTES_DEFAULT (256) /* Default value of spaceSpeedTradeoffBytes in OodleLZ_CompressOptions + Changes how the encoder makes decisions in the bit stream + Higher spaceSpeedTradeoffBytes favors decode speed more (larger compressed files) + Lower spaceSpeedTradeoffBytes favors smaller compressed files (slower decoder) + Goes in a power of 2 scale; so try 64,128 and 512,1024 + (OODLELZ_SPACESPEEDTRADEOFFBYTES_DEFAULT/2) or (OODLELZ_SPACESPEEDTRADEOFFBYTES_DEFAULT*2) +*/ + + +typedef OOSTRUCT OodleLZ_CompressOptions +{ + OO_U32 unused_was_verbosity; // unused ; was verbosity (set to zero) + OO_S32 minMatchLen; // minimum match length ; cannot be used to reduce a compressor's default MML, but can be higher. On some types of data, a large MML (6 or 8) is a space-speed win. + OO_BOOL seekChunkReset; // whether chunks should be independent, for seeking and parallelism + OO_S32 seekChunkLen; // length of independent seek chunks (if seekChunkReset) ; must be a power of 2 and >= $OODLELZ_BLOCK_LEN ; you can use $OodleLZ_MakeSeekChunkLen + OodleLZ_Profile profile; // decoder profile to target (set to zero) + OO_S32 dictionarySize; // sets a maximum offset for matches, if lower than the maximum the format supports. <= 0 means infinite (use whole buffer). Often power of 2 but doesn't have to be. + OO_S32 spaceSpeedTradeoffBytes; // this is a number of bytes; I must gain at least this many bytes of compressed size to accept a speed-decreasing decision + OO_S32 unused_was_maxHuffmansPerChunk; // unused ; was maxHuffmansPerChunk + OO_BOOL sendQuantumCRCs; // should the encoder send a CRC of each compressed quantum, for integrity checks; this is necessary if you want to use OodleLZ_CheckCRC_Yes on decode + OO_S32 maxLocalDictionarySize; // (Optimals) size of local dictionary before needing a long range matcher. This does not set a window size for the decoder; it's useful to limit memory use and time taken in the encoder. maxLocalDictionarySize must be a power of 2. Must be <= OODLELZ_LOCALDICTIONARYSIZE_MAX + OO_BOOL makeLongRangeMatcher; // (Optimals) should the encoder find matches beyond maxLocalDictionarySize using an LRM + OO_S32 matchTableSizeLog2; //(non-Optimals) when variable, sets the size of the match finder structure (often a hash table) ; use 0 for the compressor's default + + OodleLZ_Jobify jobify; // controls internal job usage by compressors + void * jobifyUserPtr; // user pointer passed through to RunJob and WaitJob callbacks + + OO_S32 farMatchMinLen; // far matches must be at least this len + OO_S32 farMatchOffsetLog2; // if not zero, the log2 of an offset that must meet farMatchMinLen + + OO_U32 reserved[4]; // reserved space for adding more options; zero these! +} OodleLZ_CompressOptions; +/* Options for the compressor + + Typically filled by calling $OodleLZ_CompressOptions_GetDefault , then individual options may be modified, like : + + OodleLZ_CompressOptions my_options = *OodleLZ_CompressOptions_GetDefault() + + To ensure you have set up the options correctly, call $OodleLZ_CompressOptions_Validate. + + _unused_was_verbosity_ : place holder, set to zero + + _minMatchLen_ : rarely useful. Default value of 0 means let the compressor decide. On some types of data, + bumping this up to 4,6, or 8 can improve decode speed with little effect on compression ratio. Most of the + Oodle compressors use a default MML of 4 at levels below 7, and MML 3 at levels >= 7. If you want to keep MML 4 + at the higher levels, set _minMatchLen_ here to 4. _minMatchLen_ cannot be used to reduce the base MML of the compressor, only to increase it. + + _seekChunkReset_ must be true if you want the decode to be able to run "Wide", with pieces that can be + decoded independently (not keeping previous pieces in memory for match references). + + _seekChunkLen_ : length of independent seek chunks (if seekChunkReset) ; must be a power of 2 and >= $OODLELZ_BLOCK_LEN ; you can use $OodleLZ_MakeSeekChunkLen + + _profile_ : tells the encoder to target alternate bitstream profile. Default value of zero for normal use. + + _dictionarySize_ : limits the encoder to partial buffer access for matches. Can be useful for decoding incrementally + without keeping the entire output buffer in memory. + + _spaceSpeedTradeoffBytes_ is a way to trade off compression ratio for decode speed. If you make it smaller, + you get more compression ratio and slower decodes. It's the number of bytes that a decision must save to + be worth a slower decode. Default is 256 (OODLELZ_SPACESPEEDTRADEOFFBYTES_DEFAULT). So that means the encoder must be able to save >= 256 bytes to + accept something that will slow down decoding (like adding another Huffman table). The typical range is + 64-1024. + + Lower _spaceSpeedTradeoffBytes_ = more compression, slower decode + Higher _spaceSpeedTradeoffBytes_ = less compression, faster decode + + _spaceSpeedTradeoffBytes_ is the primary parameter for controlling Hydra. The default value of 256 will make + Hydra decodes that are just a little bit faster than Kraken. You get Kraken speeds around 200, and Mermaid + speeds around 1200. + + At the extreme, a _spaceSpeedTradeoffBytes_ of zero would mean all you care about is compression ratio, not decode + speed, you want the encoder to make the smallest possible output. (you cannot actually set zero, as zero values + always mean "use default" in this struct; you never really want zero anyway) + Generally _spaceSpeedTradeoffBytes_ below 16 provides diminishing gains in size with pointless decode speed loss. + + _spaceSpeedTradeoffBytes_ is on sort of powers of 2 scale, so you might want to experiment with 32,64,128,256,512 + + _spaceSpeedTradeoffBytes_ outside the range [16 - 2048] is not recommended. + + _unused_was_maxHuffmansPerChunk_ : place holder, set to zero + + _sendQuantumCRCs_ : send hashes of the compressed data to verify in the decoder; not recommended, if you need data + verification, use your own system outside of Oodle. DEPRECATED, not recommended. For backwards compatibility only. + + _maxLocalDictionarySize_ : only applies to optimal parsers at level >= Optimal2. This limits the encoder memory use. + Making it larger = more compression, higher memory use. Matches within maxLocalDictionarySize are found exactly, + outside the maxLocalDictionarySize window an approximate long range matcher is used. + + _makeLongRangeMatcher_ : whether an LRM should be used to find matches outside the _maxLocalDictionarySize_ window + (Optimal levels only) + + _matchTableSizeLog2_ : for non-optimal levels (level <= Normal), controls the hash table size. Making this very + small can sometimes boost encoder speed. For the very fastest encoding, use the SuperFast level and change + _matchTableSizeLog2_ to 12 or 13. + + _matchTableSizeLog2_ should usually be left zero to use the encoder's default + + _matchTableSizeLog2_ allows you to limit memory use of the non-Optimal encoder levels. Memory use is roughly + ( 1 MB + 4 << matchTableSizeLog2 ) + + _jobify_ tells compressors how to use internal jobs for compression tasks. Jobs can be run in parallel using the + job system plugins set with $OodleCore_Plugins_SetJobSystem. Not all compressors or compression level support + jobs, but the slower ones generally do. The default value of jobify is to use a thread system if one is installed. + + _farMatchMinLen_ and _farMatchOffsetLog2_ can be used to tune the encoded stream for a known cache size on the + decoding hardware. If set, then offsets with log2 greater or each to _farMatchOffsetLog2_ must have a minimum + length of _farMatchMinLen_. For example to target a machine with a 2 MB cache, set _farMatchOffsetLog2_ to 21, + and _farMatchMinLen_ to something large, like 16 or 20. + + Without _farMatchMinLen_ and _farMatchOffsetLog2_ set, the Oodle encoders tune for a blend of cache sizes that works + well on most machines. _dictionarySize_ can also be used to tune for cache size, but cuts off all matches + beyond a certain distance. That may be more appropriate when you don't want to go out of cache at all. + _farMatchMinLen_ can only be used to make the standard blend target more restrictive; it can reduce the target cache size + but can't make it larger (or it can raise min match len outside cache but can't make it shorter). + + For help on setting up OodleLZ_CompressOptions contact support at oodle@radgametools.com + + NOTE : fields you do not set should always be zero initialized. In particular the _reserved_ fields should be zeroed. + Zero always means "use default" and is a future-portable initialization value. + + If you set fields to zero to mean "use default" you can call $OodleLZ_CompressOptions_Validate to change them + to default values. This is done automatically internally if you don't do it explicitly. + +*/ + +typedef enum OodleLZ_Decode_ThreadPhase +{ + OodleLZ_Decode_ThreadPhase1 = 1, + OodleLZ_Decode_ThreadPhase2 = 2, + OodleLZ_Decode_ThreadPhaseAll = 3, + OodleLZ_Decode_Unthreaded = OodleLZ_Decode_ThreadPhaseAll +} OodleLZ_Decode_ThreadPhase; +/* ThreadPhase for threaded Oodle decode + + Check $OodleLZ_Compressor_CanDecodeThreadPhased + (currently only used by Kraken) + + See $OodleLZ_About_ThreadPhasedDecode + +*/ + +typedef enum OodleLZ_FuzzSafe +{ + OodleLZ_FuzzSafe_No = 0, + OodleLZ_FuzzSafe_Yes = 1 +} OodleLZ_FuzzSafe; +/* OodleLZ_FuzzSafe (deprecated) + + About fuzz safety: + + Fuzz Safe decodes will not crash on corrupt data. They may or may not return failure, and produce garbage output. + + Fuzz safe decodes will not read out of bounds. They won't put data on the stack or previously in memory + into the output buffer. + + As of Oodle 2.9.0 all compressors supported are fuzzsafe, so OodleLZ_FuzzSafe_Yes should always be used and this + enum is deprecated. + +*/ + +#define OODLELZ_BLOCK_LEN (1<<18) /* The number of raw bytes per "seek chunk" + Seek chunks can be decompressed independently if $(OodleLZ_CompressOptions:seekChunkReset) is set. +*/ + +#define OODLELZ_BLOCK_MAXIMUM_EXPANSION (2) +#define OODLELZ_BLOCK_MAX_COMPLEN (OODLELZ_BLOCK_LEN+OODLELZ_BLOCK_MAXIMUM_EXPANSION) /* Maximum expansion per $OODLELZ_BLOCK_LEN is 1 byte. + Note that the compressed buffer must be allocated bigger than this (use $OodleLZ_GetCompressedBufferSizeNeeded) +*/ + +#define OODLELZ_QUANTUM_LEN (1<<14) /* Minimum decompression quantum (for old legacy codecs only) + + Deprecated. + + The new sea monster family of compressors use a whole block quantum (OODLELZ_BLOCK_LEN). + Check $OodleLZ_Compressor_UsesWholeBlockQuantum +*/ + +// 5 byte expansion per-quantum with CRC's +#define OODLELZ_QUANTUM_MAXIMUM_EXPANSION (5) + +#define OODLELZ_QUANTUM_MAX_COMPLEN (OODLELZ_QUANTUM_LEN+OODLELZ_QUANTUM_MAXIMUM_EXPANSION) + +#define OODLELZ_SEEKCHUNKLEN_MIN OODLELZ_BLOCK_LEN +#define OODLELZ_SEEKCHUNKLEN_MAX (1<<29) // half GB + +typedef OOSTRUCT OodleLZ_DecodeSome_Out +{ + OO_S32 decodedCount; // number of uncompressed bytes decoded + OO_S32 compBufUsed; // number of compressed bytes consumed + + + OO_S32 curQuantumRawLen; // tells you the current quantum size. you must have at least this much room available in the output buffer to be able to decode anything. + OO_S32 curQuantumCompLen; // if you didn't pass in enough data, nothing will decode (decodedCount will be 0), and this will tell you how much is needed +} OodleLZ_DecodeSome_Out; +/* Output value of $OodleLZDecoder_DecodeSome +*/ + +//--------------------------------------------- + +//======================================================= + +typedef OOSTRUCT OodleLZ_SeekTable +{ + OodleLZ_Compressor compressor; // which compressor was used + OO_BOOL seekChunksIndependent; // are the seek chunks independent, or must they be decompressed in sequence + + OO_S64 totalRawLen; // total uncompressed data lenth + OO_S64 totalCompLen; // sum of seekChunkCompLens + + OO_S32 numSeekChunks; // derived from rawLen & seekChunkLen + OO_S32 seekChunkLen; // multiple of OODLELZ_BLOCK_LEN + + OO_U32 * seekChunkCompLens; // array of compressed lengths of seek chunks + OO_U32 * rawCRCs; // crc of the raw bytes of the chunk (optional; NULL unless $OodleLZSeekTable_Flags_MakeRawCRCs was specified) +} OodleLZ_SeekTable; + +typedef enum OodleLZSeekTable_Flags +{ + OodleLZSeekTable_Flags_None = 0, // default + OodleLZSeekTable_Flags_MakeRawCRCs = 1, // make the _rawCRCs_ member of $OodleLZ_SeekTable + OodleLZSeekTable_Flags_Force32 = 0x40000000 +} OodleLZSeekTable_Flags; + +//===================================================== + + +typedef OOSTRUCT OodleConfigValues +{ + OO_S32 m_OodleLZ_LW_LRM_step; // LZHLW LRM : bytes between LRM entries + OO_S32 m_OodleLZ_LW_LRM_hashLength; // LZHLW LRM : bytes hashed for each LRM entries + OO_S32 m_OodleLZ_LW_LRM_jumpbits; // LZHLW LRM : bits of hash used for jump table + + OO_S32 m_OodleLZ_Decoder_Max_Stack_Size; // if OodleLZ_Decompress needs to allocator a Decoder object, and it's smaller than this size, it's put on the stack instead of the heap + OO_S32 m_OodleLZ_Small_Buffer_LZ_Fallback_Size_Unused; // deprecated + OO_S32 m_OodleLZ_BackwardsCompatible_MajorVersion; // if you need to encode streams that can be read with an older version of Oodle, set this to the Oodle2 MAJOR version number that you need compatibility with. eg to be compatible with oodle 2.7.3 you would put 7 here + + OO_U32 m_oodle_header_version; // = OODLE_HEADER_VERSION + +} OodleConfigValues; +/* OodleConfigValues + + Struct of user-settable low level config values. See $Oodle_SetConfigValues. + + May have different defaults per platform. +*/ + +OOFUNC1 void OOFUNC2 Oodle_GetConfigValues(OodleConfigValues * ptr); +/* Get $OodleConfigValues + + $:ptr filled with OodleConfigValues + + Gets the current $OodleConfigValues. + + May be different per platform. +*/ + +OOFUNC1 void OOFUNC2 Oodle_SetConfigValues(const OodleConfigValues * ptr); +/* Set $OodleConfigValues + + $:ptr your desired OodleConfigValues + + Sets the global $OodleConfigValues from your struct. + + You should call $Oodle_GetConfigValues to fill the struct, then change the values you + want to change, then call $Oodle_SetConfigValues. + + This should generally be done before doing anything with Oodle (eg. even before OodleX_Init). + Changing OodleConfigValues after Oodle has started has undefined effects. +*/ + +typedef enum Oodle_UsageWarnings +{ + Oodle_UsageWarnings_Enabled = 0, + Oodle_UsageWarnings_Disabled = 1, + Oodle_UsageWarnings_Force32 = 0x40000000 +} Oodle_UsageWarnings; +/* Whether Oodle usage warnings are enable or disabled. */ + +OOFUNC1 void OOFUNC2 Oodle_SetUsageWarnings(Oodle_UsageWarnings state); +/* Enables or disables Oodle usage warnings. + + $:state whether usage warnings should be enabled or disabled. + + Usage warnings are enabled by default and try to be low-noise, but in case you want to + disable them, this is how. + + This should generally be done once at startup. Setting this state while there are Oodle + calls running on other threads has undefined results. +*/ + +// function pointers to mallocs needed : + +OODEFFUNC typedef void * (OODLE_CALLBACK t_fp_OodleCore_Plugin_MallocAligned)( OO_SINTa bytes, OO_S32 alignment); +/* Function pointer type for OodleMallocAligned + + $:bytes number of bytes to allocate + $:alignment required alignment of returned pointer + $:return pointer to memory allocated (must not be NULL) + + _alignment_ will always be a power of two + + _alignment_ will always be >= $OODLE_MALLOC_MINIMUM_ALIGNMENT + +*/ + +OODEFFUNC typedef void (OODLE_CALLBACK t_fp_OodleCore_Plugin_Free)( void * ptr ); +/* Function pointer type for OodleFree + + $:return pointer to memory to free + +*/ + +OOFUNC1 void OOFUNC2 OodleCore_Plugins_SetAllocators( + t_fp_OodleCore_Plugin_MallocAligned * fp_OodleMallocAligned, + t_fp_OodleCore_Plugin_Free * fp_OodleFree); +/* Set the function pointers for allocation needed by Oodle2 Core + + If these are not set, the default implementation on most platforms uses the C stdlib. + On Microsoft platforms the default implementation uses HeapAlloc. + + These must not be changed once they are set! Set them once then don't change them. + + NOTE: if you are using Oodle Ext, do NOT call this. OodleX_Init will install an allocator for Oodle Core. Do not mix your own allocator with the OodleX allocator. See $OodleXAPI_Malloc. + + If you want to ensure that Oodle is not doing any allocations, you can call OodleCore_Plugins_SetAllocators(NULL,NULL); + If you do that, then any time Oodle needs to allocate memory internally, it will stop the process. + It is STRONGLY not recommended that you ship that way. You can verify that Oodle is not allocating, but then leave some + fallback allocator installed when you actually ship just in case. + + Also note that on many consoles the standard allocation practices may not + leave much heap memory for the C stdlib malloc. In this case Oodle may fail to allocate. + +*/ + +OODEFFUNC typedef OO_U64 (OODLE_CALLBACK t_fp_OodleCore_Plugin_RunJob)( t_fp_Oodle_Job * fp_job, void * job_data , OO_U64 * dependencies, int num_dependencies, void * user_ptr ); +/* Function pointer type for OodleCore_Plugins_SetJobSystem + + $:dependencies array of handles of other pending jobs. All guaranteed to be nonzero. + $:num_dependencies number of dependencies. Guaranteed to be no more than OODLE_JOB_MAX_DEPENDENCIES. + $:user_ptr is passed through from the OodleLZ_CompressOptions. + $:return handle to the async job, or 0 if it was run synchronously + + RunJob will call fp_job(job_data) + + it may be done on a thread, or it may run the function synchronously and return 0, indicating the job is already done. + The returned OO_U64 is a handle passed to WaitJob, unless it is 0, in which case WaitJob won't get called. + + fp_job should not run until all the dependencies are done. This function should not delete the dependencies. + + RunJob must be callable from within an Oodle Job, i.e. jobs may spawn their own sub-jobs directly. + However, the matching WaitJob calls will only ever occur on the thread that called the + internally threaded Oodle API function. + + See $Oodle_About_Job_Threading_Plugins +*/ + +OODEFFUNC typedef void (OODLE_CALLBACK t_fp_OodleCore_Plugin_WaitJob)( OO_U64 job_handle, void * user_ptr ); +/* Function pointer type for OodleCore_Plugins_SetJobSystem + + $:job_handle a job handle returned from RunJob. Never 0. + $:user_ptr is passed through from the OodleLZ_CompressOptions. + + Waits until the job specified by job_handle is done and cleans up any associated resources. Oodle + will call WaitJob exactly once for every RunJob call that didn't return 0. + + If job_handle was already completed, this should clean it up without waiting. + + A handle value should not be reused by another RunJob until WaitJob has been done with that value. + + WaitJob will not be called from running jobs. It will be only be called from the original thread that + invoked Oodle. If you are running Oodle from a worker thread, ensure that that thread is allowed to wait + on other job threads. + + See $Oodle_About_Job_Threading_Plugins +*/ + +OOFUNC1 void OOFUNC2 OodleCore_Plugins_SetJobSystem( + t_fp_OodleCore_Plugin_RunJob * fp_RunJob, + t_fp_OodleCore_Plugin_WaitJob * fp_WaitJob); +/* DEPRECATED use OodleCore_Plugins_SetJobSystemAndCount instead + + See $OodleCore_Plugins_SetJobSystemAndCount +*/ + + +OOFUNC1 void OOFUNC2 OodleCore_Plugins_SetJobSystemAndCount( + t_fp_OodleCore_Plugin_RunJob * fp_RunJob, + t_fp_OodleCore_Plugin_WaitJob * fp_WaitJob, + int target_parallelism); +/* Set the function pointers for async job system needed by Oodle2 Core + + $:fp_RunJob pointer to RunJob function + $:fp_WaitJob pointer to WaitJob function + $:target_parallelism goal of number of jobs to run simultaneously + + If these are not set, the default implementation runs jobs synchronously on the calling thread. + + These must not be changed once they are set! Set them once then don't change them. + + _target_parallelism_ allows you to tell Oodle how many Jobs it should try to keep in flight at once. + Depending on the operation it may not be able to split work into this many jobs (so fewer will be used), + but it will not exceed this count. + + For Oodle Data LZ work, typically _target_parallelism_ is usually best at the number of hardware cores + not including hyper threads). + + For Oodle Texture BCN encoding work, _target_parallelism_ is usually best as the full number of hyper cores. + + In some cases you may wish to reduce _target_parallelism_ by 1 or 2 cores to leave some of the CPU free for + other work. + + For example on a CPU with 16 cores and 32 hardware threads, for LZ work you might set _target_parallelism_ to 15 + when calling OodleCorePlugins. For BC7 encoding you might set _target_parallelism_ to 30 when calling OodleTexPlugins. + + NOTE : if you are using Oodle Ext, do NOT call this. OodleX_Init will install a job system for Oodle Core. + Note OodleX only installs automatically to Oodle Core, not Net or Tex. See example_jobify.cpp for manual + plugin. + + Replaces deprecated $OodleCore_Plugins_SetJobSystem + + See $Oodle_About_Job_Threading_Plugins +*/ + +// the main func pointer for log : +OODEFFUNC typedef void (OODLE_CALLBACK t_fp_OodleCore_Plugin_Printf)(int verboseLevel,const char * file,int line,const char * fmt,...); +/* Function pointer to Oodle Core printf + + $:verboseLevel verbosity of the message; 0-2 ; lower = more important + $:file C file that sent the message + $:line C line that sent the message + $:fmt vararg printf format string + + The logging function installed here must parse varargs like printf. + + _verboseLevel_ may be used to omit verbose messages. +*/ + +OOFUNC1 t_fp_OodleCore_Plugin_Printf * OOFUNC2 OodleCore_Plugins_SetPrintf(t_fp_OodleCore_Plugin_Printf * fp_rrRawPrintf); +/* Install the callback used by Oodle Core for logging + + $:fp_rrRawPrintf function pointer to your log function; may be NULL to disable all logging + $:return returns the previous function pointer + + Use this function to install your own printf for Oodle Core. + + The default implementation in debug builds, if you install nothing, uses the C stdio printf for logging. + On Microsoft platforms, it uses OutputDebugString and not stdio. + + To disable all logging, call OodleCore_Plugins_SetPrintf(NULL) + + WARNING : this function is NOT thread safe! It should be done only once and done in a place where the caller can guarantee thread safety. + + In the debug build of Oodle, you can install OodleCore_Plugin_Printf_Verbose to get more verbose logging + +*/ + +OODEFFUNC typedef OO_BOOL (OODLE_CALLBACK t_fp_OodleCore_Plugin_DisplayAssertion)(const char * file,const int line,const char * function,const char * message); +/* Function pointer to Oodle Core assert callback + + $:file C file that triggered the assert + $:line C line that triggered the assert + $:function C function that triggered the assert (may be NULL) + $:message assert message + $:return true to break execution at the assertion site, false to continue + + This callback is called by Oodle Core when it detects an assertion condition. + + This will only happen in debug builds. + + +*/ + +OOFUNC1 t_fp_OodleCore_Plugin_DisplayAssertion * OOFUNC2 OodleCore_Plugins_SetAssertion(t_fp_OodleCore_Plugin_DisplayAssertion * fp_rrDisplayAssertion); +/* Install the callback used by Oodle Core for asserts + + $:fp_rrDisplayAssertion function pointer to your assert display function + $:return returns the previous function pointer + + Use this function to install your own display for Oodle Core assertions. + This will only happen in debug builds. + + The default implementation in debug builds, if you install nothing, uses the C stderr printf for logging, + except on Microsoft platforms where it uses OutputDebugString. + + WARNING : this function is NOT thread safe! It should be done only once and done in a place where the caller can guarantee thread safety. + +*/ + +//============================================================= + + +OOFUNC1 void * OOFUNC2 OodleCore_Plugin_MallocAligned_Default(OO_SINTa size,OO_S32 alignment); +OOFUNC1 void OOFUNC2 OodleCore_Plugin_Free_Default(void * ptr); +OOFUNC1 void OOFUNC2 OodleCore_Plugin_Printf_Default(int verboseLevel,const char * file,int line,const char * fmt,...); +OOFUNC1 void OOFUNC2 OodleCore_Plugin_Printf_Verbose(int verboseLevel,const char * file,int line,const char * fmt,...); +OOFUNC1 OO_BOOL OOFUNC2 OodleCore_Plugin_DisplayAssertion_Default(const char * file,const int line,const char * function,const char * message); +OOFUNC1 OO_U64 OOFUNC2 OodleCore_Plugin_RunJob_Default( t_fp_Oodle_Job * fp_job, void * job_data, OO_U64 * dependencies, int num_dependencies, void * user_ptr ); +OOFUNC1 void OOFUNC2 OodleCore_Plugin_WaitJob_Default( OO_U64 job_handle, void * user_ptr ); + +//============================================================= + +//---------------------------------------------- +// OodleLZ + +#define OODLELZ_FAILED (0) /* Return value of OodleLZ_Decompress on failure +*/ + +//======================================================= + +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_Compress(OodleLZ_Compressor compressor, + const void * rawBuf,OO_SINTa rawLen,void * compBuf, + OodleLZ_CompressionLevel level, + const OodleLZ_CompressOptions * pOptions OODEFAULT(NULL), + const void * dictionaryBase OODEFAULT(NULL), + const void * lrm OODEFAULT(NULL), + void * scratchMem OODEFAULT(NULL), + OO_SINTa scratchSize OODEFAULT(0) ); +/* Compress some data from memory to memory, synchronously, with OodleLZ + + $:compressor which OodleLZ variant to use in compression + $:rawBuf raw data to compress + $:rawLen number of bytes in rawBuf to compress + $:compBuf pointer to write compressed data to ; should be at least $OodleLZ_GetCompressedBufferSizeNeeded + $:level OodleLZ_CompressionLevel controls how much CPU effort is put into maximizing compression + $:pOptions (optional) options; if NULL, $OodleLZ_CompressOptions_GetDefault is used + $:dictionaryBase (optional) if not NULL, provides preceding data to prime the dictionary; must be contiguous with rawBuf, the data between the pointers _dictionaryBase_ and _rawBuf_ is used as the preconditioning data. The exact same precondition must be passed to encoder and decoder. + $:lrm (optional) long range matcher + $:scratchMem (optional) pointer to scratch memory + $:scratchSize (optional) size of scratch memory (see $OodleLZ_GetCompressScratchMemBound) + $:return size of compressed data written, or $OODLELZ_FAILED for failure + + Performs synchronous memory to memory LZ compression. + + In tools, you should generally use $OodleXLZ_Compress_AsyncAndWait instead to get parallelism. (in the Oodle2 Ext lib) + + You can compress a large buffer in several calls by setting _dictionaryBase_ to the start + of the buffer, and then making _rawBuf_ and _rawLen_ select portions of that buffer. As long + as _rawLen_ is a multiple of $OODLELZ_BLOCK_LEN , the compressed chunks can simply be + concatenated together. + + If _scratchMem_ is provided, it will be used for the compressor's scratch memory needs before OodleMalloc is + called. If the scratch is big enough, no malloc will be done. If the scratch is not big enough, the compress + will not fail, instead OodleMalloc will be used. OodleMalloc should not return null. There is currently no way + to make compress fail cleanly due to using too much memory, it must either succeed or abort the process. + + If _scratchSize_ is at least $OodleLZ_GetCompressScratchMemBound , additional allocations will not be needed. + + See $OodleLZ_About for tips on setting the compression options. + + If _dictionaryBase_ is provided, the backup distance from _rawBuf_ must be a multiple of $OODLELZ_BLOCK_LEN + + If $(OodleLZ_CompressOptions:seekChunkReset) is enabled, and _dictionaryBase_ is not NULL or _rawBuf_ , then the + seek chunk boundaries are relative to _dictionaryBase_, not to _rawBuf_. + +*/ + +// Decompress returns raw (decompressed) len received +// Decompress returns 0 (OODLELZ_FAILED) if it detects corruption +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_Decompress(const void * compBuf,OO_SINTa compBufSize,void * rawBuf,OO_SINTa rawLen, + OodleLZ_FuzzSafe fuzzSafe OODEFAULT(OodleLZ_FuzzSafe_Yes), + OodleLZ_CheckCRC checkCRC OODEFAULT(OodleLZ_CheckCRC_No), + OodleLZ_Verbosity verbosity OODEFAULT(OodleLZ_Verbosity_None), + void * decBufBase OODEFAULT(NULL), + OO_SINTa decBufSize OODEFAULT(0), + OodleDecompressCallback * fpCallback OODEFAULT(NULL), + void * callbackUserData OODEFAULT(NULL), + void * decoderMemory OODEFAULT(NULL), + OO_SINTa decoderMemorySize OODEFAULT(0), + OodleLZ_Decode_ThreadPhase threadPhase OODEFAULT(OodleLZ_Decode_Unthreaded) + ); +/* Decompress a some data from memory to memory, synchronously. + + $:compBuf pointer to compressed data + $:compBufSize number of compressed bytes available (must be greater or equal to the number consumed) + $:rawBuf pointer to output uncompressed data into + $:rawLen number of uncompressed bytes to output + $:fuzzSafe (optional) should the decode fail if it contains non-fuzz safe codecs? + $:checkCRC (optional) if data could be corrupted and you want to know about it, pass OodleLZ_CheckCRC_Yes + $:verbosity (optional) if not OodleLZ_Verbosity_None, logs some info + $:decBufBase (optional) if not NULL, provides preceding data to prime the dictionary; must be contiguous with rawBuf, the data between the pointers _dictionaryBase_ and _rawBuf_ is used as the preconditioning data. The exact same precondition must be passed to encoder and decoder. The decBufBase must be a reset point. + $:decBufSize (optional) size of decode buffer starting at decBufBase, if 0, _rawLen_ is assumed + $:fpCallback (optional) OodleDecompressCallback to call incrementally as decode proceeds + $:callbackUserData (optional) passed as userData to fpCallback + $:decoderMemory (optional) pre-allocated memory for the Decoder, of size _decoderMemorySize_ + $:decoderMemorySize (optional) size of the buffer at _decoderMemory_; must be at least $OodleLZDecoder_MemorySizeNeeded bytes to be used + $:threadPhase (optional) for threaded decode; see $OodleLZ_About_ThreadPhasedDecode (default OodleLZ_Decode_Unthreaded) + $:return the number of decompressed bytes output, $OODLELZ_FAILED (0) if none can be decompressed + + Decodes data encoded with any $OodleLZ_Compressor. + + Note : _rawLen_ must be the actual number of bytes to output, the same as the number that were encoded with the corresponding + OodleLZ_Compress size. You must store this somewhere in your own header and pass it in to this call. _compBufSize_ does NOT + need to be the exact number of compressed bytes, is the number of bytes available in the buffer, it must be greater or equal to + the actual compressed length. + + Note that the new compressors (Kraken,Mermaid,Selkie,BitKnit) are all fuzz safe and you can use OodleLZ_FuzzSafe_Yes + with them and no padding of the decode target buffer. + + If checkCRC is OodleLZ_CheckCRC_Yes, then corrupt data will be detected and the decode aborted. + If checkCRC is OodleLZ_CheckCRC_No, then corruption might result in invalid data, but no detection of any error (garbage in, garbage out). + + If corruption is possible, _fuzzSafe_ is No and _checkCRC_ is OodleLZ_CheckCRC_No, $OodleLZ_GetDecodeBufferSize must be used to allocate + _rawBuf_ large enough to prevent overrun. + + $OodleLZ_GetDecodeBufferSize should always be used to ensure _rawBuf_ is large enough, even when corruption is not + possible (when fuzzSafe is No). + + _compBuf_ and _rawBuf_ are allowed to overlap for "in place" decoding, but then _rawBuf_ must be allocated to + the size given by $OodleLZ_GetInPlaceDecodeBufferSize , and the compressed data must be at the end of that buffer. + + An easy way to take the next step to parallel decoding is with $OodleXLZ_Decompress_MakeSeekTable_Wide_Async (in the Oodle2 Ext lib) + + NOTE : the return value is the *total* number of decompressed bytes output so far. If rawBuf is > decBufBase, that means + the initial inset of (rawBuf - decBufBase) is included! (eg. you won't just get _rawLen_) + + If _decBufBase_ is provided, the backup distance from _rawBuf_ must be a multiple of $OODLELZ_BLOCK_LEN + + About fuzz safety: + + OodleLZ_Decompress is guaranteed not to crash even if the data is corrupted when _fuzzSafe_ is set to OodleLZ_FuzzSafe_Yes. + When _fuzzSafe_ is Yes, the target buffer (_rawBuf_ and _rawLen_) will never be overrun. Note that corrupted data might not + be detected (the return value might indicate success). + + Fuzz Safe decodes will not crash on corrupt data. They may or may not return failure, and produce garbage output. + + Fuzz safe decodes will not read out of bounds. They won't put data on the stack or previously in memory + into the output buffer. + + Fuzz safe decodes will not output more than the uncompressed size. (eg. the output buffer does not need to + be padded like OodleLZ_GetDecodeBufferSize) + + If you ask for a fuzz safe decode and the compressor doesn't satisfy OodleLZ_Compressor_CanDecodeFuzzSafe + then it will return failure. + + The _fuzzSafe_ argument should always be OodleLZ_FuzzSafe_Yes as of Oodle 2.9.0 ; older compressors did not + support fuzz safety but they now all do. + + Use of OodleLZ_FuzzSafe_No is deprecated. + +*/ + + +//------------------------------------------- +// Incremental Decoder functions : + +struct _OodleLZDecoder; +typedef struct _OodleLZDecoder OodleLZDecoder; +/* Opaque type for OodleLZDecoder + + See $OodleLZDecoder_Create +*/ + + +OOFUNC1 OodleLZDecoder * OOFUNC2 OodleLZDecoder_Create(OodleLZ_Compressor compressor,OO_S64 rawLen,void * memory, OO_SINTa memorySize); +/* Create a OodleLZDecoder + + $:compressor the type of data you will decode; use $OodleLZ_Compressor_Invalid if unknown + $:rawLen total raw bytes of the decode + $:memory (optional) provide memory for the OodleLZDecoder object (not the window) + $:memorySize (optional) if memory is provided, this is its size in bytes + $:return the OodleLZDecoder + + If memory is provided, it must be of size $OodleLZDecoder_MemorySizeNeeded. If it is NULL it will be + allocated with the malloc specified by $OodleAPI_OodleCore_Plugins. + + Free with $OodleLZDecoder_Destroy. You should Destroy even if you passed in the memory. + + Providing _compressor_ lets the OodleLZDecoder be the minimum size needed for that type of data. + If you pass $OodleLZ_Compressor_Invalid, then any type of data may be decoded, and the Decoder is allocated + large enought to handle any of them. + + If you are going to pass rawLen to OodleLZDecoder_Reset , then you can pass 0 to rawLen here. + + See $OodleLZDecoder_DecodeSome for more. +*/ + +OOFUNC1 OO_S32 OOFUNC2 OodleLZDecoder_MemorySizeNeeded(OodleLZ_Compressor compressor OODEFAULT(OodleLZ_Compressor_Invalid), OO_SINTa rawLen OODEFAULT(-1)); +/* If you want to provide the memory needed by $OodleLZDecoder_Create , this tells you how big it must be. + + $:compressor the type of data you will decode; use $OodleLZ_Compressor_Invalid if unknown + $:rawLen should almost always be -1, which supports any size of raw data decompression + $:return bytes to allocate or reserve, 0 for failure + + NOTE : using $OodleLZ_Compressor_Invalid lets you decode any time of compressed data. + It requests as much memory as the largest compressor. This may be a *lot* more than your data needs; + try to use the correct compressor type. + + If _rawLen_ is -1 (default) then the Decoder object created can be used on any length of raw data + decompression. If _rawLen_ is specified here, then you can only use it to decode data shorter than + the length you specified here. This use case is very rare, contact support for details. +*/ + +OOFUNC1 OO_S32 OOFUNC2 OodleLZ_ThreadPhased_BlockDecoderMemorySizeNeeded(void); +/* Returns the size of the decoder needed for ThreadPhased decode + + For use with $OodleLZ_Decode_ThreadPhase + See $OodleLZ_About_ThreadPhasedDecode +*/ + +OOFUNC1 void OOFUNC2 OodleLZDecoder_Destroy(OodleLZDecoder * decoder); +/* Pairs with $OodleLZDecoder_Create + + You should always call Destroy even if you provided the memory for $OodleLZDecoder_Create +*/ + +// Reset decoder - can reset to the start of any OODLELZ_BLOCK_LEN chunk +OOFUNC1 OO_BOOL OOFUNC2 OodleLZDecoder_Reset(OodleLZDecoder * decoder, OO_SINTa decPos, OO_SINTa decLen OODEFAULT(0)); +/* Reset an OodleLZDecoder to restart at given pos + + $:decoder the OodleLZDecoder, made by $OodleLZDecoder_Create + $:decPos position to reset to; must be a multiple of OODLELZ_BLOCK_LEN + $:decLen (optional) if not zero, change the length of the data we expect to decode + $:return true for success + + If you are seeking in a packed stream, you must seek to a seek chunk reset point, as was made at compress time. + + That is, $(OodleLZ_CompressOptions:seekChunkReset) must have been true, and + _decPos_ must be a multiple of $(OodleLZ_CompressOptions:seekChunkLen) that was used at compress time. + + You can use $OodleLZ_GetChunkCompressor to verify that you are at a valid + independent chunk start point. + +*/ + +// returns false if corruption detected +OOFUNC1 OO_BOOL OOFUNC2 OodleLZDecoder_DecodeSome( + OodleLZDecoder * decoder, + OodleLZ_DecodeSome_Out * out, + + // the decode sliding window : we output here & read from this for matches + void * decBuf, + OO_SINTa decBufPos, + OO_SINTa decBufferSize, // decBufferSize should be the result of OodleLZDecoder_MakeDecodeBufferSize() + OO_SINTa decBufAvail, // usually Size - Pos, but maybe less if you have pending IO flushes + + // compressed data : + const void * compPtr, + OO_SINTa compAvail, + + OodleLZ_FuzzSafe fuzzSafe OODEFAULT(OodleLZ_FuzzSafe_No), + OodleLZ_CheckCRC checkCRC OODEFAULT(OodleLZ_CheckCRC_No), + OodleLZ_Verbosity verbosity OODEFAULT(OodleLZ_Verbosity_None), + OodleLZ_Decode_ThreadPhase threadPhase OODEFAULT(OodleLZ_Decode_Unthreaded) + + ); +/* Incremental decode some LZ compressed data + + $:decoder the OodleLZDecoder, made by $OodleLZDecoder_Create + $:out filled with results + $:decBuf the decode buffer (window) + $:decBufPos the current position in the buffer + $:decBufferSize size of decBuf ; this must be either equal to the total decompressed size (_rawLen_ passed to $OodleLZDecoder_Create) or the result of $OodleLZDecoder_MakeValidCircularWindowSize + $:decBufAvail the number of bytes available after decBufPos in decBuf ; usually (decBufferSize - decBufPos), but can be less + $:compPtr pointer to compressed data to read + $:compAvail number of compressed bytes available at compPtr + $:fuzzSafe (optional) should the decode be fuzz safe + $:checkCRC (optional) if data could be corrupted and you want to know about it, pass OodleLZ_CheckCRC_Yes + $:verbosity (optional) if not OodleLZ_Verbosity_None, logs some info + $:threadPhase (optional) for threaded decode; see $OodleLZ_About_ThreadPhasedDecode (default OodleLZ_Decode_Unthreaded) + $:return true if success, false if invalid arguments or data is encountered + + Decodes data encoded with an OodleLZ compressor. + + Decodes an integer number of quanta; quanta are $OODLELZ_QUANTUM_LEN uncompressed bytes. + + _decBuf_ can either be a circular window or the whole _rawLen_ array. + In either case, _decBufPos_ should be in the range [0,_decBufferSize_). + If _decBuf_ is a circular window, then _decBufferSize_ should come from $OodleLZDecoder_MakeValidCircularWindowSize. + + (circular windows are deprecated as of 2.9.0) + + NOTE : all the new LZ codecs (Kraken, etc.) do not do circular windows. They can do sliding windows, see lz_test_11 in $example_lz. + They should always have decBufferSize = total raw size, even if the decode buffer is smaller than that. + + NOTE : insufficient data provided (with _compAvail_ > 0 but not enough to decode a quantum) is a *success* case + (return value of true), even though nothing is decoded. A return of false always indicates a non-recoverable error. + + If _decBufAvail_ or _compAvail_ is insufficient for any decompression, the "curQuantum" fields of $OodleLZ_DecodeSome_Out + will tell you how much you must provide to proceed. That is, if enough compressed bytes are provided to get a quantum header, but not enough to decode a quantum, this + function returns true and fills out the $OodleLZ_DecodeSome_Out structure with the size of the quantum. + + See $OodleLZ_Decompress about fuzz safety. + + NOTE : DecodeSome expect to decode either one full quantum (of len $OODLELZ_QUANTUM_LEN) or up to the length of the total buffer specified in the +call to $OodleLZDecoder_Create or $OodleLZDecoder_Reset. That total buffer length +must match what was use during compression (or be a seek-chunk portion thereof). +That is, you cannot decompress partial streams in intervals smaller than +$OODLELZ_QUANTUM_LEN except for the final partial quantum at the end of the stream. + +*/ + +// pass in how much you want to alloc and it will tell you a valid size as close that as possible +// the main use is just to call OodleLZDecoder_MakeDecodeBufferSize(0) to get the min size; the min size is a good size +OOFUNC1 OO_S32 OOFUNC2 OodleLZDecoder_MakeValidCircularWindowSize(OodleLZ_Compressor compressor,OO_S32 minWindowSize OODEFAULT(0)); +/* Get a valid "Window" size for an LZ + + $:compressor which compressor you will be decoding + $:minWindowSize (optional) minimum size of the window + + NOTE: circular windows are deprecated as of 2.9.0 + + Most common usage is OodleLZDecoder_MakeValidCircularWindowSize(0) to get the minimum window size. + + Only compressors which pass $OodleLZ_Compressor_CanDecodeInCircularWindow can be decoded in a circular window. + + WARNING : this is NOT the size to malloc the window! you need to call $OodleLZ_GetDecodeBufferSize() and + pass in the window size to get the malloc size. +*/ + +//======================================================= + +//======================================================= +// remember if you want to IO the SeekEntries you need to make them endian-independent +// see WriteOOZHeader for example + +#define OODLELZ_SEEKPOINTCOUNT_DEFAULT 16 + +OOFUNC1 OO_S32 OOFUNC2 OodleLZ_MakeSeekChunkLen(OO_S64 rawLen, OO_S32 desiredSeekPointCount); +/* Compute a valid seekChunkLen + + $:rawLen total length of uncompressed data + $:desiredSeekPointCount desired number of seek chunks + $:return a valid seekChunkLen for use in $OodleLZ_CreateSeekTable + + Returns a seekChunkLen which is close to (rawLen/desiredSeekPointCount) but is a power of two multiple of $OODLELZ_BLOCK_LEN + + _desiredSeekPointCount_ = 16 is good for parallel decompression. + (OODLELZ_SEEKPOINTCOUNT_DEFAULT) +*/ + +OOFUNC1 OO_S32 OOFUNC2 OodleLZ_GetNumSeekChunks(OO_S64 rawLen, OO_S32 seekChunkLen); +/* Compute the number of seek chunks + + $:rawLen total length of uncompressed data + $:seekChunkLen the length of a seek chunk (eg from $OodleLZ_MakeSeekChunkLen) + $:return the number of seek chunks + + returns (rawLen+seekChunkLen-1)/seekChunkLen +*/ + +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetSeekTableMemorySizeNeeded(OO_S32 numSeekChunks,OodleLZSeekTable_Flags flags); +/* Tells you the size in bytes to allocate the seekTable before calling $OodleLZ_FillSeekTable + + $:numSeekChunks number of seek chunks (eg from $OodleLZ_GetNumSeekChunks) + $:flags options that will be passed to $OodleLZ_CreateSeekTable + $:return size in bytes of memory needed for seek table + + If you wish to provide the memory for the seek table yourself, you may call this to get the required size, + allocate the memory, and then simply point a $OodleLZ_SeekTable at your memory. + Then use $OodleLZ_FillSeekTable to fill it out. + + Do NOT use sizeof(OodleLZ_SeekTable) ! +*/ + +OOFUNC1 OO_BOOL OOFUNC2 OodleLZ_FillSeekTable(OodleLZ_SeekTable * pTable,OodleLZSeekTable_Flags flags,OO_S32 seekChunkLen,const void * rawBuf, OO_SINTa rawLen,const void * compBuf,OO_SINTa compLen); +/* scan compressed LZ stream to fill the seek table + + $:pTable pointer to table to be filled + $:flags options + $:seekChunkLen the length of a seek chunk (eg from $OodleLZ_MakeSeekChunkLen) + $:rawBuf (optional) uncompressed buffer; used to compute the _rawCRCs_ member of $OodleLZ_SeekTable + $:rawLen size of rawBuf + $:compBuf compressed buffer + $:compLen size of compBuf + $:return true for success + + _pTable_ must be able to hold at least $OodleLZ_GetSeekTableMemorySizeNeeded + + _seekChunkLen_ must be a multiple of $OODLELZ_BLOCK_LEN. + _seekChunkLen_ must match what was in CompressOptions when the buffer was made, or any integer multiple thereof. +*/ + + +OOFUNC1 OodleLZ_SeekTable * OOFUNC2 OodleLZ_CreateSeekTable(OodleLZSeekTable_Flags flags,OO_S32 seekChunkLen,const void * rawBuf, OO_SINTa rawLen,const void * compBuf,OO_SINTa compLen); +/* allocate a table, then scan compressed LZ stream to fill the seek table + + $:flags options + $:seekChunkLen the length of a seek chunk (eg from $OodleLZ_MakeSeekChunkLen) + $:rawBuf (optional) uncompressed buffer; used to compute the _rawCRCs_ member of $OodleLZ_SeekTable + $:rawLen size of rawBuf + $:compBuf compressed buffer + $:compLen size of compBuf + $:return pointer to table if succeeded, null if failed + + Same as $OodleLZ_FillSeekTable , but allocates the memory for you. Use $OodleLZ_FreeSeekTable to free. + + _seekChunkLen_ must be a multiple of $OODLELZ_BLOCK_LEN. + _seekChunkLen_ must match what was in CompressOptions when the buffer was made, or any integer multiple thereof. + +*/ + +OOFUNC1 void OOFUNC2 OodleLZ_FreeSeekTable(OodleLZ_SeekTable * pTable); +/* Frees a table allocated by $OodleLZ_CreateSeekTable +*/ + +OOFUNC1 OO_BOOL OOFUNC2 OodleLZ_CheckSeekTableCRCs(const void * rawBuf,OO_SINTa rawLen, const OodleLZ_SeekTable * seekTable); +/* Check the CRC's in seekTable vs rawBuf + + $:rawBuf uncompressed buffer + $:rawLen size of rawBuf + $:seekTable result of $OodleLZ_CreateSeekTable + $:return true if the CRC's check out + + Note that $OodleLZ_Decompress option of $OodleLZ_CheckCRC checks the CRC of *compressed* data, + this call checks the CRC of the *raw* (uncompressed) data. + + OodleLZ data contains a CRC of the compressed data if it was made with $(OodleLZ_CompressOptions:sendQuantumCRCs). + The SeekTable contains a CRC of the raw data if it was made with $OodleLZSeekTable_Flags_MakeRawCRCs. + + Checking the CRC of compressed data is faster, but does not verify that the decompress succeeded. +*/ + +OOFUNC1 OO_S32 OOFUNC2 OodleLZ_FindSeekEntry( OO_S64 rawPos, const OodleLZ_SeekTable * seekTable); +/* Find the seek entry that contains a raw position + + $:rawPos uncompressed position to look for + $:seekTable result of $OodleLZ_CreateSeekTable + $:return a seek entry index + + returns the index of the chunk that contains _rawPos_ +*/ + +OOFUNC1 OO_S64 OOFUNC2 OodleLZ_GetSeekEntryPackedPos( OO_S32 seekI , const OodleLZ_SeekTable * seekTable ); +/* Get the compressed position of a seek entry + + $:seekI seek entry index , in [0,numSeekEntries) + $:seekTable result of $OodleLZ_CreateSeekTable + $:return compressed buffer position of the start of this seek entry + + +*/ + +//============================================================= + +OOFUNC1 const char * OOFUNC2 OodleLZ_CompressionLevel_GetName(OodleLZ_CompressionLevel compressSelect); +/* Provides a string naming a $OodleLZ_CompressionLevel compressSelect +*/ + +OOFUNC1 const char * OOFUNC2 OodleLZ_Compressor_GetName(OodleLZ_Compressor compressor); +/* Provides a string naming a $OodleLZ_Compressor compressor +*/ + +OOFUNC1 const char * OOFUNC2 OodleLZ_Jobify_GetName(OodleLZ_Jobify jobify); +/* Provides a string naming a $OodleLZ_Jobify enum +*/ + +OOFUNC1 const OodleLZ_CompressOptions * OOFUNC2 OodleLZ_CompressOptions_GetDefault( + OodleLZ_Compressor compressor OODEFAULT(OodleLZ_Compressor_Invalid), + OodleLZ_CompressionLevel lzLevel OODEFAULT(OodleLZ_CompressionLevel_Normal)); +/* Provides a pointer to default compression options + + $:compressor deprecated, ignored + $:lzLevel deprecated, ignored + + Use to fill your own $OodleLZ_CompressOptions then change individual fields. + +*/ + +// after you fiddle with options, call this to ensure they are allowed +OOFUNC1 void OOFUNC2 OodleLZ_CompressOptions_Validate(OodleLZ_CompressOptions * pOptions); +/* Clamps the values in _pOptions_ to be in valid range + +*/ + +// inline functions for compressor property queries +OODEFSTART + +OO_BOOL OodleLZ_Compressor_UsesWholeBlockQuantum(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor is "whole block quantum" ; must decode in steps of + $OODLELZ_BLOCK_LEN , not $OODLELZ_QUANTUM_LEN like others. +*/ +OO_BOOL OodleLZ_Compressor_UsesLargeWindow(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor is "LargeWindow" or not, meaning it can benefit from + a Long-Range-Matcher and windows larger than $OODLELZ_BLOCK_LEN +*/ +OO_BOOL OodleLZ_Compressor_CanDecodeInCircularWindow(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor can be decoded using a fixed size circular window. + deprecated as of 2.9.0 +*/ +OO_BOOL OodleLZ_Compressor_CanDecodeThreadPhased(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor can be used with the $OodleLZ_Decode_ThreadPhase. + + See $OodleLZ_About_ThreadPhasedDecode +*/ +OO_BOOL OodleLZ_Compressor_CanDecodeInPlace(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor can be used with "in-place" decoding. + + This is now always true (all compressors support in-place decoding). The function is left + for backward compatibility. + + All compressors in the future will support in-place, you don't need to check this property. + +*/ +OO_BOOL OodleLZ_Compressor_MustDecodeWithoutResets(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor must decode contiguous ranges of buffer with the same Decoder. + + That is, most of the compressors can be Reset and restart on any block, not just seek blocks, + as long as the correct window data is provided. That is, if this returns false then the only + state required across a non-reset block is the dictionary of previously decoded data. + + But if OodleLZ_Compressor_MustDecodeWithoutResets returns true, then you cannot do that, + because the Decoder object must carry state across blocks (except reset blocks). + + This does not apply to seek points - you can always reset and restart decompression at a seek point. +*/ +OO_BOOL OodleLZ_Compressor_CanDecodeFuzzSafe(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor is "fuzz safe" which means it can accept corrupted data + and won't crash or overrun any buffers. +*/ + +OO_BOOL OodleLZ_Compressor_RespectsDictionarySize(OodleLZ_Compressor compressor); +/* OodleLZ_Compressor properties helper. + + Tells you if this compressor obeys $(OodleLZ_CompressOptions:dictionarySize) which limits + match references to a finite bound. (eg. for sliding window decompression). + + All the new codecs do (Kraken,Mermaid,Selkie,Leviathan). Some old codecs don't. +*/ +//===================================================================== + +#define OODLELZ_COMPRESSOR_MASK(c) (((OO_U32)1)<<((OO_S32)(c))) +// OODLELZ_COMPRESSOR_BOOLBIT : extract a value of 1 or 0 so it maps to "bool" +#define OODLELZ_COMPRESSOR_BOOLBIT(s,c) (((s)>>(OO_S32)(c))&1) + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_IsNewLZFamily(OodleLZ_Compressor compressor) +{ + const OO_U32 set = + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Kraken) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Leviathan) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Mermaid) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Selkie) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Hydra); + return OODLELZ_COMPRESSOR_BOOLBIT(set,compressor); +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_CanDecodeFuzzSafe(OodleLZ_Compressor compressor) +{ + #ifdef OODLE_ALLOW_DEPRECATED_COMPRESSORS + const OO_U32 set = + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_None) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Kraken) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Leviathan) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Mermaid) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Selkie) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Hydra) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_BitKnit) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZB16); + return OODLELZ_COMPRESSOR_BOOLBIT(set,compressor); + #else + // all new compressors are fuzz safe + return compressor != OodleLZ_Compressor_Invalid; + #endif +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_RespectsDictionarySize(OodleLZ_Compressor compressor) +{ + #ifdef OODLE_ALLOW_DEPRECATED_COMPRESSORS + const OO_U32 set = + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_None) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Kraken) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Leviathan) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Mermaid) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Selkie) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_Hydra) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZNA) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_BitKnit); + return OODLELZ_COMPRESSOR_BOOLBIT(set,compressor); + #else + // all new compressors respect dictionarySize + return compressor != OodleLZ_Compressor_Invalid; + #endif +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_UsesWholeBlockQuantum(OodleLZ_Compressor compressor) +{ + return OodleLZ_Compressor_IsNewLZFamily(compressor); +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_CanDecodeThreadPhased(OodleLZ_Compressor compressor) +{ + return OodleLZ_Compressor_IsNewLZFamily(compressor); +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_CanDecodeInPlace(OodleLZ_Compressor compressor) +{ + // all compressors can now decode in place : + return compressor != OodleLZ_Compressor_Invalid; +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_CanDecodeInCircularWindow(OodleLZ_Compressor compressor) +{ + #ifdef OODLE_ALLOW_DEPRECATED_COMPRESSORS + const OO_U32 set = + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZH) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZB16); + #else + const OO_U32 set = 0; + #endif + + return OODLELZ_COMPRESSOR_BOOLBIT(set,compressor); +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_UsesLargeWindow(OodleLZ_Compressor compressor) +{ + // all but LZH and LZB16 now are large window + return ! OodleLZ_Compressor_CanDecodeInCircularWindow(compressor); +} + +OOINLINEFUNC OO_BOOL OodleLZ_Compressor_MustDecodeWithoutResets(OodleLZ_Compressor compressor) +{ + #ifdef OODLE_ALLOW_DEPRECATED_COMPRESSORS + const OO_U32 set = + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_BitKnit) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZA) | + OODLELZ_COMPRESSOR_MASK(OodleLZ_Compressor_LZNA); + #else + const OO_U32 set = 0; + #endif + + return OODLELZ_COMPRESSOR_BOOLBIT(set,compressor); +} + +OODEFEND + +//======================================================= + + +#define OODLELZ_SCRATCH_MEM_NO_BOUND (-1) /* Scratch mem size when bound is unknown. + Installed allocator may be used no matter how much scratch mem you provide. +*/ + +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetCompressScratchMemBound( + OodleLZ_Compressor compressor, + OodleLZ_CompressionLevel level, + OO_SINTa rawLen, + const OodleLZ_CompressOptions * pOptions OODEFAULT(NULL) + ); +/* Return the maximum amount of scratch mem that will be needed by OodleLZ_Compress + + $:compressor which OodleLZ variant to use in compression + $:level OodleLZ_CompressionLevel controls how much CPU effort is put into maximizing compression + $:rawLen maximum number of bytes you will compress (plus dictionary backup) + $:pOptions (optional) options; if NULL, $OodleLZ_CompressOptions_GetDefault is used + + If you pass scratch mem to $OodleLZ_Compress of this size, it is gauranteed to do no allocations. + (normally if it runs out of scratch mem, it falls back to the installed allocator) + + For _rawLen_ pass at least the maximum size you will ever encode. If your data is divided into chunks, + pass the chunk size. If you will encode full buffers of unbounded size, pass -1. + + The options must be the same as when you call $OodleLZ_Compress + + Some options and levels may not have simple finite bounds. Then $OODLELZ_SCRATCH_MEM_NO_BOUND is returned + and the call to $OodleLZ_Compress may use the allocator even if infinite scratch memory is provided. + Currently this applies to all the Optimal levels. + + When OODLELZ_SCRATCH_MEM_NO_BOUND is returned, you can still pass in scratch mem which will be used before + going to the plugin allocator. + +*/ + +// get maximum expanded size for compBuf alloc : +// (note this is actually larger than the maximum compressed stream, it includes trash padding) +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetCompressedBufferSizeNeeded(OodleLZ_Compressor compressor,OO_SINTa rawSize); +/* Return the size you must malloc the compressed buffer + + $:compressor compressor used; OodleLZ_Compressor_Invalid to make it enough for any compressor + $:rawSize uncompressed size you will compress into this buffer + + The _compBuf_ passed to $OodleLZ_Compress must be allocated at least this big. + + note this is actually larger than the maximum size of a compressed stream, it includes overrun padding. + +*/ + +// decBuf needs to be a little larger than rawLen, +// this will tell you exactly how much : +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetDecodeBufferSize(OodleLZ_Compressor compressor,OO_SINTa rawSize,OO_BOOL corruptionPossible); +/* Get the size you must malloc the decode (raw) buffer + + $:compressor compressor used; OodleLZ_Compressor_Invalid to make it enough for any compressor + $:rawSize uncompressed (raw) size without padding + $:corruptionPossible true if it is possible for the decoder to get corrupted data + $:return size of buffer to malloc; slightly larger than rawSize if padding is needed + + As of Oodle 2.9.0 this function is deprecated. For all new codecs you can just use the size of the + uncompressed data for the decode buffer size (_rawSize_), no padding is needed. + + Note that LZB16 is still supported in 2.9.0 but does require padding when used in a circular + window (which is deprecated). + + This padding is necessary for the older compressors when FuzzSafe_No is used. The old compressors + and FuzzSafe_No are no longer supported. + + If _corruptionPossible_ is true, a slightly larger buffer size is returned. + + If _corruptionPossible_ is false, then you must ensure that the decoder does not get corrupted data, + either by passing $OodleLZ_CheckCRC_Yes , or by your own mechanism. + + Note about possible overrun in LZ decoding (applies to the old non-fuzz-safe compressors) : + as long as the compresseddata is not corrupted, + and you decode either the entire compressed buffer, or an integer number of "seek chunks" ($OODLELZ_BLOCK_LEN), + then there will be no overrun. So you can decode LZ data in place and it won't stomp any following bytes. + If those conditions are not true (eg. decoding only part of a larger compressed stream, decoding + around a circular window, decoding data that may be corrupted), then there may be some limited amount of + overrun on decode, as returned by $OodleLZ_GetDecodeBufferSize. + + +*/ + +// OodleLZ_GetInPlaceDecodeBufferSize : +// after compressing, ask how big the in-place buffer needs to be +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetInPlaceDecodeBufferSize(OodleLZ_Compressor compressor,OO_SINTa compLen, OO_SINTa rawLen); +/* Get the size of buffer needed for "in place" decode + + $:compressor compressor used; OodleLZ_Compressor_Invalid to make it enough for any compressor + $:compLen compressed data length + $:rawLen decompressed data length + $:return size of buffer needed for "in place" decode ; slighly larger than rawLen + + To do an "in place" decode, allocate a buffer of this size (or larger). Read the compressed data into the end of + the buffer, and decompress to the front of the buffer. The size returned here guarantees that the writes to the + front of the buffer don't conflict with the reads from the end. + + If _compressor_ is one of the new codecs (Kraken,Mermaid,Selkie,Leviathan), the padding for in place decodes can be + very small indeed. It is assumed you will be passing FuzzSafe_Yes to the decompress call. + + If _compLen_ is unknown, you want an in place buffer size that can accomodate any compressed data, then + pass compLen = 0. + + See $OodleLZ_Decompress for more. +*/ + +// GetCompressedStepForRawStep is at OODLELZ_QUANTUM_LEN granularity +// returns how many packed bytes to step to get the desired raw count step +OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_GetCompressedStepForRawStep( + const void * compPtr, OO_SINTa compAvail, + OO_SINTa startRawPos, OO_SINTa rawSeekBytes, + OO_SINTa * pEndRawPos OODEFAULT(NULL), + OO_BOOL * pIndependent OODEFAULT(NULL) ); +/* How many bytes to step a compressed pointer to advance a certain uncompressed amount + + $:compPtr current compressed pointer + $:compAvail compressed bytes available at compPtr + $:startRawPos initial raw pos (corresponding to compPtr) + $:rawSeekBytes the desired step in raw bytes, must be a multiple of $OODLELZ_QUANTUM_LEN or $OODLELZ_BLOCK_LEN + $:pEndRawPos (optional) filled with the end raw pos actually reached + $:pIndependent (optional) filled with a bool that is true if the current chunk is independent from previous + $:return the number of compressed bytes to step + + You should try to use GetCompressedStepForRawStep only at block granularity - both _startRawPos_ and + _rawSeekBytes_ should be multiples of OODLELZ_BLOCK_LEN (except at the end of the stream). As long as you + do that, then *pEndRawPos will = startRawPos + rawSeekBytes. + + You can use it at quantum granularity (OODLELZ_QUANTUM_LEN), but there are some caveats. You cannot step + quanta inside uncompressed blocks, only in normal LZ blocks. If you try to seek quanta inside an uncompressed + block, you will get *pEndRawPos = the end of the block. + + You can only resume seeking from *pEndRawPos . + + returns 0 for valid not-enough-data case + returns -1 for error + + If _compAvail_ is not the whole compressed buffer, then the returned step may be less than the amount you requested. + eg. if the compressed data in _compAvail_ does not contain enough data to make a step of _rawSeekBytes_ a smaller + step will be taken. + NOTE : *can* return comp step > comp avail ! + + +*/ + +OOFUNC1 OodleLZ_Compressor OOFUNC2 OodleLZ_GetAllChunksCompressor(const void * compBuf,OO_SINTa compBufSize, + OO_SINTa rawLen); +/* ask who compressed all chunks in this buf chunk + + $:compBuf pointer to compressed data; must be the start of compressed buffer, or a step of $OODLELZ_BLOCK_LEN raw bytes + $:compBufSize size of _compBuf_ + $:rawLen rawlen of data in _compBuf_ + $:return the $OodleLZ_Compressor used to encode this chunk + + returns a simple compressor (for example OodleLZ_Compressor_Kraken) if that was used on all chunks + + returns OodleLZ_Compressor_Hydra if different NewLZ encoders were used (for example Kraken+Mermaid) + + returns OodleLZ_Compressor_Count if a heterogenous mix of compressors was used (not just NewLZ) + + returns OodleLZ_Compressor_Invalid on error + + note this is only for this chunk - later chunks may have different compressors (eg. with Hydra) + if you compressed all chunks the same it's up to you to store that info in your header + + returns OodleLZ_Compressor_Invalid if _compBufSize_ is too small or any chunk is corrupt +*/ + +OOFUNC1 OodleLZ_Compressor OOFUNC2 OodleLZ_GetFirstChunkCompressor(const void * compChunkPtr, + OO_SINTa compBufAvail, + OO_BOOL * pIndependent); +/* ask who compressed this chunk + + $:compChunkPtr pointer to compressed data; must be the start of compressed buffer, or a step of $OODLELZ_BLOCK_LEN raw bytes + $:compBufAvail number of bytes at _compChunkPtr_ available to read + $:pIndependent (optional) filled with a bool for whether this chunk is independent of predecessors + $:return the $OodleLZ_Compressor used to encode this chunk + + note this is only for this chunk - later chunks may have different compressors (eg. with Hydra) + if you compressed all chunks the same it's up to you to store that info in your header + + Use $OodleLZ_GetAllChunksCompressor for data that might be mixed compressors. + + This replaces the deprecated function $OodleLZ_GetChunkCompressor + + returns OodleLZ_Compressor_Invalid if _compBufAvail_ is too small or the chunk is corrupt +*/ + +OOFUNC1 OodleLZ_Compressor OOFUNC2 OodleLZ_GetChunkCompressor(const void * compChunkPtr, + OO_SINTa compBufAvail, + OO_BOOL * pIndependent); +/* Deprecated entry point for backwards compatibility + + Use $OodleLZ_GetFirstChunkCompressor or $OodleLZ_GetAllChunksCompressor + +*/ + +//======================================================= + +#define OODLE_HEADER_VERSION ((46<<24)|(OODLE2_VERSION_MAJOR<<16)|(OODLE2_VERSION_MINOR<<8)|(OO_U32)sizeof(OodleLZ_SeekTable)) /* OODLE_HEADER_VERSION is used to ensure the Oodle header matches the lib. Don't copy the value of this macro, it will change when + the header is rev'ed. + + This is what you pass to $OodleX_Init or $Oodle_CheckVersion +*/ + +OOFUNC1 OO_BOOL OOFUNC2 Oodle_CheckVersion(OO_U32 oodle_header_version, OO_U32 * pOodleLibVersion OODEFAULT(NULL)); +/* Check the Oodle lib version against the header you are compiling with + + $:oodle_header_version pass $OODLE_HEADER_VERSION here + $:pOodleLibVersion (optional) filled with the Oodle lib version + $:return false if $OODLE_HEADER_VERSION is not compatible with this lib + + If you use the Oodle2 Ext lib,, $OodleX_Init does it for you. But if you want to check that you have a + compatible lib before trying to Init, then use this. +*/ + +OOFUNC1 void OOFUNC2 Oodle_LogHeader(void); +/* Log the Oodle version & copyright + + Uses the log set with $OodleCore_Plugins_SetPrintf +*/ + +// define old names so they still compile : +#define OODLECORE_PLUGIN_JOB_MAX_DEPENDENCIES OODLE_JOB_MAX_DEPENDENCIES +#define t_fp_OodleCore_Plugin_Job t_fp_Oodle_Job + +#ifdef _MSC_VER +#pragma warning(pop) +#pragma pack(pop, Oodle) +#endif + +#endif // __OODLE2_H_INCLUDED__ diff --git a/lib/oodle/oodle2base.h b/lib/oodle/oodle2base.h new file mode 100644 index 0000000..05f73f3 --- /dev/null +++ b/lib/oodle/oodle2base.h @@ -0,0 +1,167 @@ + +//=================================================== +// Oodle2 Base header +// (C) Copyright 1994-2021 Epic Games Tools LLC +//=================================================== + +#ifndef __OODLE2BASE_H_INCLUDED__ +#define __OODLE2BASE_H_INCLUDED__ + +#ifndef OODLE2BASE_PUBLIC_HEADER +#define OODLE2BASE_PUBLIC_HEADER 1 +#endif + +#ifdef _MSC_VER +#pragma pack(push, Oodle, 8) + +#pragma warning(push) +#pragma warning(disable : 4127) // conditional is constant +#endif + +#ifndef OODLE_BASE_TYPES_H +#define OODLE_BASE_TYPES_H + +#include + +#define OOCOPYRIGHT "Copyright (C) 1994-2021, Epic Games Tools LLC" + +// Typedefs +typedef int8_t OO_S8; +typedef uint8_t OO_U8; +typedef int16_t OO_S16; +typedef uint16_t OO_U16; +typedef int32_t OO_S32; +typedef uint32_t OO_U32; +typedef int64_t OO_S64; +typedef uint64_t OO_U64; +typedef float OO_F32; +typedef double OO_F64; +typedef intptr_t OO_SINTa; +typedef uintptr_t OO_UINTa; +typedef int32_t OO_BOOL; + +// Struct packing handling and inlining +#if defined(__GNUC__) || defined(__clang__) + #define OOSTRUCT struct __attribute__((__packed__)) + #define OOINLINEFUNC inline +#elif defined(_MSC_VER) + // on VC++, we use pragmas for the struct packing + #define OOSTRUCT struct + #define OOINLINEFUNC __inline +#endif + +// Linkage stuff +#if defined(_WIN32) + #define OOLINK __stdcall + #define OOEXPLINK __stdcall +#else + #define OOLINK + #define OOEXPLINK +#endif + +// C++ name demangaling +#ifdef __cplusplus + #define OODEFFUNC extern "C" + #define OODEFSTART extern "C" { + #define OODEFEND } + #define OODEFAULT( val ) =val +#else + #define OODEFFUNC + #define OODEFSTART + #define OODEFEND + #define OODEFAULT( val ) +#endif + +// ======================================================== +// Exported function declarations +#define OOEXPFUNC OODEFFUNC + +//=========================================================================== +// OO_STRING_JOIN joins strings in the preprocessor and works with LINESTRING +#define OO_STRING_JOIN(arg1, arg2) OO_STRING_JOIN_DELAY(arg1, arg2) +#define OO_STRING_JOIN_DELAY(arg1, arg2) OO_STRING_JOIN_IMMEDIATE(arg1, arg2) +#define OO_STRING_JOIN_IMMEDIATE(arg1, arg2) arg1 ## arg2 + +//=========================================================================== +// OO_NUMBERNAME is a macro to make a name unique, so that you can use it to declare +// variable names and they won't conflict with each other +// using __LINE__ is broken in MSVC with /ZI , but __COUNTER__ is an MSVC extension that works + +#ifdef _MSC_VER + #define OO_NUMBERNAME(name) OO_STRING_JOIN(name,__COUNTER__) +#else + #define OO_NUMBERNAME(name) OO_STRING_JOIN(name,__LINE__) +#endif + +//=================================================================== +// simple compiler assert +// this happens at declaration time, so if it's inside a function in a C file, drop {} around it +#ifndef OO_COMPILER_ASSERT + #if defined(__clang__) + #define OO_COMPILER_ASSERT_UNUSED __attribute__((unused)) // hides warnings when compiler_asserts are in a local scope + #else + #define OO_COMPILER_ASSERT_UNUSED + #endif + + #define OO_COMPILER_ASSERT(exp) typedef char OO_NUMBERNAME(_dummy_array) [ (exp) ? 1 : -1 ] OO_COMPILER_ASSERT_UNUSED +#endif + + +#endif + + + +// Oodle2 base header + +#ifndef OODLE2_PUBLIC_CORE_DEFINES +#define OODLE2_PUBLIC_CORE_DEFINES 1 + +#define OOFUNC1 OOEXPFUNC +#define OOFUNC2 OOEXPLINK +#define OOFUNCSTART +#define OODLE_CALLBACK OOLINK + +// Check build flags + #if defined(OODLE_BUILDING_LIB) || defined(OODLE_BUILDING_DLL) + #error Should not see OODLE_BUILDING set for users of oodle.h + #endif + +#ifndef NULL +#define NULL (0) +#endif + +// OODLE_MALLOC_MINIMUM_ALIGNMENT is 8 in 32-bit, 16 in 64-bit +#define OODLE_MALLOC_MINIMUM_ALIGNMENT ((OO_SINTa)(2*sizeof(void *))) + +typedef void (OODLE_CALLBACK t_OodleFPVoidVoid)(void); +/* void-void callback func pointer + takes void, returns void +*/ + +typedef void (OODLE_CALLBACK t_OodleFPVoidVoidStar)(void *); +/* void-void-star callback func pointer + takes void pointer, returns void +*/ + +#define OODLE_JOB_MAX_DEPENDENCIES (4) /* Maximum number of dependencies Oodle will ever pass to a RunJob callback +*/ + +#define OODLE_JOB_NULL_HANDLE (0) /* Value 0 of Jobify handles is reserved to mean none +* Wait(OODLE_JOB_NULL_HANDLE) is a nop +* if RunJob returns OODLE_JOB_NULL_HANDLE it means the job +* was run synchronously and no wait is required +*/ + +#define t_fp_Oodle_Job t_OodleFPVoidVoidStar /* Job function pointer for Plugin Jobify system + + takes void pointer returns void +*/ + +#endif // OODLE2_PUBLIC_CORE_DEFINES + +#ifdef _MSC_VER +#pragma warning(pop) +#pragma pack(pop, Oodle) +#endif + +#endif // __OODLE2BASE_H_INCLUDED__ diff --git a/lib/oodle/src/lib.rs b/lib/oodle/src/lib.rs new file mode 100644 index 0000000..76b1d16 --- /dev/null +++ b/lib/oodle/src/lib.rs @@ -0,0 +1,145 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +use std::ptr; + +use color_eyre::{eyre, Result}; + +#[allow(dead_code)] +mod bindings { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +// Hardcoded chunk size of Bitsquid's bundle compression +pub const CHUNK_SIZE: usize = 512 * 1024; +pub const COMPRESSOR: bindings::OodleLZ_Compressor = + bindings::OodleLZ_Compressor_OodleLZ_Compressor_Kraken; +pub const LEVEL: bindings::OodleLZ_CompressionLevel = + bindings::OodleLZ_CompressionLevel_OodleLZ_CompressionLevel_Optimal2; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum OodleLZ_FuzzSafe { + Yes, + No, +} + +impl From for bindings::OodleLZ_FuzzSafe { + fn from(value: OodleLZ_FuzzSafe) -> Self { + match value { + OodleLZ_FuzzSafe::Yes => bindings::OodleLZ_FuzzSafe_OodleLZ_FuzzSafe_Yes, + OodleLZ_FuzzSafe::No => bindings::OodleLZ_FuzzSafe_OodleLZ_FuzzSafe_No, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum OodleLZ_CheckCRC { + Yes, + No, +} + +impl From for bindings::OodleLZ_CheckCRC { + fn from(value: OodleLZ_CheckCRC) -> Self { + match value { + OodleLZ_CheckCRC::Yes => bindings::OodleLZ_CheckCRC_OodleLZ_CheckCRC_Yes, + OodleLZ_CheckCRC::No => bindings::OodleLZ_CheckCRC_OodleLZ_CheckCRC_No, + } + } +} + +#[tracing::instrument(skip(data))] +pub fn decompress( + data: I, + fuzz_safe: OodleLZ_FuzzSafe, + check_crc: OodleLZ_CheckCRC, +) -> Result> +where + I: AsRef<[u8]>, +{ + let data = data.as_ref(); + let mut out = vec![0; CHUNK_SIZE]; + + let verbosity = if tracing::enabled!(tracing::Level::INFO) { + bindings::OodleLZ_Verbosity_OodleLZ_Verbosity_Minimal + } else if tracing::enabled!(tracing::Level::DEBUG) { + bindings::OodleLZ_Verbosity_OodleLZ_Verbosity_Some + } else if tracing::enabled!(tracing::Level::TRACE) { + bindings::OodleLZ_Verbosity_OodleLZ_Verbosity_Lots + } else { + bindings::OodleLZ_Verbosity_OodleLZ_Verbosity_None + }; + + let ret = unsafe { + bindings::OodleLZ_Decompress( + data.as_ptr() as *const _, + data.len() as isize, + out.as_mut_ptr() as *mut _, + out.len() as isize, + fuzz_safe.into(), + check_crc.into(), + verbosity, + ptr::null_mut(), + 0, + None, + ptr::null_mut(), + ptr::null_mut(), + 0, + bindings::OodleLZ_Decode_ThreadPhase_OodleLZ_Decode_Unthreaded, + ) + }; + + if ret == 0 { + eyre::bail!("Decompression failed"); + } + + Ok(out) +} + +#[tracing::instrument(skip(data))] +pub fn compress(data: I) -> Result> +where + I: AsRef<[u8]>, +{ + let mut raw = Vec::from(data.as_ref()); + raw.resize(CHUNK_SIZE, 0); + + // TODO: Query oodle for buffer size + let mut out = vec![0u8; CHUNK_SIZE]; + + let ret = unsafe { + bindings::OodleLZ_Compress( + COMPRESSOR, + raw.as_ptr() as *const _, + raw.len() as isize, + out.as_mut_ptr() as *mut _, + LEVEL, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + 0, + ) + }; + + tracing::debug!(compressed_size = ret, "Compressed chunk"); + + if ret == 0 { + eyre::bail!("Compression failed"); + } + + out.resize(ret as usize, 0); + + Ok(out) +} + +pub fn get_decode_buffer_size(raw_size: usize, corruption_possible: bool) -> Result { + let size = unsafe { + bindings::OodleLZ_GetDecodeBufferSize( + COMPRESSOR, + raw_size as isize, + if corruption_possible { 1 } else { 0 }, + ) + }; + Ok(size as usize) +} diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index 0844518..46d02db 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -17,7 +17,7 @@ nanorand = "0.7.0" pin-project-lite = "0.2.9" serde = { version = "1.0.147", features = ["derive"] } serde_sjson = { path = "../../lib/serde_sjson", version = "*" } -oodle-sys = { path = "../../lib/oodle-sys", version = "*" } +oodle = { path = "../../lib/oodle", version = "*" } tokio = { version = "1.21.2", features = ["rt-multi-thread", "fs", "process", "macros", "tracing", "io-util", "io-std"] } tokio-stream = { version = "0.1.11", features = ["fs", "io-util"] } tracing = { version = "0.1.37", features = ["async-await"] } diff --git a/lib/sdk/src/bundle/mod.rs b/lib/sdk/src/bundle/mod.rs index 8b8d7c5..1c08530 100644 --- a/lib/sdk/src/bundle/mod.rs +++ b/lib/sdk/src/bundle/mod.rs @@ -4,7 +4,7 @@ use std::path::Path; use color_eyre::eyre::{self, Context, Result}; use color_eyre::{Help, Report, SectionExt}; -use oodle_sys::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; +use oodle::{OodleLZ_CheckCRC, OodleLZ_FuzzSafe, CHUNK_SIZE}; use crate::binary::sync::*; use crate::bundle::file::Properties; @@ -159,7 +159,7 @@ impl Bundle { decompressed.append(&mut compressed_buffer); } else { // TODO: Optimize to not reallocate? - let mut raw_buffer = oodle_sys::decompress( + let mut raw_buffer = oodle::decompress( &compressed_buffer, OodleLZ_FuzzSafe::No, OodleLZ_CheckCRC::No, @@ -257,7 +257,7 @@ impl Bundle { let mut chunk_sizes = Vec::with_capacity(num_chunks); for chunk in chunks { - let compressed = oodle_sys::compress(chunk)?; + let compressed = oodle::compress(chunk)?; tracing::trace!( raw_chunk_size = chunk.len(), compressed_chunk_size = compressed.len() @@ -359,7 +359,7 @@ where r.read_exact(&mut compressed_buffer)?; // TODO: Optimize to not reallocate? - let mut raw_buffer = oodle_sys::decompress( + let mut raw_buffer = oodle::decompress( &compressed_buffer, OodleLZ_FuzzSafe::No, OodleLZ_CheckCRC::No, -- 2.45.3