diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89eae68..b9e9c1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] name: check everything env: diff --git a/.github/workflows/jokolink_artifact.yml b/.github/workflows/jokolink_artifact.yml index 477a4d6..0a14758 100644 --- a/.github/workflows/jokolink_artifact.yml +++ b/.github/workflows/jokolink_artifact.yml @@ -1,7 +1,7 @@ on: push: paths: - - 'crates/jokolink/**' + - 'crates/joko_link_manager/**' name: Jokolink DLL env: @@ -18,9 +18,9 @@ jobs: uses: Swatinem/rust-cache@v1 - name: Build Jokolink DLL - run: cargo build --release -p jokolink + run: cargo build --release -p joko_link_manager - uses: actions/upload-artifact@v3 with: - name: jokolink.dll - path: "./target/release/jokolink.dll" + name: joko_link_manager.dll + path: "./target/release/joko_link_manager.dll" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ecce1b..7e34623 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: - name: Build Jokolink if: ${{matrix.os == 'windows'}} - run: cargo build --release -p jokolink + run: cargo build --release -p joko_link_manager - name: Upload Assets uses: xresloader/upload-to-github-release@v1 @@ -37,4 +37,4 @@ jobs: with: tags: true draft: true - file: "target/release/jokolay;target/release/jokolay.exe;target/release/jokolink.dll" \ No newline at end of file + file: "target/release/jokolay;target/release/jokolay.exe;target/release/joko_link_manager.dll" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 046f6bb..7957a17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /assets # the wasm build of crate **/dist -*.log \ No newline at end of file +*.log +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index d0414cf..8ade01e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +checksum = "6f90148830dac590fac7ccfe78ec4a8ea404c60f75a24e16407a71f0f40de775" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eb1adf08c5bcaa8490b9851fd53cca27fa9880076f178ea9d29f05196728a8" +checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" dependencies = [ "enumn", "serde", @@ -45,23 +45,23 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "serde", "version_check", - "zerocopy 0.7.25", + "zerocopy 0.7.34", ] [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -120,28 +120,196 @@ dependencies = [ ] [[package]] -name = "atk-sys" -version = "0.18.0" +name = "ashpd" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" +dependencies = [ + "concurrent-queue", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.0", + "futures-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -163,9 +331,33 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -175,9 +367,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block" @@ -185,6 +377,29 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bstr" version = "0.2.17" @@ -198,28 +413,28 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] @@ -228,16 +443,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "camino" version = "1.1.6" @@ -246,21 +451,21 @@ checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" [[package]] name = "cap-directories" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "182588d07579a8ca97dbfbea2787d450341d068b16062c8caa2205158ddb269d" +checksum = "97ba99bbc76e44242cd767689c33f5350c3646758edecdf1f8b7f4df5a8ea029" dependencies = [ "cap-std", "directories-next", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "cap-primitives" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf30c373a3bee22c292b1b6a7a26736a38376840f1af3d2d806455edf8c3899" +checksum = "fe16767ed8eee6d3f1f00d6a7576b81c226ab917eb54b96e5f77a5216ef67abb" dependencies = [ "ambient-authority", "fs-set-times", @@ -269,15 +474,15 @@ dependencies = [ "ipnet", "maybe-owned", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", "winx", ] [[package]] name = "cap-std" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bade423fa6403efeebeafe568fdb230e8c590a275fba2ba978dd112efcf6e9" +checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330" dependencies = [ "camino", "cap-primitives", @@ -288,22 +493,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-expr" -version = "0.15.5" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" -dependencies = [ - "smallvec", - "target-lexicon", -] +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -311,6 +503,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cgmath" version = "0.18.0" @@ -323,16 +521,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -360,16 +558,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -394,61 +601,61 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crunchy" @@ -456,11 +663,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cxx" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +checksum = "bb497fad022245b29c2a0351df572e2d67c1046bcef2260ebc022aec81efea82" dependencies = [ "cc", "cxxbridge-flags", @@ -470,9 +687,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" +checksum = "9327c7f9fbd6329a200a5d4aa6f674c60ab256525ff0084b52a889d4e4c60cee" dependencies = [ "cc", "codespan-reporting", @@ -480,42 +697,52 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "cxxbridge-flags" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" +checksum = "688c799a4a846f1c0acb9f36bb9c6272d9b3d9457f3633c7753c6057270df13c" [[package]] name = "cxxbridge-macro" -version = "1.0.110" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +checksum = "928bc249a7e3cd554fd2e8e08a426e9670c50bbfc9a621653cfa9accc9641783" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories-next" version = "2.0.0" @@ -545,9 +772,9 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "ecolor" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" +checksum = "03cfe80b1890e1a8cdbffc6044d6872e814aaf6011835a2a5e2db0e5c5c4ef4e" dependencies = [ "bytemuck", "serde", @@ -555,9 +782,9 @@ dependencies = [ [[package]] name = "egui" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" +checksum = "180f595432a5b615fc6b74afef3955249b86cfea72607b40740a4cd60d5297d0" dependencies = [ "accesskit", "ahash", @@ -568,29 +795,29 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ffe3fe5c00295f91c2a61a74ee271c32f74049c94ba0b1cea8f26eb478bc07" +checksum = "3f4a6962241a76da5be5e64e41b851ee1c95fda11f76635522a3c82b119b5475" dependencies = [ "egui", "enum-map", "log", - "mime_guess", + "mime_guess2", "serde", ] [[package]] name = "egui_render_glow" -version = "0.5.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df0cb60080432a2c025f00942fbd1a0f8b719338ab6a28adab5a1ca15013771" +checksum = "21691a0388394a02b9352fb31edc7a008645ef1af13bc6eace5da06c2f599e60" dependencies = [ "bytemuck", "egui", "getrandom", "glow", "js-sys", - "raw-window-handle", + "raw-window-handle 0.6.1", "tracing", "wasm-bindgen", "web-sys", @@ -598,21 +825,21 @@ dependencies = [ [[package]] name = "egui_render_three_d" -version = "0.5.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4038e7bac93f9356eb88ffabd20d9486070b79910584d662af1ac3bf64f01e2a" +checksum = "39bc7f5aab85ad422c53b2a1753a94a08bdca4b701346edc226ba015a0b2a7a8" dependencies = [ "egui", "egui_render_glow", - "raw-window-handle", + "raw-window-handle 0.6.1", "three-d", ] [[package]] name = "egui_window_glfw_passthrough" -version = "0.5.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecec3abb56e2be5104a35a4c1848f976add5167a8655f67ae7c84d45d35c8905" +checksum = "1ce8cd7260410f069d82b31b188f66900336e054f839bbe24112dc2bb29acfc4" dependencies = [ "egui", "glfw-passthrough", @@ -621,15 +848,15 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "emath" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" +checksum = "6916301ecf80448f786cdf3eb51d9dbdd831538732229d49119e2d4312eaaf09" dependencies = [ "bytemuck", "serde", @@ -643,18 +870,24 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-map" -version = "2.7.1" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed40247825a1a0393b91b51d475ea1063a6cbbf0847592e7f13fb427aca6a716" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" dependencies = [ "enum-map-derive", "serde", @@ -662,51 +895,52 @@ dependencies = [ [[package]] name = "enum-map-derive" -version = "0.15.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7933cd46e720348d29ed1493f89df9792563f272f96d8f13d18afe03b32f8cb8" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "enumflags2" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", + "serde", ] [[package]] name = "enumflags2_derive" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "enumn" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" +checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "epaint" -version = "0.23.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" +checksum = "77b9fdf617dd7f58b0c8e6e9e4a1281f730cde0831d40547da446b2bb76a47af" dependencies = [ "ab_glyph", "ahash", @@ -726,40 +960,94 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "fdeflate" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -767,115 +1055,166 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs-set-times" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" +checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" dependencies = [ "io-lifetimes", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" +name = "fsevent-sys" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", "libc", - "system-deps", ] [[package]] -name = "gdk-sys" -version = "0.18.0" +name = "futures-channel" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", + "futures-core", ] [[package]] -name = "gethostname" -version = "0.3.0" +name = "futures-core" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" -dependencies = [ - "libc", - "winapi", -] +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] -name = "getrandom" -version = "0.2.11" +name = "futures-io" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", ] [[package]] -name = "gimli" -version = "0.28.0" +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] -name = "gio-sys" -version = "0.18.1" +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" dependencies = [ - "glib-sys", - "gobject-sys", "libc", - "system-deps", "winapi", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "glam" -version = "0.24.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" dependencies = [ "bytemuck", ] [[package]] name = "glfw-passthrough" -version = "0.51.1" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89ad199bb99922313a6e97b609dab23a88e3b68a6b0233d1fafdb5044a7728f" +checksum = "17e0ee79341d32b6c490876d36f5e815bb10be943452cd3fff67d509d3143fb5" dependencies = [ "bitflags 1.3.2", "glfw-sys-passthrough", "objc", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.1", "winapi", ] @@ -888,16 +1227,6 @@ dependencies = [ "cmake", ] -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "glob" version = "0.3.1" @@ -916,77 +1245,48 @@ dependencies = [ "web-sys", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - [[package]] name = "half" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", "num-traits", - "zerocopy 0.6.5", + "zerocopy 0.6.6", ] [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "heck" -version = "0.4.1" +name = "hermit-abi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "hermit-abi" -version = "0.3.3" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1000,9 +1300,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1010,23 +1310,22 @@ dependencies = [ [[package]] name = "image" -version = "0.24.7" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-rational", "num-traits", "png", ] [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -1035,9 +1334,9 @@ dependencies = [ [[package]] name = "indextree" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40411d0e5c63ef1323c3d09ce5ec6d84d71531e18daed0743fccea279d7deb6" +checksum = "3a6f7e29c1619ec492f411b021ac9f30649d5f522ca6f287f2467ee48c8dfe10" [[package]] name = "inotify" @@ -1070,19 +1369,19 @@ dependencies = [ [[package]] name = "io-extras" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3c230ee517ee76b1cc593b52939ff68deda3fae9e41eca426c6b4993df51c4" +checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b" dependencies = [ "io-lifetimes", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "io-lifetimes" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" [[package]] name = "ipnet" @@ -1090,69 +1389,131 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "is_ci" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "joko_component_manager" +version = "0.2.1" +dependencies = [ + "bimap", + "bincode", + "joko_component_models", + "petgraph", + "tokio", + "tracing", +] + +[[package]] +name = "joko_component_models" +version = "0.2.1" +dependencies = [ + "bincode", + "serde", + "tokio", +] [[package]] name = "joko_core" version = "0.2.1" dependencies = [ - "cap-directories", - "cap-std", - "egui", - "egui_extras", + "bytemuck", "glam", - "indexmap", + "scopeguard", + "serde", + "smol_str", +] + +[[package]] +name = "joko_ext" +version = "0.1.0" + +[[package]] +name = "joko_link_manager" +version = "0.2.1" +dependencies = [ + "arcdps", + "enumflags2", + "glam", + "joko_component_models", + "joko_core", + "joko_link_models", "miette", - "rayon", - "rfd", - "ringbuffer", + "notify", + "num-derive", + "num-traits", "serde", "serde_json", + "time", + "tokio", "tracing", "tracing-appender", "tracing-subscriber", + "widestring", + "windows", + "x11rb", ] [[package]] -name = "joko_ext" -version = "0.1.0" +name = "joko_link_models" +version = "0.2.1" +dependencies = [ + "arcdps", + "enumflags2", + "glam", + "joko_core", + "miette", + "notify", + "num-derive", + "num-traits", + "serde", + "tracing-appender", + "tracing-subscriber", + "windows", +] [[package]] -name = "joko_marker_format" +name = "joko_link_ui_manager" version = "0.2.1" dependencies = [ - "base64", - "cap-std", + "egui", + "enumflags2", + "glam", + "joko_component_models", + "joko_core", + "joko_link_models", + "joko_ui_models", + "miette", + "num-derive", + "num-traits", + "serde", + "tokio", +] + +[[package]] +name = "joko_package_manager" +version = "0.2.1" +dependencies = [ + "base64 0.21.7", + "bytemuck", "cxx", "cxx-build", "data-encoding", @@ -1162,10 +1523,15 @@ dependencies = [ "image", "indexmap", "itertools", - "joko_render", + "joko_component_models", + "joko_core", + "joko_link_models", + "joko_package_models", + "joko_render_models", + "joko_ui_models", "jokoapi", - "jokolink", "miette", + "once", "paste", "phf", "rayon", @@ -1176,15 +1542,54 @@ dependencies = [ "similar-asserts", "smol_str", "time", + "tokio", "tracing", "url", "uuid", + "walkdir", "xot", "zip", ] [[package]] -name = "joko_render" +name = "joko_package_models" +version = "0.2.1" +dependencies = [ + "base64 0.21.7", + "bimap", + "bytemuck", + "cxx-build", + "data-encoding", + "enumflags2", + "glam", + "indexmap", + "itertools", + "joko_core", + "jokoapi", + "miette", + "paste", + "phf", + "rstest", + "serde", + "serde_json", + "similar-asserts", + "smol_str", + "tracing", + "url", + "uuid", + "xot", +] + +[[package]] +name = "joko_plugin_manager" +version = "0.2.1" +dependencies = [ + "joko_component_models", + "tokio", +] + +[[package]] +name = "joko_render_manager" version = "0.2.1" dependencies = [ "bytemuck", @@ -1192,13 +1597,31 @@ dependencies = [ "egui_render_three_d", "egui_window_glfw_passthrough", "glam", - "jokolink", - "raw-window-handle", - "serde", - "serde_json", + "joko_component_models", + "joko_link_models", + "joko_render_models", + "joko_ui_models", + "tokio", "tracing", ] +[[package]] +name = "joko_render_models" +version = "0.2.1" +dependencies = [ + "bytemuck", + "glam", + "joko_core", + "serde", +] + +[[package]] +name = "joko_ui_models" +version = "0.2.1" +dependencies = [ + "egui", +] + [[package]] name = "jokoapi" version = "0.2.1" @@ -1216,56 +1639,43 @@ version = "0.2.1" dependencies = [ "cap-directories", "cap-std", + "directories-next", "egui", "egui_extras", "egui_window_glfw_passthrough", + "enumflags2", "glam", "indexmap", - "joko_core", - "joko_marker_format", - "joko_render", - "jokolink", + "joko_component_manager", + "joko_component_models", + "joko_link_manager", + "joko_link_models", + "joko_link_ui_manager", + "joko_package_manager", + "joko_plugin_manager", + "joko_render_manager", + "joko_ui_models", "miette", "rayon", "rfd", "ringbuffer", + "scopeguard", "serde", "serde_json", + "smol_str", + "tokio", + "toml", "tracing", "tracing-appender", "tracing-subscriber", - "url", -] - -[[package]] -name = "jokolink" -version = "0.2.1" -dependencies = [ - "arcdps", - "egui", - "enumflags2", - "glam", - "jokoapi", - "miette", - "notify", - "num-derive", - "num-traits", - "serde", - "serde_json", - "time", - "tracing", - "tracing-appender", - "tracing-subscriber", - "widestring", - "windows", - "x11rb", + "uuid", ] [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1298,9 +1708,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -1310,13 +1720,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -1330,15 +1739,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1346,9 +1755,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "malloc_buf" @@ -1376,9 +1785,9 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" @@ -1391,24 +1800,23 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "miette" -version = "5.10.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ "backtrace", "backtrace-ext", - "is-terminal", + "cfg-if", "miette-derive", - "once_cell", "owo-colors", "supports-color", "supports-hyperlinks", @@ -1421,13 +1829,13 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.10.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] @@ -1437,10 +1845,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "mime_guess" -version = "2.0.4" +name = "mime_guess2" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" dependencies = [ "mime", "unicase", @@ -1448,9 +1856,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", @@ -1458,9 +1866,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -1501,6 +1909,19 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1513,8 +1934,10 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", + "crossbeam-channel", "filetime", + "fsevent-sys", "inotify", "kqueue", "libc", @@ -1535,42 +1958,27 @@ dependencies = [ ] [[package]] -name = "num-derive" -version = "0.4.1" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" +name = "num-derive" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "proc-macro2", + "quote", + "syn 2.0.61", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -1607,18 +2015,34 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] +[[package]] +name = "once" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60bfe75a40f755f162b794140436c57845cb106fd1467598631c76c6fff08e28" + [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] [[package]] name = "overload" @@ -1637,27 +2061,21 @@ dependencies = [ [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" [[package]] -name = "pango-sys" -version = "0.18.0" +name = "parking" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1665,28 +2083,38 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] [[package]] name = "phf" @@ -1718,7 +2146,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] @@ -1732,21 +2160,32 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] -name = "pkg-config" -version = "0.3.27" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] [[package]] name = "png" -version = "0.17.10" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1755,6 +2194,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1767,20 +2227,29 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1821,11 +2290,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +[[package]] +name = "raw-window-handle" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b" + [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1833,9 +2308,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1843,27 +2318,27 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -1872,14 +2347,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -1893,13 +2368,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -1910,33 +2385,33 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "relative-path" -version = "1.9.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rfd" -version = "0.12.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9e7b57df6e8472152674607f6cc68aa14a748a3157a857a94f516e11aeacc2" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" dependencies = [ + "ashpd", "block", "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", "js-sys", "log", "objc", "objc-foundation", "objc_id", - "raw-window-handle", + "pollster", + "raw-window-handle 0.6.1", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1945,16 +2420,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1965,9 +2441,9 @@ checksum = "4eba9638e96ac5a324654f8d47fb71c5e21abef0f072740ed9c1d4b0801faa37" [[package]] name = "rstest" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" dependencies = [ "rstest_macros", "rustc_version", @@ -1975,9 +2451,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" dependencies = [ "cfg-if", "glob", @@ -1986,15 +2462,15 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.39", + "syn 2.0.61", "unicode-ident", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" @@ -2007,46 +2483,55 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "itoa", "libc", "linux-raw-sys", "once_cell", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.8" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2069,62 +2554,74 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "semver" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2134,6 +2631,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2142,9 +2648,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "similar" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" dependencies = [ "bstr", "unicode-segmentation", @@ -2166,20 +2672,29 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "slotmap" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ "version_check", ] [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" @@ -2189,9 +2704,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smol_str" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" dependencies = [ "serde", ] @@ -2202,33 +2717,38 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "supports-color" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" dependencies = [ - "is-terminal", "is_ci", ] [[package]] name = "supports-hyperlinks" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" [[package]] name = "supports-unicode" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" -dependencies = [ - "is-terminal", -] +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" @@ -2243,9 +2763,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -2253,48 +2773,41 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.2.0" +name = "tempfile" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", ] -[[package]] -name = "target-lexicon" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" - [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" -version = "0.1.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.48.0", ] [[package]] name = "textwrap" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", @@ -2303,29 +2816,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2333,9 +2846,9 @@ dependencies = [ [[package]] name = "three-d" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2db9010227411ab0aa5948e770304e807e5c9b6d5d0719c3de248bae7be7096" +checksum = "0aecff785797175a2e56dca49da9836948eee41fab48b7b01dfcb64cae256ecb" dependencies = [ "cgmath", "glow", @@ -2358,12 +2871,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2378,10 +2892,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -2400,16 +2915,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "toml" -version = "0.8.8" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.12", ] [[package]] @@ -2423,15 +2948,26 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.8", ] [[package]] @@ -2447,11 +2983,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", + "thiserror", "time", "tracing-subscriber", ] @@ -2464,7 +3001,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] @@ -2479,9 +3016,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -2490,9 +3027,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -2513,6 +3050,23 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + [[package]] name = "unicase" version = "2.7.0" @@ -2524,9 +3078,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -2542,24 +3096,24 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -2581,15 +3135,16 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3" [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", "rustls", + "rustls-pki-types", "rustls-webpki", "serde", "serde_json", @@ -2599,9 +3154,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2609,11 +3164,17 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "rand", @@ -2623,13 +3184,13 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8c6bba9b149ee82950daefc9623b32bb1dacbfb1890e352f6b887bd582adaf" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] @@ -2638,12 +3199,6 @@ 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" @@ -2652,9 +3207,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -2668,9 +3223,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2678,24 +3233,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2705,9 +3260,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2715,28 +3270,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -2744,15 +3299,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -2772,18 +3330,18 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "winapi-wsapoll" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" dependencies = [ "winapi", ] @@ -2800,7 +3358,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows-core", + "windows-core 0.51.1", "windows-targets 0.48.5", ] @@ -2814,12 +3372,12 @@ dependencies = [ ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -2832,18 +3390,12 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -2862,10 +3414,20 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] [[package]] name = "windows_aarch64_gnullvm" @@ -2874,10 +3436,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -2886,10 +3448,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -2898,10 +3460,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -2910,10 +3478,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -2922,10 +3490,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -2934,10 +3502,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -2945,23 +3513,38 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" -version = "0.5.19" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] [[package]] name = "winx" -version = "0.36.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357bb8e2932df531f83b052264b050b81ba0df90ee5a59b2d1d3949f344f81e5" +checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346" dependencies = [ - "bitflags 2.4.1", - "windows-sys 0.48.0", + "bitflags 2.5.0", + "windows-sys 0.52.0", ] [[package]] @@ -2971,7 +3554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "gethostname", - "nix", + "nix 0.26.4", "winapi", "winapi-wsapoll", "x11rb-protocol", @@ -2983,7 +3566,17 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" dependencies = [ - "nix", + "nix 0.26.4", +] + +[[package]] +name = "xdg-home" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +dependencies = [ + "libc", + "winapi", ] [[package]] @@ -3012,47 +3605,115 @@ dependencies = [ "xmlparser", ] +[[package]] +name = "zbus" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5915716dff34abef1351d2b10305b019c8ef33dcf6c72d31a6e227d5d9d7a21" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.3.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.28.0", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fceb36d0c1c4a6b98f3ce40f410e64e5a134707ed71892e1b178abc4c695d4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96f8f25c15a0edc9b07eb66e7e6e97d124c0505435c382fde1ab7ceb188aa956" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" dependencies = [ "byteorder", - "zerocopy-derive 0.6.5", + "zerocopy-derive 0.6.6", ] [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ - "zerocopy-derive 0.7.25", + "zerocopy-derive 0.7.34", ] [[package]] name = "zerocopy-derive" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "855e0f6af9cd72b87d8a6c586f3cb583f5cdcc62c2c80869d8cd7e96fdf7ee20" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.61", ] +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zip" version = "0.6.6" @@ -3064,3 +3725,41 @@ dependencies = [ "crossbeam-utils", "flate2", ] + +[[package]] +name = "zvariant" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877ef94e5e82b231d2a309c531f191a8152baba8241a7939ee04bd76b0171308" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ca98581cc6a8120789d8f1f0997e9053837d6aa5346cbb43454d7121be6e39" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75fa7291bdd68cd13c4f97cc9d78cbf16d96305856dfc7ac942aeff4c2de7d5a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index 1646b73..fa55b98 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,71 @@ [workspace] members = [ - "crates/joko_render", - "crates/joko_marker_format", - "crates/jokolink", + "crates/joko_render_manager", + "crates/joko_render_models", + "crates/joko_package_manager", + "crates/joko_package_models", + "crates/joko_link_manager", + "crates/joko_link_ui_manager", + "crates/joko_link_models", "crates/jokoapi", "crates/jokolay", "crates/joko_core", + "crates/joko_ui_models", + "crates/joko_component_manager", + "crates/joko_component_models", + "crates/joko_plugin_manager", "crates/joko_ext", ] resolver = "2" [workspace.dependencies] -tracing = { version = "0.1" } -ringbuffer = { version = "0.14" } -egui = { version = "*" } -egui_extras = { version = "*" } +#https://docs.rs/tracing/latest/tracing/level_filters/index.html + +bimap = { version = "0.6.3", features = ["serde"] } +bincode = "1.3.3" +bytemuck = { version = "1", features = ["derive"] } +cap-directories = { version = "2" } cap-std = { version = "2", features = ["fs_utf8"] } -serde = { version = "*", features = ["derive"] } +egui = { version = "0.26" } +egui_extras = { version = "0.26" } +enumflags2 = { version = "*", features = ["serde"] } +glam = { version = "*", features = ["fast-math"] } +indexmap = { version = "2" } +itertools = { version = "*" } miette = { version = "*", features = ["fancy"] } -url = { version = "*", features = ["serde"] } -serde_json = { version = "*" } +paste = { version = "*" } +once = "0.3.4" rayon = { version = "*" } -# tokio = { version = "*", default-features = false, features = [ -# "rt-multi-thread", -# "sync", -# "time", -# "parking_lot" -# ]} -glam = { version = "*", features = ["fast-math"] } +rfd = { version = "*" } +ringbuffer = { version = "0.14" } +serde = { version = "*", features = ["derive"] } +serde_json = { version = "*" } +smol_str = { version = "*", features = ["serde"] } time = { version = "*" } +tokio = { version = "1.37.0", features = ["sync"] } +tracing = { version = "0.1", features = ["max_level_trace", "release_max_level_info"] } +tracing-appender = { version = "*" } +tracing-subscriber = { version = "0.3", features = ["env-filter", "time",] } # for ErrorLayer ureq = { version = "*" } -enumflags2 = { version = "*" } -indexmap = { version = "2" } -rfd = { version = "*" } -smol_str = { version = "*" } -itertools = { version = "*" } +url = { version = "*", features = ["serde"] } +uuid = { version = "*" } +mutually_exclusive_features = "0.1.0" +ractor = "0.9.8" + + + +#https://corrode.dev/blog/tips-for-faster-rust-compile-times/#use-cargo-check-instead-of-cargo-build +[profile.dev] +split-debuginfo = "unpacked" + +[profile.dev.build-override] +opt-level = 3 + + +[profile.release] +#https://doc.rust-lang.org/cargo/reference/profiles.html#strip +strip = "none" + +#lto make the build very slow +#lto = true diff --git a/README.md b/README.md index 5beccae..ea2482f 100755 --- a/README.md +++ b/README.md @@ -3,10 +3,14 @@ An Overlay for Guild Wars 2 in Rust Well, technically, this contains a family of crates related to jokolay. -1. `jokolink`: This is what you will run from the wine prefix of gw2 . it reads the *official* [shared memory](https://wiki.guildwars2.com/wiki/API:MumbleLink) of gw2 to get live player data and copy into a shared memory file under /dev/shm for linux native apps (like Jokolay) to use. -2. `jokoapi`: API bindings for gw2 api in rust. if anyone wants to contribute, this is the best place. its just copy pasting api endpoints and filling out all the required fields of structs, writing tests to verify. -3. `jokolay`: this is the actual overlay. -4. `joko_marker_format`: deals with marker packs. +1. `joko_core`: Contains very basic and common structures. +2. `jokolink`: This is what you will run from the wine prefix of gw2 . it reads the *official* [shared memory](https://wiki.guildwars2.com/wiki/API:MumbleLink) of gw2 to get live player data and copy into a shared memory file under /dev/shm for linux native apps (like Jokolay) to use. +3. `jokoapi`: API bindings for gw2 api in rust. if anyone wants to contribute, this is the best place. its just copy pasting api endpoints and filling out all the required fields of structs, writing tests to verify. +4. `jokolay`: this is the actual overlay. +5. `joko_package`: deals with TacO marker packs. +6. `joko_package_models`: structures that need to be shared with other modules. +7. `joko_render`: in charge of displaying on screen. +8. `joko_render_models`: structures that need to be shared with other modules. ## Minimum Requirements 1. Requires Vulkan. most GPUs after gtx 750 should be okay. diff --git a/crates/joko_component_manager/Cargo.toml b/crates/joko_component_manager/Cargo.toml new file mode 100644 index 0000000..2bbf132 --- /dev/null +++ b/crates/joko_component_manager/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "joko_component_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = ["dep:bincode"] + +[dependencies] +bimap = { workspace = true } +bincode = { workspace = true, optional = true } +tokio = { workspace = true } +joko_component_models = { path = "../joko_component_models" } +petgraph = "0.6.4" +tracing = {workspace = true} diff --git a/crates/joko_component_manager/src/lib.rs b/crates/joko_component_manager/src/lib.rs new file mode 100644 index 0000000..b588497 --- /dev/null +++ b/crates/joko_component_manager/src/lib.rs @@ -0,0 +1,629 @@ +use core::fmt; +use std::{ + collections::{HashMap, HashSet}, + hash::Hash, + sync::{Arc, RwLock}, +}; + +use joko_component_models::{Component, ComponentChannels, ComponentMessage, ComponentResult}; +use petgraph::{ + csr::IndexType, + graph::NodeIndex, + stable_graph::{EdgeReference, StableDiGraph}, + visit::{EdgeRef, IntoNodeIdentifiers}, +}; +use tracing::{info_span, trace}; + +type BroadcastChannels = ( + tokio::sync::broadcast::Sender, + tokio::sync::broadcast::Receiver, +); +pub struct ComponentManager { + //TODO: make it a component too ? + known_components: HashMap, + broadcasters: HashMap, //a receiver is kept idle in order to not close the channels. https://docs.rs/tokio/latest/tokio/sync/broadcast/#closing + notifications: HashMap>, + invocation_order: Vec, +} + +struct ComponentHandle { + name: String, + component: Arc>, + channels: ComponentChannels, + relations_to_ids: HashMap, + nb_call: u128, + execution_time: std::time::Duration, +} + +pub struct ComponentExecutor { + world: String, + broadcasters: HashMap>, + components: Vec, + has_been_initialized: bool, +} + +#[derive(Clone, Debug)] +enum RelationShip { + Requires, + Peer, + Notify, +} + +impl fmt::Display for RelationShip { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + RelationShip::Requires => write!(f, "Requires"), + RelationShip::Peer => write!(f, "Peer"), + RelationShip::Notify => write!(f, "Notify"), + } + } +} + +fn get_invocation_order( + graph: &mut StableDiGraph, + filter: F, +) -> Vec> +where + N: std::cmp::Ord, + Ix: IndexType, + F: Fn(EdgeReference) -> bool, +{ + let mut invocation_order = Vec::new(); + + //peel nodes one by one + let mut modified = true; + while modified { + modified = false; + let mut to_delete = Vec::new(); + for node_id in graph.node_indices() { + let mut is_pointed = false; + for edge in graph.edges(node_id) { + if filter(edge) { + is_pointed = true; + break; + } + } + if !is_pointed { + to_delete.push(node_id); + } + } + let mut current_level_invocation_order = Vec::new(); + for external_node in to_delete { + graph.remove_node(external_node); + modified = true; + current_level_invocation_order.push(external_node); + } + current_level_invocation_order.sort(); //This grant a deterministic order regardless of circumstances + invocation_order.extend(current_level_invocation_order); + } + + //if there is a cycle, there are remaining nodes + invocation_order +} + +fn has_unique_elements(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + Hash, +{ + let mut uniq = HashSet::new(); + iter.into_iter().all(move |x| uniq.insert(x)) +} +impl ComponentManager { + pub fn new() -> Self { + //clone itself on a world basis ? which would follow a component thread + Self { + known_components: Default::default(), + broadcasters: Default::default(), + notifications: Default::default(), + invocation_order: Default::default(), + } + } + + /// Register a component. + /// On its relationship, each component reference (names) shall be assigned an id. + /// That id is 0 based and goes in following order: peers, requirements, notify + /// A component, when binding must retrieve the with the proper id. + pub fn register( + &mut self, + component_name: &str, + component: Arc>, + ) -> Result<(), String> { + let mut relations_to_ids: HashMap = Default::default(); + { + let component = component.as_ref().read().unwrap(); + if !has_unique_elements( + component + .peers() + .iter() + .chain(component.requirements().iter()) + .chain(component.notify().iter()), + ) { + return Err(format!( + "Service {} has duplicate elements. Each name can only appear at one place", + component_name + )); + } + for (idx, name) in component + .peers() + .iter() + .chain(component.requirements().iter()) + .chain(component.notify().iter()) + .enumerate() + { + relations_to_ids.insert(name.to_string(), idx); + } + } + + let handle = ComponentHandle { + name: component_name.to_string(), + component, + channels: ComponentChannels::default(), + relations_to_ids, + nb_call: 0, + execution_time: Default::default(), + }; + self.known_components + .insert(component_name.to_string(), handle); + + Ok(()) + } + + pub fn executor(&mut self, world: &str) -> ComponentExecutor { + /* + extract the list of components of this world + bind them + insert them into the executor + */ + let mut component_names: Vec = Default::default(); + for name in self.invocation_order.iter() { + if name.starts_with(world) { + component_names.push(name.clone()); + } + } + trace!( + "executor of world: {} shall be built from: {:?}", + world, + component_names + ); + let mut components: Vec = Default::default(); + let mut broadcasters: HashMap> = + Default::default(); + for name in component_names { + components.push(self.known_components.remove(&name).unwrap()); + let channels = self.broadcasters.get(&name).unwrap(); + broadcasters.insert(name.clone(), channels.0.clone()); + } + ComponentExecutor { + world: world.to_owned(), + components, + broadcasters, + has_been_initialized: false, + } + } + + /// Check, create and bind the relationships and communication channels between the components. + /// A world define what is accessible. Mostly for execution separation purpose (another thread, server, anything). + /// "requirements" must be a DAG. It must always be in the same "world". + /// "peers" must be mutual. + /// + pub fn build_routes(&mut self) -> Result<(), String> { + //TODO: check worlds + + type G = petgraph::stable_graph::StableDiGraph; + + //TODO: those are temporary channels, one should work between existing and new channels => we need to save the work of a previous build + let mut notifications: HashMap> = + Default::default(); + let mut broadcasters: HashMap = Default::default(); + + let mut known_services: bimap::BiHashMap> = Default::default(); + let mut depgraph: G = G::default(); + + // initialize the basic channels + for (component_name, handle) in self.known_components.iter_mut() { + let component = handle.component.read().unwrap(); + let node_id = depgraph.add_node(component_name.clone()); + known_services.insert(component_name.clone(), node_id); + if component.accept_notifications() { + let (sender, receiver) = tokio::sync::mpsc::channel(10000); + handle.channels.input_notification = Some(receiver); + notifications.insert(component_name.clone(), sender); + } + broadcasters.insert(component_name.clone(), tokio::sync::broadcast::channel(1)); + } + + // register nodes + for handle in self.known_components.values() { + let component = handle.component.read().unwrap(); + for peer_name in component.peers() { + let peer_name = peer_name.to_string(); + if !known_services.contains_left(&peer_name) { + let node_id = depgraph.add_node(peer_name.clone()); + known_services.insert(peer_name.clone(), node_id); + } + } + for required_service_name in component.requirements() { + let required_service_name = required_service_name.to_string(); + if !known_services.contains_left(&required_service_name) { + let node_id = depgraph.add_node(required_service_name.clone()); + known_services.insert(required_service_name.clone(), node_id); + } + } + for notified_service_name in component.notify() { + let notified_service_name = notified_service_name.to_string(); + if !known_services.contains_left(¬ified_service_name) { + let node_id = depgraph.add_node(notified_service_name.clone()); + known_services.insert(notified_service_name.clone(), node_id); + } + } + } + + // register relationships + for (component_name, handle) in self.known_components.iter() { + let component = handle.component.read().unwrap(); + let node_id = *known_services.get_by_left(component_name).unwrap(); + + trace!("node: {}, peers: {:?}", component_name, component.peers()); + for peer_name in component.peers() { + let peer_name = peer_name.to_string(); + if let Some(peer_handle) = self.known_components.get(&peer_name) { + let peer = &peer_handle.component.read().unwrap(); + trace!("peer: {}, peers: {:?}", peer_name, peer.peers()); + if !peer.peers().contains(&component_name.as_str()) { + return Err(format!( + "Missmatch in peers: '{}' asked for '{}' to be a peer, reverse is not true", + component_name, peer_name + )); + } + let peer_id = *known_services.get_by_left(&peer_name).unwrap(); + let mut has_rel = false; + for e in depgraph.edges_connecting(node_id, peer_id) { + if let RelationShip::Peer = e.weight() { + has_rel = true; + break; + } + } + if !has_rel { + depgraph.add_edge(node_id, peer_id, RelationShip::Peer); + depgraph.add_edge(peer_id, node_id, RelationShip::Peer); + } + } + } + trace!( + "node: {}, requires: {:?}", + component_name, + component.requirements() + ); + for required_service_name in component.requirements() { + let required_service_id = + *known_services.get_by_left(required_service_name).unwrap(); + //let required_service_id = *translation.get(&required_service_id).unwrap_or(&required_service_id); + if node_id != required_service_id { + depgraph.add_edge(node_id, required_service_id, RelationShip::Requires); + //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. + } + } + trace!("node: {}, notify: {:?}", component_name, component.notify()); + for notified_service_name in component.notify() { + let notified_service_id = + *known_services.get_by_left(notified_service_name).unwrap(); + //let notified_service_id = *translation.get(¬ified_service_id).unwrap_or(¬ified_service_id); + if node_id != notified_service_id { + //there is no dep on the graph, the only worth of the notified service is it needs to exist + //The ids are improper since coming from the other graph. But both graphs are clones so it should be fine. + depgraph.add_edge(node_id, notified_service_id, RelationShip::Notify); + } + } + } + //If we reached here, it means all peers agree. + + //TODO: create a text graph file grouping nodes per world + //println!("{}", Dot::with_config(&depgraph, &[])); + + //Is there a difference between keys of known_services vs hosted_services. + let hosted_keys: HashSet = self.known_components.keys().cloned().collect(); + let known_keys: HashSet = depgraph.node_weights().cloned().collect(); + trace!("hosted_keys: {:?}", hosted_keys); + trace!("known_keys: {:?}", known_keys); + if known_keys.difference(&hosted_keys).count() > 0 { + //TODO: have error!() with details of which component asked for it + return Err(format!( + "Some relationship could not be satisfied. Missing: {:?}", + known_keys.difference(&hosted_keys) + )); + } + // no missing component + + // check for cycles + let mut graph_copy = depgraph.clone(); + let invocation_order = get_invocation_order(&mut graph_copy, |e| { + matches!(e.weight(), RelationShip::Requires) + }); + if graph_copy.node_count() > 0 { + return Err(format!( + "Found a cyclic dependancy between {:?}", + graph_copy.node_identifiers() + )); + } + // no cycle + + trace!("services: {:?}", known_services); + trace!("invocation_order: {:?}", invocation_order); + + // At this point, every relationship is sane, none missing, no cycle. We can now build the communication channels. + + /* + TODO: make use of: + requirements graph => components subscribe to it. There should be at most one element in it, eaten at each step of the loop. + => how to make sure ui does subscribe to ui only and back to back ? => introduce "worlds" "myworld:component" + notification graph + invocation order + */ + + for node_id in depgraph.node_indices() { + let notify_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Notify)); + for rel in notify_rel { + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + if let Some(sender) = notifications.get(dst_component_name) { + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + trace!( + "{} wants to notify {}", + src_component_name, + dst_component_name + ); + trace!("source map: {:?}", src_handle.relations_to_ids); + let dst_relative_id = + *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + if let Some(src_component) = self.known_components.get_mut(src_component_name) { + src_component + .channels + .notify + .insert(dst_relative_id, sender.clone()); + } + } + } + let peer_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Peer)); + for rel in peer_rel { + // we shall overwrite the channels, but this is ok since we are not using them yet. + // TODO: if in the future there is dynamic loading, there shall be a need to dynamically rebuilt and thus get and reuse the existing channels. + let (local, remote) = { + let (sender_1, receiver_1) = tokio::sync::mpsc::channel(10000); + let (sender_2, receiver_2) = tokio::sync::mpsc::channel(10000); + ((sender_1, receiver_2), (sender_2, receiver_1)) + }; + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + trace!("{} is a peer of {}", src_component_name, dst_component_name); + + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + src_handle.channels.peers.insert(dst_relative_id, local); + + let dst_handle = self.known_components.get_mut(dst_component_name).unwrap(); + let src_relative_id = *dst_handle.relations_to_ids.get(src_component_name).unwrap(); + dst_handle.channels.peers.insert(src_relative_id, remote); + } + + let requirement_rel = depgraph + .edges(node_id) + .filter(|e| matches!(e.weight(), RelationShip::Requires)); + for rel in requirement_rel { + let src_node_id = rel.source(); + let src_component_name = known_services.get_by_right(&src_node_id).unwrap(); + let dst_node_id = rel.target(); + let dst_component_name = known_services.get_by_right(&dst_node_id).unwrap(); + + let src_handle = self.known_components.get_mut(src_component_name).unwrap(); + let dst_relative_id = *src_handle.relations_to_ids.get(dst_component_name).unwrap(); + let (sender, _) = broadcasters.get(dst_component_name).unwrap(); + trace!( + "{} requires a value from {}", + src_component_name, + dst_component_name + ); + trace!( + "build broadcast of {}. Before subscribe: {}", + dst_component_name, + sender.receiver_count() + ); + src_handle + .channels + .requirements + .insert(dst_relative_id, sender.subscribe()); + trace!( + "build broadcast of {}. After subscribe: {}", + dst_component_name, + sender.receiver_count() + ); + } + } + + for (service_name, handle) in self.known_components.iter_mut() { + let mut component = handle.component.write().unwrap(); + trace!( + "bind {} with, notified: {}, notify: {}, requirements: {}, peers: {}", + service_name, + handle.channels.input_notification.is_some(), + handle.channels.notify.len(), + handle.channels.requirements.len(), + handle.channels.peers.len(), + ); + trace!("Component ids: {:?}", handle.relations_to_ids); + component.bind(std::mem::take(&mut handle.channels)); + } + + //unimplemented!("The algorithm to build and check dependancies between components is not implemented"); + self.broadcasters = broadcasters; + self.notifications = notifications; + self.invocation_order = invocation_order + .iter() + .map(|node_id| known_services.get_by_right(node_id).unwrap().to_string()) + .collect(); + Ok(()) + } +} + +impl Default for ComponentManager { + fn default() -> Self { + Self::new() + } +} + +impl ComponentExecutor { + pub fn init(&mut self) { + assert!( + !self.has_been_initialized, + "An executor can only initialize once the components" + ); + trace!( + "ComponentExecutor::init() {} {}", + self.world, + self.components.len() + ); + for handle in self.components.iter_mut() { + let start = std::time::SystemTime::now(); + let mut component = handle.component.write().unwrap(); + component.init(); + handle.nb_call += 1; + handle.execution_time += start.elapsed().unwrap(); + } + self.has_been_initialized = true; + //unimplemented!("The component executor init is not implemented"); + } + pub fn tick(&mut self, latest_time: f64) { + let span_guard = info_span!("ComponentExecutor::tick()", self.world).entered(); + //trace!("start {}", latest_time); + for handle in self.components.iter_mut() { + let mut component = handle.component.write().unwrap(); + let start = std::time::SystemTime::now(); + //trace!("flush_all_messages of {}", handle.name); + component.flush_all_messages(); + + //trace!("tick for {}", handle.name); + let res = component.tick(latest_time); + + //trace!("broadcast result for {}", handle.name); + let b = self.broadcasters.get_mut(&handle.name).unwrap(); + //trace!("broadcast size for {} before {}, {}", handle.name, b.len(), b.receiver_count()); + handle.nb_call += 1; + handle.execution_time += start.elapsed().unwrap(); + //println!("component execution statistics for {}: {} {}", handle.name, handle.execution_time.as_millis() / handle.nb_call, handle.execution_time.as_millis()); + let _ = b.send(res); + //trace!("broadcast size for {} after {}", handle.name, b.len()); + } + //trace!("end"); + drop(span_guard); + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_invocation_order_1() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + let d = my_graph.add_node("d".to_string()); + let _e = my_graph.add_node("e".to_string()); + + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + my_graph.add_edge(c, d, 1); + my_graph.add_edge(a, d, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); + } + + #[test] + fn test_invocation_order_2() { + type G = petgraph::stable_graph::StableDiGraph; + let mut my_graph = G::default(); + let a = my_graph.add_node("a".to_string()); + let b = my_graph.add_node("b".to_string()); + let c = my_graph.add_node("c".to_string()); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } + + #[test] + fn test_invocation_order_3() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(b, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } + + #[test] + fn test_invocation_order_4() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, c, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 0); + } + + #[test] + fn test_duplicate_node_value() { + type GG = petgraph::stable_graph::StableDiGraph; + let mut my_graph = GG::default(); + let a = my_graph.add_node(1); + let b = my_graph.add_node(2); + let c = my_graph.add_node(3); + let _doublon = my_graph.add_node(3); // same value, considered as a separate node + + my_graph.add_edge(a, b, 1); + my_graph.add_edge(b, a, 1); + my_graph.add_edge(a, c, 1); + + println!("nb nodes: {}", my_graph.node_count()); + let invocation_order = crate::get_invocation_order(&mut my_graph, |_e| true); + println!("nb nodes: {}", my_graph.node_count()); + println!("invocation order: {:?}", invocation_order); + assert!(my_graph.node_count() == 2); + } +} diff --git a/crates/joko_component_models/Cargo.toml b/crates/joko_component_models/Cargo.toml new file mode 100644 index 0000000..2a7b830 --- /dev/null +++ b/crates/joko_component_models/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "joko_component_models" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["messages_any"] +messages_any = [] +messages_downcast = [] +messages_bincode = ["dep:bincode"] + + +[dependencies] +bincode = { workspace = true, optional = true } +#downcast-rs = "1.2.1" # TODO: implement messages_downcast feature +tokio = { workspace = true } +serde = { workspace = true } diff --git a/crates/joko_component_models/src/lib.rs b/crates/joko_component_models/src/lib.rs new file mode 100644 index 0000000..8ec3d9a --- /dev/null +++ b/crates/joko_component_models/src/lib.rs @@ -0,0 +1,73 @@ +#[cfg(feature = "messages_any")] +mod messages_any; +#[cfg(feature = "messages_any")] +pub use messages_any::*; + +#[cfg(feature = "messages_bincode")] +mod messages_bincode; +#[cfg(feature = "messages_bincode")] +pub use messages_bincode::*; + +pub type PeerComponentChannel = ( + tokio::sync::mpsc::Sender, + tokio::sync::mpsc::Receiver, +); + +pub trait Component: Send + Sync { + /** + Names are external to traits and implementation. That way it is easy to change it without change in binary. + In case of first class components, name is hardcoded. + In case of plugins, name is part of a manifest and can be changed at will. + */ + //TODO: fn watch(&self) -> Vec<&str> {} + // elements in peer(), requires() and notify() are mutually exclusives + fn peers(&self) -> Vec<&str> { + //by default, no other plugin bound + vec![] + } + /// Shall eat a new value produced by the required components at each tick + /// By default, no requirement + fn requirements(&self) -> Vec<&str> { + vec![] + } + fn notify(&self) -> Vec<&str> { + //by default, no third party plugin + vec![] + } + fn accept_notifications(&self) -> bool { + false + } + /* + TODO: + for global values that does not need a specific new value at each frame (such as configuration), watch over the values. + fn watch(&self) -> Vec<&str> + https://docs.rs/tokio/latest/tokio/sync/watch/index.html + */ + + /// called once after building relationships + fn init(&mut self); + + /// Drain every notifications sent by any other component + fn flush_all_messages(&mut self); + + fn tick(&mut self, latest_time: f64) -> ComponentResult; + + /// when reasing the channels, the id of channels are set by their appearance order in "peers", then "requirements", then "notify" + fn bind(&mut self, channels: ComponentChannels); + /* + TODO: there could be an optional trait: Chain. + If there is a strong connection between two elements, passing values by channels and copy could be inefficient, calling a function with arguments could be better => + it's almost a macro with an unset number of arguments and unknown types. + It could be possible on plugins, not other kind of components + */ +} + +/// when reasing the channels, the id of channels are set by their appearance order in "peers", then "requirements", then "notify" +#[derive(Default)] +pub struct ComponentChannels { + pub requirements: + std::collections::HashMap>, + pub peers: std::collections::HashMap, // ??? scsc if exists, this is a private channel only two bounded modules can use between each others. + pub input_notification: Option>, + pub notify: std::collections::HashMap>, // used to send a message to another plugin. This is a reversed requirement. A plugin force itself into the path of another. +} diff --git a/crates/joko_component_models/src/messages_any.rs b/crates/joko_component_models/src/messages_any.rs new file mode 100644 index 0000000..4bf83af --- /dev/null +++ b/crates/joko_component_models/src/messages_any.rs @@ -0,0 +1,67 @@ +use std::{any::TypeId, sync::Arc}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct ComponentMessage { + data: Arc>, +} + +#[derive(Clone)] +pub struct ComponentResult { + //TODO: remove + Send + Sync + data: Arc>, +} + +pub fn default_component_result() -> ComponentResult { + ComponentResult { + data: Arc::new(Box::new(0)), + } +} + +pub fn to_data(value: T) -> ComponentMessage +where + T: Serialize + Clone + Send + Sync + 'static, +{ + ComponentMessage { + data: Arc::new(Box::new(value)), + } +} +pub fn to_broadcast(value: T) -> ComponentResult +where + T: Serialize + Clone + Send + Sync + 'static, //TODO: remove + Send + Sync +{ + ComponentResult { + data: Arc::new(Box::new(value)), + } +} + +pub fn from_data<'a, T>(value: &'a ComponentMessage) -> T +where + T: Deserialize<'a> + Clone + Send + Sync + 'static, +{ + if let Some(d) = value.data.downcast_ref::() { + d.to_owned() + } else { + panic!( + "Bad routing of elements, expected {:?} {:?}", + TypeId::of::(), + TypeId::of::() + ); + } +} + +pub fn from_broadcast<'a, T>(value: &'a ComponentResult) -> T +where + T: Deserialize<'a> + Clone + Send + Sync + 'static, //TODO: remove + Send + Sync +{ + if let Some(d) = value.data.downcast_ref::() { + d.to_owned() + } else { + panic!( + "Bad routing of elements, expected {:?} {:?}", + TypeId::of::(), + TypeId::of::() + ); + } +} diff --git a/crates/joko_component_models/src/messages_bincode.rs b/crates/joko_component_models/src/messages_bincode.rs new file mode 100644 index 0000000..1e706b1 --- /dev/null +++ b/crates/joko_component_models/src/messages_bincode.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct ComponentMessage { + data: Vec, +} +#[derive(Clone, Default)] +pub struct ComponentResult { + data: Vec, +} + +pub fn default_component_result() -> ComponentResult { + ComponentResult::default() +} + +pub fn to_data(value: T) -> ComponentMessage +where + T: Serialize, +{ + ComponentMessage { + data: bincode::serialize(&value).unwrap(), + } +} +pub fn to_broadcast(value: T) -> ComponentResult +where + T: Serialize, +{ + ComponentResult { + data: bincode::serialize(&value).unwrap(), + } +} + +pub fn from_data<'a, T>(value: &'a ComponentMessage) -> T +where + T: Deserialize<'a>, +{ + bincode::deserialize(&value.data).unwrap() +} + +pub fn from_broadcast<'a, T>(value: &'a ComponentResult) -> T +where + T: Deserialize<'a>, +{ + bincode::deserialize(&value.data).unwrap() +} diff --git a/crates/joko_core/Cargo.toml b/crates/joko_core/Cargo.toml index fe8a74d..19c231c 100644 --- a/crates/joko_core/Cargo.toml +++ b/crates/joko_core/Cargo.toml @@ -6,23 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cap-directories = { version = "*" } -cap-std = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "time", -] } # for ErrorLayer -tracing-appender = { version = "*" } -miette = { workspace = true } - -egui = { workspace = true, features = ["serde"] } -egui_extras = { workspace = true } - -ringbuffer = { workspace = true } -rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -indexmap = { workspace = true } -rfd = { workspace = true } +bytemuck = { workspace = true } glam = { workspace = true } +scopeguard = "1.2.0" +smol_str = { workspace = true } +serde = { workspace = true } diff --git a/crates/joko_core/src/lib.rs b/crates/joko_core/src/lib.rs index 415a8dc..f7864f2 100644 --- a/crates/joko_core/src/lib.rs +++ b/crates/joko_core/src/lib.rs @@ -1,4 +1,8 @@ -pub mod manager; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; + /* each manager must have 1. a main thread struct @@ -7,3 +11,111 @@ each manager must have 4. a public api for other managers to access */ + +pub mod serde_glam; +pub mod task; + +/// This newtype is used to represents relative paths in marker packs +/// 1. It won't start with `/` or `C:` like roots, because its a relative path +/// 2. It can be empty to represent current directory +/// 3. No expansion of special characters like `.` or `..` stuff. +/// 4. It is always lowercase to avoid platform specific quirks. +/// 5. It will use `/` as the path separator. +/// 6. It doesn't mean that the path is valid. It may contain many of the utf-8 characters which are not valid path names on linux/windows +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RelativePath(SmolStr); + +impl Serialize for RelativePath { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} +impl<'de> Deserialize<'de> for RelativePath { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let r = s.parse().unwrap(); + Ok(r) + } +} + +#[allow(unused)] +impl RelativePath { + pub fn normalize(path: &str) -> String { + let normalized_slash = path.replace('\\', "/"); + let trimmed_path = normalized_slash.trim_start_matches('/'); + trimmed_path.to_lowercase() + } + + pub fn join_str(&self, path: &str) -> Self { + let normalized_path = RelativePath::normalize(path); + if normalized_path.is_empty() { + return Self(self.0.clone()); + } + if self.0.is_empty() { + // no need to push `/` if we are empty, as that would make it an absolute path + return Self(normalized_path.into()); + } + + let mut new = self.0.to_string(); + if !self.0.ends_with('/') { + new.push('/'); + } + new.push_str(&normalized_path); + Self(new.into()) + } + + pub fn ends_with(&self, ext: &str) -> bool { + self.0.ends_with(ext) + } + pub fn is_png(&self) -> bool { + self.ends_with(".png") + } + pub fn is_tbin(&self) -> bool { + self.ends_with(".trl") + } + pub fn is_xml(&self) -> bool { + self.ends_with(".xml") + } + pub fn is_dir(&self) -> bool { + self.ends_with("/") + } + pub fn parent(&self) -> Option<&str> { + let path = self.0.trim_end_matches('/'); + if path.is_empty() { + return None; + } + path.rfind('/').map(|index| &path[..=index]) + } + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for RelativePath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl From for String { + fn from(val: RelativePath) -> String { + val.0.into() + } +} +impl FromStr for RelativePath { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let path = RelativePath::normalize(s); + if path.is_empty() { + return Ok(Self::default()); + } + Ok(Self(path.into())) + } +} diff --git a/crates/joko_core/src/serde_glam/mod.rs b/crates/joko_core/src/serde_glam/mod.rs new file mode 100644 index 0000000..1401592 --- /dev/null +++ b/crates/joko_core/src/serde_glam/mod.rs @@ -0,0 +1,5 @@ +mod vec2; +mod vec3; + +pub use vec2::{IVec2, UVec2, Vec2}; +pub use vec3::Vec3; diff --git a/crates/joko_core/src/serde_glam/vec2.rs b/crates/joko_core/src/serde_glam/vec2.rs new file mode 100644 index 0000000..908ab95 --- /dev/null +++ b/crates/joko_core/src/serde_glam/vec2.rs @@ -0,0 +1,162 @@ +use serde::{ + de::{SeqAccess, Visitor}, + Deserialize, Serialize, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Vec2(pub glam::Vec2); +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct IVec2(pub glam::IVec2); +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct UVec2(pub glam::UVec2); + +impl From for glam::Vec2 { + fn from(src: Vec2) -> glam::Vec2 { + src.0 + } +} +impl From for glam::IVec2 { + fn from(src: IVec2) -> glam::IVec2 { + src.0 + } +} +impl From for glam::UVec2 { + fn from(src: UVec2) -> glam::UVec2 { + src.0 + } +} + +unsafe impl bytemuck::Pod for Vec2 {} +unsafe impl bytemuck::Zeroable for Vec2 { + fn zeroed() -> Self { + Self::default() + } +} + +struct Vec2Deserializer; +impl<'de> Visitor<'de> for Vec2Deserializer { + type Value = Vec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Vec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: f32 = seq.next_element()?.unwrap(); + let y: f32 = seq.next_element()?.unwrap(); + let res = Vec2(glam::Vec2 { x, y }); + Ok(res) + } +} + +impl Serialize for Vec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for Vec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(Vec2Deserializer) + } +} + +struct IVec2Deserializer; +impl<'de> Visitor<'de> for IVec2Deserializer { + type Value = IVec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("IVec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: i32 = seq.next_element()?.unwrap(); + let y: i32 = seq.next_element()?.unwrap(); + let res = IVec2(glam::IVec2 { x, y }); + Ok(res) + } +} +impl Serialize for IVec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for IVec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(IVec2Deserializer) + } +} + +struct UVec2Deserializer; +impl<'de> Visitor<'de> for UVec2Deserializer { + type Value = UVec2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("UVec2Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: u32 = seq.next_element()?.unwrap(); + let y: u32 = seq.next_element()?.unwrap(); + let res = UVec2(glam::UVec2 { x, y }); + Ok(res) + } +} + +impl Serialize for UVec2 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for UVec2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(UVec2Deserializer) + } +} diff --git a/crates/joko_core/src/serde_glam/vec3.rs b/crates/joko_core/src/serde_glam/vec3.rs new file mode 100644 index 0000000..8446f25 --- /dev/null +++ b/crates/joko_core/src/serde_glam/vec3.rs @@ -0,0 +1,65 @@ +use serde::{ + de::{SeqAccess, Visitor}, + Deserialize, Serialize, +}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Vec3(pub glam::Vec3); + +impl From for glam::Vec3 { + fn from(src: Vec3) -> glam::Vec3 { + src.0 + } +} + +unsafe impl bytemuck::Pod for Vec3 {} +unsafe impl bytemuck::Zeroable for Vec3 { + fn zeroed() -> Self { + Self::default() + } +} + +struct Vec3Deserializer; +impl<'de> Visitor<'de> for Vec3Deserializer { + type Value = Vec3; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Vec3Deserializer key value sequence.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let _n: Option = seq.next_element()?; + let x: f32 = seq.next_element()?.unwrap(); + let y: f32 = seq.next_element()?.unwrap(); + let z: f32 = seq.next_element()?.unwrap(); + let res = Vec3(glam::Vec3 { x, y, z }); + Ok(res) + } +} + +impl Serialize for Vec3 { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.0.x)?; + seq.serialize_element(&self.0.y)?; + seq.serialize_element(&self.0.z)?; + seq.end() + } +} + +impl<'de> Deserialize<'de> for Vec3 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_seq(Vec3Deserializer) + } +} diff --git a/crates/joko_core/src/task/mod.rs b/crates/joko_core/src/task/mod.rs new file mode 100644 index 0000000..3412182 --- /dev/null +++ b/crates/joko_core/src/task/mod.rs @@ -0,0 +1,80 @@ +use std::{ + result::Result, + sync::{ + mpsc::{RecvError, SendError}, + Arc, Mutex, + }, + thread::JoinHandle, +}; + +//TODO: could this be a wrapper only and a move/copy would not impact content ? +pub struct AsyncTaskGuard { + task_sender: std::sync::mpsc::Sender, + result_receiver: std::sync::mpsc::Receiver, + thread_task: Option>, + thread_nb: Option>, + nb: Arc, +} + +pub type AsyncTask = Arc>>; + +impl AsyncTaskGuard +where + TaskItem: Send + 'static, + ResultItem: Send + 'static, +{ + pub fn new(f: F) -> AsyncTask + where + F: Fn(TaskItem) -> ResultItem + Send + 'static, + { + //https://doc.rust-lang.org/rust-by-example/std_misc/channels.html + let (task_sender, th_task_receiver) = std::sync::mpsc::channel(); + let (th_result_sender, result_receiver) = std::sync::mpsc::channel(); + let (nb_sender, nb_receiver) = std::sync::mpsc::channel(); + let nb = Arc::new(std::sync::atomic::AtomicI32::new(0)); + let res = Arc::new(Mutex::new(Self { + task_sender, + result_receiver, + thread_task: None, + thread_nb: None, + nb: Arc::clone(&nb), + })); + let thread_task = std::thread::spawn(move || { + while let Ok(elt) = th_task_receiver.recv() { + let _guard = scopeguard::guard(0, |_| { + let _ = nb_sender.send(-1); + }); + let _ = nb_sender.send(1); + let _ = th_result_sender.send(f(elt)); + } + }); + let thread_nb = std::thread::spawn(move || { + while let Ok(elt) = nb_receiver.recv() { + { + nb.fetch_add(elt, std::sync::atomic::Ordering::Relaxed); + } + } + }); + + { + let mut t = res.lock().unwrap(); + t.thread_task = Some(thread_task); + t.thread_nb = Some(thread_nb); + } + res + } + pub fn send(&self, value: TaskItem) -> Result<(), SendError> { + self.task_sender.send(value) + } + pub fn recv(&self) -> Result { + self.result_receiver.recv() + } + + pub fn count(&self) -> i32 { + self.nb.load(std::sync::atomic::Ordering::Relaxed) + } + pub fn is_running(&self) -> bool { + let nb = self.nb.load(std::sync::atomic::Ordering::Relaxed); + nb != 0 + } +} diff --git a/crates/jokolink/Cargo.toml b/crates/joko_link_manager/Cargo.toml similarity index 79% rename from crates/jokolink/Cargo.toml rename to crates/joko_link_manager/Cargo.toml index be3bbf7..15d56a7 100644 --- a/crates/jokolink/Cargo.toml +++ b/crates/joko_link_manager/Cargo.toml @@ -1,29 +1,28 @@ [package] -name = "jokolink" +name = "joko_link_manager" version = "0.2.1" edition = "2021" [lib] crate-type = ["cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] [dependencies] +joko_core = { path = "../joko_core" } +joko_link_models = { path = "../joko_link_models" } +joko_component_models = { path = "../joko_component_models" } widestring = { version = "1", default-features = false, features = ["std"] } num-derive = { version = "0", default-features = false } num-traits = { version = "0", default-features = false } -tracing-appender = { version = "0" } -tracing-subscriber = { version = "*" } -jokoapi = { path = "../jokoapi" } enumflags2 = { workspace = true } time = { workspace = true } miette = { workspace = true } tracing = { workspace = true } -egui = { workspace = true } serde = { workspace = true } glam = { workspace = true } serde_json = { workspace = true } -notify = { version = "*", default-features = false } +tokio = { workspace = true } + [target.'cfg(unix)'.dependencies] x11rb = { version = "0.12", default-features = false, features = [] } @@ -43,3 +42,7 @@ windows = { version = "0.51.1", features = [ "Win32_System_Com", ] } arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } + diff --git a/crates/jokolink/README.md b/crates/joko_link_manager/README.md similarity index 100% rename from crates/jokolink/README.md rename to crates/joko_link_manager/README.md diff --git a/crates/joko_link_manager/src/lib.rs b/crates/joko_link_manager/src/lib.rs new file mode 100644 index 0000000..acbbba2 --- /dev/null +++ b/crates/joko_link_manager/src/lib.rs @@ -0,0 +1,289 @@ +//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory + +//! Joko link is designed to primarily get the MumbleLink or the window size +//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). +//! on windows, you can use it to create/open shared memory. +//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. +//! then, you can easily read the /dev/shm file from a any number of linux native applications. +//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. +//! + +use std::vec; + +use enumflags2::BitFlags; +use joko_component_models::{ + from_data, to_broadcast, to_data, Component, ComponentChannels, ComponentMessage, + ComponentResult, +}; +use joko_core::serde_glam::{IVec2, UVec2, Vec3}; +use joko_link_models::{ctypes, MessageToMumbleLink, MumbleChanges, MumbleLink}; +//use jokoapi::end_point::{mounts::Mount, races::Race}; +use miette::{IntoDiagnostic, Result, WrapErr}; +use serde_json::from_str; +use tracing::{error, trace}; + +/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing +pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; +#[cfg(target_os = "linux")] +pub mod linux; +#[cfg(target_os = "windows")] +pub mod win; + +#[cfg(target_os = "linux")] +use linux::MumbleLinuxImpl as MumblePlatformImpl; +#[cfg(target_os = "windows")] +use win::MumbleWinImpl as MumblePlatformImpl; + +struct MumbleChannels { + notification_receiver: tokio::sync::mpsc::Receiver, + front_end_notifier: Option>, +} +// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there +// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; +/// This primarily manages the mumble backend. +/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. +/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. +/// and every frame gets the latest mumble link data, and compares with the previous frame. +/// if any of the changed this frame, it will set the relevant changed flags so that plugins +/// or other parts of program which care can run the relevant code. +pub struct MumbleManager { + /// This abstracts over the windows and linux impl of mumble link functionality. + /// we use this to get the latest mumble link and latest window dimensions of the current mumble link + backend: MumblePlatformImpl, + is_ui: bool, + repeat_last_value: bool, + /// latest mumble link + link: Option, + + channels: Option, +} + +impl MumbleManager { + pub fn new(name: &str, is_ui: bool) -> Result { + let backend = MumblePlatformImpl::new(name)?; + Ok(Self { + backend, + link: Default::default(), + channels: None, + is_ui, + repeat_last_value: false, + }) + } + pub fn is_alive(&self) -> bool { + self.backend.is_alive() + } + fn handle_message(&mut self, msg: MessageToMumbleLink) { + match msg { + MessageToMumbleLink::Autonomous => { + trace!("Handling of MessageToMumbleLink::Autonomous {}", self.is_ui); + self.repeat_last_value = false; + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = + front_end_notifier.blocking_send(to_data(MessageToMumbleLink::Autonomous)); + } + } + MessageToMumbleLink::BindedOnUI => { + trace!("Handling of MessageToMumbleLink::BindedOnUI {}", self.is_ui); + self.repeat_last_value = true; + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = + front_end_notifier.blocking_send(to_data(MessageToMumbleLink::BindedOnUI)); + } + } + MessageToMumbleLink::Value(mut link) => { + trace!("Handling of MessageToMumbleLink::Value {}", self.is_ui); + link.changes = BitFlags::all(); + self.link = Some(link); + if !self.is_ui { + let channels = self.channels.as_ref().unwrap(); + let front_end_notifier = channels.front_end_notifier.as_ref().unwrap(); + let _ = front_end_notifier.blocking_send(to_data(MessageToMumbleLink::Value( + self.link.clone().unwrap(), + ))); + } + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToMumbleLink has not been implemented yet"); + } + } + } + fn _tick(&mut self) -> Result> { + if let Err(e) = self.backend.tick() { + error!(?e, "mumble backend tick error"); + return Ok(None); + } + + //println!("mumble_link {} map found {}", self.is_ui, self.link.map_id); + if !self.backend.is_alive() { + if let Some(link) = self.link.as_mut() { + link.client_size.0.x = 0; + link.client_size.0.y = 0; + link.changes = BitFlags::all(); + } + return Ok(self.link.clone()); + } + // backend is alive and tick is successful. time to get link + let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); + let mut new_link = if cml.ui_tick == 0 && self.link.is_some() { + Default::default() + } else { + self.link.clone().unwrap_or_default() + }; + + if cml.ui_tick == 0 || cml.context.client_pos == [0; 2] { + return Ok(None); + } + let mut changes: BitFlags = Default::default(); + // safety. as the link is valid, we can use as_ref + let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) + .into_diagnostic() + .wrap_err("failed to get widestring out of cml identity")? + .to_string() + .into_diagnostic() + .wrap_err("failed to convert widestring to cstring")?; + + let identity: ctypes::CIdentity = from_str(&json_string) + .into_diagnostic() + .wrap_err("failed to deserialize identity from json string")?; + let uisz = identity + .get_uisz() + .ok_or(miette::miette!("uisz is invalid"))?; + let server_address = if cml.context.server_address[0] == 2 { + let addr = cml.context.server_address; + std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() + } else { + std::net::Ipv4Addr::UNSPECIFIED.into() + }; + if new_link.ui_tick != cml.ui_tick { + changes.insert(MumbleChanges::UiTick); + } + if new_link.name != identity.name { + changes.insert(MumbleChanges::Character); + } + if new_link.map_id != cml.context.map_id { + changes.insert(MumbleChanges::Map); + } + let client_pos = IVec2(glam::IVec2::new( + cml.context.client_pos[0], + cml.context.client_pos[1], + )); + let client_size = UVec2(glam::UVec2::new( + cml.context.client_size[0], + cml.context.client_size[1], + )); + + if new_link.client_pos != client_pos { + changes.insert(MumbleChanges::WindowPosition); + } + if new_link.client_size != client_size { + changes.insert(MumbleChanges::WindowSize); + } + let cam_pos: glam::Vec3 = cml.f_camera_position.into(); + if new_link.cam_pos.0 != cam_pos { + changes.insert(MumbleChanges::Camera); + } + + let player_pos: glam::Vec3 = cml.f_avatar_position.into(); + if new_link.player_pos.0 != player_pos { + changes.insert(MumbleChanges::Position); + } + //let player_race = Self::get_race(identity.race); + + new_link = MumbleLink { + ui_tick: cml.ui_tick, + player_pos: Vec3(player_pos), + f_avatar_front: Vec3(cml.f_avatar_front.into()), + cam_pos: Vec3(cam_pos), + f_camera_front: Vec3(cml.f_camera_front.into()), + name: identity.name, + map_id: cml.context.map_id, + fov: identity.fov, + uisz, + // window_pos, + // window_size, + changes, + // window_pos_without_borders, + // window_size_without_borders, + dpi_scaling: cml.context.dpi_scaling, + dpi: cml.context.dpi, + client_pos, + client_size, + map_type: cml.context.map_type, + server_address, + shard_id: cml.context.shard_id, + instance: cml.context.instance, + build_id: cml.context.build_id, + ui_state: cml.context.get_ui_state(), + compass_width: cml.context.compass_width, + compass_height: cml.context.compass_height, + compass_rotation: cml.context.compass_rotation, + player_x: cml.context.player_x, + player_y: cml.context.player_y, + map_center_x: cml.context.map_center_x, + map_center_y: cml.context.map_center_y, + map_scale: cml.context.map_scale, + process_id: cml.context.process_id, + mount: cml.context.mount_index, + race: identity.race, + }; + self.link = Some(new_link); + + Ok(self.link.clone()) + } +} + +impl Component for MumbleManager { + fn init(&mut self) {} + + fn accept_notifications(&self) -> bool { + // we may want to receive data from a manually edited form + true + } + + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + if self.repeat_last_value { + to_broadcast(self.link.clone()) + } else { + let link = self._tick().unwrap_or(None); + to_broadcast(link) + } + } + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = MumbleChannels { + notification_receiver: channels.input_notification.unwrap(), + front_end_notifier: channels.notify.remove(&0), + }; + self.channels = Some(channels); + } + fn notify(&self) -> Vec<&str> { + if self.is_ui { + vec![] + } else { + vec!["ui:mumble_link"] + } + } +} diff --git a/crates/jokolink/src/linux/mod.rs b/crates/joko_link_manager/src/linux/mod.rs similarity index 99% rename from crates/jokolink/src/linux/mod.rs rename to crates/joko_link_manager/src/linux/mod.rs index d88aba8..f0adab4 100644 --- a/crates/jokolink/src/linux/mod.rs +++ b/crates/joko_link_manager/src/linux/mod.rs @@ -24,6 +24,7 @@ impl MumbleLinuxImpl { pub fn new(link_name: &str) -> Result { let mumble_file_name = format!("/dev/shm/{link_name}"); info!("creating mumble file at {mumble_file_name}"); + #[allow(clippy::suspicious_open_options)] let mut mfile = File::options() .read(true) .write(true) // write/append is needed for the create flag diff --git a/crates/jokolink/src/win/dll.rs b/crates/joko_link_manager/src/win/dll.rs similarity index 99% rename from crates/jokolink/src/win/dll.rs rename to crates/joko_link_manager/src/win/dll.rs index cccbf92..09cca0a 100644 --- a/crates/jokolink/src/win/dll.rs +++ b/crates/joko_link_manager/src/win/dll.rs @@ -22,12 +22,13 @@ unsafe fn spawn_jokolink_thread() { d3d11::JOKOLINK_QUIT_REQUESTER = Some(quit_request_sender); d3d11::JOKOLINK_QUIT_RESPONDER = Some(quit_response_receiver); - match std::thread::Builder::new() + let th = std::thread::Builder::new() .name("jokolink thread".to_string()) .spawn(move || { d3d11::wine::wine_main(quit_request_receiver, quit_response_sender); "jokolink thread quit" - }) { + }); + match th { Ok(handle) => { println!("spawned jokolink thread. handle: {handle:?}"); d3d11::JOKOLINK_THREAD_HANDLE = Some(handle); @@ -252,7 +253,7 @@ pub mod d3d11 { // 0 // } pub mod wine { - use crate::mumble::ctypes::*; + use crate::ctypes::*; use crate::win::MumbleWinImpl; use crate::DEFAULT_MUMBLELINK_NAME; use miette::{Context, IntoDiagnostic, Result}; @@ -411,6 +412,7 @@ pub mod d3d11 { &dest_path ); + #[allow(clippy::blocks_in_conditions, clippy::suspicious_open_options)] let mut mfile = std::fs::File::options() .write(true) .create(true) diff --git a/crates/jokolink/src/win/mod.rs b/crates/joko_link_manager/src/win/mod.rs similarity index 93% rename from crates/jokolink/src/win/mod.rs rename to crates/joko_link_manager/src/win/mod.rs index 8910412..e786586 100644 --- a/crates/jokolink/src/win/mod.rs +++ b/crates/joko_link_manager/src/win/mod.rs @@ -3,7 +3,7 @@ pub mod dll; //putting all the winapi specific stuff here. so that i can lock it all behind a cfg attr at the mod declaration -use crate::mumble::ctypes::*; +use crate::ctypes::{CMumbleLink, C_MUMBLE_LINK_SIZE_FULL}; use miette::{bail, Context, IntoDiagnostic, Result}; use notify::Watcher; use std::{ @@ -70,7 +70,8 @@ pub struct MumbleWinImpl { last_pos_size_check: Instant, /// this is the position and size of gw2 window's client area. So, no borders or titlebar stuff. Just the viewport. - client_pos_size: [i32; 4], + client_pos: [i32; 2], + client_size: [u32; 2], /// Whether dpi scaling is enbaled or not in gw2. we parse this setting from gw2's configuration stored in AppData/Roaming/Guild Wars 2/GFXSettings.Gw2-64.exe.xml /// 0 for false /// 1 for true @@ -99,6 +100,9 @@ pub struct MumbleWinImpl { */ } +unsafe impl Send for MumbleWinImpl {} +unsafe impl Sync for MumbleWinImpl {} + impl MumbleWinImpl { pub fn new(key: &str) -> Result { unsafe { @@ -173,7 +177,8 @@ impl MumbleWinImpl { last_pos_size_check: Instant::now(), // window_pos_size_without_borders: [0; 4], dpi_scaling, - client_pos_size: [0; 4], + client_pos: [0; 2], + client_size: [0; 2], dpi: 0, _gw2_config_watcher: gw2_config_watcher, gw2_config_changed, @@ -185,7 +190,7 @@ impl MumbleWinImpl { !self.process_handle.is_invalid() } pub fn get_cmumble_link(&mut self) -> CMumbleLink { - let mut link = unsafe { std::ptr::read_volatile(self.link_ptr) }; + let mut link: CMumbleLink = unsafe { std::ptr::read_volatile(self.link_ptr) }; link.context.timestamp = OffsetDateTime::now_utc() .unix_timestamp_nanos() .to_le_bytes(); @@ -194,7 +199,8 @@ impl MumbleWinImpl { link.context.dpi_scaling = self.dpi_scaling; link.context.dpi = self.dpi; link.context.xid = self.xid; - link.context.client_pos_size = self.client_pos_size; + link.context.client_pos = self.client_pos; + link.context.client_size = self.client_size; link } /// This is the most important function which will be called every frame @@ -335,24 +341,26 @@ impl MumbleWinImpl { // return Ok(()); // } // }; - self.client_pos_size = - match get_client_rect_in_screen_coords(HWND(self.window_handle)) { - Ok(client_pos_size) => { - if self.client_pos_size != client_pos_size { - info!( - ?self.client_pos_size, - ?client_pos_size, - "window position size changed" - ); - } - client_pos_size + match get_client_rect_in_screen_coords(HWND(self.window_handle)) { + Ok((client_pos, client_size)) => { + if self.client_pos != client_pos || self.client_size != client_size { + info!( + ?self.client_pos, + ?client_pos, + ?self.client_size, + ?client_size, + "window position or size changed" + ); } - Err(e) => { - error!(?e, "failed to get client position size"); - self.reset(); // go back to being dead because it shouldn't usually fail - return Ok(()); - } - }; + self.client_pos = client_pos; + self.client_size = client_size; + } + Err(e) => { + error!(?e, "failed to get client position size"); + self.reset(); // go back to being dead because it shouldn't usually fail + return Ok(()); + } + }; } } } @@ -371,7 +379,8 @@ impl MumbleWinImpl { // self.window_pos_size = [0; 4]; // self.window_pos_size_without_borders = [0; 4]; self.dpi = 0; - self.client_pos_size = [0; 4]; + self.client_pos = [0; 2]; + self.client_size = [0; 2]; self.previous_pid = 0; self.xid = 0; } @@ -420,7 +429,7 @@ impl MumbleWinImpl { // now we have both process_handle and window_handle. We just need the window size to initialize our struct // this function only gets the suface/viewport pos/size without any borders/decoraitons. match get_client_rect_in_screen_coords(HWND(window_handle)) { - Ok(client_pos_size) => { + Ok((client_pos, client_size)) => { // this block is purely for logging purposes only to verify that all sizes are working properly. { // GetWindowRect includes drop shadow borders and titlebar @@ -489,7 +498,8 @@ impl MumbleWinImpl { info!(dpi, self.dpi, "dpi changed for gw2 window"); } info!( - ?client_pos_size, + ?client_pos, + ?client_size, dpi_awareness, dpi, pid, @@ -500,7 +510,8 @@ impl MumbleWinImpl { self.process_handle = process_handle; self.window_handle = window_handle; self.dpi = dpi; - self.client_pos_size = client_pos_size; + self.client_pos = client_pos; + self.client_size = client_size; self.last_ui_tick_update = Instant::now(); self.previous_pid = pid; } @@ -634,7 +645,7 @@ unsafe extern "system" fn get_handle_by_pid(window_handle: HWND, gw2_pid_ptr: LP /// If you check the logs of jokolink and you use `xwininfo` command to check the actual gw2 window size, you can see the difference. /// On my 4k monitor, it adds 5 pixels on left, right and bottom. And 56 pixels on top. Need to check if dpi affects this (or wayland). /// If these border sizes are universal, then we can subtract those inside this function to get the actual pos/size without borders. -fn get_window_pos_size(window_handle: isize) -> Result<[i32; 4]> { +fn get_window_pos_size(window_handle: isize) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -645,15 +656,15 @@ fn get_window_pos_size(window_handle: isize) -> Result<[i32; 4]> { if let Err(e) = GetWindowRect(HWND(window_handle), &mut rect as *mut RECT) { bail!("GetWindowRect call failed {e:#?}"); } - Ok([ - rect.left, - rect.top, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } -fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<[i32; 4]> { +fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -669,15 +680,15 @@ fn get_window_pos_size_without_borders(window_handle: HWND) -> Result<[i32; 4]> ) { bail!("DwmGetWindowAttribute call failed {e:#?}"); } - Ok([ - rect.left, - rect.top, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [rect.left, rect.top]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } -fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<[i32; 4]> { +fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<([i32; 2], [u32; 2])> { unsafe { let mut rect: RECT = RECT { left: 0, @@ -695,12 +706,12 @@ fn get_client_rect_in_screen_coords(window_handle: HWND) -> Result<[i32; 4]> { if !ClientToScreen(window_handle, &mut point as *mut POINT).as_bool() { bail!("ClientToScreen call failed"); } - Ok([ - point.x, - point.y, - (rect.right - rect.left), - (rect.bottom - rect.top), - ]) + let pos = [point.x, point.y]; + let size = [ + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ]; + Ok((pos, size)) } } impl Drop for MumbleWinImpl { diff --git a/crates/joko_link_models/Cargo.toml b/crates/joko_link_models/Cargo.toml new file mode 100644 index 0000000..3f53d8e --- /dev/null +++ b/crates/joko_link_models/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "joko_link_models" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +joko_core = { path = "../joko_core" } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +miette = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } + +[target.'cfg(unix)'.dependencies] + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.51.1", features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_Graphics_Dwm", + "Win32_UI_HiDpi", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_System_Com", +] } +arcdps = { version = "*", default-features = false } +notify = {version = "*" } +tracing-appender = {version = "*" } +tracing-subscriber = {version = "*" } diff --git a/crates/joko_link_models/README.md b/crates/joko_link_models/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link_models/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link_models/src/lib.rs b/crates/joko_link_models/src/lib.rs new file mode 100644 index 0000000..3717129 --- /dev/null +++ b/crates/joko_link_models/src/lib.rs @@ -0,0 +1,15 @@ +//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory + +//! Joko link is designed to primarily get the MumbleLink or the window size +//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). +//! on windows, you can use it to create/open shared memory. +//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. +//! then, you can easily read the /dev/shm file from a any number of linux native applications. +//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. +//! + +mod messages; +mod mumble; + +pub use messages::*; +pub use mumble::*; diff --git a/crates/joko_link_models/src/messages.rs b/crates/joko_link_models/src/messages.rs new file mode 100644 index 0000000..8b5d85d --- /dev/null +++ b/crates/joko_link_models/src/messages.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::MumbleLink; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToMumbleLink { + BindedOnUI, + Autonomous, + Value(MumbleLink), //pushed from a value imposed by UI. Either a form or a traveling for demo. +} diff --git a/crates/jokolink/src/mumble/ctypes.rs b/crates/joko_link_models/src/mumble/ctypes.rs similarity index 94% rename from crates/jokolink/src/mumble/ctypes.rs rename to crates/joko_link_models/src/mumble/ctypes.rs index 4012cc8..72dd4ac 100644 --- a/crates/jokolink/src/mumble/ctypes.rs +++ b/crates/joko_link_models/src/mumble/ctypes.rs @@ -1,5 +1,4 @@ use enumflags2::BitFlags; -use jokoapi::end_point::{mounts::Mount, races::Race}; use miette::bail; use serde::{Deserialize, Serialize}; @@ -68,6 +67,7 @@ impl Default for CMumbleLink { } } } + impl CMumbleLink { /// This takes a point and reads out the CMumbleLink struct from it. wrapper for unsafe ptr read pub fn get_cmumble_link(link_ptr: *const CMumbleLink) -> CMumbleLink { @@ -186,7 +186,8 @@ pub struct CMumbleContext { /// This is the actual dpi of the gw2 window. 96 is the default (scale 1.0) value. pub dpi: i32, /// This is the client (gw2 window's viewport/surface) position and area. This tells jokolay where to position and size itself to match gw2 window. - pub client_pos_size: [i32; 4], + pub client_pos: [i32; 2], + pub client_size: [u32; 2], /// to make the struct the right size. everything upto now is 120 bytes, so this rounds upto 256 bytes. pub padding: [u8; 96], } @@ -218,7 +219,8 @@ impl Default for CMumbleContext { // window_pos_size_without_borders: Default::default(), dpi_scaling: Default::default(), dpi: Default::default(), - client_pos_size: Default::default(), + client_pos: Default::default(), + client_size: Default::default(), } } } @@ -242,22 +244,6 @@ impl CMumbleContext { ]); Ok(ip) } - - pub fn get_mount(&self) -> Option { - Some(match self.mount_index { - 1 => Mount::Jackal, - 2 => Mount::Griffon, - 3 => Mount::Springer, - 4 => Mount::Skimmer, - 5 => Mount::Raptor, - 6 => Mount::RollerBeetle, - 7 => Mount::Warclaw, - 8 => Mount::Skyscale, - 9 => Mount::Skiff, - 10 => Mount::SiegeTurtle, - _ => return None, - }) - } } #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] @@ -299,14 +285,4 @@ impl CIdentity { _ => return None, }) } - pub fn get_race(&self) -> Option { - Some(match self.race { - 0 => Race::ASURA, - 1 => Race::CHARR, - 2 => Race::HUMAN, - 3 => Race::NORN, - 4 => Race::SYLVARI, - _ => return None, - }) - } } diff --git a/crates/jokolink/src/mumble/mod.rs b/crates/joko_link_models/src/mumble/mod.rs similarity index 90% rename from crates/jokolink/src/mumble/mod.rs rename to crates/joko_link_models/src/mumble/mod.rs index 9ded416..510dc33 100644 --- a/crates/jokolink/src/mumble/mod.rs +++ b/crates/joko_link_models/src/mumble/mod.rs @@ -4,15 +4,14 @@ pub mod ctypes; use std::net::IpAddr; use enumflags2::{bitflags, BitFlags}; -use glam::{IVec2, Vec3}; -use jokoapi::end_point::mounts::Mount; use num_derive::FromPrimitive; use num_derive::ToPrimitive; -use serde::Deserialize; -use serde::Serialize; + +use joko_core::serde_glam::*; +use serde::{Deserialize, Serialize}; /// As the CMumbleLink has all the fields multiple -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MumbleLink { /// ui tick. (more or less represents the frame number of gw2) pub ui_tick: u32, @@ -38,7 +37,7 @@ pub struct MumbleLink { /// The rest of the data from here is what gw2 provides for the benefit of addons. /// This is the current UI state of the game. refer to [UIState] /// // Bitmask: Bit 1 = IsMapOpen, Bit 2 = IsCompassTopRight, Bit 3 = DoesCompassHaveRotationEnabled, Bit 4 = Game has focus, Bit 5 = Is in Competitive game mode, Bit 6 = Textbox has focus, Bit 7 = Is in Combat - pub ui_state: u32, + pub ui_state: Option>, pub compass_width: u16, // pixels pub compass_height: u16, // pixels pub compass_rotation: f32, // radians @@ -55,7 +54,10 @@ pub struct MumbleLink { pub process_id: u32, /// refers to [Mount] /// Identifies whether the character is currently mounted, if so, identifies the specific mount. does not match gw2 api - pub mount: Option, + //pub mount: Option, + //pub race: Race, + pub mount: u8, + pub race: u32, /// Vertical field-of-view pub fov: f32, @@ -76,7 +78,7 @@ pub struct MumbleLink { /// This is the position of the gw2's viewport (client area. x/y) relative to the top left corner of the desktop in *screen coords* pub client_pos: IVec2, /// This is the size of gw2's viewport (width/height) in screen coordinates - pub client_size: IVec2, + pub client_size: UVec2, /// changes since last mumble link update pub changes: BitFlags, } @@ -88,7 +90,7 @@ impl Default for MumbleLink { f_avatar_front: Default::default(), cam_pos: Default::default(), f_camera_front: Default::default(), - name: Default::default(), + name: String::from("This Is Jokolay Dummy"), map_id: Default::default(), map_type: Default::default(), server_address: std::net::Ipv4Addr::UNSPECIFIED.into(), @@ -106,12 +108,13 @@ impl Default for MumbleLink { map_scale: Default::default(), process_id: Default::default(), mount: Default::default(), - fov: Default::default(), + race: u32::MAX, + fov: 2.0, uisz: Default::default(), dpi: Default::default(), - dpi_scaling: Default::default(), + dpi_scaling: 96, client_pos: Default::default(), - client_size: Default::default(), + client_size: UVec2(glam::UVec2 { x: 1024, y: 768 }), changes: Default::default(), } } @@ -126,6 +129,8 @@ pub enum MumbleChanges { Character = 1 << 2, WindowPosition = 1 << 3, WindowSize = 1 << 4, + Camera = 1 << 5, + Position = 1 << 6, } /// represents the ui scale set in settings -> graphics options -> interface size @@ -155,7 +160,7 @@ pub enum UISize { #[bitflags] #[repr(u32)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] /// The Uistate enum to represent the status of the UI in game pub enum UIState { IsMapOpen = 0b00000001, diff --git a/crates/joko_link_ui_manager/Cargo.toml b/crates/joko_link_ui_manager/Cargo.toml new file mode 100644 index 0000000..f8a43a7 --- /dev/null +++ b/crates/joko_link_ui_manager/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "joko_link_ui_manager" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +joko_core = { path = "../joko_core" } +joko_link_models = { path = "../joko_link_models" } +joko_ui_models = { path = "../joko_ui_models" } +joko_component_models = { path = "../joko_component_models" } +num-derive = { version = "0", default-features = false } +num-traits = { version = "0", default-features = false } +enumflags2 = { workspace = true } +miette = { workspace = true } +serde = { workspace = true } +glam = { workspace = true } +tokio = { workspace = true } +egui = { workspace = true } + diff --git a/crates/joko_link_ui_manager/README.md b/crates/joko_link_ui_manager/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_link_ui_manager/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_link_ui_manager/src/lib.rs b/crates/joko_link_ui_manager/src/lib.rs new file mode 100644 index 0000000..1c5f5e6 --- /dev/null +++ b/crates/joko_link_ui_manager/src/lib.rs @@ -0,0 +1,387 @@ +use std::borrow::BorrowMut; + +use egui::DragValue; +use joko_component_models::{ + default_component_result, from_broadcast, to_data, Component, ComponentMessage, ComponentResult, +}; +use joko_link_models::{MessageToMumbleLink, MumbleLink}; +use joko_ui_models::{UIArea, UIPanel}; + +struct MumbleUIManagerChannels { + subscription_mumble_link: tokio::sync::broadcast::Receiver, + back_end_notifier: tokio::sync::mpsc::Sender, +} + +pub struct MumbleUIManager { + egui_context: egui::Context, + editable_mumble: bool, + last_known_link: MumbleLink, + channels: Option, +} + +impl MumbleUIManager { + pub fn new(egui_context: egui::Context) -> Self { + Self { + egui_context, + editable_mumble: false, + last_known_link: Default::default(), + channels: None, + } + } + fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.compass_height)); + ui.add(DragValue::new(&mut link.compass_width)); + ui.add(DragValue::new(&mut link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.horizontal(|ui| { + ui.label(&link.name); + ui.label(format!("{:?}", link.race)); + }); + ui.end_row(); + + ui.label("map id"); + ui.add(DragValue::new(&mut link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut link.map_type)); + ui.end_row(); + ui.label("world position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.map_center_x)); + ui.add(DragValue::new(&mut link.map_center_y)); + ui.add(DragValue::new(&mut link.map_scale)); + }); + ui.end_row(); + + ui.label("address"); + ui.label(format!("{}", link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut link.dpi)); + ui.end_row(); + }); + } + + fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut dummy_link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut dummy_link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut dummy_link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut dummy_link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut dummy_link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + + ui.label("ui state"); + if let Some(ui_state) = dummy_link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.compass_height)); + ui.add(DragValue::new(&mut dummy_link.compass_width)); + ui.add(DragValue::new(&mut dummy_link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut dummy_link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = dummy_link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.label(&dummy_link.name); + ui.end_row(); + ui.label("map id"); + ui.add(DragValue::new(&mut dummy_link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut dummy_link.map_type)); + ui.end_row(); + ui.label("address"); + ui.label(format!("{}", dummy_link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut dummy_link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut dummy_link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", dummy_link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut dummy_link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut dummy_link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut dummy_link.dpi)); + ui.end_row(); + + // ui.label("position"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos.x)); + // ui.add(DragValue::new(&mut link.window_pos.y)); + // }); + // ui.end_row(); + // ui.label("size"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size.x)); + // ui.add(DragValue::new(&mut link.window_size.y)); + // }); + // ui.end_row(); + // ui.label("position_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); + // }); + // ui.end_row(); + // ui.label("size_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); + // }); + // ui.end_row(); + }); + } +} + +impl Component for MumbleUIManager { + fn bind(&mut self, mut channels: joko_component_models::ComponentChannels) { + let channels = MumbleUIManagerChannels { + subscription_mumble_link: channels.requirements.remove(&0).unwrap(), + back_end_notifier: channels.notify.remove(&1).unwrap(), + }; + self.channels = Some(channels); + } + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + } + fn accept_notifications(&self) -> bool { + false + } + fn init(&mut self) {} + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn notify(&self) -> Vec<&str> { + vec!["back:mumble_link"] + } + fn peers(&self) -> Vec<&str> { + vec![] + } + fn tick(&mut self, _latest_time: f64) -> joko_component_models::ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + + if let Ok(link) = channels.subscription_mumble_link.try_recv() { + let link: Option = from_broadcast(&link); + if self.editable_mumble { + } else if let Some(link) = link { + self.last_known_link = link; + } + } + default_component_result() + } +} + +impl UIPanel for MumbleUIManager { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Mumble Manager".to_string(), + id: "mumble_ui".to_string(), + }] + } + fn init(&mut self) {} + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + let back_end_notifier = channels.back_end_notifier.borrow_mut(); + let egui_context = &self.egui_context; + + egui::Window::new("Mumble Manager") + .open(is_open) + .show(egui_context, |ui| { + ui.horizontal(|ui| { + if ui.selectable_label(!self.editable_mumble, "live").clicked() { + self.editable_mumble = false; + let _ = back_end_notifier + .blocking_send(to_data(MessageToMumbleLink::Autonomous)); + } + if ui + .selectable_label(self.editable_mumble, "editable") + .clicked() + { + self.editable_mumble = true; + let _ = back_end_notifier + .blocking_send(to_data(MessageToMumbleLink::BindedOnUI)); + } + }); + if self.editable_mumble { + ui.label( + egui::RichText::new( + "Mumble is not live, values need to be manually updated.", + ) + .color(egui::Color32::RED), + ); + //TODO: how to detect there was a change in value, to only propagate changed values ? + Self::editable_mumble_ui(ui, &mut self.last_known_link); + } else { + let link: MumbleLink = self.last_known_link.clone(); + Self::live_mumble_ui(ui, link); + } + }); + if self.editable_mumble { + let _ = back_end_notifier.blocking_send(to_data(MessageToMumbleLink::Value( + self.last_known_link.clone(), + ))); + } + } +} diff --git a/crates/joko_marker_format/src/io/deserialize.rs b/crates/joko_marker_format/src/io/deserialize.rs deleted file mode 100644 index bd12fe8..0000000 --- a/crates/joko_marker_format/src/io/deserialize.rs +++ /dev/null @@ -1,836 +0,0 @@ -use crate::{ - pack::{Category, CommonAttributes, Marker, PackCore, RelativePath, TBin, Trail}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::Dir; -use glam::Vec3; -use indexmap::IndexMap; -use miette::{bail, Context, IntoDiagnostic, Result}; -use std::{collections::BTreeMap, io::Read}; -use tracing::{info, info_span, instrument, warn}; -use uuid::Uuid; -use xot::{Node, Xot}; - -use super::XotAttributeNameIDs; - -pub(crate) fn load_pack_core_from_dir(dir: &Dir) -> Result { - let mut pack = PackCore::default(); - // walks the directory and loads all files into the hashmap - recursive_walk_dir_and_read_images_and_tbins( - dir, - &mut pack.textures, - &mut pack.tbins, - &RelativePath::default(), - ) - .wrap_err("failed to walk dir when loading a markerpack")?; - - // parse map data of the pack - for entry in dir - .entries() - .into_diagnostic() - .wrap_err("failed to read entries of pack dir")? - { - let entry = entry - .into_diagnostic() - .wrap_err("entry error whiel reading xml files")?; - - let name = entry - .file_name() - .into_diagnostic() - .wrap_err("map data entry name not utf-8")? - .to_string(); - - if name.ends_with("xml") { - if let Some(name) = name.strip_suffix(".xml") { - match name { - "categories" => { - // parse categories - { - let cats_xml = dir - .read_to_string("categories.xml") - .into_diagnostic() - .wrap_err("failed to read categories.xml")?; - parse_categories_file(&cats_xml, &mut pack) - .wrap_err("failed to parse category file")?; - } - } - map_id => { - // parse map file - if let Ok(map_id) = map_id.parse() { - let mut xml_str = String::new(); - entry - .open() - .into_diagnostic() - .wrap_err("failed to open xml file")? - .read_to_string(&mut xml_str) - .into_diagnostic() - .wrap_err("faield to read xml string")?; - parse_map_file(map_id, &xml_str, &mut pack).wrap_err_with(|| { - miette::miette!("error parsing map file: {map_id}") - })?; - } else { - info!("unrecognized xml file {map_id}") - } - } - } - } - } - } - Ok(pack) -} -fn recursive_walk_dir_and_read_images_and_tbins( - dir: &Dir, - images: &mut BTreeMap>, - tbins: &mut BTreeMap, - parent_path: &RelativePath, -) -> Result<()> { - for entry in dir - .entries() - .into_diagnostic() - .wrap_err("failed to get directory entries")? - { - let entry = entry - .into_diagnostic() - .wrap_err("dir entry error when iterating dir entries")?; - let name = entry.file_name().into_diagnostic()?; - let path = parent_path.join_str(&name); - - if entry - .file_type() - .into_diagnostic() - .wrap_err("failed to get file type")? - .is_file() - { - if path.ends_with("png") || path.ends_with("trl") { - let mut bytes = vec![]; - entry - .open() - .into_diagnostic() - .wrap_err("failed to open file")? - .read_to_end(&mut bytes) - .into_diagnostic() - .wrap_err("failed to read file contents")?; - if name.ends_with("png") { - images.insert(path, bytes); - } else if name.ends_with("trl") { - if let Some(tbin) = parse_tbin_from_slice(&bytes) { - tbins.insert(path, tbin); - } else { - info!("invalid tbin: {path}"); - } - } - } - } else { - recursive_walk_dir_and_read_images_and_tbins( - &entry.open_dir().into_diagnostic()?, - images, - tbins, - &path, - )?; - } - } - Ok(()) -} -fn parse_tbin_from_slice(bytes: &[u8]) -> Option { - let content_length = bytes.len(); - // content_length must be atleast 8 to contain version + map_id - if content_length < 8 { - info!("failed to parse tbin because the len is less than 8"); - return None; - } - - let mut version_bytes = [0_u8; 4]; - version_bytes.copy_from_slice(&bytes[4..8]); - let version = u32::from_ne_bytes(version_bytes); - let mut map_id_bytes = [0_u8; 4]; - map_id_bytes.copy_from_slice(&bytes[4..8]); - let map_id = u32::from_ne_bytes(map_id_bytes); - - // this will either be empty vec or series of vec3s. - let nodes: Vec = bytes[8..] - .chunks_exact(12) - .map(|float_bytes| { - // make [f32 ;3] out of those 12 bytes - let arr = [ - f32::from_le_bytes([ - // first float - float_bytes[0], - float_bytes[1], - float_bytes[2], - float_bytes[3], - ]), - f32::from_le_bytes([ - // second float - float_bytes[4], - float_bytes[5], - float_bytes[6], - float_bytes[7], - ]), - f32::from_le_bytes([ - // third float - float_bytes[8], - float_bytes[9], - float_bytes[10], - float_bytes[11], - ]), - ]; - - Vec3::from_array(arr) - }) - .collect(); - Some(TBin { - map_id, - version, - nodes, - }) -} -// a recursive function to parse the marker category tree. -fn recursive_marker_category_parser( - tree: &Xot, - tags: impl Iterator, - cats: &mut IndexMap, - names: &XotAttributeNameIDs, -) { - for tag in tags { - let ele = match tree.element(tag) { - Some(ele) => ele, - None => continue, - }; - if ele.name() != names.marker_category { - continue; - } - - let name = ele.get_attribute(names.name).unwrap_or_default(); - if name.is_empty() { - continue; - } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); - - let separator = ele - .get_attribute(names.separator) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or_default(); - - let default_enabled = ele - .get_attribute(names.default_enabled) - .unwrap_or_default() - .parse() - .map(|u: u8| u != 0) - .unwrap_or(true); - recursive_marker_category_parser( - tree, - tree.children(tag), - &mut cats - .entry(name.to_string()) - .or_insert_with(|| Category { - display_name: display_name.to_string(), - separator, - default_enabled, - props: ca, - children: Default::default(), - }) - .children, - names, - ); - } -} - -fn parse_categories_file(cats_xml_str: &str, pack: &mut PackCore) -> Result<()> { - let mut tree = xot::Xot::new(); - let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); - let root_node = tree - .parse(cats_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; - - let overlay_data_node = tree - .document_element(root_node) - .into_diagnostic() - .wrap_err("no doc element")?; - - if let Some(od) = tree.element(overlay_data_node) { - if od.name() == xot_names.overlay_data { - recursive_marker_category_parser_categories_xml( - &tree, - tree.children(overlay_data_node), - &mut pack.categories, - &xot_names, - ); - } else { - bail!("root tag is not OverlayData") - } - } else { - bail!("doc element is not element???"); - } - Ok(()) -} -fn parse_map_file(map_id: u32, map_xml_str: &str, pack: &mut PackCore) -> Result<()> { - let mut tree = Xot::new(); - let root_node = tree - .parse(map_xml_str) - .into_diagnostic() - .wrap_err("invalid xml")?; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let overlay_data_node = tree - .document_element(root_node) - .into_diagnostic() - .wrap_err("missing doc element")?; - - let overlay_data_element = tree - .element(overlay_data_node) - .ok_or_else(|| miette::miette!("no doc ele"))?; - - if overlay_data_element.name() != names.overlay_data { - bail!("root tag is not OverlayData"); - } - let pois = tree - .children(overlay_data_node) - .find(|node| match tree.element(*node) { - Some(ele) => ele.name() == names.pois, - None => false, - }) - .ok_or_else(|| miette::miette!("missing pois node"))?; - for child in tree.children(pois) { - if let Some(child) = tree.element(child) { - let category = child - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - - let guid = child - .get_attribute(names.guid) - .and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - }) - .ok_or_else(|| miette::miette!("invalid guid"))?; - if child.name() == names.poi { - if child - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } - let xpos = child - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let ypos = child - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let zpos = child - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .into_diagnostic()?; - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child, &names); - - let marker = Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category, - attrs: ca, - guid, - }; - - pack.maps.entry(map_id).or_default().markers.push(marker); - } else if child.name() == names.trail { - if child - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - .ok_or_else(|| miette::miette!("invalid mapid"))? - != map_id - { - bail!("mapid doesn't match the file name"); - } - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(child, &names); - - let trail = Trail { - category, - map_id, - props: ca, - guid, - }; - pack.maps.entry(map_id).or_default().trails.push(trail); - } - } - } - Ok(()) -} - -// a temporary recursive function to parse the marker category tree. -fn recursive_marker_category_parser_categories_xml( - tree: &Xot, - tags: impl Iterator, - cats: &mut IndexMap, - names: &XotAttributeNameIDs, -) { - for tag in tags { - if let Some(ele) = tree.element(tag) { - if ele.name() != names.marker_category { - continue; - } - - let name = ele.get_attribute(names.name).unwrap_or_default(); - if name.is_empty() { - info!("category doesn't have a name attribute: {ele:#?}"); - continue; - } - let span_guard = info_span!("category {name}").entered(); - let mut ca = CommonAttributes::default(); - ca.update_common_attributes_from_element(ele, names); - - let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); - - let separator = match ele.get_attribute(names.separator).unwrap_or("0") { - "0" => false, - "1" => true, - ors => { - info!("separator attribute has invalid value: {ors}"); - false - } - }; - - let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { - "0" => false, - "1" => true, - ors => { - info!("default_enabled attribute has invalid value: {ors}"); - true - } - }; - recursive_marker_category_parser_categories_xml( - tree, - tree.children(tag), - &mut cats - .entry(name.to_string()) - .or_insert_with(|| Category { - display_name: display_name.to_string(), - separator, - default_enabled, - props: ca, - children: Default::default(), - }) - .children, - names, - ); - std::mem::drop(span_guard); - } - } -} - -/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. -/// will return error if there's an issue with zipfile. -/// -/// but any other errors like invalid attributes or missing markers etc.. will just be logged. -/// the intention is "best effort" parsing and not "validating" xml marker packs. -/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. -#[instrument(skip_all)] -pub(crate) fn get_pack_from_taco_zip(taco: &[u8]) -> Result { - // all the contents of ZPack - let mut pack = PackCore::default(); - // parse zip file - let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco)) - .into_diagnostic() - .wrap_err("failed to read zip archive")?; - - // file paths of different file types - let mut images = vec![]; - let mut tbins = vec![]; - let mut xmls = vec![]; - // we collect the names first, because reading a file from zip is a mutating operation. - // So, we can't iterate AND read the file at the same time - for name in zip_archive.file_names() { - if name.ends_with("png") { - images.push(name.to_string()); - } else if name.ends_with("trl") { - tbins.push(name.to_string()); - } else if name.ends_with("xml") { - xmls.push(name.to_string()); - } else if name.ends_with('/') { - // directory. so, we can ignore this. - } else { - info!("ignoring file: {name}"); - } - } - for name in images { - let span = info_span!("load image", name).entered(); - let file_path: RelativePath = name.parse().unwrap(); - if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { - match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { - Ok(_) => assert!( - pack.textures.insert(file_path, bytes).is_none(), - "duplicate image file {name}" - ), - Err(e) => { - info!(?e, "failed to parse image file"); - } - } - } - std::mem::drop(span); - } - - for name in tbins { - let span = info_span!("load tbin {name}").entered(); - - let file_path: RelativePath = name.parse().unwrap(); - if let Some(bytes) = read_file_bytes_from_zip_by_name(&name, &mut zip_archive) { - if let Some(tbin) = parse_tbin_from_slice(&bytes) { - assert!( - pack.tbins.insert(file_path, tbin).is_none(), - "duplicate tbin file {name}" - ); - } else { - info!("failed to parse tbin from slice: {file_path}"); - } - } else { - info!(name, "failed to read tbin from zipfile"); - } - std::mem::drop(span); - } - for name in xmls { - let mut xml_str = String::new(); - let xml_file_name = name.clone(); - let span_guard = info_span!("deserialize xml", xml_file_name).entered(); - if zip_archive - .by_name(&name) - .ok() - .and_then(|mut file| file.read_to_string(&mut xml_str).ok()) - .is_none() - { - info!("failed to read file from zip"); - continue; - }; - - let filtered_xml_str = crate::rapid_filter_rust(xml_str); - let mut tree = Xot::new(); - let root_node = match tree.parse(&filtered_xml_str) { - Ok(root) => root, - Err(e) => { - info!(?e, "failed to parse as xml"); - continue; - } - }; - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = match tree - .document_element(root_node) - .ok() - .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) - { - Some(od) => od, - None => { - info!("missing overlay data tag"); - continue; - } - }; - - // parse_categories - recursive_marker_category_parser(&tree, tree.children(od), &mut pack.categories, &names); - - let pois = match tree.children(od).find(|node| { - tree.element(*node) - .map(|ele: &xot::Element| ele.name() == names.pois) - .unwrap_or_default() - }) { - Some(pois) => pois, - None => { - info!("missing pois tag"); - continue; - } - }; - - for child_node in tree.children(pois) { - let child = match tree.element(child_node) { - Some(ele) => ele, - None => continue, - }; - let category = child - .get_attribute(names.category) - .unwrap_or_default() - .to_lowercase(); - let guid = child - .get_attribute(names.guid) - .and_then(|guid| { - let mut buffer = [0u8; 20]; - BASE64_ENGINE - .decode_slice(guid, &mut buffer) - .ok() - .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) - .or_else(|| { - info!(guid, "failed to deserialize guid"); - None - }) - }) - .unwrap_or_else(Uuid::new_v4); - - if category.is_empty() { - info!(?guid, "missing category (type) attribute on marker"); - } - if child.name() == names.poi { - if let Some(map_id) = child - .get_attribute(names.map_id) - .and_then(|map_id| map_id.parse::().ok()) - { - let xpos = child - .get_attribute(names.xpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let ypos = child - .get_attribute(names.ypos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let zpos = child - .get_attribute(names.zpos) - .unwrap_or_default() - .parse::() - .unwrap_or_default(); - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(child, &names); - if let Some(icon_file) = common_attributes.get_icon_file() { - if !pack.textures.contains_key(icon_file) { - info!(%icon_file, "failed to find this texture in this pack"); - } - } else if let Some(icf) = child.get_attribute(names.icon_file) { - info!(icf, "marker's icon file attribute failed to parse"); - } - let marker = Marker { - position: [xpos, ypos, zpos].into(), - map_id, - category, - attrs: common_attributes, - guid, - }; - pack.maps.entry(map_id).or_default().markers.push(marker); - } else { - info!("missing map id") - } - } else if child.name() == names.trail { - if let Some(map_id) = child - .get_attribute(names.trail_data) - .and_then(|trail_data| { - let path: RelativePath = trail_data.parse().unwrap(); - pack.tbins.get(&path).map(|tb| tb.map_id) - }) - { - let mut common_attributes = CommonAttributes::default(); - common_attributes.update_common_attributes_from_element(child, &names); - - if let Some(tex) = common_attributes.get_texture() { - if !pack.textures.contains_key(tex) {} - } - - let trail = Trail { - category, - map_id, - props: common_attributes, - guid, - }; - pack.maps.entry(map_id).or_default().trails.push(trail); - } else { - let td = child.get_attribute(names.trail_data); - let rp: RelativePath = td.unwrap_or_default().parse().unwrap(); - let tbin = pack.tbins.get(&rp).map(|tbin| (tbin.map_id, tbin.version)); - info!("missing map_id: {td:?} {rp} {tbin:?}"); - } - } else { - info!("unknown tag: {:?}", child.name()); - } - } - - drop(span_guard); - } - - Ok(pack) -} -#[instrument(skip(zip_archive))] -fn read_file_bytes_from_zip_by_name( - name: &str, - zip_archive: &mut zip::ZipArchive, -) -> Option> { - let mut bytes = vec![]; - match zip_archive.by_name(name) { - Ok(mut file) => match file.read_to_end(&mut bytes) { - Ok(size) => { - if size == 0 { - info!("empty file {name}"); - } else { - return Some(bytes); - } - } - Err(e) => { - info!(?e, "failed to read file"); - } - }, - Err(e) => { - info!(?e, "failed to get file from zip"); - } - } - None -} -// #[cfg(test)] -// mod test { - -// use indexmap::IndexMap; -// use rstest::*; - -// use semver::Version; -// use similar_asserts::assert_eq; -// use std::io::Write; -// use std::sync::Arc; - -// use zip::write::FileOptions; -// use zip::ZipWriter; - -// use crate::{ -// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, -// INCHES_PER_METER, -// }; - -// const TEST_XML: &str = include_str!("test.xml"); -// const TEST_MARKER_PNG_NAME: &str = "marker.png"; -// const TEST_TRL_NAME: &str = "basic.trl"; - -// #[fixture] -// #[once] -// fn test_zip() -> Vec { -// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); -// // category.xml -// writer -// .start_file("category.xml", FileOptions::default()) -// .expect("failed to create category.xml"); -// writer -// .write_all(TEST_XML.as_bytes()) -// .expect("failed to write category.xml"); -// // marker.png -// writer -// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) -// .expect("failed to create marker.png"); -// writer -// .write_all(MARKER_PNG) -// .expect("failed to write marker.png"); -// // basic.trl -// writer -// .start_file(TEST_TRL_NAME, FileOptions::default()) -// .expect("failed to create basic trail"); -// writer -// .write_all(&0u32.to_ne_bytes()) -// .expect("failed to write version"); -// writer -// .write_all(&15u32.to_ne_bytes()) -// .expect("failed to write mapid "); -// writer -// .write_all(bytemuck::cast_slice(&[0f32; 3])) -// .expect("failed to write first node"); -// // done -// writer -// .finish() -// .expect("failed to finalize zip") -// .into_inner() -// } - -// #[fixture] -// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { -// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); -// assert_eq!(file_entries.len(), 3); -// let test_xml = std::str::from_utf8( -// file_entries -// .get(String::new("category.xml")) -// .expect("failed to get category.xml"), -// ) -// .expect("failed to get str from category.xml contents"); -// assert_eq!(test_xml, TEST_XML); -// let test_marker_png = file_entries -// .get(String::new("marker.png")) -// .expect("failed to get marker.png"); -// assert_eq!(test_marker_png, MARKER_PNG); -// file_entries -// } -// #[fixture] -// #[once] -// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { -// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); -// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); -// assert_eq!(pack.tbins.len(), 1); -// assert_eq!(pack.textures.len(), 1); -// assert_eq!( -// pack.textures -// .get(String::new(TEST_MARKER_PNG_NAME)) -// .expect("failed to get marker.png from textures"), -// MARKER_PNG -// ); - -// let tbin = pack -// .tbins -// .get(String::new(TEST_TRL_NAME)) -// .expect("failed to get basic trail") -// .clone(); - -// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); -// pack -// } - -// // #[rstest] -// // fn test_tag(test_pack: &Pack) { -// // let mut test_category_menu = CategoryMenu::default(); -// // let parent_path = String::new("parent"); -// // let child1_path = String::new("parent/child1"); -// // let subchild_path = String::new("parent/child1/subchild"); -// // let child2_path = String::new("parent/child2"); -// // test_category_menu.create_category(subchild_path); -// // test_category_menu.create_category(child2_path); -// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); -// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); -// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); -// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); - -// // assert_eq!(test_category_menu, test_pack.category_menu) -// // } - -// #[rstest] -// fn test_markers(test_pack: &Pack) { -// let marker = test_pack -// .markers -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// marker.props.texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); -// } -// #[rstest] -// fn test_trails(test_pack: &Pack) { -// let trail = test_pack -// .trails -// .values() -// .next() -// .expect("failed to get queensdale mapdata"); -// assert_eq!( -// trail.props.tbin.as_ref().unwrap(), -// String::new(TEST_TRL_NAME) -// ); -// assert_eq!( -// trail.props.trail_texture.as_ref().unwrap(), -// String::new(TEST_MARKER_PNG_NAME) -// ); -// } -// } diff --git a/crates/joko_marker_format/src/io/mod.rs b/crates/joko_marker_format/src/io/mod.rs deleted file mode 100644 index d4e114e..0000000 --- a/crates/joko_marker_format/src/io/mod.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! This modules primarily deals with serializing and deserializing xml data from marker packs -//! - -use xot::{NameId, Xot}; - -mod deserialize; -mod error; -mod serialize; - -pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_dir}; -pub(crate) use serialize::save_pack_core_to_dir; -pub(crate) struct XotAttributeNameIDs { - // xml tags - pub overlay_data: NameId, - pub marker_category: NameId, - pub pois: NameId, - pub poi: NameId, - pub trail: NameId, - // marker specific attributes - pub category: NameId, - pub guid: NameId, - pub map_id: NameId, - pub xpos: NameId, - pub ypos: NameId, - pub zpos: NameId, - // marker category specific attributes - pub default_enabled: NameId, - pub display_name: NameId, - pub name: NameId, - pub separator: NameId, - // inheritable attributes - pub achievement_id: NameId, - pub achievement_bit: NameId, - pub alpha: NameId, - pub anim_speed: NameId, - pub auto_trigger: NameId, - pub behavior: NameId, - pub bounce: NameId, - pub bounce_delay: NameId, - pub bounce_duration: NameId, - pub bounce_height: NameId, - pub can_fade: NameId, - pub color: NameId, - pub copy: NameId, - pub copy_message: NameId, - pub cull: NameId, - pub fade_far: NameId, - pub fade_near: NameId, - pub festival: NameId, - pub has_countdown: NameId, - pub height_offset: NameId, - pub hide: NameId, - pub icon_file: NameId, - pub icon_size: NameId, - pub in_game_visibility: NameId, - pub info: NameId, - pub info_range: NameId, - pub invert_behavior: NameId, - pub is_wall: NameId, - pub keep_on_map_edge: NameId, - pub map_display_size: NameId, - pub map_fade_out_scale_level: NameId, - pub map_type: NameId, - pub map_visibility: NameId, - pub max_size: NameId, - pub min_size: NameId, - pub mini_map_visibility: NameId, - pub mount: NameId, - pub profession: NameId, - pub race: NameId, - pub reset_length: NameId, - pub reset_offset: NameId, - pub rotate: NameId, - pub rotate_x: NameId, - pub rotate_y: NameId, - pub rotate_z: NameId, - pub scale_on_map_with_zoom: NameId, - pub show: NameId, - pub specialization: NameId, - pub text: NameId, - pub texture: NameId, - pub tip_name: NameId, - pub tip_description: NameId, - pub title: NameId, - pub title_color: NameId, - pub toggle_category: NameId, - pub trail_data: NameId, - pub trail_scale: NameId, - pub trigger_range: NameId, -} -impl XotAttributeNameIDs { - pub fn register_with_xot(tree: &mut Xot) -> Self { - Self { - // tags - overlay_data: tree.add_name("OverlayData"), - marker_category: tree.add_name("MarkerCategory"), - pois: tree.add_name("POIs"), - poi: tree.add_name("POI"), - trail: tree.add_name("Trail"), - // non inheritable attributes - category: tree.add_name("type"), - xpos: tree.add_name("xpos"), - ypos: tree.add_name("ypos"), - zpos: tree.add_name("zpos"), - map_id: tree.add_name("MapID"), - guid: tree.add_name("GUID"), - - // marker category specific attrs - separator: tree.add_name("IsSeparator"), - default_enabled: tree.add_name("defaulttoggle"), - display_name: tree.add_name("DisplayName"), - name: tree.add_name("name"), - // inheritable attributes - achievement_id: tree.add_name("achievementId"), - achievement_bit: tree.add_name("achievementBit"), - alpha: tree.add_name("alpha"), - anim_speed: tree.add_name("animSpeed"), - auto_trigger: tree.add_name("autotrigger"), - behavior: tree.add_name("behavior"), - color: tree.add_name("color"), - copy: tree.add_name("copy"), - copy_message: tree.add_name("copy-message"), - fade_near: tree.add_name("fadeNear"), - fade_far: tree.add_name("fadeFar"), - festival: tree.add_name("festival"), - has_countdown: tree.add_name("hasCountdown"), - height_offset: tree.add_name("heightOffset"), - icon_file: tree.add_name("iconFile"), - icon_size: tree.add_name("iconSize"), - in_game_visibility: tree.add_name("inGameVisibility"), - info: tree.add_name("info"), - info_range: tree.add_name("infoRange"), - map_display_size: tree.add_name("mapDisplaySize"), - map_visibility: tree.add_name("mapVisibility"), - max_size: tree.add_name("maxSize"), - min_size: tree.add_name("minSize"), - mini_map_visibility: tree.add_name("miniMapVisibility"), - mount: tree.add_name("mount"), - profession: tree.add_name("profession"), - race: tree.add_name("race"), - reset_length: tree.add_name("resetLength"), - reset_offset: tree.add_name("resetOffset"), - scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), - tip_name: tree.add_name("tip-name"), - tip_description: tree.add_name("tip-description"), - toggle_category: tree.add_name("togglecateogry"), - texture: tree.add_name("texture"), - trail_data: tree.add_name("trailData"), - trail_scale: tree.add_name("trailScale"), - trigger_range: tree.add_name("triggerRange"), - bounce_delay: tree.add_name("bounce-delay"), - bounce_duration: tree.add_name("bounce-duration"), - bounce_height: tree.add_name("bounce-height"), - can_fade: tree.add_name("canfade"), - cull: tree.add_name("cull"), - hide: tree.add_name("hide"), - is_wall: tree.add_name("iswall"), - invert_behavior: tree.add_name("invertbehavior"), - map_type: tree.add_name("maptype"), - rotate: tree.add_name("rotate"), - rotate_x: tree.add_name("rotate-x"), - rotate_y: tree.add_name("rotate-y"), - rotate_z: tree.add_name("rotate-z"), - show: tree.add_name("show"), - specialization: tree.add_name("specialization"), - title: tree.add_name("title"), - title_color: tree.add_name("title-color"), - text: tree.add_name("text"), - bounce: tree.add_name("bounce"), - keep_on_map_edge: tree.add_name("keepOnMapEdge"), - map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), - } - } -} diff --git a/crates/joko_marker_format/src/io/serialize.rs b/crates/joko_marker_format/src/io/serialize.rs deleted file mode 100644 index 51c2a8a..0000000 --- a/crates/joko_marker_format/src/io/serialize.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::{ - pack::{Category, Marker, PackCore, RelativePath, Trail}, - BASE64_ENGINE, -}; -use base64::Engine; -use cap_std::fs_utf8::Dir; -use indexmap::IndexMap; -use miette::{Context, IntoDiagnostic, Result}; -use std::{collections::HashSet, io::Write}; -use tracing::info; -use xot::{Element, Node, SerializeOptions, Xot}; - -use super::XotAttributeNameIDs; -/// Save the pack core as xml pack using the given directory as pack root path. -pub(crate) fn save_pack_core_to_dir( - pack_core: &PackCore, - dir: &Dir, - cats: bool, - mut maps: HashSet, - mut textures: HashSet, - mut tbins: HashSet, - all: bool, -) -> Result<()> { - if cats || all { - // save categories - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create new root with overlay data node")?; - recursive_cat_serializer(&mut tree, &names, &pack_core.categories, od) - .wrap_err("failed to serialize cats")?; - let cats = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to convert cats xot to string")?; - dir.create("categories.xml") - .into_diagnostic() - .wrap_err("failed to create categories.xml")? - .write_all(cats.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write to categories.xml")?; - } - // save maps - for (map_id, map_data) in pack_core.maps.iter() { - if maps.remove(map_id) || all { - if map_data.markers.is_empty() && map_data.trails.is_empty() { - if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - let mut tree = Xot::new(); - let names = XotAttributeNameIDs::register_with_xot(&mut tree); - let od = tree.new_element(names.overlay_data); - let root_node: Node = tree - .new_root(od) - .into_diagnostic() - .wrap_err("failed to create root wiht overlay data for pois")?; - let pois = tree.new_element(names.pois); - tree.append(od, pois) - .into_diagnostic() - .wrap_err("faild to append pois to od node")?; - for marker in &map_data.markers { - let poi = tree.new_element(names.poi); - tree.append(pois, poi) - .into_diagnostic() - .wrap_err("failed to append poi (marker) to pois")?; - let ele = tree.element_mut(poi).unwrap(); - serialize_marker_to_element(marker, ele, &names); - } - for trail in &map_data.trails { - let trail_node = tree.new_element(names.trail); - tree.append(pois, trail_node) - .into_diagnostic() - .wrap_err("failed to append a trail node to pois")?; - let ele = tree.element_mut(trail_node).unwrap(); - serialize_trail_to_element(trail, ele, &names); - } - let map_xml = tree - .with_serialize_options(SerializeOptions { pretty: true }) - .to_string(root_node) - .into_diagnostic() - .wrap_err("failed to serialize map data to string")?; - dir.create(format!("{map_id}.xml")) - .into_diagnostic() - .wrap_err("failed to create map xml file")? - .write_all(map_xml.as_bytes()) - .into_diagnostic() - .wrap_err("failed to write map data to file")?; - } - } - // if any other map remained in the maps, then it means the map was deleted from pack, so we remove the xml file too - for map_id in maps { - if let Err(e) = dir.remove_file(format!("{map_id}.xml")) { - info!( - ?e, - map_id, "failed to remove xml file that had nothing to write to" - ); - } - } - // save images - for (img_path, img) in pack_core.textures.iter() { - if textures.remove(img_path) || all { - if let Some(parent) = img_path.parent() { - dir.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir for an image: {img_path}") - })?; - } - dir.create(img_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? - .write(img) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to write image bytes to file: {img_path}") - })?; - } - } - for img_path in textures { - if let Err(e) = dir.remove_file(img_path.as_str()) { - info!( - ?e, - %img_path, "failed to remove file" - ); - } - } - // save tbins - for (tbin_path, tbin) in pack_core.tbins.iter() { - if tbins.remove(tbin_path) || all { - if let Some(parent) = tbin_path.parent() { - dir.create_dir_all(parent) - .into_diagnostic() - .wrap_err_with(|| { - miette::miette!("failed to create parent dir of tbin: {tbin_path}") - })?; - } - let mut bytes: Vec = vec![]; - bytes.reserve(8 + tbin.nodes.len() * 12); - bytes.extend_from_slice(&tbin.version.to_ne_bytes()); - bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); - for node in &tbin.nodes { - bytes.extend_from_slice(&node[0].to_ne_bytes()); - bytes.extend_from_slice(&node[1].to_ne_bytes()); - bytes.extend_from_slice(&node[2].to_ne_bytes()); - } - dir.create(tbin_path.as_str()) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? - .write_all(&bytes) - .into_diagnostic() - .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; - } - } - for tbin_path in tbins { - if let Err(e) = dir.remove_file(tbin_path.as_str()) { - info!( - ?e, - %tbin_path, "failed to remove file" - ); - } - } - Ok(()) -} -fn recursive_cat_serializer( - tree: &mut Xot, - names: &XotAttributeNameIDs, - cats: &IndexMap, - parent: Node, -) -> Result<()> { - for (cat_name, cat) in cats { - let cat_node = tree.new_element(names.marker_category); - tree.append(parent, cat_node).into_diagnostic()?; - { - let ele = tree.element_mut(cat_node).unwrap(); - ele.set_attribute(names.display_name, &cat.display_name); - // let cat_name = tree.add_name(cat_name); - ele.set_attribute(names.name, cat_name); - // no point in serializing default values - if !cat.default_enabled { - ele.set_attribute(names.default_enabled, "0"); - } - if cat.separator { - ele.set_attribute(names.separator, "1"); - } - cat.props.serialize_to_element(ele, names); - } - recursive_cat_serializer(tree, names, &cat.children, cat_node)?; - } - Ok(()) -} -fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); - ele.set_attribute(names.category, &trail.category); - ele.set_attribute(names.map_id, format!("{}", trail.map_id)); - trail.props.serialize_to_element(ele, names); -} - -fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { - ele.set_attribute(names.xpos, format!("{}", marker.position[0])); - ele.set_attribute(names.ypos, format!("{}", marker.position[1])); - ele.set_attribute(names.zpos, format!("{}", marker.position[2])); - ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); - ele.set_attribute(names.map_id, format!("{}", marker.map_id)); - ele.set_attribute(names.category, &marker.category); - marker.attrs.serialize_to_element(ele, names); -} diff --git a/crates/joko_marker_format/src/manager/live_pack.rs b/crates/joko_marker_format/src/manager/live_pack.rs deleted file mode 100644 index 8bc0ada..0000000 --- a/crates/joko_marker_format/src/manager/live_pack.rs +++ /dev/null @@ -1,798 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, -}; - -use cap_std::fs_utf8::Dir; -use egui::{ColorImage, TextureHandle}; -use glam::{vec2, Vec2, Vec3}; -use image::EncodableLayout; -use indexmap::IndexMap; -use joko_render::billboard::{MarkerObject, MarkerVertex, TrailObject}; -use tracing::{debug, error, info}; -use uuid::Uuid; - -use crate::{ - io::{load_pack_core_from_dir, save_pack_core_to_dir}, - pack::{Category, CommonAttributes, PackCore, RelativePath}, - INCHES_PER_METER, -}; -use jokolink::MumbleLink; -use miette::{bail, Context, IntoDiagnostic, Result}; -use serde::{Deserialize, Serialize}; - -pub(crate) struct LoadedPack { - /// The directory inside which the pack data is stored - /// There should be a subdirectory called `core` which stores the pack core - /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. - /// eg: Active categories, activation data etc.. - pub dir: Arc, - /// The actual xml pack. - pub core: PackCore, - /// The selection of categories which are "enabled" and markers belonging to these may be rendered - cats_selection: HashMap, - dirty: Dirty, - activation_data: ActivationData, - current_map_data: CurrentMapData, -} - -#[derive(Debug, Default, Clone)] -struct Dirty { - all: bool, - /// whether categories need to be saved - cats: bool, - /// whether cats selection needs to be saved - cats_selection: bool, - /// Whether any mapdata needs saving - map_dirty: HashSet, - /// whether any texture needs saving - texture: HashSet, - /// whether any tbin needs saving - tbin: HashSet, -} - -impl Dirty { - fn is_dirty(&self) -> bool { - self.cats - || self.cats_selection - || !self.map_dirty.is_empty() - || !self.texture.is_empty() - || !self.tbin.is_empty() - } -} -/// This is the activation data per pack -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] -pub struct ActivationData { - /// this is for markers which are global and only activate once regardless of account - pub global: IndexMap, - /// this is the activation data per character - /// for markers which trigger once per character - pub character: IndexMap>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum ActivationType { - /// clean these up when the map is changed - ReappearOnMapChange, - /// clean these up when the timestamp is reached - TimeStamp(time::OffsetDateTime), - Instance(std::net::IpAddr), -} -impl LoadedPack { - const CORE_PACK_DIR_NAME: &str = "core"; - const CATEGORY_SELECTION_FILE_NAME: &str = "cats.json"; - const ACTIVATION_DATA_FILE_NAME: &str = "activation.json"; - - pub fn new(core: PackCore, dir: Arc) -> Self { - let cats_selection = CategorySelection::default_from_pack_core(&core); - LoadedPack { - core, - cats_selection, - dirty: Dirty { - all: true, - ..Default::default() - }, - current_map_data: Default::default(), - dir, - activation_data: Default::default(), - } - } - pub fn category_sub_menu(&mut self, ui: &mut egui::Ui) { - CategorySelection::recursive_selection_ui( - &mut self.cats_selection, - ui, - &mut self.dirty.cats_selection, - ); - } - pub fn load_from_dir(dir: Arc) -> Result { - if !dir - .try_exists(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to check if pack core exists")? - { - bail!("pack core doesn't exist in this pack"); - } - let core_dir = dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - let core = load_pack_core_from_dir(&core_dir).wrap_err("failed to load pack from dir")?; - - let cats_selection = (if dir.exists(Self::ACTIVATION_DATA_FILE_NAME) { - match dir.read_to_string(Self::CATEGORY_SELECTION_FILE_NAME) { - Ok(cd_json) => match serde_json::from_str(&cd_json) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize category data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_else(|| { - let cs = CategorySelection::default_from_pack_core(&core); - match serde_json::to_string_pretty(&cs) { - Ok(cs_json) => match dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - cs - }); - let activation_data = (if dir.exists(Self::ACTIVATION_DATA_FILE_NAME) { - match dir.read_to_string(Self::ACTIVATION_DATA_FILE_NAME) { - Ok(contents) => match serde_json::from_str(&contents) { - Ok(cd) => Some(cd), - Err(e) => { - error!(?e, "failed to deserialize activation data"); - None - } - }, - Err(e) => { - error!(?e, "failed to read string of category data"); - None - } - } - } else { - None - }) - .flatten() - .unwrap_or_default(); - Ok(LoadedPack { - dir, - core, - cats_selection, - dirty: Default::default(), - current_map_data: Default::default(), - activation_data, - }) - } - pub fn tick( - &mut self, - etx: &egui::Context, - _timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, - default_tex_id: &TextureHandle, - ) { - let categories_changed = self.dirty.cats_selection; - if self.dirty.is_dirty() { - match self.save() { - Ok(_) => {} - Err(e) => { - error!(?e, "failed to save marker pack"); - } - } - } - let link = match link { - Some(link) => link, - None => return, - }; - - if self.current_map_data.map_id != link.map_id || categories_changed { - self.on_map_changed(etx, link, default_tex_id); - } - let z_near = joko_renderer.get_z_near(); - for marker in self.current_map_data.active_markers.values() { - if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { - joko_renderer.add_billboard(mo); - } - } - for trail in self.current_map_data.active_trails.values() { - joko_renderer.add_trail(TrailObject { - vertices: trail.trail_object.vertices.clone(), - texture: trail.trail_object.texture, - }); - } - } - fn on_map_changed( - &mut self, - etx: &egui::Context, - link: &MumbleLink, - default_tex_id: &TextureHandle, - ) { - info!( - self.current_map_data.map_id, - link.map_id, "current map data is updated." - ); - self.current_map_data = Default::default(); - if link.map_id == 0 { - return; - } - self.current_map_data.map_id = link.map_id; - let mut enabled_cats_list = Default::default(); - CategorySelection::recursive_get_full_names( - &self.cats_selection, - &self.core.categories, - &mut enabled_cats_list, - "", - &Default::default(), - ); - for (index, marker) in self - .core - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .markers - .iter() - .enumerate() - { - if let Some(category_attributes) = enabled_cats_list.get(&marker.category) { - let mut attrs = marker.attrs.clone(); - attrs.inherit_if_attr_none(category_attributes); - let key = &marker.guid; - if let Some(behavior) = attrs.get_behavior() { - use crate::pack::Behavior; - if match behavior { - Behavior::AlwaysVisible => false, - Behavior::ReappearOnMapChange - | Behavior::ReappearOnDailyReset - | Behavior::OnlyVisibleBeforeActivation - | Behavior::ReappearAfterTimer - | Behavior::ReappearOnMapReset - | Behavior::WeeklyReset => self.activation_data.global.contains_key(key), - Behavior::OncePerInstance => self - .activation_data - .global - .get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default(), - Behavior::DailyPerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| a.contains_key(key)) - .unwrap_or_default(), - Behavior::OncePerInstancePerChar => self - .activation_data - .character - .get(&link.name) - .map(|a| { - a.get(key) - .map(|a| match a { - ActivationType::Instance(a) => a == &link.server_address, - _ => false, - }) - .unwrap_or_default() - }) - .unwrap_or_default(), - Behavior::WvWObjective => { - false // ??? - } - } { - continue; - } - } - if let Some(tex_path) = attrs.get_icon_file() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, ?self.core.textures, "failed to find this texture"); - } - } - } else { - info!("no texture attribute on this marker"); - } - let th = attrs - .get_icon_file() - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - let texture_id = match th.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }; - - let max_pixel_size = attrs.get_max_size().copied().unwrap_or(2048.0); // default taco max size - let min_pixel_size = attrs.get_min_size().copied().unwrap_or(5.0); // default taco min size - self.current_map_data.active_markers.insert( - index, - ActiveMarker { - texture_id, - _texture: th.clone(), - attrs, - pos: marker.position, - max_pixel_size, - min_pixel_size, - }, - ); - } - } - - for (index, trail) in self - .core - .maps - .get(&link.map_id) - .unwrap_or(&Default::default()) - .trails - .iter() - .enumerate() - { - if let Some(category_attributes) = enabled_cats_list.get(&trail.category) { - let mut common_attributes = trail.props.clone(); - common_attributes.inherit_if_attr_none(category_attributes); - if let Some(tex_path) = common_attributes.get_texture() { - if !self.current_map_data.active_textures.contains_key(tex_path) { - if let Some(tex) = self.core.textures.get(tex_path) { - let img = image::load_from_memory(tex).unwrap(); - self.current_map_data.active_textures.insert( - tex_path.clone(), - etx.load_texture( - tex_path.as_str(), - ColorImage::from_rgba_unmultiplied( - [img.width() as _, img.height() as _], - img.into_rgba8().as_bytes(), - ), - Default::default(), - ), - ); - } else { - info!(%tex_path, ?self.core.textures, "failed to find this texture"); - } - } - } else { - info!("no texture attribute on this marker"); - } - let th = common_attributes - .get_texture() - .and_then(|path| self.current_map_data.active_textures.get(path)) - .unwrap_or(default_tex_id); - - let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { - tbin - } else { - info!(?trail, "missing tbin path"); - continue; - }; - let tbin = if let Some(tbin) = self.core.tbins.get(tbin_path) { - tbin - } else { - info!(%tbin_path, "failed to find tbin"); - continue; - }; - if let Some(active_trail) = ActiveTrail::get_vertices_and_texture( - &common_attributes, - &tbin.nodes, - th.clone(), - ) { - self.current_map_data - .active_trails - .insert(index, active_trail); - } - } - } - } - pub fn save_all(&mut self) -> Result<()> { - self.dirty.all = true; - self.save() - } - #[tracing::instrument(skip(self))] - pub fn save(&mut self) -> Result<()> { - if std::mem::take(&mut self.dirty.cats_selection) || self.dirty.all { - match serde_json::to_string_pretty(&self.cats_selection) { - Ok(cs_json) => match self.dir.write(Self::CATEGORY_SELECTION_FILE_NAME, cs_json) { - Ok(_) => { - debug!("wrote cat selections to disk after creating a default from pack"); - } - Err(e) => { - debug!(?e, "failed to write category data to disk"); - } - }, - Err(e) => { - error!(?e, "failed to serialize cat selection"); - } - } - } - self.dir - .create_dir_all(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to create xmlpack directory")?; - let core_dir = self - .dir - .open_dir(Self::CORE_PACK_DIR_NAME) - .into_diagnostic() - .wrap_err("failed to open core pack directory")?; - save_pack_core_to_dir( - &self.core, - &core_dir, - std::mem::take(&mut self.dirty.cats), - std::mem::take(&mut self.dirty.map_dirty), - std::mem::take(&mut self.dirty.texture), - std::mem::take(&mut self.dirty.tbin), - std::mem::take(&mut self.dirty.all), - )?; - Ok(()) - } -} - -#[derive(Default)] -pub(crate) struct CurrentMapData { - /// the map to which the current map data belongs to - pub map_id: u32, - /// The textures that are being used by the markers, so must be kept alive by this hashmap - pub active_textures: HashMap, - /// The key is the index of the marker in the map markers - /// Their position in the map markers serves as their "id" as uuids can be duplicates. - pub active_markers: IndexMap, - /// The key is the position/index of this trail in the map trails. same as markers - pub active_trails: IndexMap, -} - -/* -- activation data with uuids and track the latest timestamp that will be activated -- category activation data -> track and changes to propagate to markers of this map -- current active markers, which will keep track of their original marker, so as to propagate any changes easily -*/ -pub struct ActiveTrail { - pub trail_object: TrailObject, - pub texture_handle: TextureHandle, -} -/// This is an active marker. -/// It stores all the info that we need to scan every frame -pub(crate) struct ActiveMarker { - /// texture id from managed textures - pub texture_id: u64, - /// owned texture handle to keep it alive - pub _texture: TextureHandle, - /// position - pub pos: Vec3, - /// billboard must not be bigger than this size in pixels - pub max_pixel_size: f32, - /// billboard must not be smaller than this size in pixels - pub min_pixel_size: f32, - pub attrs: CommonAttributes, -} -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -struct CategorySelection { - pub selected: bool, - pub display_name: String, - pub children: HashMap, -} - -impl CategorySelection { - fn default_from_pack_core(pack: &PackCore) -> HashMap { - let mut selection = HashMap::new(); - Self::recursive_create_category_selection(&mut selection, &pack.categories); - selection - } - fn recursive_get_full_names( - selection: &HashMap, - cats: &IndexMap, - list: &mut HashMap, - parent_name: &str, - parent_common_attributes: &CommonAttributes, - ) { - for (name, cat) in cats { - if let Some(selected_cat) = selection.get(name) { - if !selected_cat.selected { - continue; - } - let full_name = if parent_name.is_empty() { - name.clone() - } else { - format!("{parent_name}.{name}") - }; - let mut common_attributes = cat.props.clone(); - common_attributes.inherit_if_attr_none(parent_common_attributes); - Self::recursive_get_full_names( - &selected_cat.children, - &cat.children, - list, - &full_name, - &common_attributes, - ); - list.insert(full_name, common_attributes); - } - } - } - fn recursive_create_category_selection( - selection: &mut HashMap, - cats: &IndexMap, - ) { - for (cat_name, cat) in cats.iter() { - let s = selection.entry(cat_name.clone()).or_default(); - s.selected = cat.default_enabled; - s.display_name = cat.display_name.clone(); - Self::recursive_create_category_selection(&mut s.children, &cat.children); - } - } - fn recursive_selection_ui( - selection: &mut HashMap, - ui: &mut egui::Ui, - changed: &mut bool, - ) { - egui::ScrollArea::vertical().show(ui, |ui| { - for cat in selection.values_mut() { - ui.horizontal(|ui| { - if ui.checkbox(&mut cat.selected, "").changed() { - *changed = true; - } - if !cat.children.is_empty() { - ui.menu_button(&cat.display_name, |ui: &mut egui::Ui| { - Self::recursive_selection_ui(&mut cat.children, ui, changed); - }); - } else { - ui.label(&cat.display_name); - } - }); - } - }); - } -} - -pub const _BILLBOARD_MAX_VISIBILITY_DISTANCE: f32 = 10000.0; - -impl ActiveMarker { - pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { - let Self { - texture_id, - pos, - attrs, - _texture, - max_pixel_size, - min_pixel_size, - .. - } = self; - // let width = *width; - // let height = *height; - let texture_id = *texture_id; - let pos = *pos; - // filters - if let Some(mounts) = attrs.get_mount() { - if let Some(current) = link.mount { - if !mounts.contains(current) { - return None; - } - } else { - return None; - } - } - let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); - let player_distance = pos.distance(link.player_pos); - let camera_distance = pos.distance(link.cam_pos); - let fade_near_far = Vec2::new(fade_near, fade_far); - - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let color = attrs.get_color().copied().unwrap_or_default(); - /* - 1. we need to filter the markers - 1. statically - mapid, character, map_type, race, profession - 2. dynamically - achievement, behavior, mount, fade_far, cull - 3. force hide/show by user discretion - 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior - 3. store the state for these markers activation data, and temporary data like bounce - */ - /* - skip if: - alpha is 0.0 - achievement id/bit is done (maybe this should be at map filter level?) - behavior (activation) - cull - distance > fade_far - visibility (ingame/map/minimap) - mount - specialization - */ - if fade_far > 0.0 && player_distance > fade_far { - return None; - } - // markers are 1 meter in width/height by default - let mut pos = pos; - pos.y += height_offset; - let direction_to_marker = link.cam_pos - pos; - let direction_to_side = direction_to_marker.normalize().cross(Vec3::Y); - - let far_offset = { - let dpi = if link.dpi_scaling <= 0 { - 96.0 - } else { - link.dpi as f32 - } / 96.0; - let gw2_width = link.client_size.as_vec2().x / dpi; - - // offset (half width i.e. distance from center of the marker to the side of the marker) - const SIDE_OFFSET_FAR: f32 = 1.0; - // the size of the projected on to the near plane - let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); - // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width - let near_offset_in_pixels = near_offset * gw2_width; - - // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width - let near_offset_in_pixels = near_offset_in_pixels - .clamp(*min_pixel_size, *max_pixel_size) - .min(gw2_width / 2.0); - - let near_offset_of_marker = near_offset_in_pixels / gw2_width; - near_offset_of_marker * camera_distance / z_near - }; - // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; - // we want to map 100 pixels to one meter in game - // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard - // But, i will ignore that as that makes markers too small - let x_offset = far_offset; - let y_offset = x_offset; // seems all markers are squares - let bottom_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 1.0), - alpha, - color, - fade_near_far, - }; - - let top_left = MarkerVertex { - position: (pos - (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(0.0, 0.0), - alpha, - color, - fade_near_far, - }; - let top_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) + (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 0.0), - alpha, - color, - fade_near_far, - }; - let bottom_right = MarkerVertex { - position: (pos + (direction_to_side * x_offset) - (Vec3::Y * y_offset)), - texture_coordinates: vec2(1.0, 1.0), - alpha, - color, - fade_near_far, - }; - let vertices = [ - top_left, - bottom_left, - bottom_right, - bottom_right, - top_right, - top_left, - ]; - Some(MarkerObject { - vertices, - texture: texture_id, - distance: player_distance, - }) - } -} - -impl ActiveTrail { - fn get_vertices_and_texture( - attrs: &CommonAttributes, - positions: &[Vec3], - texture: TextureHandle, - ) -> Option { - // can't have a trail without atleast two nodes - if positions.len() < 2 { - return None; - } - let alpha = attrs.get_alpha().copied().unwrap_or(1.0); - let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_far = attrs.get_fade_far().copied().unwrap_or(-1.0) / INCHES_PER_METER; - let fade_near_far = Vec2::new(fade_near, fade_far); - let color = attrs.get_color().copied().unwrap_or([0u8; 4]); - // default taco width - let horizontal_offset = 20.0 / INCHES_PER_METER; - // scale it trail scale - let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); - let height = horizontal_offset * 2.0; - - let mut vertices = vec![]; - // trail mesh is split by separating different parts with a [0, 0, 0] - // we will call each separate trail mesh as a "strip" of trail. - // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. - for strip in positions.split(|&v| v == Vec3::ZERO) { - let mut y_offset = 1.0; - for two_positions in strip.windows(2) { - let first = two_positions[0]; - let second = two_positions[1]; - // right side of the vector from first to second - let right_side = (second - first).normalize().cross(Vec3::Y).normalize(); - - let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; - let first_left = MarkerVertex { - position: first - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, y_offset), - alpha, - color, - fade_near_far, - }; - let first_right = MarkerVertex { - position: first + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, y_offset), - alpha, - color, - fade_near_far, - }; - let second_left = MarkerVertex { - position: second - (right_side * horizontal_offset), - texture_coordinates: vec2(0.0, new_offset), - alpha, - color, - fade_near_far, - }; - let second_right = MarkerVertex { - position: second + (right_side * horizontal_offset), - texture_coordinates: vec2(1.0, new_offset), - alpha, - color, - fade_near_far, - }; - y_offset = if new_offset.is_sign_positive() { - new_offset - } else { - 1.0 - new_offset.fract().abs() - }; - vertices.extend([ - second_left, - first_left, - first_right, - first_right, - second_right, - second_left, - ]); - } - } - - Some(ActiveTrail { - trail_object: TrailObject { - vertices: vertices.into(), - texture: match texture.id() { - egui::TextureId::Managed(i) => i, - egui::TextureId::User(_) => todo!(), - }, - }, - texture_handle: texture, - }) - } -} diff --git a/crates/joko_marker_format/src/manager/mod.rs b/crates/joko_marker_format/src/manager/mod.rs deleted file mode 100644 index 93f3438..0000000 --- a/crates/joko_marker_format/src/manager/mod.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! How should the pack be stored by jokolay? -//! 1. Inside a directory called packs, we will have a separate directory for each pack. -//! 2. the name of the directory will serve as an ID for each pack. -//! 3. Inside the directory, we will have -//! 1. categories.xml -> The xml file which contains the whole category tree -//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. -//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. - -/* -expensive: -categories being a tree with order among siblings (better to use a tree crate?) -markers/trails referring to a category via full path. -editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. - -We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. - -*/ -mod live_pack; -use std::{ - collections::BTreeMap, - io::Read, - sync::{Arc, Mutex}, -}; - -use cap_std::fs_utf8::Dir; -use egui::{CollapsingHeader, ColorImage, TextureHandle, Window}; -use image::EncodableLayout; - -use tracing::{error, info, info_span}; - -use jokolink::MumbleLink; -use miette::{Context, IntoDiagnostic, Result}; - -use self::live_pack::LoadedPack; - -use super::pack::PackCore; - -// pub const PACK_LIST_URL: &str = "https://packlist.jokolay.com/packlist.json"; - -pub const MARKER_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; -pub const MARKER_PACKS_DIRECTORY_NAME: &str = "packs"; -// pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; - -/// It manage everything that has to do with marker packs. -/// 1. imports, loads, saves and exports marker packs. -/// 2. maintains the categories selection data for every pack -/// 3. contains activation data globally and per character -/// 4. When we load into a map, it filters the markers and runs the logic every frame -/// 1. If a marker needs to be activated (based on player position or whatever) -/// 2. marker needs to be drawn -/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture -/// 4. render that marker use joko_render -pub struct MarkerManager { - /// holds data that is useful for the ui - ui_data: MarkerManagerUI, - /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. - _marker_manager_dir: Arc, - /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. - /// The name of the child directory is the name of the pack - marker_packs_dir: Arc, - /// These are the marker packs - /// The key is the name of the pack - /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. - packs: BTreeMap, - missing_texture: Option, - /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. - /// This allows us to avoid saving the pack too often. - pub save_interval: f64, -} - -#[derive(Debug, Default)] -pub(crate) enum ImportStatus { - #[default] - UnInitialized, - WaitingForFileChooser, - LoadingPack(std::path::PathBuf), - PackDone(String, PackCore, bool), - PackError(miette::Report), -} -#[derive(Debug, Default)] -pub(crate) struct MarkerManagerUI { - // tf is this type supposed to be? maybe we should have used a ECS for this reason. - pub import_status: Option>>, -} - -#[derive(Debug, Default)] -pub struct PackList { - pub packs: BTreeMap, -} - -#[derive(Debug)] -pub struct PackEntry { - pub url: url::Url, - pub description: String, -} - -impl MarkerManager { - /// Creates a new instance of [MarkerManager]. - /// 1. It opens the marker manager directory - /// 2. loads its configuration - /// 3. opens the packs directory - /// 4. loads all the packs - /// 5. loads all the activation data - /// 6. returns self - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(MARKER_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker manager directory")?; - let marker_manager_dir = jdir - .open_dir(MARKER_MANAGER_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker manager directory")?; - marker_manager_dir - .create_dir_all(MARKER_PACKS_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to create marker packs directory")?; - let marker_packs_dir = marker_manager_dir - .open_dir(MARKER_PACKS_DIRECTORY_NAME) - .into_diagnostic() - .wrap_err("failed to open marker packs dir")?; - let mut packs: BTreeMap = Default::default(); - - for entry in marker_packs_dir - .entries() - .into_diagnostic() - .wrap_err("failed to get entries of marker packs dir")? - { - let entry = entry.into_diagnostic()?; - if entry.metadata().into_diagnostic()?.is_file() { - continue; - } - if let Ok(name) = entry.file_name() { - let pack_dir = entry - .open_dir() - .into_diagnostic() - .wrap_err("failed to open pack entry as directory")?; - { - let span_guard = info_span!("loading pack from dir", name).entered(); - match LoadedPack::load_from_dir(pack_dir.into()) { - Ok(lp) => { - packs.insert(name, lp); - } - Err(e) => { - error!(?e, "failed to load pack from directory"); - } - } - drop(span_guard); - } - } - } - - Ok(Self { - packs, - marker_packs_dir: marker_packs_dir.into(), - _marker_manager_dir: marker_manager_dir.into(), - ui_data: Default::default(), - save_interval: 0.0, - missing_texture: None, - }) - } - - fn pack_importer(import_status: Arc>) { - rayon::spawn(move || { - *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; - - if let Some(file_path) = rfd::FileDialog::new() - .add_filter("taco", &["zip", "taco"]) - .pick_file() - { - *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path.clone()); - - let result = import_pack_from_zip_file_path(file_path); - match result { - Ok((name, pack)) => { - *import_status.lock().unwrap() = ImportStatus::PackDone(name, pack, false); - } - Err(e) => { - *import_status.lock().unwrap() = ImportStatus::PackError(e); - } - } - } else { - *import_status.lock().unwrap() = - ImportStatus::PackError(miette::miette!("file chooser was cancelled")); - } - }); - } - pub fn tick( - &mut self, - etx: &egui::Context, - timestamp: f64, - joko_renderer: &mut joko_render::JokoRenderer, - link: &Option>, - ) { - if self.missing_texture.is_none() { - let img = image::load_from_memory(include_bytes!("../pack/marker.png")).unwrap(); - let size = [img.width() as _, img.height() as _]; - self.missing_texture = Some(etx.load_texture( - "default marker", - ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), - egui::TextureOptions { - magnification: egui::TextureFilter::Linear, - minification: egui::TextureFilter::Linear, - }, - )); - } - - for pack in self.packs.values_mut() { - pack.tick( - etx, - timestamp, - joko_renderer, - link, - self.missing_texture.as_ref().unwrap(), - ); - } - } - pub fn menu_ui(&mut self, ui: &mut egui::Ui) { - ui.menu_button("Markers", |ui| { - for pack in self.packs.values_mut() { - pack.category_sub_menu(ui); - } - }); - } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { - Window::new("Marker Manager").open(open).show(etx, |ui| -> Result<()> { - CollapsingHeader::new("Loaded Packs").show(ui, |ui| { - egui::Grid::new("packs").striped(true).show(ui, |ui| { - let mut delete = vec![]; - for pack in self.packs.keys() { - ui.label(pack); - if ui.button("delete").clicked() { - delete.push(pack.clone()); - } - } - for pack_name in delete { - self.packs.remove(&pack_name); - if let Err(e) = self.marker_packs_dir.remove_dir_all(&pack_name) { - error!(?e, pack_name,"failed to remove pack"); - } else { - info!("deleted marker pack: {pack_name}"); - } - } - }); - }); - - if self.ui_data.import_status.is_some() { - if ui.button("clear").on_hover_text( - "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { - self.ui_data.import_status = None; - } - } else if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { - let import_status = Arc::new(Mutex::default()); - self.ui_data.import_status = Some(import_status.clone()); - Self::pack_importer(import_status); - } - if let Some(import_status) = self.ui_data.import_status.as_ref() { - if let Ok(mut status) = import_status.lock() { - match &mut *status { - ImportStatus::UnInitialized => { - ui.label("import not started yet"); - } - ImportStatus::WaitingForFileChooser => { - ui.label( - "wailting for the file dialog. choose a taco/zip file to import", - ); - } - ImportStatus::LoadingPack(p) => { - ui.label(format!("pack is being imported from {p:?}")); - } - ImportStatus::PackDone(name, pack, saved) => { - - if !*saved { - ui.horizontal(|ui| { - ui.label("choose a pack name: "); - ui.text_edit_singleline(name); - }); - let name = name.as_str(); - if ui.button("save").clicked() { - - if self.marker_packs_dir.exists(name) { - self.marker_packs_dir - .remove_dir_all(name) - .into_diagnostic()?; - } - if let Err(e) = self.marker_packs_dir.create_dir_all(name) { - error!(?e, "failed to create directory for pack"); - - } - match self.marker_packs_dir.open_dir(name) { - Ok(dir) => { - let core = std::mem::take(pack); - let mut loaded_pack = LoadedPack::new(core, dir.into()); - match loaded_pack.save_all() { - Ok(_) => { - self.packs.insert(name.to_string(), loaded_pack); - *saved = true; - }, - Err(e) => { - error!(?e, "failed to save marker pack"); - }, - } - }, - Err(e) => { - error!(?e, "failed to open marker pack directory to save pack"); - } - }; - } - } else { - ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); - } - } - ImportStatus::PackError(e) => { - ui.colored_label( - egui::Color32::RED, - format!("failed to import pack due to error: {e:#?}"), - ); - } - } - } - } - - Ok(()) - }); - } -} - -fn import_pack_from_zip_file_path(file_path: std::path::PathBuf) -> Result<(String, PackCore)> { - let mut taco_zip = vec![]; - std::fs::File::open(&file_path) - .into_diagnostic()? - .read_to_end(&mut taco_zip) - .into_diagnostic()?; - - info!("starting to get pack from taco"); - crate::io::get_pack_from_taco_zip(&taco_zip).map(|pack| { - ( - file_path - .file_name() - .map(|ostr| ostr.to_string_lossy().to_string()) - .unwrap_or_default(), - pack, - ) - }) -} diff --git a/crates/joko_marker_format/src/pack/marker.rs b/crates/joko_marker_format/src/pack/marker.rs deleted file mode 100644 index 4dbc187..0000000 --- a/crates/joko_marker_format/src/pack/marker.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::CommonAttributes; -use glam::Vec3; -use uuid::Uuid; - -#[derive(Debug, Clone)] -pub(crate) struct Marker { - pub guid: Uuid, - pub position: Vec3, - pub map_id: u32, - pub category: String, - pub attrs: CommonAttributes, -} diff --git a/crates/joko_marker_format/src/pack/mod.rs b/crates/joko_marker_format/src/pack/mod.rs deleted file mode 100644 index f2fa977..0000000 --- a/crates/joko_marker_format/src/pack/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -mod common; -mod marker; -mod trail; - -use std::{collections::BTreeMap, str::FromStr}; - -use indexmap::IndexMap; - -pub use common::*; -pub(crate) use marker::*; -use smol_str::SmolStr; -pub(crate) use trail::*; - -#[derive(Default, Debug, Clone)] -pub(crate) struct PackCore { - pub textures: BTreeMap>, - pub tbins: BTreeMap, - pub categories: IndexMap, - pub maps: BTreeMap, -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct MapData { - pub markers: Vec, - pub trails: Vec, -} - -#[derive(Debug, Clone)] -pub(crate) struct Category { - pub display_name: String, - pub separator: bool, - pub default_enabled: bool, - pub props: CommonAttributes, - pub children: IndexMap, -} - -/// This newtype is used to represents relative paths in marker packs -/// 1. It won't start with `/` or `C:` like roots, because its a relative path -/// 2. It can be empty to represent current directory -/// 3. No expansion of special characters like `.` or `..` stuff. -/// 4. It is always lowercase to avoid platform specific quirks. -/// 5. It will use `/` as the path separator. -/// 6. It doesn't mean that the path is valid. It may contain many of the utf-8 characters which are not valid path names on linux/windows -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RelativePath(SmolStr); -#[allow(unused)] -impl RelativePath { - pub fn join_str(&self, path: &str) -> Self { - let path = path.trim_start_matches('/'); - if path.is_empty() { - return Self(self.0.clone()); - } - let lower_case = path.to_lowercase(); - if self.0.is_empty() { - // no need to push `/` if we are empty, as that would make it an absolute path - return Self(lower_case.into()); - } - - let mut new = self.0.to_string(); - if !self.0.ends_with('/') { - new.push('/'); - } - new.push_str(&lower_case); - Self(new.into()) - } - - pub fn ends_with(&self, ext: &str) -> bool { - self.0.ends_with(ext) - } - pub fn is_png(&self) -> bool { - self.ends_with(".png") - } - pub fn is_tbin(&self) -> bool { - self.ends_with(".trl") - } - pub fn is_xml(&self) -> bool { - self.ends_with(".xml") - } - pub fn is_dir(&self) -> bool { - self.ends_with("/") - } - pub fn parent(&self) -> Option<&str> { - let path = self.0.trim_end_matches('/'); - if path.is_empty() { - return None; - } - path.rfind('/').map(|index| &path[..=index]) - } - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for RelativePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} -impl From for String { - fn from(val: RelativePath) -> String { - val.0.into() - } -} -impl FromStr for RelativePath { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let path = s.trim_start_matches('/'); - if path.is_empty() { - return Ok(Self::default()); - } - Ok(Self(path.to_lowercase().into())) - } -} diff --git a/crates/joko_marker_format/src/pack/trail.rs b/crates/joko_marker_format/src/pack/trail.rs deleted file mode 100644 index e51f66b..0000000 --- a/crates/joko_marker_format/src/pack/trail.rs +++ /dev/null @@ -1,20 +0,0 @@ -use uuid::Uuid; - -use super::CommonAttributes; - -#[derive(Debug, Clone)] -pub(crate) struct Trail { - pub guid: Uuid, - pub map_id: u32, - pub category: String, - pub props: CommonAttributes, -} - -#[derive(Debug, Clone)] -pub(crate) struct TBin { - pub map_id: u32, - pub version: u32, - pub nodes: Vec, -} - -impl TBin {} diff --git a/crates/joko_marker_format/Cargo.toml b/crates/joko_package_manager/Cargo.toml old mode 100755 new mode 100644 similarity index 51% rename from crates/joko_marker_format/Cargo.toml rename to crates/joko_package_manager/Cargo.toml index c901644..2bc7157 --- a/crates/joko_marker_format/Cargo.toml +++ b/crates/joko_package_manager/Cargo.toml @@ -1,44 +1,48 @@ [package] -name = "joko_marker_format" +name = "joko_package_manager" version = "0.2.1" edition = "2021" + [dependencies] # jmf deps # for marker packs -xot = { version = "0" } -# to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip -indexmap = { workspace = true, features = ["serde"]} -uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } - - -# for easier extraction to folers and compression of folders into zip files (.taco format alias) -zip = { version = "0.6", default-features = false, features = ["deflate"] } -# for dealing with png files in marker packs. -image = { version = "0.24", default-features = false, features = ["png"] } -# for rapid xml bindings -cxx = { version = "1.0", features = ["std"] } base64 = "0.21.2" +bytemuck = { workspace = true } +cxx = { version = "1.0", features = ["std"] } # for rapid xml bindings data-encoding = "2.4.0" +egui = { workspace = true } enumflags2 = { workspace = true } -cap-std = { workspace = true } -tracing = { workspace = true } -miette = { workspace = true } glam = { workspace = true } -egui = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -url = { workspace = true } +image = { version = "0.24", default-features = false, features = ["png"] } # for dealing with png files in marker packs. +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +joko_component_models = { path = "../joko_component_models" } +joko_render_models = { path = "../joko_render_models" } +joko_package_models = { path = "../joko_package_models" } +joko_ui_models = { path = "../joko_ui_models" } +jokoapi = { path = "../jokoapi" } +joko_link_models = { path = "../joko_link_models" } +miette = { workspace = true } +once = {workspace = true} +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } rayon = { workspace = true } rfd = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } smol_str = { workspace = true } -itertools = { workspace = true } time = { workspace = true , features = ["serde"]} -phf = { version = "*", features = ["macros"] } -paste = { version = "*" } -joko_render = { path = "../joko_render" } -jokolink = { path = "../jokolink" } -jokoapi = { path = "../jokoapi" } +tokio = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } +zip = { version = "0.6", default-features = false, features = ["deflate"] } # for easier extraction to folers and compression of folders into zip files (.taco format alias) +walkdir = "2.5.0" + + [dev-dependencies] diff --git a/crates/joko_marker_format/README.md b/crates/joko_package_manager/README.md similarity index 100% rename from crates/joko_marker_format/README.md rename to crates/joko_package_manager/README.md diff --git a/crates/joko_marker_format/build.rs b/crates/joko_package_manager/build.rs similarity index 100% rename from crates/joko_marker_format/build.rs rename to crates/joko_package_manager/build.rs diff --git a/crates/joko_marker_format/src/pack/marker.png b/crates/joko_package_manager/images/marker.png old mode 100755 new mode 100644 similarity index 100% rename from crates/joko_marker_format/src/pack/marker.png rename to crates/joko_package_manager/images/marker.png diff --git a/crates/joko_marker_format/src/pack/question.png b/crates/joko_package_manager/images/question.png similarity index 100% rename from crates/joko_marker_format/src/pack/question.png rename to crates/joko_package_manager/images/question.png diff --git a/crates/joko_package_manager/images/trail.png b/crates/joko_package_manager/images/trail.png new file mode 100644 index 0000000..7529ba0 Binary files /dev/null and b/crates/joko_package_manager/images/trail.png differ diff --git a/crates/joko_marker_format/src/pack/trail.png b/crates/joko_package_manager/images/trail_black.png old mode 100755 new mode 100644 similarity index 100% rename from crates/joko_marker_format/src/pack/trail.png rename to crates/joko_package_manager/images/trail_black.png diff --git a/crates/joko_package_manager/images/trail_rainbow.png b/crates/joko_package_manager/images/trail_rainbow.png new file mode 100644 index 0000000..ea3ff6d Binary files /dev/null and b/crates/joko_package_manager/images/trail_rainbow.png differ diff --git a/crates/joko_package_manager/src/io/deserialize.rs b/crates/joko_package_manager/src/io/deserialize.rs new file mode 100644 index 0000000..788295d --- /dev/null +++ b/crates/joko_package_manager/src/io/deserialize.rs @@ -0,0 +1,1656 @@ +use crate::BASE64_ENGINE; +use base64::Engine; +use indexmap::IndexMap; +use joko_core::{serde_glam::Vec3, RelativePath}; +use joko_package_models::{ + attributes::{CommonAttributes, XotAttributeNameIDs}, + category::{prefix_parent, Category, RawCategory}, + marker::Marker, + package::{PackCore, PackageImportReport}, + route::Route, + trail::{TBin, TBinStatus, Trail}, +}; +use std::{ + collections::VecDeque, + io::{Cursor, Read}, + path::Path, + str::FromStr, +}; +use tracing::{debug, error, info, info_span, instrument, trace, warn}; +use uuid::Uuid; +use xot::{Element, Node, Xot}; +use zip::result::{ZipError, ZipResult}; + +const MAX_TRAIL_CHUNK_LENGTH: f32 = 400.0; + +pub(crate) fn load_pack_core_from_normalized_folder( + core_path: &Path, + import_report: Option, +) -> Result { + //called from already parsed data + let mut core_pack = PackCore::new(); + if let Some(mut import_report) = import_report { + import_report.reset_counters(); + import_report.uuid = core_pack.uuid; + core_pack.report = import_report; + } + // walks the directory and loads all files into the hashmap + let start = std::time::SystemTime::now(); + recursive_walk_dir_and_read_images_and_tbins( + core_path, + &mut core_pack, + &RelativePath::default(), + ) + .or(Err("failed to walk dir when loading a markerpack"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of core package textures from disk took {} ms", + elaspsed.as_millis() + ); + + //categories are required to register other objects + let cats_xml = std::fs::read_to_string(core_path.join("categories.xml")) + .or(Err("failed to read categories.xml"))?; + let categories_file = String::from("categories.xml"); + let parse_categories_file_start = std::time::SystemTime::now(); + parse_categories_from_normalized_file(&categories_file, &cats_xml, &mut core_pack) + .or(Err("failed to parse category file"))?; + let elapsed = parse_categories_file_start.elapsed().unwrap_or_default(); + info!("parse_categories_file took {} ms", elapsed.as_millis()); + + // parse map data of the pack + for entry in std::fs::read_dir(core_path).or(Err("failed to read entries of pack dir"))? { + let dir_entry = entry.or(Err("entry error whiel reading xml files"))?; + + let name = dir_entry + .file_name() + .into_string() + .or(Err("map data entry name not utf-8"))?; + + if name.ends_with(".xml") { + if let Some(name_as_str) = name.strip_suffix(".xml") { + match name_as_str { + "categories" => { + //already done + } + file_name => { + // parse map file + let span_guard = info_span!("load file", file_name).entered(); + //let mut partial_pack = PackCore::partial(&core_pack.all_categories); + load_xml_from_normalized_file( + file_name, + &dir_entry.path(), + &mut core_pack, + )?; + //core_pack.merge_partial(partial_pack); + std::mem::drop(span_guard); + } + } + } + } else { + trace!("file ignored: {name}") + } + } + info!( + "Entities registered (category + markers): {}", + core_pack.entities_parents.len() + ); + info!("Categories registered: {}", core_pack.all_categories.len()); + info!( + "Markers registered: {}", + core_pack.entities_parents.len() - core_pack.all_categories.len() + ); + info!("Maps registered: {}", core_pack.maps.len()); + info!("Textures registered: {}", core_pack.textures.len()); + info!("Trail binaries registered: {}", core_pack.tbins.len()); + Ok(core_pack) +} + +fn recursive_walk_dir_and_read_images_and_tbins( + core_path: &Path, + pack: &mut PackCore, + parent_path: &RelativePath, +) -> Result<(), String> { + for entry in std::fs::read_dir(core_path).or(Err("failed to get directory entries"))? { + let entry = entry.or(Err("dir entry error when iterating dir entries"))?; + let name = entry + .file_name() + .into_string() + .or(Err("No file name found"))?; + let path = parent_path.join_str(&name); + + if entry + .file_type() + .or(Err("failed to get file type"))? + .is_file() + { + if path.ends_with(".png") || path.ends_with(".trl") { + let bytes = std::fs::read(entry.path()).or(Err("failed to read file contents"))?; + if name.ends_with(".png") { + pack.register_texture(name, &path, bytes); + } else if name.ends_with(".trl") { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + /*let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + }*/ + pack.tbins.insert(path, tbs.tbin); + } else { + info!("invalid tbin: {path}"); + } + } + } + } else { + recursive_walk_dir_and_read_images_and_tbins(&entry.path(), pack, &path)?; + } + } + Ok(()) +} +fn parse_tbin_from_slice(bytes: &[u8]) -> Option { + let content_length = bytes.len(); + // content_length must be atleast 8 to contain version + map_id + if content_length < 8 { + info!("failed to parse tbin because the len is less than 8"); + return None; + } + + let mut version_bytes = [0_u8; 4]; + version_bytes.copy_from_slice(&bytes[4..8]); + let version = u32::from_ne_bytes(version_bytes); + let mut map_id_bytes = [0_u8; 4]; + map_id_bytes.copy_from_slice(&bytes[4..8]); + let map_id = u32::from_ne_bytes(map_id_bytes); + + let zero = glam::Vec3 { + x: 0.0, + y: 0.0, + z: 0.0, + }; + + // this will either be empty vec or series of vec3s. + let nodes: VecDeque = bytes[8..] + .chunks_exact(12) + .map(|float_bytes| { + // make [f32 ;3] out of those 12 bytes + let arr = [ + f32::from_le_bytes([ + // first float + float_bytes[0], + float_bytes[1], + float_bytes[2], + float_bytes[3], + ]), + f32::from_le_bytes([ + // second float + float_bytes[4], + float_bytes[5], + float_bytes[6], + float_bytes[7], + ]), + f32::from_le_bytes([ + // third float + float_bytes[8], + float_bytes[9], + float_bytes[10], + float_bytes[11], + ]), + ]; + + glam::Vec3::from_array(arr) + }) + .collect(); + + //There are zeroes in trails. Reason may be either bad trail or used as a separator for several trails in same file. + let mut iso_x = false; + let mut iso_y = false; + let mut iso_z = false; + let mut closed = false; + let mut resulting_nodes: Vec = Vec::new(); + if !nodes.is_empty() { + //at least the first exist and can be accessed + let ref_node = nodes[0]; + let mut c_iso_x = true; + let mut c_iso_y = true; + let mut c_iso_z = true; + // ensure there is not too much distance between two points, if it is the case, we do split the path in several parts + resulting_nodes.push(Vec3(ref_node)); + for (a, b) in nodes.iter().zip(nodes.iter().skip(1)) { + //ignore zeroes since they would be separators + if a.distance_squared(zero) > 0.01 && b.distance_squared(zero) > 0.01 { + let distance_to_next_point = a.distance_squared(*b); + let mut current_cursor = distance_to_next_point; + while current_cursor > MAX_TRAIL_CHUNK_LENGTH { + let c = a.lerp(*b, 1.0 - current_cursor / distance_to_next_point); + resulting_nodes.push(Vec3(c)); + current_cursor -= MAX_TRAIL_CHUNK_LENGTH; + } + } + resulting_nodes.push(Vec3(*b)); + } + for node in &nodes { + if resulting_nodes.len() > 1 { + //TODO: load epsilon from a configuration somewhere, with a default value + if (node.x - ref_node.x).abs() < 0.1 { + c_iso_x = false; + } + if (node.y - ref_node.y).abs() < 0.1 { + c_iso_y = false; + } + if (node.z - ref_node.z).abs() < 0.1 { + c_iso_z = false; + } + } + } + iso_x = c_iso_x; + iso_y = c_iso_y; + iso_z = c_iso_z; + if nodes.len() > 1 { + // TODO: get this threshold from configuration + closed = nodes + .front() + .unwrap() + .distance(*nodes.back().unwrap()) + .abs() + < 0.1 + } + } + Some(TBinStatus { + tbin: TBin { + map_id, + version, + nodes: resulting_nodes, + }, + iso_x, + iso_y, + iso_z, + closed, + }) +} + +fn parse_categories( + pack: &mut PackCore, + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut IndexMap, + names: &XotAttributeNameIDs, + source_file_uuid: &Uuid, +) { + //called once per file + parse_categories_recursive( + pack, + tree, + tags, + first_pass_categories, + names, + None, + source_file_uuid, + ) +} + +// a recursive function to parse the marker category tree. +fn parse_categories_recursive( + pack: &mut PackCore, + tree: &Xot, + tags: impl Iterator, + first_pass_categories: &mut IndexMap, + names: &XotAttributeNameIDs, + parent_name: Option, + source_file_uuid: &Uuid, +) { + for tag in tags { + let ele = match tree.element(tag) { + Some(ele) => ele, + None => continue, + }; + if ele.name() != names.marker_category { + continue; + } + + let name = ele + .get_attribute(names.name) + .or(ele.get_attribute(names.capital_name)) + .unwrap_or_default() + .to_lowercase(); + if name.is_empty() { + continue; + } + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(ele, names); + let display_name = ele.get_attribute(names.display_name).unwrap_or(&name); + + let separator = ele + .get_attribute(names.separator) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or_default(); + + let default_enabled = ele + .get_attribute(names.default_enabled) + .unwrap_or_default() + .parse() + .map(|u: u8| u != 0) + .unwrap_or(true); + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, name) + } else { + name.to_string() + }; + let guid = parse_guid(names, ele); + trace!( + "recursive_marker_category_parser {} {} {:?}", + name, + guid, + parent_name + ); + if !first_pass_categories.contains_key(&full_category_name) { + let mut sources: IndexMap = IndexMap::new(); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_inherited_texture( + icon_file.as_str().to_string(), + full_category_name.clone(), + source_file_uuid, + ); + } + } + + sources.insert(guid, *source_file_uuid); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + guid, + parent_name: parent_name.clone(), + display_name: display_name.to_string(), + relative_category_name: name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: common_attributes, + sources, + }, + ); + } + parse_categories_recursive( + pack, + tree, + tree.children(tag), + first_pass_categories, + names, + Some(full_category_name), + source_file_uuid, + ); + } +} + +fn parse_categories_from_normalized_file( + file_name: &String, + cats_xml_str: &str, + pack: &mut PackCore, +) -> Result<(), String> { + let mut tree = xot::Xot::new(); + let xot_names = XotAttributeNameIDs::register_with_xot(&mut tree); + let root_node = tree.parse(cats_xml_str).or(Err("invalid xml"))?; + + let overlay_data_node = tree.document_element(root_node).or(Err("no doc element"))?; + + if let Some(od) = tree.element(overlay_data_node) { + let mut categories: IndexMap = Default::default(); + if od.name() == xot_names.overlay_data { + parse_category_categories_xml_recursive( + file_name, + &tree, + tree.children(overlay_data_node), + &mut categories, + &xot_names, + None, + None, + )?; + trace!("loaded categories: {:?}", categories); + pack.categories = categories; + pack.register_categories(); + } else { + return Err("root tag is not OverlayData".to_string()); + } + } else { + return Err("doc element is not element???".to_string()); + } + Ok(()) +} + +fn load_xml_from_normalized_file( + file_name: &str, + file_path: &Path, + target: &mut PackCore, +) -> Result<(), String> { + let mut xml_str = String::new(); + std::fs::OpenOptions::new() + .read(true) + .open(file_path) + .or(Err("failed to open xml file"))? + .read_to_string(&mut xml_str) + .or(Err("failed to read xml string"))?; + //TODO: launch an async load of the file + make a priority queue to have current map first + parse_map_xml_string(file_name, &xml_str, target) + .or(Err(format!("error parsing file: {file_name}"))) +} + +fn parse_map_xml_string( + file_name: &str, + map_xml_str: &str, + target: &mut PackCore, +) -> Result<(), String> { + let mut tree = Xot::new(); + let root_node = tree.parse(map_xml_str).or(Err("invalid xml"))?; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let overlay_data_node = tree + .document_element(root_node) + .or(Err("missing doc element"))?; + + let overlay_data_element = tree.element(overlay_data_node).ok_or("no doc ele")?; + + if overlay_data_element.name() != names.overlay_data { + return Err("root tag is not OverlayData".to_string()); + } + let pois = tree + .children(overlay_data_node) + .find(|node| match tree.element(*node) { + Some(ele) => ele.name() == names.pois, + None => false, + }) + .ok_or("missing pois node")?; + + for poi_node in tree.children(pois) { + if let Some(child_element) = tree.element(poi_node) { + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + let span_guard = info_span!("category", full_category_name).entered(); + + let opt_source_file_uuid = Uuid::from_str( + child_element + .get_attribute(names._source_file_name) + .unwrap_or_default(), + ); + let source_file_uuid = if let Ok(uuid) = opt_source_file_uuid { + uuid + } else { + error!("Package corrupted, invalid source file uuid"); + //return Err(miette::Report::msg("Package corrupted, invalid source file uuid")); + Uuid::new_v4() + }; + + if let Some(source_file_name) = + target.report.source_file_uuid_to_name(&source_file_uuid) + { + let source_file_name = source_file_name.clone(); // this is to bypass borrow checker which has no idea this cannot be changed + target.register_source_file(&source_file_name); + } else { + println!("{:?}", source_file_uuid); + } + + //There is no file name, only an uuid to register + target.active_source_files.insert(source_file_uuid, true); + + if child_element.name() == names.route { + debug!("Found a route in core pack {:?}", child_element); + let route = parse_route( + &names, + &tree, + &poi_node, + child_element, + &full_category_name, + source_file_uuid, + ); + if let Some(route) = route { + target.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + panic!( + "full_category_name is empty {:?} {:?}", + map_xml_str, child_element + ); + } + let raw_uid = child_element.get_attribute(names.guid); + if raw_uid.is_none() { + info!( + "This POI is either invalid or inside a Route {:?}", + child_element + ); + span_guard.exit(); + continue; + } + //FIXME: this needs to be changed for partial load + let opt_cat_uuid = target.get_category_uuid(&full_category_name); + if opt_cat_uuid.is_none() { + error!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + file_name, child_element + ); + return Err(format!( + "Mandatory category missing, packge is corrupted {:?} {:?}", + map_xml_str, child_element + )); + } + let category_uuid = opt_cat_uuid.unwrap(); //categories MUST exist, they have already been parsed + let guid = raw_uid + .and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + }) + .ok_or(format!("invalid guid {:?}", raw_uid))?; + + if child_element.name() == names.poi { + debug!("Found a POI in core pack {:?}", child_element); + let map_id = child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or("invalid mapid")?; + + let xpos = child_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .or(Err("invalid x position"))?; + let ypos = child_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .or(Err("invalid y position"))?; + let zpos = child_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .or(Err("invalid z position"))?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + let marker = Marker { + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), + map_id, + category: full_category_name.clone(), + parent: *category_uuid, + attrs: ca, + guid, + source_file_uuid, + }; + target.register_marker(full_category_name, marker)?; + } else if child_element.name() == names.trail { + debug!("Found a trail in core pack {:?}", child_element); + let map_id = child_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + .ok_or("invalid mapid")?; + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(child_element, &names); + + let trail = Trail { + category: full_category_name.clone(), + parent: *category_uuid, + map_id, + props: ca, + guid, + dynamic: false, + source_file_uuid, + }; + target.register_trail(full_category_name, trail)?; + } + } + span_guard.exit(); + } + } + Ok(()) +} + +// a temporary recursive function to parse the marker category tree. +fn parse_category_categories_xml_recursive( + _file_name: &String, //meant for future implementation of source file definition for categories + tree: &Xot, + tags: impl Iterator, + cats: &mut IndexMap, + names: &XotAttributeNameIDs, + parent_uuid: Option, + parent_name: Option, +) -> Result<(), String> { + for tag in tags { + if let Some(ele) = tree.element(tag) { + if ele.name() != names.marker_category { + continue; + } + + let relative_category_name = ele + .get_attribute(names.name) + .or(ele + .get_attribute(names.display_name) + .or(ele.get_attribute(names.capital_name))) + .unwrap_or_default() + .to_lowercase(); + if relative_category_name.is_empty() { + info!("category doesn't have a name attribute: {ele:#?}"); + continue; + } + let span_guard = info_span!("category", relative_category_name).entered(); + let mut ca = CommonAttributes::default(); + ca.update_common_attributes_from_element(ele, names); + + let display_name = ele.get_attribute(names.display_name).unwrap_or_default(); + + let separator = match ele.get_attribute(names.separator).unwrap_or("0") { + "0" => false, + "1" => true, + ors => { + info!("separator attribute has invalid value: {ors}"); + false + } + }; + + let default_enabled = match ele.get_attribute(names.default_enabled).unwrap_or("1") { + "0" => false, + "1" => true, + ors => { + info!("default_enabled attribute has invalid value: {ors}"); + true + } + }; + let full_category_name: String = if let Some(parent_name) = &parent_name { + format!("{}.{}", parent_name, relative_category_name) + } else { + relative_category_name.to_string() + }; + let guid = parse_guid(names, ele); + trace!( + "recursive_marker_category_parser_categories_xml {} {} {:?}", + full_category_name, + guid, + parent_uuid + ); + if display_name.is_empty() { + if parent_name.is_some() { + return Err( + "Package is corrupted, please import it again with current version" + .to_string(), + ); + } + parse_category_categories_xml_recursive( + _file_name, + tree, + tree.children(tag), + cats, + names, + Some(guid), + Some(full_category_name), + )?; + } else { + let current_category = if let Some(c) = cats.get_mut(&guid) { + c + } else { + let c = Category { + guid, + parent: parent_uuid, + display_name: display_name.to_string(), + relative_category_name: relative_category_name.to_string(), + full_category_name: full_category_name.clone(), + separator, + default_enabled, + props: ca, + children: Default::default(), + }; + cats.insert(guid, c); + cats.last_mut().unwrap().1 + }; + parse_category_categories_xml_recursive( + _file_name, + tree, + tree.children(tag), + &mut current_category.children, + names, + Some(guid), + Some(full_category_name), + )?; + }; + + std::mem::drop(span_guard); + } else { + //it may be a comment, a space, anything + //info!("In file {}, ignore node {:?}", file_name, tag); + } + } + Ok(()) +} + +//copy of zip::ZipArchive extract, but handling the bad windows path +fn extract>( + zip_archive: &mut zip::ZipArchive>>, + directory: P, +) -> ZipResult<()> { + use std::fs; + use std::io; + + for i in 0..zip_archive.len() { + let mut file = zip_archive.by_index(i)?; + let filepath = file + .enclosed_name() + .ok_or(ZipError::InvalidArchive("Invalid file path"))?; + + let filepath = filepath + .to_owned() + .as_mut_os_str() + .to_str() + .unwrap() + .replace('\\', "/") + .trim_start_matches('/') + .to_lowercase(); + let filepath = std::path::Path::new(&filepath); + let outpath = directory.as_ref().join(filepath); + + if file.name().replace('\\', "/").ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut outfile = fs::File::create(&outpath)?; + io::copy(&mut file, &mut outfile)?; + } + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } + Ok(()) +} +pub(crate) fn get_pack_from_taco_zip( + input_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result { + let mut taco_zip = vec![]; + std::fs::File::open(input_path) + .or(Err("Could not open target folder"))? + .read_to_end(&mut taco_zip) + .or(Err("Could not read target folder"))?; + + let mut zip_archive = zip::ZipArchive::new(std::io::Cursor::new(taco_zip)) + .or(Err("failed to read zip archive"))?; + if extract_temporary_path.exists() { + std::fs::remove_dir_all(extract_temporary_path).or(Err("Could not purge target folder"))?; + } + extract(&mut zip_archive, extract_temporary_path) + .or(Err("Could not extract archive into target folder"))?; + + _get_pack_from_taco_folder(extract_temporary_path) +} + +/// This first parses all the files in a zipfile into the memory and then it will try to parse a zpack out of all the files. +/// will return error if there's an issue with zipfile. +/// +/// but any other errors like invalid attributes or missing markers etc.. will just be logged. +/// the intention is "best effort" parsing and not "validating" xml marker packs. +/// we will ignore any issues like unknown attributes or xml tags. "unknown" attributes means Any attributes that jokolay doesn't parse into Zpack. + +#[instrument(skip_all)] +fn _get_pack_from_taco_folder(package_path: &std::path::PathBuf) -> Result { + let mut pack = PackCore::new(); + + // file paths of different file types + let mut images = vec![]; + let mut tbins = vec![]; + let mut xmls = vec![]; + // we collect the names first, because reading a file from zip is a mutating operation. + // So, we can't iterate AND read the file at the same time + for entry in walkdir::WalkDir::new(package_path).into_iter() { + let entry = entry.or(Err("Could not walk directory"))?; + let path_as_string = entry + .path() + .strip_prefix(package_path) + .unwrap() + .to_str() + .unwrap() + .to_string(); + if path_as_string.ends_with(".png") { + images.push(path_as_string); + } else if path_as_string.ends_with(".trl") { + tbins.push(path_as_string); + } else if path_as_string.ends_with(".xml") { + xmls.push(path_as_string); + } else if path_as_string.replace('\\', "/").ends_with('/') { + // directory. so, we can silently ignore this. + } else { + //info!("ignoring file: {name}"); + } + } + xmls.sort(); //build back the intended order in folder, since zip_archive may not give the files in order. + let start_texture_loading = std::time::SystemTime::now(); + for file_path in images { + let span = info_span!("load image", file_path).entered(); + let relative_file_path: RelativePath = file_path.parse().unwrap(); + if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { + match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { + Ok(_) => { + pack.register_texture(file_path, &relative_file_path, bytes); + } + Err(e) => { + info!(?e, "failed to parse image file"); + } + } + } + std::mem::drop(span); + } + + for file_path in tbins { + let span = info_span!("load tbin", file_path).entered(); + let relative_path: RelativePath = file_path.parse().unwrap(); + if let Ok(bytes) = std::fs::read(package_path.join(&file_path)) { + if let Some(tbs) = parse_tbin_from_slice(&bytes) { + /*let is_closed: bool = tbs.closed; + if is_closed { + if tbs.iso_x {} + if tbs.iso_y {} + if tbs.iso_z {} + }*/ + assert!( + pack.tbins.insert(relative_path, tbs.tbin).is_none(), + "duplicate tbin file {file_path}" + ); + } else { + info!("failed to parse tbin from slice: {relative_path}"); + } + } else { + info!(file_path, "failed to read tbin from zipfile"); + } + std::mem::drop(span); + } + let elapsed_texture_loading = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.texture_loading = elapsed_texture_loading.as_millis(); + tracing::info!( + "Loading of taco package textures from disk took {} ms", + elapsed_texture_loading.as_millis() + ); + + let span_guard_categories = info_span!("deserialize xml: categories").entered(); + let start_categories_loading = std::time::SystemTime::now(); + //first pass: categories only + let span_guard_first_pass = + info_span!("deserialize xml first pass: load MarkerCategory").entered(); + let mut first_pass_categories: IndexMap = Default::default(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml first pass: load file", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + parse_categories( + &mut pack, + &tree, + tree.children(od), + &mut first_pass_categories, + &names, + &source_file_uuid, + ); + drop(span_guard); + } + span_guard_first_pass.exit(); + let elaspsed_first_pass = start_categories_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_first_pass = elaspsed_first_pass.as_millis(); + + //second pass: orphan categories + let span_guard_second_pass = + info_span!("deserialize xml second pass: orphan categories").entered(); + let start_categories_loading_second_pass = std::time::SystemTime::now(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml second pass: load file", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + debug!("missing overlay data tag"); + continue; + } + }; + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + debug!("missing pois tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let mut full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + if full_category_name.is_empty() { + if child_element.name() == names.route { + // If route, take the first element inside + if let Some(category) = + parse_route_category(&names, &tree, &child_node, child_element) + { + if category.is_empty() { + continue; + } + full_category_name = category; + } else { + continue; + } + } else { + continue; + } + } + let guid = parse_guid(&names, child_element); + if !pack.category_exists(&full_category_name) + && !first_pass_categories.contains_key(&full_category_name) + { + let category_uuid = Uuid::new_v4(); + let mut sources: IndexMap = IndexMap::new(); + sources.insert(guid, source_file_uuid); + first_pass_categories.insert( + full_category_name.clone(), + RawCategory { + default_enabled: true, + guid: category_uuid, + parent_name: prefix_parent(&full_category_name, '.'), + display_name: full_category_name.clone(), + full_category_name: full_category_name.clone(), + relative_category_name: full_category_name.clone(), + props: Default::default(), + separator: false, + sources, + }, + ); + debug!( + "There is an orphan missing category '{}' which was created", + full_category_name + ); + } else { + let cat = first_pass_categories.get_mut(&full_category_name); + cat.unwrap().sources.insert(guid, source_file_uuid); + } + } + drop(span_guard); + } + span_guard_second_pass.exit(); + + let elaspsed_second_pass = start_categories_loading_second_pass + .elapsed() + .unwrap_or_default(); + pack.report.telemetry.categories_second_pass = elaspsed_second_pass.as_millis(); + + let start_categories_reassemble = std::time::SystemTime::now(); + pack.categories = Category::reassemble(&first_pass_categories, &mut pack.report); + let elaspsed_reassemble = start_categories_reassemble.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_reassemble.total = elaspsed_reassemble.as_millis(); + + let start_categories_registering = std::time::SystemTime::now(); + pack.register_categories(); + let elaspsed_categories_registering = + start_categories_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.categories_registering = elaspsed_categories_registering.as_millis(); + + let elaspsed = start_categories_loading.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of taco package categories from disk took {} ms, {} + {} + {}", + elaspsed.as_millis(), + elaspsed_first_pass.as_millis(), + elaspsed_second_pass.as_millis(), + elaspsed_reassemble.as_millis(), + ); + + //third and last pass: elements + let span_guard_third_pass = info_span!("deserialize xml third pass: load elements").entered(); + let start_elements_registering = std::time::SystemTime::now(); + for source_file_name in xmls.iter() { + let source_file_name = source_file_name.to_string(); + let span_guard = + info_span!("deserialize xml third pass load file ", source_file_name).entered(); + let r = std::fs::read_to_string(package_path.join(&source_file_name)); + let xml_str = if r.is_ok() { + r.unwrap() + } else { + info!("failed to read file from zip"); + continue; + }; + let source_file_uuid = pack.register_source_file(&source_file_name); + + let filtered_xml_str = crate::rapid_filter_rust(xml_str); + let mut tree = Xot::new(); + let root_node = match tree.parse(&filtered_xml_str) { + Ok(root) => root, + Err(e) => { + info!(?e, "failed to parse as xml"); + continue; + } + }; + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = match tree + .document_element(root_node) + .ok() + .filter(|od| (tree.element(*od).unwrap().name() == names.overlay_data)) + { + Some(od) => od, + None => { + info!("missing overlay data tag"); + continue; + } + }; + + let pois = match tree.children(od).find(|node| { + tree.element(*node) + .map(|ele: &xot::Element| ele.name() == names.pois) + .unwrap_or_default() + }) { + Some(pois) => pois, + None => { + debug!("missing POIs tag"); + continue; + } + }; + + for child_node in tree.children(pois) { + let child_element = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + let full_category_name = child_element + .get_attribute(names.category) + .unwrap_or_default() + .to_lowercase(); + + debug!("import element: {:?}", child_element); + if child_element.name() == names.route { + let route = parse_route( + &names, + &tree, + &child_node, + child_element, + &full_category_name, + source_file_uuid, + ); + if let Some(mut route) = route { + //one must not create category anymore + route.parent = *pack.get_category_uuid(&route.category).unwrap(); + pack.register_route(route)?; + } else { + info!("Could not parse route {:?}", child_element); + } + } else { + if full_category_name.is_empty() { + info!("full_category_name is empty {:?}", child_element); + continue; + } + if !pack.category_exists(&full_category_name) { + panic!( + "Missing category {}, previous pass should have taken care of this", + full_category_name + ); + } + let guid = parse_guid(&names, child_element); + let category_uuid = + pack.get_or_create_category_uuid(&full_category_name, guid, &source_file_uuid); + if child_element.name() == names.poi { + if let Some(marker) = parse_marker( + &mut pack, + &names, + child_element, + guid, + &full_category_name, + &category_uuid, + source_file_uuid, + ) { + pack.register_marker(full_category_name, marker)?; + } else { + debug!("Could not parse POI"); + } + } else if child_element.name() == names.trail { + if let Some(trail) = parse_trail( + &mut pack, + &names, + child_element, + guid, + &full_category_name, + &category_uuid, + source_file_uuid, + ) { + pack.register_trail(full_category_name, trail)?; + } else { + debug!("Could not parse Trail"); + } + } else { + info!("unknown element: {:?}", child_element); + } + } + } + + drop(span_guard); + } + span_guard_third_pass.exit(); + span_guard_categories.exit(); + let elaspsed_elements_registering = start_elements_registering.elapsed().unwrap_or_default(); + pack.report.telemetry.elements_registering = elaspsed_elements_registering.as_millis(); + + let elapsed_import = start_texture_loading.elapsed().unwrap_or_default(); + pack.report.telemetry.total = elapsed_import.as_millis(); + Ok(pack) +} + +fn parse_optional_guid(names: &XotAttributeNameIDs, child: &Element) -> Option { + child.get_attribute(names.guid).and_then(|guid| { + let mut buffer = [0u8; 20]; + BASE64_ENGINE + .decode_slice(guid, &mut buffer) + .ok() + .and_then(|_| Uuid::from_slice(&buffer[..16]).ok()) + .or_else(|| { + info!(guid, "failed to deserialize guid"); + None + }) + }) +} +fn parse_guid(names: &XotAttributeNameIDs, child: &Element) -> Uuid { + parse_optional_guid(names, child).unwrap_or_else(Uuid::new_v4) +} + +fn parse_marker( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + poi_element: &Element, + guid: Uuid, + category_name: &str, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(poi_element, names); + if let Some(icon_file) = common_attributes.get_icon_file() { + if !pack.textures.contains_key(icon_file) { + debug!(%icon_file, "failed to find this texture in this pack"); + pack.found_missing_element_texture( + icon_file.as_str().to_string(), + guid, + &source_file_uuid, + ); + } + } else if let Some(icf) = poi_element.get_attribute(names.icon_file) { + debug!(icf, "marker's icon file attribute failed to parse"); + pack.found_missing_element_texture(icf.to_string(), guid, &source_file_uuid); + } + + if let Some(map_id) = poi_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + let xpos = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let ypos = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let zpos = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Some(Marker { + position: Vec3(glam::Vec3::from_array([xpos, ypos, zpos])), + map_id, + category: category_name.to_owned(), + parent: *category_uuid, + attrs: common_attributes, + guid, + source_file_uuid, + }) + } else { + debug!("missing map id"); + None + } +} + +fn parse_position(names: &XotAttributeNameIDs, poi_element: &Element) -> Vec3 { + let x = poi_element + .get_attribute(names.xpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let y = poi_element + .get_attribute(names.ypos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let z = poi_element + .get_attribute(names.zpos) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + Vec3(glam::Vec3 { x, y, z }) +} + +fn parse_route_category( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, +) -> Option { + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + if let Some(cat) = child.get_attribute(names.category) { + return Some(cat.to_string()); + } + } + } + info!("Could not find a category for route element: {route_element:?}"); + None +} + +fn parse_route( + names: &XotAttributeNameIDs, + tree: &Xot, + route_node: &Node, + route_element: &Element, + category_name: &str, + source_file_uuid: Uuid, +) -> Option { + let mut path: Vec = Vec::new(); + let resetposx = route_element + .get_attribute(names.resetposx) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposy = route_element + .get_attribute(names.resetposy) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let resetposz = route_element + .get_attribute(names.resetposz) + .unwrap_or_default() + .parse::() + .unwrap_or_default(); + let reset_position = glam::Vec3::new(resetposx, resetposy, resetposz); + let reset_range = route_element + .get_attribute(names.reset_range) + .and_then(|map_id| map_id.parse::().ok()); + let name = route_element + .get_attribute(names.name) + .or(route_element.get_attribute(names.capital_name)); + + if name.is_none() { + info!("route element is missing name: {route_element:?}"); + return None; + } + let mut category: String = category_name.to_owned(); + let mut category_uuid: Option = parse_optional_guid(names, route_element); + let mut map_id: Option = route_element + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()); + for child_node in tree.children(*route_node) { + let child = match tree.element(child_node) { + Some(ele) => ele, + None => continue, + }; + if child.name() == names.poi { + let marker = parse_position(names, child); + path.push(marker); + if category.is_empty() { + if let Some(cat) = child.get_attribute(names.category) { + category = cat.to_string(); + } + } + if category_uuid.is_none() { + category_uuid = parse_optional_guid(names, child) + } + if map_id.is_none() { + if let Some(node_map_id) = child + .get_attribute(names.map_id) + .and_then(|map_id| map_id.parse::().ok()) + { + map_id = Some(node_map_id); + } + } + } + } + if category.is_empty() { + info!("Could not find a category for route element: {route_element:?}"); + return None; + } + if map_id.is_none() { + info!("Could not find a map_id for route element: {route_element:?}"); + return None; + } + if category_uuid.is_none() { + info!("Could not find a uuid for route element: {route_element:?}"); + return None; + } + debug!( + "found route with {:?} elements {route_element:?}", + path.len() + ); + + Some(Route { + category, + parent: category_uuid.unwrap(), + path, + reset_position: Vec3(reset_position), + reset_range: reset_range.unwrap_or(0.0), + map_id: map_id.unwrap(), + name: name.unwrap().into(), + guid: parse_guid(names, route_element), + source_file_uuid, + }) +} + +fn parse_trail( + pack: &mut PackCore, + names: &XotAttributeNameIDs, + trail_element: &Element, + guid: Uuid, + category_name: &str, + category_uuid: &Uuid, + source_file_uuid: Uuid, +) -> Option { + //http://www.gw2taco.com/2022/04/a-proper-marker-editor-finally.html + + let mut common_attributes = CommonAttributes::default(); + common_attributes.update_common_attributes_from_element(trail_element, names); + + if let Some(tex) = common_attributes.get_texture() { + if !pack.textures.contains_key(tex) { + info!(%tex, "failed to find this texture in this pack"); + pack.found_missing_element_texture(tex.as_str().to_string(), guid, &source_file_uuid); + } + } + + #[allow(clippy::manual_map)] + // This is not exactly a manual map, we register something more in pack on some condition: a missing trail. + if let Some(map_id) = trail_element + .get_attribute(names.trail_data) + .and_then(|trail_data| { + //fix the path which may be a mix of windows and linux path + let file_path: RelativePath = trail_data.parse().unwrap(); + if let Some(tb) = pack.tbins.get(&file_path) { + Some(tb.map_id) + } else { + pack.found_missing_trail(&file_path, guid, &source_file_uuid); + None + } + }) + { + Some(Trail { + category: category_name.to_owned(), + parent: *category_uuid, + map_id, + props: common_attributes, + guid, + dynamic: false, + source_file_uuid, + }) + } else { + /*let td = trail_element.get_attribute(names.trail_data); + let file_path: RelativePath = td.unwrap_or_default().parse().unwrap(); + //pack.report.found_orphan_trail(&file_path, guid, &source_file_name); + let tbin = pack.tbins.get(&file_path).map(|tbin| (tbin.map_id, tbin.version)); + info!("missing map_id: {td:?} {file_path} {tbin:?}"); + */ + None + } +} + +#[instrument(skip(zip_archive))] +fn read_file_bytes_from_zip_by_name( + name: &str, + zip_archive: &mut zip::ZipArchive, +) -> Option> { + let mut bytes = vec![]; + match zip_archive.by_name(name) { + Ok(mut file) => match file.read_to_end(&mut bytes) { + Ok(size) => { + if size == 0 { + info!("empty file {name}"); + } else { + return Some(bytes); + } + } + Err(e) => { + info!(?e, "failed to read file"); + } + }, + Err(e) => { + info!(?e, "failed to get file from zip"); + } + } + None +} + +// #[cfg(test)] +// mod test { + +// use indexmap::IndexMap; +// use rstest::*; + +// use semver::Version; +// use similar_asserts::assert_eq; +// use std::io::Write; +// use std::sync::Arc; + +// use zip::write::FileOptions; +// use zip::ZipWriter; + +// use crate::{ +// pack::{xml::zpack_from_xml_entries, Pack, MARKER_PNG}, +// INCHES_PER_METER, +// }; + +// const TEST_XML: &str = include_str!("test.xml"); +// const TEST_MARKER_PNG_NAME: &str = "marker.png"; +// const TEST_TRL_NAME: &str = "basic.trl"; + +// #[fixture] +// #[once] +// fn test_zip() -> Vec { +// let mut writer = ZipWriter::new(std::io::Cursor::new(vec![])); +// // category.xml +// writer +// .start_file("category.xml", FileOptions::default()) +// .expect("failed to create category.xml"); +// writer +// .write_all(TEST_XML.as_bytes()) +// .expect("failed to write category.xml"); +// // marker.png +// writer +// .start_file(TEST_MARKER_PNG_NAME, FileOptions::default()) +// .expect("failed to create marker.png"); +// writer +// .write_all(MARKER_PNG) +// .expect("failed to write marker.png"); +// // basic.trl +// writer +// .start_file(TEST_TRL_NAME, FileOptions::default()) +// .expect("failed to create basic trail"); +// writer +// .write_all(&0u32.to_ne_bytes()) +// .expect("failed to write version"); +// writer +// .write_all(&15u32.to_ne_bytes()) +// .expect("failed to write mapid "); +// writer +// .write_all(bytemuck::cast_slice(&[0f32; 3])) +// .expect("failed to write first node"); +// // done +// writer +// .finish() +// .expect("failed to finalize zip") +// .into_inner() +// } + +// #[fixture] +// fn test_file_entries(test_zip: &[u8]) -> IndexMap, Vec> { +// let file_entries = super::read_files_from_zip(test_zip).expect("failed to deserialize"); +// assert_eq!(file_entries.len(), 3); +// let test_xml = std::str::from_utf8( +// file_entries +// .get(String::new("category.xml")) +// .expect("failed to get category.xml"), +// ) +// .expect("failed to get str from category.xml contents"); +// assert_eq!(test_xml, TEST_XML); +// let test_marker_png = file_entries +// .get(String::new("marker.png")) +// .expect("failed to get marker.png"); +// assert_eq!(test_marker_png, MARKER_PNG); +// file_entries +// } +// #[fixture] +// #[once] +// fn test_pack(test_file_entries: IndexMap, Vec>) -> Pack { +// let (pack, failures) = zpack_from_xml_entries(test_file_entries, Version::new(0, 0, 0)); +// assert!(failures.errors.is_empty() && failures.warnings.is_empty()); +// assert_eq!(pack.tbins.len(), 1); +// assert_eq!(pack.textures.len(), 1); +// assert_eq!( +// pack.textures +// .get(String::new(TEST_MARKER_PNG_NAME)) +// .expect("failed to get marker.png from textures"), +// MARKER_PNG +// ); + +// let tbin = pack +// .tbins +// .get(String::new(TEST_TRL_NAME)) +// .expect("failed to get basic trail") +// .clone(); + +// assert_eq!(tbin.nodes[0], [0.0f32; 3].into()); +// pack +// } + +// // #[rstest] +// // fn test_tag(test_pack: &Pack) { +// // let mut test_category_menu = CategoryMenu::default(); +// // let parent_path = String::new("parent"); +// // let child1_path = String::new("parent/child1"); +// // let subchild_path = String::new("parent/child1/subchild"); +// // let child2_path = String::new("parent/child2"); +// // test_category_menu.create_category(subchild_path); +// // test_category_menu.create_category(child2_path); +// // test_category_menu.set_display_name(parent_path, "Parent".to_string()); +// // test_category_menu.set_display_name(child1_path, "Child 1".to_string()); +// // test_category_menu.set_display_name(subchild_path, "Sub Child".to_string()); +// // test_category_menu.set_display_name(child2_path, "Child 2".to_string()); + +// // assert_eq!(test_category_menu, test_pack.category_menu) +// // } + +// #[rstest] +// fn test_markers(test_pack: &Pack) { +// let marker = test_pack +// .markers +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// marker.props.texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// assert_eq!(marker.position, [INCHES_PER_METER; 3].into()); +// } +// #[rstest] +// fn test_trails(test_pack: &Pack) { +// let trail = test_pack +// .trails +// .values() +// .next() +// .expect("failed to get queensdale mapdata"); +// assert_eq!( +// trail.props.tbin.as_ref().unwrap(), +// String::new(TEST_TRL_NAME) +// ); +// assert_eq!( +// trail.props.trail_texture.as_ref().unwrap(), +// String::new(TEST_MARKER_PNG_NAME) +// ); +// } +// } diff --git a/crates/joko_marker_format/src/io/error.rs b/crates/joko_package_manager/src/io/error.rs similarity index 100% rename from crates/joko_marker_format/src/io/error.rs rename to crates/joko_package_manager/src/io/error.rs diff --git a/crates/joko_package_manager/src/io/export.rs b/crates/joko_package_manager/src/io/export.rs new file mode 100644 index 0000000..8073c97 --- /dev/null +++ b/crates/joko_package_manager/src/io/export.rs @@ -0,0 +1,263 @@ +use crate::{ + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use cap_std::fs_utf8::Dir; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, package::PackCore, + route::Route, trail::Trail, +}; +use miette::{Context, IntoDiagnostic, Result}; +use std::io::Write; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +pub(crate) fn export_package_v2( + pack: &PackCore, + writing_directory: &Dir, + name: String, +) -> Result<()> { + Ok(()) +} + +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn export_package_v1( + pack_data: &LoadedPackData, + pack_textures: &LoadedPackData, + writing_directory: &Dir, +) -> Result<()> { + // save categories + info!( + "Saving data pack {}, {} categories, {} maps", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len() + ); + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create new root with overlay data node")?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od) + .wrap_err("failed to serialize cats")?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to convert cats xot to string")?; + writing_directory + .create("categories.xml") + .into_diagnostic() + .wrap_err("failed to create categories.xml")? + .write_all(cats.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write to categories.xml")?; + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + if let Err(e) = writing_directory.remove_file(format!("{map_id}.xml")) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .into_diagnostic() + .wrap_err("failed to create root wiht overlay data for pois")?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .into_diagnostic() + .wrap_err("faild to append pois to od node")?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .into_diagnostic() + .wrap_err("failed to append poi (marker) to pois")?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .into_diagnostic() + .wrap_err("failed to append a trail node to pois")?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .into_diagnostic() + .wrap_err("failed to serialize map data to string")?; + writing_directory + .create(format!("{map_id}.xml")) + .into_diagnostic() + .wrap_err("failed to create map xml file")? + .write_all(map_xml.as_bytes()) + .into_diagnostic() + .wrap_err("failed to write map data to file")?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Dir, +) -> Result<()> { + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + writing_directory + .create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir for an image: {img_path}") + })?; + } + writing_directory + .create(img_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create file for image: {img_path}"))? + .write(img) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write image bytes to file: {img_path}"))?; + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + writing_directory + .create_dir_all(parent) + .into_diagnostic() + .wrap_err_with(|| { + miette::miette!("failed to create parent dir of tbin: {tbin_path}") + })?; + } + let mut bytes: Vec = vec![]; + bytes.reserve(8 + tbin.nodes.len() * 12); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + writing_directory + .create(tbin_path.as_str()) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to create tbin file: {tbin_path}"))? + .write_all(&bytes) + .into_diagnostic() + .wrap_err_with(|| miette::miette!("failed to write tbin to path: {tbin_path}"))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &IndexMap, + parent: Node, +) -> Result<()> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node).into_diagnostic()?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(&cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.xpos, format!("{}", marker.position[0])); + ele.set_attribute(names.ypos, format!("{}", marker.position[1])); + ele.set_attribute(names.zpos, format!("{}", marker.position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<()> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .into_diagnostic() + .wrap_err("failed to append route to pois")?; + let ele = tree.element_mut(route_node).unwrap(); + + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", route.reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", route.reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", route.reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); + for pos in &route.path { + let child = tree.new_element(names.poi); + tree.append(route_node, child); + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} diff --git a/crates/joko_package_manager/src/io/mod.rs b/crates/joko_package_manager/src/io/mod.rs new file mode 100644 index 0000000..310a6bb --- /dev/null +++ b/crates/joko_package_manager/src/io/mod.rs @@ -0,0 +1,9 @@ +//! This modules primarily deals with serializing and deserializing xml data from marker packs +//! + +mod deserialize; +mod error; +mod serialize; + +pub(crate) use deserialize::{get_pack_from_taco_zip, load_pack_core_from_normalized_folder}; +pub(crate) use serialize::{save_pack_data_to_dir, save_pack_texture_to_dir}; diff --git a/crates/joko_package_manager/src/io/serialize.rs b/crates/joko_package_manager/src/io/serialize.rs new file mode 100644 index 0000000..cbd3957 --- /dev/null +++ b/crates/joko_package_manager/src/io/serialize.rs @@ -0,0 +1,260 @@ +use crate::{ + manager::{LoadedPackData, LoadedPackTexture}, + BASE64_ENGINE, +}; +use base64::Engine; +use glam::Vec3; +use indexmap::IndexMap; +use joko_package_models::{ + attributes::XotAttributeNameIDs, category::Category, marker::Marker, route::Route, trail::Trail, +}; +use miette::Result; +use std::{io::Write, path::Path}; +use tracing::info; +use uuid::Uuid; +use xot::{Element, Node, SerializeOptions, Xot}; + +/// Save the pack core as xml pack using the given directory as pack root path. +pub(crate) fn save_pack_data_to_dir( + pack_data: &LoadedPackData, + writing_directory: &Path, +) -> Result<(), String> { + // save categories + info!( + "Saving data pack {}, {} topmost categories, {} maps into {:?}", + pack_data.name, + pack_data.categories.len(), + pack_data.maps.len(), + writing_directory + ); + std::fs::create_dir_all(writing_directory).or(Err("failed to create core pack directory"))?; + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node = tree + .new_root(od) + .or(Err("failed to create new root with overlay data node"))?; + recursive_cat_serializer(&mut tree, &names, &pack_data.categories, od)?; + let cats = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .or(Err("failed to convert cats xot to string"))?; + + let target = writing_directory.join("categories.xml"); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .or(Err("failed to create categories.xml"))? + .write_all(cats.as_bytes()) + .or(Err("failed to write to categories.xml"))?; + + // save maps + for (map_id, map_data) in pack_data.maps.iter() { + if map_data.markers.is_empty() && map_data.trails.is_empty() { + let map_file_name = writing_directory.join(format!("{map_id}.xml")); + if let Err(e) = std::fs::remove_file(map_file_name) { + info!( + ?e, + map_id, "failed to remove xml file that had nothing to write to" + ); + } + } + let mut tree = Xot::new(); + let names = XotAttributeNameIDs::register_with_xot(&mut tree); + let od = tree.new_element(names.overlay_data); + let root_node: Node = tree + .new_root(od) + .or(Err("failed to create root wiht overlay data for pois"))?; + let pois = tree.new_element(names.pois); + tree.append(od, pois) + .or(Err("faild to append pois to od node"))?; + for marker in map_data.markers.values() { + let poi = tree.new_element(names.poi); + tree.append(pois, poi) + .or(Err("failed to append poi (marker) to pois"))?; + let ele = tree.element_mut(poi).unwrap(); + serialize_marker_to_element(marker, ele, &names); + } + for route_path in map_data.routes.values() { + serialize_route_to_element(&mut tree, route_path, &pois, &names)?; + } + for trail in map_data.trails.values() { + if trail.dynamic { + continue; + } + let trail_node = tree.new_element(names.trail); + tree.append(pois, trail_node) + .or(Err("failed to append a trail node to pois"))?; + let ele = tree.element_mut(trail_node).unwrap(); + serialize_trail_to_element(trail, ele, &names); + } + let map_xml = tree + .with_serialize_options(SerializeOptions { pretty: true }) + .to_string(root_node) + .or(Err("failed to serialize map data to string"))?; + let target = writing_directory.join(format!("{map_id}.xml")); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .or(Err("failed to create map xml file"))? + .write_all(map_xml.as_bytes()) + .or(Err("failed to write map data to file"))?; + } + Ok(()) +} +pub(crate) fn save_pack_texture_to_dir( + pack_texture: &LoadedPackTexture, + writing_directory: &Path, +) -> Result<(), String> { + info!( + "Saving texture pack {}, {} textures, {} tbins", + pack_texture.name, + pack_texture.textures.len(), + pack_texture.tbins.len() + ); + std::fs::create_dir_all(writing_directory).or(Err("failed to create core pack directory"))?; + // save images + for (img_path, img) in pack_texture.textures.iter() { + if let Some(parent) = img_path.parent() { + std::fs::create_dir_all(writing_directory.join(parent)).or(Err(format!( + "failed to create parent dir for an image: {img_path}" + )))?; + } + let target = writing_directory.join(img_path.as_str()); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .or(Err(format!("failed to create file for image: {img_path}")))? + .write_all(img) + .or(Err(format!( + "failed to write image bytes to file: {img_path}" + )))?; + } + // save tbins + for (tbin_path, tbin) in pack_texture.tbins.iter() { + if let Some(parent) = tbin_path.parent() { + std::fs::create_dir_all(writing_directory.join(parent)).or(Err(format!( + "failed to create parent dir of tbin: {tbin_path}" + )))?; + } + let mut bytes: Vec = + Vec::with_capacity(8 + tbin.nodes.len() * std::mem::size_of::()); + bytes.extend_from_slice(&tbin.version.to_ne_bytes()); + bytes.extend_from_slice(&tbin.map_id.to_ne_bytes()); + for node in &tbin.nodes { + let node = &node.0; + bytes.extend_from_slice(&node[0].to_ne_bytes()); + bytes.extend_from_slice(&node[1].to_ne_bytes()); + bytes.extend_from_slice(&node[2].to_ne_bytes()); + } + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(writing_directory.join(tbin_path.as_str())) + .or(Err(format!("failed to create tbin file: {tbin_path}")))? + .write_all(bytes.as_slice()) + .or(Err(format!("failed to write tbin to file: {tbin_path}")))?; + } + Ok(()) +} + +fn recursive_cat_serializer( + tree: &mut Xot, + names: &XotAttributeNameIDs, + cats: &IndexMap, + parent: Node, +) -> Result<(), String> { + for (_, cat) in cats { + let cat_node = tree.new_element(names.marker_category); + tree.append(parent, cat_node) + .or(Err("Could not insert category node"))?; + { + let ele = tree.element_mut(cat_node).unwrap(); + ele.set_attribute(names.display_name, &cat.display_name); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(cat.guid)); + // let cat_name = tree.add_name(cat_name); + ele.set_attribute(names.name, &cat.relative_category_name); + // no point in serializing default values + if !cat.default_enabled { + ele.set_attribute(names.default_enabled, "0"); + } + if cat.separator { + ele.set_attribute(names.separator, "1"); + } + cat.props.serialize_to_element(ele, names); + } + recursive_cat_serializer(tree, names, &cat.children, cat_node)?; + } + Ok(()) +} +fn serialize_trail_to_element(trail: &Trail, ele: &mut Element, names: &XotAttributeNameIDs) { + ele.set_attribute(names.guid, BASE64_ENGINE.encode(trail.guid)); + ele.set_attribute(names.category, &trail.category); + ele.set_attribute(names.map_id, format!("{}", trail.map_id)); + ele.set_attribute( + names._source_file_name, + format!("{}", trail.source_file_uuid), + ); + trail.props.serialize_to_element(ele, names); +} + +fn serialize_marker_to_element(marker: &Marker, ele: &mut Element, names: &XotAttributeNameIDs) { + let position = &marker.position.0; + ele.set_attribute(names.xpos, format!("{}", position[0])); + ele.set_attribute(names.ypos, format!("{}", position[1])); + ele.set_attribute(names.zpos, format!("{}", position[2])); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(marker.guid)); + ele.set_attribute(names.map_id, format!("{}", marker.map_id)); + ele.set_attribute(names.category, &marker.category); + ele.set_attribute( + names._source_file_name, + format!("{}", marker.source_file_uuid), + ); + marker.attrs.serialize_to_element(ele, names); +} + +fn serialize_route_to_element( + tree: &mut Xot, + route: &Route, + parent: &Node, + names: &XotAttributeNameIDs, +) -> Result<(), String> { + let route_node = tree.new_element(names.route); + tree.append(*parent, route_node) + .or(Err("failed to append route to pois"))?; + let ele = tree.element_mut(route_node).unwrap(); + + let reset_position = &route.reset_position.0; + ele.set_attribute(names.category, route.category.clone()); + ele.set_attribute(names.resetposx, format!("{}", reset_position[0])); + ele.set_attribute(names.resetposy, format!("{}", reset_position[1])); + ele.set_attribute(names.resetposz, format!("{}", reset_position[2])); + ele.set_attribute(names.reset_range, format!("{}", route.reset_range)); + ele.set_attribute(names.name, route.name.clone()); + ele.set_attribute(names.guid, BASE64_ENGINE.encode(route.guid)); + ele.set_attribute(names.map_id, format!("{}", route.map_id)); + ele.set_attribute(names.texture, "default_trail_texture.png"); + ele.set_attribute( + names._source_file_name, + format!("{}", route.source_file_uuid), + ); + for pos in &route.path { + let pos = &pos.0; + let child = tree.new_element(names.poi); + tree.append(route_node, child) + .or(Err("Could not inser child node"))?; + let child_elt = tree.element_mut(child).unwrap(); + child_elt.set_attribute(names.xpos, format!("{}", pos.x)); + child_elt.set_attribute(names.ypos, format!("{}", pos.y)); + child_elt.set_attribute(names.zpos, format!("{}", pos.z)); + //child_elt.set_attribute(names.guid, BASE64_ENGINE.encode(uuid::Uuid::new_v4())); + } + Ok(()) +} diff --git a/crates/joko_marker_format/src/io/test.xml b/crates/joko_package_manager/src/io/test.xml similarity index 100% rename from crates/joko_marker_format/src/io/test.xml rename to crates/joko_package_manager/src/io/test.xml diff --git a/crates/joko_marker_format/src/io/xmlfile_schema.xsd b/crates/joko_package_manager/src/io/xmlfile_schema.xsd similarity index 100% rename from crates/joko_marker_format/src/io/xmlfile_schema.xsd rename to crates/joko_package_manager/src/io/xmlfile_schema.xsd diff --git a/crates/joko_marker_format/src/lib.rs b/crates/joko_package_manager/src/lib.rs similarity index 71% rename from crates/joko_marker_format/src/lib.rs rename to crates/joko_package_manager/src/lib.rs index 14b1d3b..d6e90e7 100644 --- a/crates/joko_marker_format/src/lib.rs +++ b/crates/joko_package_manager/src/lib.rs @@ -4,9 +4,14 @@ pub(crate) mod io; pub(crate) mod manager; -pub(crate) mod pack; +pub mod message; + +pub use manager::{ + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, load_all_from_dir, ImportStatus, LoadedPackData, LoadedPackTexture, + PackageDataManager, PackageUIManager, +}; -pub use manager::MarkerManager; // for compile time build info like pkg version or build timestamp or git hash etc.. // shadow_rs::shadow!(build); @@ -14,7 +19,7 @@ pub use manager::MarkerManager; #[cxx::bridge(namespace = "rapid")] mod ffi { unsafe extern "C++" { - include!("joko_marker_format/vendor/rapid/rapid.hpp"); + include!("joko_package_manager/vendor/rapid/rapid.hpp"); pub fn rapid_filter(src_xml: String) -> String; } diff --git a/crates/joko_package_manager/src/manager/mod.rs b/crates/joko_package_manager/src/manager/mod.rs new file mode 100644 index 0000000..8063da8 --- /dev/null +++ b/crates/joko_package_manager/src/manager/mod.rs @@ -0,0 +1,29 @@ +//! How should the pack be stored by jokolay? +//! 1. Inside a directory called packs, we will have a separate directory for each pack. +//! 2. the name of the directory will serve as an ID for each pack. +//! 3. Inside the directory, we will have +//! 1. categories.xml -> The xml file which contains the whole category tree +//! 2. $mapid.xml -> where the $mapid is the id (u16) of a map which contains markers/trails belonging to that particular map. +//! 3. **/{.png | .trl} -> Any number of png images or trl binaries, in any location within this pack directory. + +/* +expensive: +categories being a tree with order among siblings (better to use a tree crate?) +markers/trails referring to a category via full path. +editing a category's name/path means that you have to load all the maps that refer to the category and change the reference. + +We will make not having a valid category/texture/tbin path as allowed. So, users can deal with the headache themselves. + +*/ + +mod pack; +mod package_data; +mod package_ui; + +pub use pack::import::{import_pack_from_zip_file_path, ImportStatus}; +pub use pack::loaded::{ + build_from_core, jokolay_to_editable_path, jokolay_to_extract_path, load_all_from_dir, + LoadedPackData, LoadedPackTexture, +}; +pub use package_data::PackageDataManager; +pub use package_ui::PackageUIManager; diff --git a/crates/joko_package_manager/src/manager/pack/activation.rs b/crates/joko_package_manager/src/manager/pack/activation.rs new file mode 100644 index 0000000..71f76ff --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/activation.rs @@ -0,0 +1,20 @@ +use indexmap::IndexMap; +use uuid::Uuid; + +/// This is the activation data per pack +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct ActivationData { + /// this is for markers which are global and only activate once regardless of account + pub global: IndexMap, + /// this is the activation data per character + /// for markers which trigger once per character + pub character: IndexMap>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum ActivationType { + /// clean these up when the map is changed + ReappearOnMapChange, + /// clean these up when the timestamp is reached + TimeStamp(time::OffsetDateTime), + Instance(std::net::IpAddr), +} diff --git a/crates/joko_package_manager/src/manager/pack/active.rs b/crates/joko_package_manager/src/manager/pack/active.rs new file mode 100644 index 0000000..f58194e --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/active.rs @@ -0,0 +1,299 @@ +use joko_package_models::attributes::CommonAttributes; +use jokoapi::end_point::mounts::Mount; + +use egui::TextureHandle; +use indexmap::IndexMap; +use uuid::Uuid; + +use crate::INCHES_PER_METER; +use joko_core::{ + serde_glam::{Vec2, Vec3}, + RelativePath, +}; +use joko_link_models::MumbleLink; +use joko_render_models::{ + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject, +}; + +/* +- activation data with uuids and track the latest timestamp that will be activated +- category activation data -> track and changes to propagate to markers of this map +- current active markers, which will keep track of their original marker, so as to propagate any changes easily +*/ +#[derive(Clone)] +pub struct ActiveTrail { + pub trail_object: TrailObject, + //pub texture_handle: TextureHandle, +} +/// This is an active marker. +/// It stores all the info that we need to scan every frame +#[derive(Clone)] +pub(crate) struct ActiveMarker { + /// texture id from managed textures + pub texture_id: u64, + /// owned texture handle to keep it alive + //pub _texture: TextureHandle, + /// position + pub pos: Vec3, + /// billboard must not be bigger than this size in pixels + pub max_pixel_size: f32, + /// billboard must not be smaller than this size in pixels + pub min_pixel_size: f32, + pub common_attributes: CommonAttributes, +} + +pub const BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME: f32 = 20000.0; // in game metric, for GW2, inches + +impl ActiveMarker { + pub fn get_vertices_and_texture(&self, link: &MumbleLink, z_near: f32) -> Option { + let Self { + texture_id, + pos, + common_attributes: attrs, + //_texture, + max_pixel_size, + min_pixel_size, + .. + } = self; + // let width = *width; + // let height = *height; + let texture_id = *texture_id; + let pos = *pos; + // filters + if let Some(mounts) = attrs.get_mount() { + if let Some(current) = Mount::try_from_mumble_link(link.mount) { + if !mounts.contains(current) { + return None; + } + } else { + return None; + } + } + let height_offset = attrs.get_height_offset().copied().unwrap_or(1.5); // default taco height offset + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; + let icon_size = attrs.get_icon_size().copied().unwrap_or(1.0); + let player_distance = pos.0.distance(link.player_pos.0); + let camera_distance = pos.0.distance(link.cam_pos.0); + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); + + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let color = attrs.get_color().copied().unwrap_or_default(); + /* + 1. we need to filter the markers + 1. statically - mapid, character, map_type, race, profession + 2. dynamically - achievement, behavior, mount, fade_far, cull + 3. force hide/show by user discretion + 2. for active markers (not forcibly shown), we must do the dynamic checks every frame like behavior + 3. store the state for these markers activation data, and temporary data like bounce + */ + /* + skip if: + alpha is 0.0 + achievement id/bit is done (maybe this should be at map filter level?) + behavior (activation) + cull + distance > fade_far + visibility (ingame/map/minimap) + mount + specialization + */ + if fade_far > 0.0 && player_distance > fade_far { + return None; + } + // markers are 1 meter in width/height by default + let mut pos = pos.0; + pos.y += height_offset; + let direction_to_marker = link.cam_pos.0 - pos; + let direction_to_side = direction_to_marker.normalize().cross(glam::Vec3::Y); + + let far_offset = { + let dpi = if link.dpi_scaling <= 0 { + 96.0 + } else { + link.dpi as f32 + } / 96.0; + let gw2_width = link.client_size.0.as_vec2().x / dpi; + + // offset (half width i.e. distance from center of the marker to the side of the marker) + const SIDE_OFFSET_FAR: f32 = 1.0; + // the size of the projected on to the near plane + let near_offset = SIDE_OFFSET_FAR * icon_size * (z_near / camera_distance); + // convert the near_plane width offset into pixels by multiplying the near_ffset with gw2 window width + let near_offset_in_pixels = near_offset * gw2_width; + + // we will clamp the texture width between min and max widths, and make sure that it is less than gw2 window width + let near_offset_in_pixels = near_offset_in_pixels + .clamp(*min_pixel_size, *max_pixel_size) + .min(gw2_width / 2.0); + + let near_offset_of_marker = near_offset_in_pixels / gw2_width; + near_offset_of_marker * camera_distance / z_near + }; + // let pixel_ratio = width as f32 * (distance / z_near);// (near width / far width) = near_z / far_z; + // we want to map 100 pixels to one meter in game + // we are supposed to half the width/height too, as offset from the center will be half of the whole billboard + // But, i will ignore that as that makes markers too small + let x_offset = far_offset; + let y_offset = x_offset; // seems all markers are squares + let bottom_left = MarkerVertex { + position: Vec3(pos - (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 1.0)), + alpha, + color, + fade_near_far, + }; + + let top_left = MarkerVertex { + position: Vec3(pos - (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, 0.0)), + alpha, + color, + fade_near_far, + }; + let top_right = MarkerVertex { + position: Vec3(pos + (direction_to_side * x_offset) + (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 0.0)), + alpha, + color, + fade_near_far, + }; + let bottom_right = MarkerVertex { + position: Vec3(pos + (direction_to_side * x_offset) - (glam::Vec3::Y * y_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, 1.0)), + alpha, + color, + fade_near_far, + }; + let vertices = [ + top_left, + bottom_left, + bottom_right, + bottom_right, + top_right, + top_left, + ]; + Some(MarkerObject { + vertices, + texture: texture_id, + distance: player_distance, + }) + } +} + +impl ActiveTrail { + pub fn get_vertices_and_texture( + attrs: &CommonAttributes, + positions: &[Vec3], + texture: TextureHandle, + ) -> Option { + // can't have a trail without atleast two nodes + if positions.len() < 2 { + return None; + } + let alpha = attrs.get_alpha().copied().unwrap_or(1.0); + let fade_near = attrs.get_fade_near().copied().unwrap_or(-1.0) / INCHES_PER_METER; + let fade_far = attrs + .get_fade_far() + .copied() + .unwrap_or(BILLBOARD_MAX_VISIBILITY_DISTANCE_IN_GAME) + / INCHES_PER_METER; + let fade_near_far = Vec2(glam::Vec2::new(fade_near, fade_far)); + let color = attrs.get_color().copied().unwrap_or([0u8; 4]); + // default taco width + let horizontal_offset = 20.0 / INCHES_PER_METER; + // scale it trail scale + let horizontal_offset = horizontal_offset * attrs.get_trail_scale().copied().unwrap_or(1.0); + let height = horizontal_offset * 2.0; + + let mut vertices = vec![]; + // trail mesh is split by separating different parts with a [0, 0, 0] + // we will call each separate trail mesh as a "strip" of trail. + // each strip should *almost* act as an independent trail, but they all are drawn at the same time with the same parameters. + for strip in positions.split(|&v| v.0 == glam::Vec3::ZERO) { + let mut y_offset = 1.0; + for two_positions in strip.windows(2) { + let first = two_positions[0].0; + let second = two_positions[1].0; + // right side of the vector from first to second + let right_side = (second - first) + .normalize() + .cross(glam::Vec3::Y) + .normalize(); + + let new_offset = (-1.0 * (first.distance(second) / height)) + y_offset; + let first_left = MarkerVertex { + position: Vec3(first - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, y_offset)), + alpha, + color, + fade_near_far, + }; + let first_right = MarkerVertex { + position: Vec3(first + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, y_offset)), + alpha, + color, + fade_near_far, + }; + let second_left = MarkerVertex { + position: Vec3(second - (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(0.0, new_offset)), + alpha, + color, + fade_near_far, + }; + let second_right = MarkerVertex { + position: Vec3(second + (right_side * horizontal_offset)), + texture_coordinates: Vec2(glam::vec2(1.0, new_offset)), + alpha, + color, + fade_near_far, + }; + y_offset = if new_offset.is_sign_positive() { + new_offset + } else { + 1.0 - new_offset.fract().abs() + }; + vertices.extend([ + second_left, + first_left, + first_right, + first_right, + second_right, + second_left, + ]); + } + } + + Some(ActiveTrail { + trail_object: TrailObject { + vertices: vertices.into(), + texture: match texture.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }, + }, + //texture_handle: texture, + }) + } +} + +#[derive(Default, Clone)] +pub(crate) struct CurrentMapData { + /// the map to which the current map data belongs to + //pub map_id: u32, + //pub active_elements: HashSet, + /// The textures that are being used by the markers, so must be kept alive by this hashmap + pub active_textures: IndexMap, + /// The key is the index of the marker in the map markers + /// Their position in the map markers serves as their "id" as uuids can be duplicates. + pub active_markers: IndexMap, + /// The key is the position/index of this trail in the map trails. same as markers + pub active_trails: IndexMap, +} diff --git a/crates/joko_package_manager/src/manager/pack/category_selection.rs b/crates/joko_package_manager/src/manager/pack/category_selection.rs new file mode 100644 index 0000000..1d77ee3 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/category_selection.rs @@ -0,0 +1,295 @@ +use indexmap::IndexMap; +use joko_component_models::{to_data, ComponentMessage}; +use joko_package_models::{ + attributes::CommonAttributes, + category::Category, + package::{PackCore, PackageImportReport}, +}; +use std::collections::{HashMap, HashSet}; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::message::MessageToPackageBack; + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct CategorySelection { + //#[serde(skip)] + pub uuid: Uuid, //FIXME: if not present, one MUST fix it or mark the current import as a failure and reset all information + #[serde(skip)] + pub parent: Option, + pub is_selected: bool, //has it been selected in configuration to be displayed + pub is_active: bool, //currently being displayed (i.e.: active) + pub separator: bool, + pub display_name: String, + pub children: IndexMap, +} + +pub struct SelectedCategoryManager { + data: IndexMap, +} +impl<'a> SelectedCategoryManager { + pub fn new( + selected_categories: &IndexMap, + categories: &IndexMap, + ) -> Self { + let mut list_of_enabled_categories = Default::default(); + CategorySelection::get_list_of_enabled_categories( + selected_categories, + categories, + &mut list_of_enabled_categories, + &Default::default(), + ); + + Self { + data: list_of_enabled_categories, + } + } + #[allow(dead_code)] + pub fn cloned_data(&self) -> IndexMap { + self.data.clone() + } + pub fn is_selected(&self, category: &Uuid) -> bool { + self.data.contains_key(category) + } + pub fn get(&self, key: &Uuid) -> &CommonAttributes { + self.data.get(key).unwrap() + } + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.data.len() + } + pub fn keys(&'a self) -> indexmap::map::Keys<'a, Uuid, CommonAttributes> { + self.data.keys() + } +} + +impl CategorySelection { + pub fn default_from_pack_core(pack: &PackCore) -> IndexMap { + let mut selectable_categories = IndexMap::new(); + Self::recursive_create_selectable_categories(&mut selectable_categories, &pack.categories); + selectable_categories + } + fn get_list_of_enabled_categories( + selection: &IndexMap, + categories: &IndexMap, + list_of_enabled_categories: &mut IndexMap, + parent_common_attributes: &CommonAttributes, + ) { + for (_, cat) in categories { + if let Some(selectable_category) = selection.get(&cat.relative_category_name) { + if !selectable_category.is_selected { + continue; + } + let mut common_attributes = cat.props.clone(); + common_attributes.inherit_if_attr_none(parent_common_attributes); + Self::get_list_of_enabled_categories( + &selectable_category.children, + &cat.children, + list_of_enabled_categories, + &common_attributes, + ); + list_of_enabled_categories.insert(cat.guid, common_attributes); + } + } + } + pub fn get( + selection: &mut IndexMap, + uuid: Uuid, + ) -> Option<&mut CategorySelection> { + if selection.is_empty() { + None + } else { + for cat in selection.values_mut() { + if cat.uuid == uuid { + return Some(cat); + } + if let Some(res) = Self::get(&mut cat.children, uuid) { + return Some(res); + } + } + None + } + } + #[allow(dead_code)] + pub fn recursive_populate_guids( + selection: &mut IndexMap, + entities_parents: &mut HashMap, + parent_uuid: Option, + ) { + for cat in selection.values_mut() { + if cat.uuid.is_nil() { + cat.uuid = Uuid::new_v4(); + } + cat.parent = parent_uuid; + Self::recursive_populate_guids(&mut cat.children, entities_parents, Some(cat.uuid)); + if let Some(parent_uuid) = parent_uuid { + entities_parents.insert(cat.uuid, parent_uuid); + } + //assert!(cat.guid.len() > 0); + } + } + fn recursive_create_selectable_categories( + selectable_categories: &mut IndexMap, + cats: &IndexMap, + ) { + for (_, cat) in cats.iter() { + if !selectable_categories.contains_key(&cat.relative_category_name) { + let to_insert = CategorySelection { + uuid: cat.guid, + parent: cat.parent, + is_selected: cat.default_enabled, + is_active: !cat.separator, //by default separators are not considered active since they contain nothing + separator: cat.separator, + display_name: cat.display_name.clone(), + children: Default::default(), + }; + //println!("recursive_create_category_selection {} {}", cat_name, to_insert.uuid); + selectable_categories.insert(cat.relative_category_name.clone(), to_insert); + } + let s = selectable_categories + .get_mut(&cat.relative_category_name) + .unwrap(); + Self::recursive_create_selectable_categories(&mut s.children, &cat.children); + } + } + + pub fn recursive_set( + selection: &mut IndexMap, + uuid: Uuid, + status: bool, + ) -> bool { + if selection.is_empty() { + false + } else { + for cat in selection.values_mut() { + if cat.separator { + continue; + } + if cat.uuid == uuid { + cat.is_selected = status; + return true; + } + if Self::recursive_set(&mut cat.children, uuid, status) { + return true; + } + } + false + } + } + pub fn recursive_set_all(selection: &mut IndexMap, status: bool) { + if selection.is_empty() { + return; + } + for cat in selection.values_mut() { + if cat.separator { + continue; + } + cat.is_selected = status; + Self::recursive_set_all(&mut cat.children, status); + } + } + + pub fn recursive_update_active_categories( + selection: &mut IndexMap, + active_elements: &HashSet, + ) -> bool { + let mut is_active = false; + if selection.is_empty() { + //println!("recursive_update_active_categories is_empty"); + return is_active; + } + for cat in selection.values_mut() { + cat.is_active = active_elements.contains(&cat.uuid) + || Self::recursive_update_active_categories(&mut cat.children, active_elements); + if cat.is_active { + is_active = true; + } + } + is_active + } + + fn context_menu( + back_end_notifier: &tokio::sync::mpsc::Sender, + cs: &mut CategorySelection, + ui: &mut egui::Ui, + ) { + if ui.button("Activate branch").clicked() { + cs.is_selected = true; + CategorySelection::recursive_set_all(&mut cs.children, true); + let _ = back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, true), + )); + ui.close_menu(); + } + if ui.button("Deactivate branch").clicked() { + CategorySelection::recursive_set_all(&mut cs.children, false); + cs.is_selected = false; + let _ = back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationBranchStatusChange(cs.uuid, false), + )); + ui.close_menu(); + } + } + + pub fn recursive_selection_ui( + back_end_notifier: &tokio::sync::mpsc::Sender, + selection: &mut IndexMap, + ui: &mut egui::Ui, + is_dirty: &mut bool, + show_only_active: bool, + import_quality_report: &PackageImportReport, + ) { + if selection.is_empty() { + return; + } + egui::ScrollArea::vertical().show(ui, |ui| { + for cat in selection.values_mut() { + if !cat.is_active && show_only_active && !cat.separator { + continue; + } + ui.horizontal(|ui| { + if cat.separator { + ui.add_space(3.0); + } else { + let cb = ui.checkbox(&mut cat.is_selected, ""); + if cb.changed() { + let _ = back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationElementStatusChange( + cat.uuid, + cat.is_selected, + ), + )); + *is_dirty = true; + } + } + //println!("Look for {} {} among displayed elements {}", name, cat.uuid, on_screen.contains(&cat.uuid)); + let color = if import_quality_report.is_category_discovered_late(cat.uuid) { + egui::Color32::LIGHT_RED + } else if cat.is_active { + egui::Color32::LIGHT_GREEN + } else { + egui::Color32::GRAY + }; + let label = egui::RichText::new(&cat.display_name).color(color); + if cat.children.is_empty() { + ui.label(label); + } else { + ui.menu_button(label, |ui: &mut egui::Ui| { + Self::recursive_selection_ui( + back_end_notifier, + &mut cat.children, + ui, + is_dirty, + show_only_active, + import_quality_report, + ); + }) + .response + .context_menu(|ui| Self::context_menu(back_end_notifier, cat, ui)); + } + }); + } + }); + } +} diff --git a/crates/joko_package_manager/src/manager/pack/dirty.rs b/crates/joko_package_manager/src/manager/pack/dirty.rs new file mode 100644 index 0000000..f7fd509 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/dirty.rs @@ -0,0 +1,28 @@ +use ordered_hash_map::OrderedHashSet; + +use joko_core::RelativePath; + +#[derive(Debug, Default, Clone)] +pub(crate) struct DirtyMarker { + pub all: bool, + /// whether categories need to be saved + pub categories: bool, + /// whether selected categories needs to be saved + pub selected_categories: bool, + /// Whether any mapdata needs saving + pub map: OrderedHashSet, + /// whether any texture needs saving + pub texture: OrderedHashSet, + /// whether any tbin needs saving + pub tbin: OrderedHashSet, +} + +impl DirtyMarker { + pub fn is_dirty(&self) -> bool { + self.categories + || self.selected_categories + || !self.map.is_empty() + || !self.texture.is_empty() + || !self.tbin.is_empty() + } +} diff --git a/crates/joko_package_manager/src/manager/pack/entry.rs b/crates/joko_package_manager/src/manager/pack/entry.rs new file mode 100644 index 0000000..ad78681 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/entry.rs @@ -0,0 +1,6 @@ +#[derive(Debug)] +pub struct PackEntry { + pub url: url::Url, + pub description: String, +} + diff --git a/crates/joko_package_manager/src/manager/pack/file_selection.rs b/crates/joko_package_manager/src/manager/pack/file_selection.rs new file mode 100644 index 0000000..c3ddc03 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/file_selection.rs @@ -0,0 +1,47 @@ +use std::collections::BTreeMap; + +use uuid::Uuid; + +pub struct SelectedFileManager { + data: BTreeMap, +} +impl SelectedFileManager { + pub fn new( + selected_files: &BTreeMap, + pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + ) -> Self { + let mut list_of_enabled_files: BTreeMap = Default::default(); + SelectedFileManager::recursive_get_full_names( + selected_files, + pack_source_files, + currently_used_files, + &mut list_of_enabled_files, + ); + Self { + data: list_of_enabled_files, + } + } + fn recursive_get_full_names( + _selected_files: &BTreeMap, + _pack_source_files: &BTreeMap, + currently_used_files: &BTreeMap, + list_of_enabled_files: &mut BTreeMap, + ) { + for (key, v) in currently_used_files.iter() { + list_of_enabled_files.insert(*key, *v); + } + } + #[allow(dead_code)] + pub fn cloned_data(&self) -> BTreeMap { + self.data.clone() + } + pub fn is_selected(&self, source_file_uuid: &Uuid) -> bool { + let default = false; + self.data.is_empty() || *self.data.get(source_file_uuid).unwrap_or(&default) + } + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.data.len() + } +} diff --git a/crates/joko_package_manager/src/manager/pack/import.rs b/crates/joko_package_manager/src/manager/pack/import.rs new file mode 100644 index 0000000..d8843b2 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/import.rs @@ -0,0 +1,31 @@ +use joko_package_models::package::PackCore; +use serde::{Deserialize, Serialize}; +use tracing::info; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub enum ImportStatus { + #[default] + UnInitialized, + WaitingForFileChooser, + LoadingPack(std::path::PathBuf), + WaitingLoading(std::path::PathBuf), + PackDone(String, PackCore, bool), + WaitingForSave, + PackError(String), +} + +pub fn import_pack_from_zip_file_path( + file_path: std::path::PathBuf, + extract_temporary_path: &std::path::PathBuf, +) -> Result<(String, PackCore), String> { + info!("starting to get pack from taco"); + crate::io::get_pack_from_taco_zip(file_path.clone(), extract_temporary_path).map(|pack| { + ( + file_path + .file_name() + .map(|ostr| ostr.to_string_lossy().to_string()) + .unwrap_or_default(), + pack, + ) + }) +} diff --git a/crates/joko_package_manager/src/manager/pack/list.rs b/crates/joko_package_manager/src/manager/pack/list.rs new file mode 100644 index 0000000..499fe2f --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/list.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Default)] +pub struct PackList { + pub packs: BTreeMap, +} + + diff --git a/crates/joko_package_manager/src/manager/pack/loaded.rs b/crates/joko_package_manager/src/manager/pack/loaded.rs new file mode 100644 index 0000000..d2fb175 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/loaded.rs @@ -0,0 +1,997 @@ +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use indexmap::IndexMap; +use joko_component_models::{to_data, ComponentMessage}; +use joko_package_models::{ + attributes::{Behavior, CommonAttributes}, + category::Category, + map::MapData, + package::{PackCore, PackageImportReport}, + trail::TBin, +}; + +use egui::{ColorImage, TextureHandle}; +use image::EncodableLayout; +use serde::{Deserialize, Serialize}; +use tracing::{debug, error, info, info_span, trace}; +use uuid::Uuid; + +use crate::message::MessageToPackageUI; +use crate::{ + io::{load_pack_core_from_normalized_folder, save_pack_data_to_dir, save_pack_texture_to_dir}, + manager::{ + pack::{category_selection::SelectedCategoryManager, file_selection::SelectedFileManager}, + package_data::EXTRACT_DIRECTORY_NAME, + }, +}; +use joko_core::{ + serde_glam::Vec3, + task::{AsyncTask, AsyncTaskGuard}, + RelativePath, +}; +use joko_link_models::MumbleLink; +use joko_render_models::{messages::MessageToRenderer, trail::TrailObject}; +use miette::Result; + +use super::activation::{ActivationData, ActivationType}; +use super::active::{ActiveMarker, ActiveTrail, CurrentMapData}; +use crate::manager::pack::category_selection::CategorySelection; +use crate::manager::package_data::{ + EDITABLE_PACKAGE_NAME, LOCAL_EXPANDED_PACKAGE_NAME, PACKAGES_DIRECTORY_NAME, + PACKAGE_MANAGER_DIRECTORY_NAME, +}; + +type ImportAllTriplet = ( + BTreeMap, + BTreeMap, + BTreeMap, +); +type ImportTriplet = (LoadedPackData, LoadedPackTexture, PackageImportReport); + +//TODO: separate in front and back tasks +pub(crate) struct PackTasks { + //an object that can handle such tasks should be passed as argument of any function that may required an async action + save_texture_task: AsyncTask>, + save_data_task: AsyncTask>, + save_report_task: AsyncTask<(PathBuf, PackageImportReport), Result<(), String>>, + load_all_packs_task: AsyncTask>, +} + +//TOOD: move the LoadedPackData & LoadedPackTexture to joko_package_models ? The problem is about the messages to be sent. Where to put them ? and at the cost of which dependancy ? +#[derive(Clone)] +pub struct LoadedPackData { + pub name: String, + pub uuid: Uuid, + pub path: PathBuf, + /// The actual xml pack. + //pub core: PackCore, + pub categories: IndexMap, + pub all_categories: HashMap, + pub source_files: BTreeMap, //TODO: have a reference containing pack name and maybe even path inside the package + pub maps: HashMap, + selected_files: BTreeMap, + _is_dirty: bool, //there was an edition in the package itself + + // loca copy in the data side of what is exposed in UI + selectable_categories: IndexMap, + pub entities_parents: HashMap, + activation_data: ActivationData, + active_elements: HashSet, //keep track of which elements are active +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct LoadedPackTexture { + //TODO: there is a need for a late loading of texture to avoid transmitting them (serialize) + pub name: String, + pub uuid: Uuid, + /// The directory inside which the pack data is stored + /// There should be a subdirectory called `core` which stores the pack core + /// Files related to Jokolay thought will have to be stored directly inside this directory, to keep the xml subdirectory clean. + /// eg: Active categories, activation data etc.. + //pub dir: Arc, + pub path: std::path::PathBuf, + pub source_files: BTreeMap, + pub tbins: HashMap, + pub textures: HashMap>, + + /// The selection of categories which are "enabled" and markers belonging to these may be rendered + selectable_categories: IndexMap, + #[serde(skip)] + current_map_data: CurrentMapData, + activation_data: ActivationData, + //active_elements: HashSet, //which are the active elements (loaded) + _is_dirty: bool, +} + +impl PackTasks { + pub fn new() -> Self { + Self { + save_texture_task: AsyncTaskGuard::new(PackTasks::async_save_texture), + save_data_task: AsyncTaskGuard::new(PackTasks::async_save_data), + save_report_task: AsyncTaskGuard::new(PackTasks::async_save_report), + load_all_packs_task: AsyncTaskGuard::new(load_all_from_dir), + } + } + pub fn is_running(&self) -> bool { + self.save_texture_task.lock().unwrap().is_running() + || self.save_data_task.lock().unwrap().is_running() + } + pub fn count(&self) -> i32 { + self.save_texture_task.lock().unwrap().count() + + self.save_data_task.lock().unwrap().count() + + self.load_all_packs_task.lock().unwrap().count() + } + + pub fn save_texture(&self, texture_pack: &mut LoadedPackTexture, status: bool) { + //saved on load, or change of list of what to display + if status { + std::mem::take(&mut texture_pack._is_dirty); + let t = self.save_texture_task.lock().unwrap(); + let _ = t.send(texture_pack.clone()); + t.recv().unwrap().unwrap(); //expose errors of the save function call. If it had an error, we shall crash. + } + } + + pub fn save_data(&self, data_pack: &mut LoadedPackData, status: bool) { + if status { + std::mem::take(&mut data_pack._is_dirty); + let _ = self.save_data_task.lock().unwrap().send(data_pack.clone()); + } + } + pub fn save_report(&self, target_dir: PathBuf, report: PackageImportReport, status: bool) { + if status { + let _ = self + .save_report_task + .lock() + .unwrap() + .send((target_dir, report)); + } + } + pub fn load_all_packs(&self, root_path: std::path::PathBuf) { + match self.load_all_packs_task.lock().unwrap().send(root_path) { + Ok(_) => {} + Err(e) => error!(?e), + } + } + pub fn wait_for_load_all_packs(&self) -> Result { + self.load_all_packs_task.lock().unwrap().recv().unwrap() + } + + #[allow(dead_code, unused)] + fn change_map( + &self, + pack: &mut LoadedPackData, + b2u_sender: &std::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + ) { + //TODO + unimplemented!("PackTask::change_map is not implemented"); + } + + fn async_save_texture(pack_texture: LoadedPackTexture) -> Result<(), String> { + trace!("Save texture package {:?}", pack_texture.path); + + match serde_json::to_string_pretty(&pack_texture.selectable_categories) { + Ok(cs_json) => { + let target = pack_texture + .path + .join(LoadedPackData::CATEGORY_SELECTION_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open category selection data file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write category selection data to disk"); + } + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + match serde_json::to_string_pretty(&pack_texture.activation_data) { + Ok(ad_json) => { + let target = pack_texture + .path + .join(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open activation data file on disk") + .write_all(ad_json.as_bytes()) + .expect("failed to write activation data to disk"); + } + Err(e) => { + error!(?e, "failed to serialize activation"); + } + } + let target = pack_texture.path.join(LoadedPackData::CORE_PACK_DIR_NAME); + save_pack_texture_to_dir(&pack_texture, &target) + } + + fn async_save_data(pack_data: LoadedPackData) -> Result<(), String> { + trace!("Save data package {:?}", pack_data.path); + let target = pack_data.path.join(LoadedPackData::CORE_PACK_DIR_NAME); + save_pack_data_to_dir(&pack_data, &target)?; + Ok(()) + } + + fn async_save_report(input: (PathBuf, PackageImportReport)) -> Result<(), String> { + let (writing_directory, report) = input; + trace!("Save report package {:?}", writing_directory); + match serde_json::to_string_pretty(&report) { + Ok(cs_json) => { + let target = writing_directory.join(PackageImportReport::REPORT_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open import quality report file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write import quality report to disk"); + } + Err(e) => { + error!(?e, "failed to serialize import quality report"); + } + } + Ok(()) + } +} + +impl LoadedPackData { + const CORE_PACK_DIR_NAME: &'static str = "core"; + const CATEGORY_SELECTION_FILE_NAME: &'static str = "cats.json"; + + fn load_selectable_categories( + path: &Path, + pack: &PackCore, + ) -> IndexMap { + //FIXME: we need to patch those categories from the one in the files + let target = path.join(Self::CATEGORY_SELECTION_FILE_NAME); + trace!("load_selectable_categories open {:?}", target); + let mut cd_json = String::new(); + (if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(&target) { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize category data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_else(|| { + let cs = CategorySelection::default_from_pack_core(pack); + match serde_json::to_string_pretty(&cs) { + Ok(cs_json) => { + let target = path.join(Self::CATEGORY_SELECTION_FILE_NAME); + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(target) + .expect("failed to open category file on disk") + .write_all(cs_json.as_bytes()) + .expect("failed to write category data to disk"); + } + Err(e) => { + error!(?e, "failed to serialize cat selection"); + } + } + cs + }) + } + + fn load_import_report(pack_path: &Path) -> Option { + let report_file_name = pack_path.join(PackageImportReport::REPORT_FILE_NAME); + let mut cd_json = String::new(); + (if let Ok(mut file) = std::fs::OpenOptions::new() + .read(true) + .open(report_file_name) + { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize import report"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of import report"); + None + } + } + } else { + None + }) + .flatten() + } + pub fn load_from_dir(name: String, path: PathBuf) -> Result { + let core_path = path.join(Self::CORE_PACK_DIR_NAME); + if !core_path.exists() { + return Err("pack core doesn't exist in this pack".to_string()); + } + let start = std::time::SystemTime::now(); + let import_report = LoadedPackData::load_import_report(&path); + let core = load_pack_core_from_normalized_folder(&core_path, import_report) + .or(Err("failed to load pack from dir"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); + + //FIXME: Since categories have randomly generated uuids (and not saved), one need to build from those, all the time. + //let selectable_categories = CategorySelection::default_from_pack_core(&core); + let selectable_categories = Self::load_selectable_categories(&path, &core); + + Ok(LoadedPackData { + name, + uuid: core.uuid, + path, + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.active_source_files, + _is_dirty: false, + active_elements: Default::default(), + activation_data: Default::default(), + selectable_categories, + entities_parents: core.entities_parents, + }) + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) -> bool { + if CategorySelection::recursive_set(&mut self.selectable_categories, uuid, status) { + self._is_dirty = true; + true + } else { + false + } + } + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) -> bool { + if let Some(cs) = CategorySelection::get(&mut self.selectable_categories, uuid) { + cs.is_selected = status; + self._is_dirty = true; + if CategorySelection::recursive_set(&mut cs.children, uuid, status) { + return true; + } + } + false + } + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn tick( + &mut self, + front_end_notifier: &tokio::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + _tasks: &PackTasks, + next_loaded: &mut HashSet, + ) { + //since the loading of texture is lazy, there is no problem when calling this regularly + //tasks.change_map(self, b2u_sender, link, currently_used_files); + let mut active_elements: HashSet = Default::default(); + self.on_map_changed( + front_end_notifier, + link, + currently_used_files, + &mut active_elements, + ); + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::PackageActiveElements(self.uuid, active_elements.clone()), + )); + self.active_elements.clone_from(&active_elements); + next_loaded.extend(active_elements); + } + + fn on_map_changed( + &mut self, + front_end_notifier: &tokio::sync::mpsc::Sender, + link: &MumbleLink, + currently_used_files: &BTreeMap, + active_elements: &mut HashSet, + ) { + info!(link.map_id, "current map data is updated. {}", self.name); + if link.map_id == 0 { + info!("No map do not do anything"); + return; + } + debug!( + "Start building SelectedCategoryManager {}", + self.selectable_categories.len() + ); + let selected_categories_manager = + SelectedCategoryManager::new(&self.selectable_categories, &self.categories); + + debug!("Start building SelectedFileManager"); + let selected_files_manager = SelectedFileManager::new( + &self.selected_files, + &self.source_files, + currently_used_files, + ); + + debug!("Start loading markers"); + let mut nb_markers_attempt = 0; + let mut nb_markers_loaded = 0; + for marker in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .markers + .values() + { + nb_markers_attempt += 1; + if selected_files_manager.is_selected(&marker.source_file_uuid) { + active_elements.insert(marker.guid); + active_elements.insert(marker.parent); + if selected_categories_manager.is_selected(&marker.parent) { + let category_attributes = selected_categories_manager.get(&marker.parent); + let mut common_attributes = marker.attrs.clone(); // why a clone ? + common_attributes.inherit_if_attr_none(category_attributes); + let key = &marker.guid; + if let Some(behavior) = common_attributes.get_behavior() { + if match behavior { + Behavior::AlwaysVisible => false, + Behavior::ReappearOnMapChange + | Behavior::ReappearOnDailyReset + | Behavior::OnlyVisibleBeforeActivation + | Behavior::ReappearAfterTimer + | Behavior::ReappearOnMapReset + | Behavior::WeeklyReset => { + self.activation_data.global.contains_key(key) + } + Behavior::OncePerInstance => self + .activation_data + .global + .get(key) + .map(|a| match a { + ActivationType::Instance(a) => a == &link.server_address, + _ => false, + }) + .unwrap_or_default(), + Behavior::DailyPerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| a.contains_key(key)) + .unwrap_or_default(), + Behavior::OncePerInstancePerChar => self + .activation_data + .character + .get(&link.name) + .map(|a| { + a.get(key) + .map(|a| match a { + ActivationType::Instance(a) => { + a == &link.server_address + } + _ => false, + }) + .unwrap_or_default() + }) + .unwrap_or_default(), + Behavior::WvWObjective => { + false // ??? + } + } { + continue; + } + } + if let Some(tex_path) = common_attributes.get_icon_file() { + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::MarkerTexture( + self.uuid, + tex_path.clone(), + marker.guid, + marker.position, + common_attributes, + ), + )); + } else { + debug!("no texture attribute on this marker"); + } + + nb_markers_loaded += 1; + } else { + debug!( + "category {} = {} is not enabled", + marker.category, marker.parent + ); + } + } + } + + debug!("Start loading trails"); + let mut nb_trails_attempt = 0; + let mut nb_trails_loaded = 0; + for trail in self + .maps + .get(&link.map_id) + .unwrap_or(&Default::default()) + .trails + .values() + { + nb_trails_attempt += 1; + if selected_files_manager.is_selected(&trail.source_file_uuid) { + active_elements.insert(trail.guid); + active_elements.insert(trail.parent); + if selected_categories_manager.is_selected(&trail.parent) { + let category_attributes = selected_categories_manager.get(&trail.parent); + let mut common_attributes = trail.props.clone(); + common_attributes.inherit_if_attr_none(category_attributes); + if let Some(tex_path) = common_attributes.get_texture() { + let _ = front_end_notifier.blocking_send(to_data( + MessageToPackageUI::TrailTexture( + self.uuid, + tex_path.clone(), + trail.guid, + common_attributes, + ), + )); + } else { + debug!("no texture attribute on this trail"); + } + nb_trails_loaded += 1; + } else { + debug!( + "category {} = {} is not enabled", + trail.category, trail.parent + ); + } + } + } + info!( + "Load notifications for {} on map {}: {}/{} markers and {}/{} trails", + self.name, + link.map_id, + nb_markers_loaded, + nb_markers_attempt, + nb_trails_loaded, + nb_trails_attempt + ); + debug!( + "active categories: {:?}", + selected_categories_manager.keys() + ); + } +} + +impl LoadedPackTexture { + const ACTIVATION_DATA_FILE_NAME: &'static str = "activation.json"; + + pub fn category_set_all(&mut self, status: bool) { + CategorySelection::recursive_set_all(&mut self.selectable_categories, status); + self._is_dirty = true; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + CategorySelection::recursive_update_active_categories( + &mut self.selectable_categories, + active_elements, + ); + } + pub fn category_sub_menu( + &mut self, + back_end_notifier: &tokio::sync::mpsc::Sender, + ui: &mut egui::Ui, + show_only_active: bool, + import_quality_report: &PackageImportReport, + ) { + //it is important to generate a new id each time to avoid collision + ui.push_id(ui.next_auto_id(), |ui| { + CategorySelection::recursive_selection_ui( + back_end_notifier, + &mut self.selectable_categories, + ui, + &mut self._is_dirty, + show_only_active, + import_quality_report, + ); + }); + } + + pub fn is_dirty(&self) -> bool { + self._is_dirty + } + pub(crate) fn tick( + &mut self, + renderer_notifier: &tokio::sync::mpsc::Sender, + _timestamp: f64, + link: &MumbleLink, + //next_on_screen: &mut HashSet, + z_near: f32, + _tasks: &PackTasks, + ) -> Result<()> { + tracing::trace!( + "LoadedPackTexture.tick: {} {} {}", + self.name, + self.current_map_data.active_markers.len(), + self.current_map_data.active_trails.len(), + ); + let mut marker_objects = Vec::new(); + for marker in self.current_map_data.active_markers.values() { + if let Some(mo) = marker.get_vertices_and_texture(link, z_near) { + marker_objects.push(mo); + } + } + tracing::trace!( + "LoadedPackTexture.tick: {}, markers {}", + self.name, + marker_objects.len() + ); + let _ = renderer_notifier + .blocking_send(to_data(MessageToRenderer::BulkMarkerObject(marker_objects))); + let mut trail_objects = Vec::new(); + for trail in self.current_map_data.active_trails.values() { + trail_objects.push(TrailObject { + vertices: trail.trail_object.vertices.clone(), + texture: trail.trail_object.texture, + }); + //next_on_screen.insert(*uuid); + } + tracing::trace!( + "LoadedPackTexture.tick: {}, trails {}", + self.name, + trail_objects.len() + ); + let _ = renderer_notifier + .blocking_send(to_data(MessageToRenderer::BulkTrailObject(trail_objects))); + Ok(()) + } + + pub fn clear(&mut self) { + info!( + "clear {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.active_markers.len(), + self.current_map_data.active_trails.len() + ); + self.current_map_data.active_markers.clear(); + self.current_map_data.active_trails.clear(); + } + /*pub fn swap(&mut self) { + info!( + "swap {} to display {} textures, {} markers, {} trails", + self.name, + self.current_map_data.active_textures.len(), + self.current_map_data.wip_markers.len(), + self.current_map_data.wip_trails.len() + ); + self.current_map_data.active_markers = + std::mem::take(&mut self.current_map_data.wip_markers); + self.current_map_data.active_trails = std::mem::take(&mut self.current_map_data.wip_trails); + }*/ + + pub fn load_marker_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + + //TODO: insertion must happen inside the UI => egui_context should never be transmitted on a tick() + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + error!(%tex_path, "failed to find this icon texture"); + } + } + let th = self + .current_map_data + .active_textures + .get(tex_path) + .unwrap_or(default_tex_id); + let texture_id = match th.id() { + egui::TextureId::Managed(i) => i, + egui::TextureId::User(_) => todo!(), + }; + + let max_pixel_size = common_attributes.get_max_size().copied().unwrap_or(2048.0); // default taco max size + let min_pixel_size = common_attributes.get_min_size().copied().unwrap_or(5.0); // default taco min size + let am = ActiveMarker { + texture_id, + //_texture: th.clone(), + common_attributes, + pos: position, + max_pixel_size, + min_pixel_size, + }; + self.current_map_data.active_markers.insert(marker_uuid, am); + } + + pub fn load_trail_texture( + &mut self, + egui_context: &egui::Context, + default_tex_id: &TextureHandle, + tex_path: &RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if !self.current_map_data.active_textures.contains_key(tex_path) { + if let Some(tex) = self.textures.get(tex_path) { + let img = image::load_from_memory(tex).unwrap(); + self.current_map_data.active_textures.insert( + tex_path.clone(), + egui_context.load_texture( + tex_path.as_str(), + ColorImage::from_rgba_unmultiplied( + [img.width() as _, img.height() as _], + img.into_rgba8().as_bytes(), + ), + Default::default(), + ), + ); + } else { + error!(%tex_path, "failed to find this trail texture"); + } + } else { + trace!("Trail texture already loaded {:?}", tex_path); + } + let texture_path = common_attributes.get_texture(); + let th = texture_path + .and_then(|path| self.current_map_data.active_textures.get(path)) + .unwrap_or(default_tex_id); + + let tbin_path = if let Some(tbin) = common_attributes.get_trail_data() { + debug!(?texture_path, "tbin path"); + tbin + } else { + info!(?trail_uuid, "missing tbin path"); + return; + }; + let tbin = if let Some(tbin) = self.tbins.get(tbin_path) { + tbin + } else { + info!(%tbin_path, "failed to find tbin"); + return; + }; + if let Some(active_trail) = + ActiveTrail::get_vertices_and_texture(&common_attributes, &tbin.nodes, th.clone()) + { + self.current_map_data + .active_trails + .insert(trail_uuid, active_trail); + } else { + info!("Cannot display {texture_path:?}") + } + } +} + +pub fn jokolay_to_editable_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(EDITABLE_PACKAGE_NAME) +} + +pub fn jokolay_to_extract_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(EXTRACT_DIRECTORY_NAME) +} + +pub fn jokolay_to_marker_path(jokolay_path: &std::path::Path) -> std::path::PathBuf { + jokolay_path + .join(PACKAGE_MANAGER_DIRECTORY_NAME) + .join(PACKAGES_DIRECTORY_NAME) +} + +/* +pub fn jokolay_to_marker_dir(jokolay_dir: &Arc) -> Result { + jokolay_dir + .create_dir_all(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to create marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; + let marker_manager_dir = jokolay_dir + .open_dir(PACKAGE_MANAGER_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to open marker manager directory {}", + PACKAGE_MANAGER_DIRECTORY_NAME + ))?; + + marker_manager_dir + .create_dir_all(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to create marker packs directory {}", + PACKAGES_DIRECTORY_NAME + ))?; + let marker_packs_dir = marker_manager_dir + .open_dir(PACKAGES_DIRECTORY_NAME) + .into_diagnostic() + .wrap_err(format!( + "failed to open marker packs dir {}", + PACKAGES_DIRECTORY_NAME + ))?; + + marker_manager_dir + .create_dir_all(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to create editable package directory")?; + let editable_package = marker_manager_dir + .open_dir(EDITABLE_PACKAGE_NAME) + .into_diagnostic() + .wrap_err("failed to open editable package directory")?; + + editable_package + .create_dir_all("data") + .into_diagnostic() + .wrap_err("failed to create data folder for editable package")?; + + Ok(marker_packs_dir) +}*/ + +pub fn load_all_from_dir(root_path: std::path::PathBuf) -> Result { + trace!("load_all_from_dir {:?}", root_path); + let marker_packs_path = jokolay_to_marker_path(&root_path); + let mut data_packs: BTreeMap = Default::default(); + let mut texture_packs: BTreeMap = Default::default(); + let mut report_packs: BTreeMap = Default::default(); + + for entry in + std::fs::read_dir(marker_packs_path).or(Err("failed to get entries of marker packs dir"))? + { + let entry = entry.or(Err("Failed to read packages directory"))?; + if entry + .metadata() + .or(Err("Could not read folder metadata"))? + .is_file() + { + continue; + } + let name = entry.file_name().into_string().unwrap(); + let pack_path = entry.path(); + { + if name == EDITABLE_PACKAGE_NAME { + //TODO: have a version of loading that does not involve already ingested packages + if let Ok(pack_core) = load_pack_core_from_normalized_folder(&pack_path, None) { + let lp = build_from_core(name.clone(), pack_path, pack_core); + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + } else if name == LOCAL_EXPANDED_PACKAGE_NAME { + //ignore this package, it'll be overwriten + } else { + let span_guard = info_span!("loading pack from dir", name).entered(); + + match build_from_dir(name.clone(), pack_path) { + Ok(lp) => { + let (data, tex, report) = lp; + data_packs.insert(data.uuid, data); + texture_packs.insert(tex.uuid, tex); + report_packs.insert(report.uuid, report); + } + Err(e) => { + error!(?e, "failed to load pack from directory: {}", name); + } + } + drop(span_guard); + } + } + } + Ok((data_packs, texture_packs, report_packs)) +} + +fn build_from_dir(name: String, pack_path: PathBuf) -> Result { + let core_path = pack_path.join(LoadedPackData::CORE_PACK_DIR_NAME); + if !core_path.exists() { + return Err("pack core doesn't exist in this pack".to_string()); + } + let start = std::time::SystemTime::now(); + let import_report = LoadedPackData::load_import_report(&pack_path); + let core = load_pack_core_from_normalized_folder(&core_path, import_report) + .or(Err("failed to load pack from dir"))?; + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of package from disk {} took {} ms", + name, + elaspsed.as_millis() + ); + let res = build_from_core(name.clone(), pack_path, core); + Ok(res) +} + +pub fn build_from_core(name: String, path: PathBuf, core: PackCore) -> ImportTriplet { + let selectable_categories = LoadedPackData::load_selectable_categories(&path, &core); + let data = LoadedPackData { + name: name.clone(), + uuid: core.uuid, + path: path.clone(), + selected_files: Default::default(), + all_categories: core.all_categories, + categories: core.categories, + maps: core.maps, + source_files: core.active_source_files.clone(), + _is_dirty: false, + activation_data: Default::default(), + active_elements: Default::default(), + selectable_categories: selectable_categories.clone(), + entities_parents: core.entities_parents, + }; + let target = path.join(LoadedPackTexture::ACTIVATION_DATA_FILE_NAME); + let mut cd_json = String::new(); + let activation_data = + (if let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(target) { + match file.read_to_string(&mut cd_json) { + Ok(_n) => match serde_json::from_str(&cd_json) { + Ok(cd) => Some(cd), + Err(e) => { + error!(?e, "failed to deserialize activation data"); + None + } + }, + Err(e) => { + error!(?e, "failed to read string of category data"); + None + } + } + } else { + None + }) + .flatten() + .unwrap_or_default(); + let tex = LoadedPackTexture { + uuid: core.uuid, + selectable_categories, + textures: core.textures, + current_map_data: Default::default(), + _is_dirty: false, + activation_data, + path, + name, + tbins: core.tbins, + //active_elements: Default::default(), + source_files: core.active_source_files, + }; + let report = core.report; + (data, tex, report) +} diff --git a/crates/joko_package_manager/src/manager/pack/mod.rs b/crates/joko_package_manager/src/manager/pack/mod.rs new file mode 100644 index 0000000..908a692 --- /dev/null +++ b/crates/joko_package_manager/src/manager/pack/mod.rs @@ -0,0 +1,6 @@ +pub mod activation; +pub mod active; +pub mod category_selection; +pub mod file_selection; +pub mod import; +pub mod loaded; diff --git a/crates/joko_package_manager/src/manager/package_data.rs b/crates/joko_package_manager/src/manager/package_data.rs new file mode 100644 index 0000000..8f0f8e4 --- /dev/null +++ b/crates/joko_package_manager/src/manager/package_data.rs @@ -0,0 +1,562 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; + +use joko_component_models::{ + default_component_result, from_broadcast, from_data, to_data, Component, ComponentChannels, + ComponentMessage, ComponentResult, +}; +use joko_link_models::MumbleLink; +use joko_package_models::package::PackageImportReport; + +use tracing::{error, info, info_span, trace}; + +use crate::{ + build_from_core, import_pack_from_zip_file_path, jokolay_to_editable_path, + jokolay_to_extract_path, + message::{MessageToPackageBack, MessageToPackageUI}, +}; +use miette::{IntoDiagnostic, Result}; +use uuid::Uuid; + +use crate::manager::pack::loaded::{LoadedPackData, PackTasks}; + +use super::pack::loaded::jokolay_to_marker_path; + +pub const PACKAGE_MANAGER_DIRECTORY_NAME: &str = "marker_manager"; //name kept for compatibility purpose +pub const PACKAGES_DIRECTORY_NAME: &str = "packs"; //name kept for compatibility purpose +pub const EXTRACT_DIRECTORY_NAME: &str = "_work"; //working dir where a package is extracted before reading +pub const EDITABLE_PACKAGE_NAME: &str = "editable"; //package automatically created and always imported as an overwrite +pub const LOCAL_EXPANDED_PACKAGE_NAME: &str = "_local_expanded"; //result of import of the editable package + // pub const MARKER_MANAGER_CONFIG_NAME: &str = "marker_manager_config.json"; + +#[derive(Clone)] +pub struct PackageBackSharedState { + choice_of_category_changed: bool, //Meant as an optimisation to only update when there is a change in UI + pub root_path: std::path::PathBuf, + #[allow(dead_code)] + pub editable_path: std::path::PathBuf, //copy of the editable path in ui_configuration + extract_path: std::path::PathBuf, +} + +struct PackageDataChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, + + front_end_notifier: tokio::sync::mpsc::Sender, + front_end_receiver: tokio::sync::mpsc::Receiver, +} + +/// It manage everything that has to do with marker packs. +/// 1. imports, loads, saves and exports marker packs. +/// 2. maintains the categories selection data for every pack +/// 3. contains activation data globally and per character +/// 4. When we load into a map, it filters the markers and runs the logic every frame +/// 1. If a marker needs to be activated (based on player position or whatever) +/// 2. marker needs to be drawn +/// 3. marker's texture is uploaded or being uploaded? if not ready, we will upload or use a temporary "loading" texture +/// 4. render that marker use joko_render + +#[must_use] +pub struct PackageDataManager { + /// marker manager directory. not useful yet, but in future we could be using this to store config files etc.. + //_marker_manager_dir: Arc, + /// packs directory which contains marker packs. each directory inside pack directory is an individual marker pack. + /// The name of the child directory is the name of the pack + //pub marker_packs_dir: Arc, + pub marker_packs_path: std::path::PathBuf, + /// These are the marker packs + /// The key is the name of the pack + /// The value is a loaded pack that contains additional data for live marker packs like what needs to be saved or category selections etc.. + pub packs: BTreeMap, + tasks: PackTasks, + current_map_id: u32, + /// This is the interval in number of seconds when we check if any of the packs need to be saved due to changes. + /// This allows us to avoid saving the pack too often. + pub save_interval: f64, + + pub currently_used_files: BTreeMap, + parents: HashMap, + loaded_elements: HashSet, + channels: Option, + + pub state: PackageBackSharedState, +} + +impl PackageDataManager { + /// Creates a new instance of [MarkerManager]. + /// 1. It opens the marker manager directory + /// 2. loads its configuration + /// 3. opens the packs directory + /// 4. loads all the packs + /// 5. loads all the activation data + /// 6. returns self + pub fn new(root_path: &std::path::Path) -> Result { + let marker_packs_path = jokolay_to_marker_path(root_path); + //TODO: load configuration from disk (ui.toml) + let editable_path = jokolay_to_editable_path(root_path) + .to_str() + .unwrap() + .to_string(); + let state = PackageBackSharedState { + choice_of_category_changed: false, + root_path: root_path.to_owned(), + editable_path: std::path::PathBuf::from(editable_path), + extract_path: jokolay_to_extract_path(root_path), + }; + Ok(Self { + packs: Default::default(), + tasks: PackTasks::new(), + //marker_packs_dir: Arc::new(marker_packs_dir), + marker_packs_path, + current_map_id: 0, + save_interval: 0.0, + currently_used_files: Default::default(), + parents: Default::default(), + loaded_elements: Default::default(), + channels: None, + state, + }) + } + + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn category_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_set(uuid, status) { + break; + } + } + } + + pub fn category_branch_set(&mut self, uuid: Uuid, status: bool) { + for pack in self.packs.values_mut() { + if pack.category_branch_set(uuid, status) { + break; + } + } + } + + pub fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn register(&mut self, element: Uuid, parent: Uuid) { + self.parents.insert(element, parent); + } + pub fn get_parent(&self, element: &Uuid) -> Option<&Uuid> { + self.parents.get(element) + } + pub fn get_parents<'a, I>(&self, input: I) -> HashSet + where + I: Iterator, + { + let iter = input.into_iter(); + let mut result: HashSet = HashSet::new(); + let mut current_generation: Vec = Vec::new(); + for elt in iter { + current_generation.push(*elt) + } + //info!("starts with {}", current_generation.len()); + loop { + if current_generation.is_empty() { + //info!("ends with {}", result.len()); + return result; + } + let mut next_gen: Vec = Vec::new(); + for elt in current_generation.iter() { + if let Some(p) = self.get_parent(elt) { + if result.contains(p) { + //avoid duplicate, redundancy or loop + continue; + } + next_gen.push(*p); + } + } + let to_insert = std::mem::replace(&mut current_generation, next_gen); + result.extend(to_insert); + } + #[allow(unreachable_code)] // sillyness of some tools + { + unreachable!("The loop should always return") + } + } + + pub fn get_active_elements_parents( + &mut self, + categories_and_elements_to_be_loaded: HashSet, + ) { + trace!( + "There are {} active elements", + categories_and_elements_to_be_loaded.len() + ); + + //first merge the parents to iterate overit + let mut parents: HashMap = Default::default(); + for pack in self.packs.values_mut() { + parents.extend(pack.entities_parents.clone()); + } + self.parents = parents; + //then climb up the tree of parent's categories + self.loaded_elements = self.get_parents(categories_and_elements_to_be_loaded.iter()); + } + + fn handle_message(&mut self, msg: MessageToPackageBack) { + match msg { + MessageToPackageBack::ActiveFiles(currently_used_files) => { + trace!( + "Handling of MessageToPackageBack::ActiveFiles {}", + currently_used_files.len() + ); + trace!( + "Handling of MessageToPackageBack::ActiveFiles {:?}", + currently_used_files + ); + self.set_currently_used_files(currently_used_files); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategoryActivationElementStatusChange(category_uuid, status) => { + trace!("Handling of MessageToPackageBack::CategoryActivationElementStatusChange"); + self.category_set(category_uuid, status); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategoryActivationBranchStatusChange(category_uuid, status) => { + trace!("Handling of MessageToPackageBack::CategoryActivationBranchStatusChange"); + self.category_branch_set(category_uuid, status); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategoryActivationStatusChanged => { + trace!("Handling of MessageToPackageBack::CategoryActivationStatusChanged"); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::CategorySetAll(status) => { + trace!( + "Handling of MessageToPackageBack::CategorySetAll {}", + status + ); + self.category_set_all(status); + self.state.choice_of_category_changed = true; + } + MessageToPackageBack::DeletePacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageBack::DeletePacks"); + + let mut deleted = Vec::new(); + + for pack_uuid in to_delete { + if let Some(pack) = self.packs.remove(&pack_uuid) { + let target = self.marker_packs_path.join(&pack.name); + if let Err(e) = std::fs::remove_dir_all(target) { + error!(?e, pack.name, "failed to remove pack"); + } else { + info!("deleted marker pack: {}", pack.name); + deleted.push(pack_uuid); + } + } + } + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::DeletedPacks(deleted))); + } + MessageToPackageBack::ImportPack(file_path) => { + tracing::trace!("Handling of MessageToPackageBack::ImportPack"); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(1))); + let start = std::time::SystemTime::now(); + let result = import_pack_from_zip_file_path(file_path, &self.state.extract_path); + let elaspsed = start.elapsed().unwrap_or_default(); + tracing::info!( + "Loading of taco package from disk took {} ms", + elaspsed.as_millis() + ); + match result { + Ok((file_name, pack)) => { + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::ImportedPack(file_name, pack), + )); + } + Err(e) => { + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::ImportFailure(e))); + } + } + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(0))); + } + MessageToPackageBack::ReloadPack => { + unimplemented!( + "Handling of MessageToPackageBack::ReloadPack has not been implemented yet" + ); + } + MessageToPackageBack::SavePack(name, pack) => { + tracing::trace!("Handling of MessageToPackageBack::SavePack"); + trace!("save in {:?}", self.marker_packs_path); + + /*let std_file = std::fs::OpenOptions::new() + .open(&self.marker_packs_path) + .unwrap(); + let marker_packs_dir = cap_std::fs_utf8::Dir::from_std_file(std_file);*/ + let name = name.as_str(); + let pack_path = self.marker_packs_path.join(name); + + if pack_path.exists() { + match std::fs::remove_dir_all(pack_path.clone()).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to delete already existing marker pack"); + } + } + } + if let Err(e) = std::fs::create_dir_all(pack_path.clone()) { + error!(?e, "failed to create directory for pack"); + } + + let (data_pack, mut texture_pack, mut report) = + build_from_core(name.to_string(), pack_path, pack); + tracing::trace!("Package loaded into data and texture"); + let uuid_of_insertion = self.save(data_pack, report.clone()); + report.uuid = uuid_of_insertion; + texture_pack.uuid = uuid_of_insertion; + let channels = self.channels.as_mut().unwrap(); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::LoadedPack(texture_pack, report), + )); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageBack has not been implemented yet"); + } + } + } + + pub fn _tick(&mut self, link: &Option) { + if let Some(link) = link { + //TODO: how to save/load the active files ? + let mut have_used_files_list_changed = false; + let map_changed = self.current_map_id != link.map_id; + self.current_map_id = link.map_id; + trace!( + "PackageDataManager::tick map id is: {}", + self.current_map_id + ); + let mut currently_used_files: BTreeMap = Default::default(); + for pack in self.packs.values_mut() { + if let Some(current_map) = pack.maps.get(&link.map_id) { + for marker in current_map.markers.values() { + if let Some(is_active) = pack.source_files.get(&marker.source_file_uuid) { + currently_used_files.insert( + marker.source_file_uuid, + *self + .currently_used_files + .get(&marker.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + for trail in current_map.trails.values() { + if let Some(is_active) = pack.source_files.get(&trail.source_file_uuid) { + currently_used_files.insert( + trail.source_file_uuid, + *self + .currently_used_files + .get(&trail.source_file_uuid) + .unwrap_or_else(|| { + have_used_files_list_changed = true; + is_active + }), + ); + } + } + } + } + trace!( + "currently_used_files: {} {:?}", + currently_used_files.len(), + currently_used_files + ); + let tasks = &self.tasks; + if map_changed || have_used_files_list_changed || self.state.choice_of_category_changed + { + let mut categories_and_elements_to_be_loaded: HashSet = Default::default(); + { + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::TextureBegin)); + } + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(tasks.count()))); + tasks.save_data(pack, pack.is_dirty()); + pack.tick( + &channels.front_end_notifier, + link, + ¤tly_used_files, + tasks, + &mut categories_and_elements_to_be_loaded, + ); + std::mem::drop(span_guard); + } + + self.get_active_elements_parents(categories_and_elements_to_be_loaded); + + //there is no point in sending a new list if nothing changed + + let channels = self.channels.as_mut().unwrap(); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files.clone()), + )); + self.currently_used_files = currently_used_files; + + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::ActiveElements(self.loaded_elements.clone()), + )); + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::TextureSwapChain)); + } + self.state.choice_of_category_changed = false; + } else { + trace!("PackageDataManager::tick no link") + } + } + + fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + } + } + pub fn save(&mut self, mut data_pack: LoadedPackData, report: PackageImportReport) -> Uuid { + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == data_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks.save_report(data_pack.path.clone(), report, true); + self.tasks.save_data(&mut data_pack, true); + let mut uuid_to_insert = data_pack.uuid; + while self.packs.contains_key(&uuid_to_insert) { + //collision avoidance + trace!( + "Uuid collision detected for {} for package {}", + uuid_to_insert, + data_pack.name + ); + uuid_to_insert = Uuid::new_v4(); + } + data_pack.uuid = uuid_to_insert; + self.packs.insert(uuid_to_insert, data_pack); + uuid_to_insert + } + + pub fn load_all(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + once::assert_has_not_been_called!("Early load must happen only once"); + trace!("Load all packages"); + let channels = self.channels.as_mut().unwrap(); + // Called only once at application start. + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(1))); + self.tasks.load_all_packs(self.state.root_path.clone()); + if let Ok((data_packages, texture_packages, report_packages)) = + self.tasks.wait_for_load_all_packs() + { + for (uuid, data_pack) in data_packages { + self.packs.insert(uuid, data_pack); + } + for ((_, texture_pack), (_, report)) in + std::iter::zip(texture_packages, report_packages) + { + trace!("load_all notify front of a valid loaded package"); + let _ = channels.front_end_notifier.blocking_send(to_data( + MessageToPackageUI::LoadedPack(texture_pack, report), + )); + } + + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::NbTasksRunning(0))); + } + let _ = channels + .front_end_notifier + .blocking_send(to_data(MessageToPackageUI::FirstLoadDone)); + } +} + +impl Component for PackageDataManager { + fn init(&mut self) { + self.load_all(); + } + + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + tracing::trace!( + "choice_of_category_changed: {}", + self.state.choice_of_category_changed + ); + + let channels = self.channels.as_mut().unwrap(); + //println!("PackageDataManager: nb messages to read: {}", channels.front_end_receiver.len()); + let mut messages = Vec::new(); + while let Ok(msg) = channels.front_end_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + fn bind(&mut self, mut channels: ComponentChannels) { + let (front_end_notifier, front_end_receiver) = channels.peers.remove(&0).unwrap(); + let channels = PackageDataChannels { + subscription_mumblelink: channels.requirements.remove(&1).unwrap(), + front_end_notifier, + front_end_receiver, + }; + self.channels = Some(channels); + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); + let raw_mlr = channels.subscription_mumblelink.try_recv().unwrap(); + let mumble_link_result: Option = from_broadcast(&raw_mlr); + //trace!("subscription_mumblelink provided data"); + self._tick(&mumble_link_result); + default_component_result() + } + fn notify(&self) -> Vec<&str> { + vec![] + } + fn peers(&self) -> Vec<&str> { + vec!["ui:jokolay_package_manager"] + } + fn requirements(&self) -> Vec<&str> { + vec!["back:mumble_link"] + } + fn accept_notifications(&self) -> bool { + false + } +} diff --git a/crates/joko_package_manager/src/manager/package_ui.rs b/crates/joko_package_manager/src/manager/package_ui.rs new file mode 100644 index 0000000..64f7687 --- /dev/null +++ b/crates/joko_package_manager/src/manager/package_ui.rs @@ -0,0 +1,822 @@ +use std::{ + borrow::BorrowMut, + collections::{BTreeMap, HashSet}, + sync::{Arc, Mutex}, +}; + +use egui::{CollapsingHeader, ColorImage, TextureHandle, Ui, Window}; +use image::EncodableLayout; +use joko_package_models::{attributes::CommonAttributes, package::PackageImportReport}; + +use joko_render_models::messages::MessageToRenderer; +use joko_ui_models::{UIArea, UIPanel}; +use serde::{Deserialize, Serialize}; +use tracing::{info_span, trace}; + +use crate::message::MessageToPackageBack; +use joko_component_models::{ + from_broadcast, from_data, to_broadcast, to_data, Component, ComponentChannels, + ComponentMessage, ComponentResult, +}; +use joko_core::{serde_glam::Vec3, RelativePath}; +use joko_link_models::{MumbleChanges, MumbleLink}; +use miette::Result; +use uuid::Uuid; + +use crate::manager::pack::import::ImportStatus; +use crate::manager::pack::loaded::{LoadedPackTexture, PackTasks}; +use crate::message::MessageToPackageUI; + +//FIXME: there is an interest to merge the PackageUIManager and the render +#[derive(Clone, Serialize, Deserialize)] +pub struct PackageUISharedState { + list_of_textures_changed: bool, //Meant as an optimisation to only update when choice_of_category_changed have produced the list of textures to display + first_load_done: bool, + nb_running_tasks_on_back: i32, // store the number of running tasks in background thread + import_status: Arc>, +} + +struct PackageUIChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, + + back_end_notifier: tokio::sync::mpsc::Sender, + back_end_receiver: tokio::sync::mpsc::Receiver, + renderer_notifier: tokio::sync::mpsc::Sender, +} + +#[must_use] +pub struct PackageUIManager { + default_marker_texture: Option, + default_trail_texture: Option, + packs: BTreeMap, + nb_swap: u128, + reports: BTreeMap, + tasks: PackTasks, + + egui_context: egui::Context, + z_near: f32, + currently_used_files: BTreeMap, + all_files_activation_status: bool, // this consume a change of display event + show_only_active: bool, + pack_details: Option, // if filled, display the details of the package + + delayed_marker_texture: Vec<(Uuid, RelativePath, Uuid, Vec3, CommonAttributes)>, + delayed_trail_texture: Vec<(Uuid, RelativePath, Uuid, CommonAttributes)>, + + channels: Option, + state: PackageUISharedState, +} + +impl PackageUIManager { + pub fn new(egui_context: egui::Context, z_near: f32) -> Self { + //z_near is a constant, make it a https://docs.rs/tokio/latest/tokio/sync/watch/index.html if required to be dynamic + let state = PackageUISharedState { + list_of_textures_changed: false, + first_load_done: false, + nb_running_tasks_on_back: 0, + import_status: Default::default(), + }; + let mut res = Self { + packs: Default::default(), + nb_swap: 0, + tasks: PackTasks::new(), + reports: Default::default(), + default_marker_texture: None, + default_trail_texture: None, + + egui_context, + z_near, + all_files_activation_status: false, + show_only_active: true, + currently_used_files: Default::default(), // UI copy to (de-)activate files + pack_details: None, + + delayed_marker_texture: Default::default(), + delayed_trail_texture: Default::default(), + channels: None, + state, + }; + res._init(); + res + } + + fn handle_message(&mut self, msg: MessageToPackageUI) { + match msg { + MessageToPackageUI::ActiveElements(active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::ActiveElements"); + self.update_active_categories(&active_elements); + } + MessageToPackageUI::CurrentlyUsedFiles(currently_used_files) => { + tracing::trace!("Handling of MessageToPackageUI::CurrentlyUsedFiles"); + self.set_currently_used_files(currently_used_files); + } + MessageToPackageUI::DeletedPacks(to_delete) => { + tracing::trace!("Handling of MessageToPackageUI::DeletedPacks"); + self.delete_packs(to_delete); + } + MessageToPackageUI::FirstLoadDone => { + self.state.first_load_done = true; + self.state.list_of_textures_changed = true; + } + MessageToPackageUI::ImportedPack(file_name, pack) => { + tracing::trace!("Handling of MessageToPackageUI::ImportedPack"); + *self.state.import_status.lock().unwrap() = + ImportStatus::PackDone(file_name, pack, false); + } + MessageToPackageUI::ImportFailure(message) => { + tracing::trace!("Handling of MessageToPackageUI::ImportFailure"); + *self.state.import_status.lock().unwrap() = ImportStatus::PackError(message); + } + MessageToPackageUI::LoadedPack(pack_texture, report) => { + tracing::trace!("Handling of MessageToPackageUI::LoadedPack"); + self.save(pack_texture, report); + self.state.import_status = Default::default(); + let channels = self.channels.as_mut().unwrap(); + /*let _ = channels.back_end_notifier.blocking_send(to_data( + MessageToPackageBack::CategoryActivationStatusChanged, + )); + self.state.list_of_textures_changed = true;*/ + let renderer_notifier = &channels.renderer_notifier; + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderFlush)); + } + MessageToPackageUI::MarkerTexture( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::MarkerTexture"); + self.delayed_marker_texture.push(( + pack_uuid, + tex_path, + marker_uuid, + position, + common_attributes, + )); + } + MessageToPackageUI::NbTasksRunning(nb_tasks) => { + tracing::trace!("Handling of MessageToPackageUI::NbTasksRunning"); + self.state.nb_running_tasks_on_back = nb_tasks; + } + MessageToPackageUI::PackageActiveElements(pack_uuid, active_elements) => { + tracing::trace!("Handling of MessageToPackageUI::PackageActiveElements"); + self.update_pack_active_categories(pack_uuid, &active_elements); + } + MessageToPackageUI::TextureBegin => { + tracing::trace!("Handling of MessageToPackageUI::TextureBegin"); + self.clear(); + } + MessageToPackageUI::TextureSwapChain => { + tracing::trace!( + "Handling of MessageToPackageUI::TextureSwapChain {}", + self.nb_swap + ); + self.state.list_of_textures_changed = true; + } + MessageToPackageUI::TrailTexture( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + ) => { + tracing::trace!("Handling of MessageToPackageUI::TrailTexture"); + self.delayed_trail_texture.push(( + pack_uuid, + tex_path, + trail_uuid, + common_attributes, + )); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToPackageUI has not been implemented yet"); + } + } + } + + fn _init(&mut self) { + let egui_context: &egui::Context = &self.egui_context; + //TODO: make it even later, at another place + if self.default_marker_texture.is_none() { + let img = image::load_from_memory(include_bytes!("../../images/marker.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_marker_texture = Some(egui_context.load_texture( + "default marker", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + if self.default_trail_texture.is_none() { + let img = + image::load_from_memory(include_bytes!("../../images/trail_rainbow.png")).unwrap(); + let size = [img.width() as _, img.height() as _]; + self.default_trail_texture = Some(egui_context.load_texture( + "default trail", + ColorImage::from_rgba_unmultiplied(size, img.into_rgba8().as_bytes()), + egui::TextureOptions { + magnification: egui::TextureFilter::Linear, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + }, + )); + } + } + + pub fn delete_packs(&mut self, to_delete: Vec) { + for uuid in to_delete { + self.packs.remove(&uuid); + self.reports.remove(&uuid); + } + } + pub fn set_currently_used_files(&mut self, currently_used_files: BTreeMap) { + self.currently_used_files = currently_used_files; + } + + pub fn update_active_categories(&mut self, active_elements: &HashSet) { + trace!("There are {} active elements", active_elements.len()); + for pack in self.packs.values_mut() { + pack.update_active_categories(active_elements); + } + } + + pub fn update_pack_active_categories( + &mut self, + pack_uuid: Uuid, + active_elements: &HashSet, + ) { + trace!("There are {} active elements", active_elements.len()); + for (uuid, pack) in self.packs.iter_mut() { + if uuid == &pack_uuid { + pack.update_active_categories(active_elements); + break; + } + } + } + pub fn clear(&mut self) { + self.nb_swap += 1; + for pack in self.packs.values_mut() { + pack.clear(); + } + } + + pub fn load_marker_texture( + &mut self, + pack_uuid: Uuid, + egui_context: egui::Context, + tex_path: RelativePath, + marker_uuid: Uuid, + position: Vec3, + common_attributes: CommonAttributes, + ) { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { + pack.load_marker_texture( + &egui_context, + self.default_marker_texture.as_ref().unwrap(), + &tex_path, + marker_uuid, + position, + common_attributes, + ); + }; + } + pub fn load_trail_texture( + &mut self, + pack_uuid: Uuid, + egui_context: egui::Context, + tex_path: RelativePath, + trail_uuid: Uuid, + common_attributes: CommonAttributes, + ) { + if let Some(pack) = self.packs.get_mut(&pack_uuid) { + pack.load_trail_texture( + &egui_context, + self.default_trail_texture.as_ref().unwrap(), + &tex_path, + trail_uuid, + common_attributes, + ); + }; + } + + fn pack_importer(import_status: Arc>) { + //called when a new pack is imported + rayon::spawn(move || { + *import_status.lock().unwrap() = ImportStatus::WaitingForFileChooser; + + if let Some(file_path) = rfd::FileDialog::new() + .add_filter("taco", &["zip", "taco"]) + .pick_file() + { + *import_status.lock().unwrap() = ImportStatus::LoadingPack(file_path); + } else { + *import_status.lock().unwrap() = + ImportStatus::PackError("file chooser was cancelled".to_string()); + } + }); + } + + fn category_set_all(&mut self, status: bool) { + for pack in self.packs.values_mut() { + pack.category_set_all(status); + } + } + + pub fn _tick(&mut self, timestamp: f64, link: &MumbleLink, z_near: f32) -> Result<()> { + trace!("PackageUIManager::_tick for {} packages", self.packs.len()); + if self.packs.is_empty() { + return Ok(()); + } + let tasks = &self.tasks; + let channels = self.channels.as_ref().unwrap(); + let renderer_notifier = &channels.renderer_notifier; + for pack in self.packs.values_mut() { + tasks.save_texture(pack, pack.is_dirty()); + } + if link.changes.contains(MumbleChanges::Position) + || link.changes.contains(MumbleChanges::Map) + || self.state.list_of_textures_changed + { + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderBegin)); + + for pack in self.packs.values_mut() { + let span_guard = info_span!("Updating package status").entered(); + pack.tick(renderer_notifier, timestamp, link, z_near, tasks)?; // compute the vertices: textures position, size, rotation and so on + std::mem::drop(span_guard); + } + let _ = renderer_notifier.blocking_send(to_data(MessageToRenderer::RenderSwapChain)); + self.state.list_of_textures_changed = false; + } + Ok(()) + } + + pub fn status_as_color( + &self, + nb_running_tasks_on_back: i32, + nb_running_tasks_on_network: i32, + ) -> egui::Color32 { + //we can choose whatever color code we want to focus on load, save, network queries, anything. + let nb_running_tasks_on_ui = self.tasks.count(); + //Integer overflow avoidance example: value * 0x80 / 4 <=> value * 0x20 + let color_ui = if nb_running_tasks_on_ui > 0 { + let nb_ui_tasks = nb_running_tasks_on_ui.clamp(0, 1) as u8; + let res = nb_ui_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_back = if nb_running_tasks_on_back > 0 { + let nb_bask_tasks = nb_running_tasks_on_back.clamp(0, 1) as u8; + let res = nb_bask_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + let color_network = if nb_running_tasks_on_network > 0 { + let nb_network_tasks = nb_running_tasks_on_network.clamp(0, 1) as u8; + let res = nb_network_tasks * 0x80; + res + 0x7f + } else { + 0 + }; + + egui::Color32::from_rgb(color_ui, color_back, color_network) + } + + fn gui_file_manager(&mut self, is_open: &mut bool) { + let egui_context = self.egui_context.borrow_mut(); + let channels = self.channels.as_mut().unwrap(); + let mut files_changed = false; + Window::new("File Manager") + .open(is_open) + .show(egui_context, |ui| -> Result<()> { + egui::ScrollArea::vertical().show(ui, |ui| { + egui::Grid::new("link grid") + .num_columns(4) + .striped(true) + .show(ui, |ui| { + let mut all_files_toggle = false; + ui.horizontal(|ui| { + if ui.button("activate all").clicked() { + self.all_files_activation_status = true; + all_files_toggle = true; + files_changed = true; + } + if ui.button("deactivate all").clicked() { + self.all_files_activation_status = false; + all_files_toggle = true; + files_changed = true; + } + }); + //ui.label("Trails"); + //ui.label("Markers"); + ui.end_row(); + + for pack in self.packs.values_mut() { + let report = self.reports.get(&pack.uuid).unwrap(); + let mut pack_files_toggle = false; + let mut pack_files_activation_status = true; + ui.horizontal(|ui| { + ui.label(&pack.name); + if ui.button("activate all").clicked() { + pack_files_activation_status = true; + pack_files_toggle = true; + files_changed = true; + } + if ui.button("deactivate all").clicked() { + pack_files_activation_status = false; + pack_files_toggle = true; + files_changed = true; + } + }); + ui.end_row(); + for source_file_uuid in pack.source_files.keys() { + if let Some(is_selected) = + self.currently_used_files.get_mut(source_file_uuid) + { + if all_files_toggle { + *is_selected = self.all_files_activation_status; + } + if pack_files_toggle { + *is_selected = pack_files_activation_status; + } + ui.add_space(3.0); + //reports may be corrupted or not loaded, files are there + if let Some(source_file_name) = + report.source_file_uuid_to_name(source_file_uuid) + { + //format the file from reports and packages + prefix with the package name + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_name), + ); + if cb.changed() { + files_changed = true; + } + } else { + // Import report is corrupted, only print reference + let cb = ui.checkbox( + is_selected, + format!("{}: {}", pack.name, source_file_uuid), + ); + if cb.changed() { + files_changed = true; + } + } + ui.end_row(); + } + } + } + ui.end_row(); + }) + }); + Ok(()) + }); + if files_changed { + let _ = channels.back_end_notifier.blocking_send(to_data( + MessageToPackageBack::ActiveFiles(self.currently_used_files.clone()), + )); + } + } + + fn gui_package_details(ui: &mut Ui, data: (&LoadedPackTexture, &PackageImportReport)) { + // protection against deletion while displaying details + let (pack, report) = data; + + let collapsing = + CollapsingHeader::new(format!("Last load details of package {}", pack.name)); + let _header_response = collapsing + .open(Some(true)) + .show(ui, |ui| { + egui::Grid::new("packs details") + .striped(true) + .show(ui, |ui| { + let number_of = &report.number_of; + ui.label("categories"); + ui.label(format!("{}", number_of.categories)); + ui.end_row(); + ui.label("missing_categories"); + ui.label(format!("{}", number_of.missing_categories)); + ui.end_row(); + ui.label("textures"); + ui.label(format!("{}", number_of.textures)); + ui.end_row(); + ui.label("missing_textures"); + ui.label(format!("{}", number_of.missing_textures)); + ui.end_row(); + ui.label("entities"); + ui.label(format!("{}", number_of.entities)); + ui.end_row(); + ui.label("markers"); + ui.label(format!("{}", number_of.markers)); + ui.end_row(); + ui.label("trails"); + ui.label(format!("{}", number_of.trails)); + ui.end_row(); + ui.label("routes"); + ui.label(format!("{}", number_of.routes)); + ui.end_row(); + ui.label("maps"); + ui.label(format!("{}", number_of.maps)); + ui.end_row(); + ui.label("source_files"); + ui.label(format!("{}", number_of.source_files)); + ui.end_row(); + }) + }) + .header_response; + /*if header_response.clicked() { + self.pack_details = None; + }*/ + } + fn gui_package_list(&mut self, is_open: &mut bool) { + let egui_context = self.egui_context.borrow_mut(); + let import_status = self.state.import_status.clone(); + let details = if let Some(uuid) = self.pack_details { + if let Some(pack) = self.packs.get(&uuid) { + if let Some(report) = self.reports.get(&uuid) { + Some((pack, report)) + } else { + self.pack_details = None; + None + } + } else { + self.pack_details = None; + None + } + } else { + None + }; + Window::new("Package Loader").open(is_open).show(egui_context, |ui| -> Result<()> { + let channels = self.channels.as_mut().unwrap(); + if !self.state.first_load_done { + ui.label("Loading in progress..."); + } else { + CollapsingHeader::new("Loaded Packs").show(ui, |ui| { + egui::Grid::new("packs").striped(true).show(ui, |ui| { + let mut to_delete = vec![]; + for pack in self.packs.values() { + ui.label(pack.name.clone()); + if ui.button("delete").clicked() { + to_delete.push(pack.uuid); + } + if ui.button("Details").clicked() { + self.pack_details = Some(pack.uuid); + } + if ui.button("Export").clicked() { + //TODO + } + ui.end_row(); + } + if !to_delete.is_empty() { + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::DeletePacks(to_delete))); + } + }); + }); + if let Some(data) = details { + Self::gui_package_details(ui, data); + } else if let Ok(mut status) = import_status.lock() { + match &mut *status { + ImportStatus::UnInitialized => { + if ui.button("import pack").on_hover_text("select a taco/zip file to import the marker pack from").clicked() { + Self::pack_importer(Arc::clone(&import_status)); + } + //ui.label("import not started yet"); + } + ImportStatus::WaitingForFileChooser => { + ui.label( + "waiting for the file dialog. choose a taco/zip file to import", + ); + } + ImportStatus::LoadingPack(p) | ImportStatus::WaitingLoading(p) => { + ui.label(format!("pack is being imported from {p:?}")); + } + ImportStatus::PackDone(name, pack, saved) => { + if *saved { + ui.colored_label(egui::Color32::GREEN, "pack is saved. press click `clear` button to remove this message"); + } else { + ui.horizontal(|ui| { + ui.label("choose a pack name: "); + ui.text_edit_singleline(name); + }); + if ui.button("save").clicked() { + let _ = channels.back_end_notifier.blocking_send(to_data(MessageToPackageBack::SavePack(name.clone(), pack.clone()))); + *status = ImportStatus::WaitingForSave; + } + } + } + ImportStatus::WaitingForSave => { + ui.colored_label(egui::Color32::GREEN, "Waiting for pack to be saved."); + } + ImportStatus::PackError(e) => { + let error_msg = format!("failed to import pack due to error: {e:#?}"); + if ui.button("clear").on_hover_text( + "This will cancel any pack import in progress. If import is already finished, then it wil simply clear the import status").clicked() { + *status = ImportStatus::UnInitialized; + } + ui.colored_label( + egui::Color32::RED, + error_msg, + ); + } + } + } + } + + Ok(()) + }); + } + + pub fn save(&mut self, mut texture_pack: LoadedPackTexture, report: PackageImportReport) { + /* + We save in a file with the name of the package, while we keep track of it from a uuid point of view. + It means we can have duplicates unless package with same name is deleted. + */ + let mut to_delete: Vec = Vec::new(); + for (uuid, pack) in self.packs.iter() { + if pack.name == texture_pack.name { + to_delete.push(*uuid); + } + } + self.delete_packs(to_delete); + self.tasks.save_texture(&mut texture_pack, true); + self.packs.insert(texture_pack.uuid, texture_pack); + self.reports.insert(report.uuid, report); + } +} + +impl Component for PackageUIManager { + fn init(&mut self) {} + + fn flush_all_messages(&mut self) { + assert!(self.channels.is_some()); + let channels = self.channels.as_mut().unwrap(); + + if let Ok(mut import_status) = self.state.import_status.lock() { + if let ImportStatus::LoadingPack(file_path) = &mut *import_status { + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::ImportPack(file_path.clone()))); + *import_status = ImportStatus::WaitingLoading(file_path.clone()); + } + } + let mut messages = Vec::new(); + while let Ok(msg) = channels.back_end_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + + fn tick(&mut self, timestamp: f64) -> ComponentResult { + assert!(self.channels.is_some()); + + let raw_link = { + let channels = self.channels.as_mut().unwrap(); + //trace!("blocking waiting for subscription_mumblelink {}", channels.subscription_mumblelink.len()); + channels.subscription_mumblelink.try_recv().unwrap() + }; + let link: Option = from_broadcast(&raw_link); + //trace!("subscription_mumblelink provided data"); + + for (pack_uuid, tex_path, marker_uuid, position, common_attributes) in + std::mem::take(&mut self.delayed_marker_texture) + { + self.load_marker_texture( + pack_uuid, + self.egui_context.clone(), + tex_path, + marker_uuid, + position, + common_attributes, + ); + } + for (pack_uuid, tex_path, trail_uuid, common_attributes) in + std::mem::take(&mut self.delayed_trail_texture) + { + self.load_trail_texture( + pack_uuid, + self.egui_context.clone(), + tex_path, + trail_uuid, + common_attributes, + ); + } + + //let channels = self.channels.as_mut().unwrap(); + //let raw_z_near = channels.subscription_near_scene.blocking_recv().unwrap(); + //let z_near: f32 = from_data(raw_z_near); + if let Some(link) = link.as_ref() { + let _ = self._tick(timestamp, link, self.z_near); + } + to_broadcast(self.state.clone()) + } + fn bind(&mut self, mut channels: ComponentChannels) { + let (back_end_notifier, back_end_receiver) = channels.peers.remove(&0).unwrap(); + let channels = PackageUIChannels { + subscription_mumblelink: channels.requirements.remove(&1).unwrap(), + back_end_notifier, + back_end_receiver, + renderer_notifier: channels.notify.remove(&2).unwrap(), + }; + + self.channels = Some(channels); + } + fn notify(&self) -> Vec<&str> { + vec!["ui:jokolay_renderer"] + } + fn peers(&self) -> Vec<&str> { + vec!["back:jokolay_package_manager"] + } + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn accept_notifications(&self) -> bool { + false + } +} + +impl UIPanel for PackageUIManager { + fn areas(&self) -> Vec { + vec![ + UIArea { + is_open: false, + name: "Package Manager".to_string(), + id: "package_loading".to_string(), + }, + UIArea { + is_open: false, + name: "File Manager".to_string(), + id: "file_manager".to_string(), + }, + ] + } + fn init(&mut self) {} + fn gui(&mut self, is_open: &mut bool, area_id: &str, _latest_time: f64) { + match area_id { + "package_loading" => { + self.gui_package_list(is_open); + } + "file_manager" => { + self.gui_file_manager(is_open); + } + _ => {} + } + } + fn menu_ui(&mut self, ui: &mut egui::Ui) { + let nb_running_tasks_on_back: i32 = 0; + let nb_running_tasks_on_network: i32 = 0; + ui.menu_button("Markers", |ui| { + if self.show_only_active { + if ui.button("Show everything").clicked() { + self.show_only_active = false; + } + } else if ui.button("Show only active").clicked() { + self.show_only_active = true; + } + if ui.button("Activate all elements").clicked() { + self.category_set_all(true); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(true))); + } + if ui.button("Deactivate all elements").clicked() { + self.category_set_all(false); + let channels = self.channels.as_mut().unwrap(); + let _ = channels + .back_end_notifier + .blocking_send(to_data(MessageToPackageBack::CategorySetAll(false))); + } + + let channels = self.channels.as_mut().unwrap(); + for (pack, import_quality_report) in + std::iter::zip(self.packs.values_mut(), self.reports.values()) + { + //pack.is_dirty = pack.is_dirty || force_activation || force_deactivation; + //category_sub_menu is for display only, it's a bad idea to use it to manipulate status + pack.category_sub_menu( + &channels.back_end_notifier, + ui, + self.show_only_active, + import_quality_report, + ); + } + }); + if self.tasks.is_running() + || nb_running_tasks_on_back > 0 + || nb_running_tasks_on_network > 0 + { + let sp = egui::Spinner::new() + .color(self.status_as_color(nb_running_tasks_on_back, nb_running_tasks_on_network)); + ui.add(sp); + } + } +} diff --git a/crates/joko_package_manager/src/message.rs b/crates/joko_package_manager/src/message.rs new file mode 100644 index 0000000..734974e --- /dev/null +++ b/crates/joko_package_manager/src/message.rs @@ -0,0 +1,42 @@ +use std::collections::{BTreeMap, HashSet}; + +use joko_package_models::{ + attributes::CommonAttributes, + package::{PackCore, PackageImportReport}, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use joko_core::{serde_glam::Vec3, RelativePath}; + +use crate::LoadedPackTexture; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToPackageUI { + ActiveElements(HashSet), //list of all elements that are loaded for current map + CurrentlyUsedFiles(BTreeMap), //when there is a change in map or anything else, the list of files is sent to ui for display + LoadedPack(LoadedPackTexture, PackageImportReport), //push a loaded pack to UI + DeletedPacks(Vec), //push a deleted set of packs to UI + FirstLoadDone, + ImportedPack(String, PackCore), + ImportFailure(String), + MarkerTexture(Uuid, RelativePath, Uuid, Vec3, CommonAttributes), + NbTasksRunning(i32), //tell the number of taks running in background + PackageActiveElements(Uuid, HashSet), // first is the package reference, second is the list of active elements in the package. + TextureBegin, // start to produce new set of textures + TextureSwapChain, // The list of texture to load was changed, will be soon followed by a RenderSwapChain + TrailTexture(Uuid, RelativePath, Uuid, CommonAttributes), +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToPackageBack { + ActiveFiles(BTreeMap), //when there is a change of files activated, send whole list to data for save. + CategoryActivationElementStatusChange(Uuid, bool), //sent each time there is a category whose activation status has been changed. With uuid being the reference of the category and bool the status. + CategoryActivationBranchStatusChange(Uuid, bool), //same, for a whole branch + CategoryActivationStatusChanged, //something happened that needs to reload the whole set + CategorySetAll(bool), //signal all categories should be now at this status + DeletePacks(Vec), //uuid of the pack to delete + ImportPack(std::path::PathBuf), + ReloadPack, + SavePack(String, PackCore), +} diff --git a/crates/joko_marker_format/vendor/rapid/license.txt b/crates/joko_package_manager/vendor/rapid/license.txt similarity index 100% rename from crates/joko_marker_format/vendor/rapid/license.txt rename to crates/joko_package_manager/vendor/rapid/license.txt diff --git a/crates/joko_marker_format/vendor/rapid/rapid.cpp b/crates/joko_package_manager/vendor/rapid/rapid.cpp similarity index 89% rename from crates/joko_marker_format/vendor/rapid/rapid.cpp rename to crates/joko_package_manager/vendor/rapid/rapid.cpp index 98ea7db..680f0fa 100644 --- a/crates/joko_marker_format/vendor/rapid/rapid.cpp +++ b/crates/joko_package_manager/vendor/rapid/rapid.cpp @@ -1,7 +1,7 @@ -#include "joko_marker_format/vendor/rapid/rapid.hpp" -#include "joko_marker_format/vendor/rapid/rapidxml.hpp" -#include "joko_marker_format/vendor/rapid/rapidxml_print.hpp" -#include "joko_marker_format/src/lib.rs.h" +#include "joko_package_manager/vendor/rapid/rapid.hpp" +#include "joko_package_manager/vendor/rapid/rapidxml.hpp" +#include "joko_package_manager/vendor/rapid/rapidxml_print.hpp" +#include "joko_package_manager/src/lib.rs.h" #include #include #include diff --git a/crates/joko_marker_format/vendor/rapid/rapid.hpp b/crates/joko_package_manager/vendor/rapid/rapid.hpp similarity index 70% rename from crates/joko_marker_format/vendor/rapid/rapid.hpp rename to crates/joko_package_manager/vendor/rapid/rapid.hpp index 248935a..760a161 100644 --- a/crates/joko_marker_format/vendor/rapid/rapid.hpp +++ b/crates/joko_package_manager/vendor/rapid/rapid.hpp @@ -1,5 +1,5 @@ #pragma once -#include "joko_marker_format/src/lib.rs.h" +#include "joko_package_manager/src/lib.rs.h" #include "rust/cxx.h" namespace rapid { diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml.hpp similarity index 100% rename from crates/joko_marker_format/vendor/rapid/rapidxml.hpp rename to crates/joko_package_manager/vendor/rapid/rapidxml.hpp diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp similarity index 100% rename from crates/joko_marker_format/vendor/rapid/rapidxml_iterators.hpp rename to crates/joko_package_manager/vendor/rapid/rapidxml_iterators.hpp diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp similarity index 100% rename from crates/joko_marker_format/vendor/rapid/rapidxml_print.hpp rename to crates/joko_package_manager/vendor/rapid/rapidxml_print.hpp diff --git a/crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp b/crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp similarity index 100% rename from crates/joko_marker_format/vendor/rapid/rapidxml_utils.hpp rename to crates/joko_package_manager/vendor/rapid/rapidxml_utils.hpp diff --git a/crates/joko_package_models/Cargo.toml b/crates/joko_package_models/Cargo.toml new file mode 100644 index 0000000..6124f14 --- /dev/null +++ b/crates/joko_package_models/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "joko_package_models" +version = "0.2.1" +edition = "2021" + + +[dependencies] +# jmf deps +# for marker packs +base64 = "0.21.2" +bimap = { workspace = true } +bytemuck = { workspace = true } +data-encoding = "2.4.0" +enumflags2 = { workspace = true } +glam = { workspace = true } +indexmap = { workspace = true, features = ["serde"]} # to keep the order of files inside zip. markers packs rely on some files like aaa.xml being read first for marker category order# for representing the paths of files inside xml pack zip +itertools = { workspace = true } +joko_core = { path = "../joko_core" } +jokoapi = { path = "../jokoapi" } +miette = { workspace = true } +paste = { workspace = true } +phf = { version = "*", features = ["macros"] } +serde = { workspace = true } +serde_json = { workspace = true } +smol_str = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } +uuid = { version = "1", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } +xot = { version = "0.16.0" } + + +[dev-dependencies] +# jmf deps +rstest = { version = "0", default-features = false } +# rstest_reuse = "0.3.0" +similar-asserts = "1" + + +[build-dependencies] +# for rapidxml +cxx-build = { version = "1" } diff --git a/crates/joko_marker_format/src/pack/common.rs b/crates/joko_package_models/src/attributes.rs similarity index 85% rename from crates/joko_marker_format/src/pack/common.rs rename to crates/joko_package_models/src/attributes.rs index 97fd40b..099fc42 100644 --- a/crates/joko_marker_format/src/pack/common.rs +++ b/crates/joko_package_models/src/attributes.rs @@ -1,17 +1,198 @@ use std::str::FromStr; use enumflags2::{bitflags, BitFlags}; -use glam::Vec3; use itertools::Itertools; +use joko_core::serde_glam::Vec3; +use jokoapi::end_point::professions::Profession; +use jokoapi::end_point::specializations::Specialization; +use serde::{Deserialize, Serialize}; use tracing::info; -use xot::Element; +use xot::{Element, NameId, Xot}; -use crate::io::XotAttributeNameIDs; - -use super::RelativePath; +use joko_core::RelativePath; use jokoapi::end_point::mounts::Mount; use jokoapi::end_point::races::Race; use smol_str::SmolStr; + +pub struct XotAttributeNameIDs { + // xml tags + pub overlay_data: NameId, + pub marker_category: NameId, + pub pois: NameId, + pub poi: NameId, + pub trail: NameId, + pub route: NameId, + // marker specific attributes + pub category: NameId, + pub guid: NameId, + pub map_id: NameId, + pub xpos: NameId, + pub ypos: NameId, + pub zpos: NameId, + // marker category specific attributes + pub default_enabled: NameId, + pub display_name: NameId, + pub name: NameId, + pub capital_name: NameId, //same than "name" but with a starting capital letter + pub separator: NameId, + // inheritable attributes + pub achievement_id: NameId, + pub achievement_bit: NameId, + pub alpha: NameId, + pub anim_speed: NameId, + pub auto_trigger: NameId, + pub behavior: NameId, + pub bounce: NameId, + pub bounce_delay: NameId, + pub bounce_duration: NameId, + pub bounce_height: NameId, + pub can_fade: NameId, + pub color: NameId, + pub copy: NameId, + pub copy_message: NameId, + pub cull: NameId, + pub fade_far: NameId, + pub fade_near: NameId, + pub festival: NameId, + pub has_countdown: NameId, + pub height_offset: NameId, + pub hide: NameId, + pub icon_file: NameId, + pub icon_size: NameId, + pub in_game_visibility: NameId, + pub info: NameId, + pub info_range: NameId, + pub invert_behavior: NameId, + pub is_wall: NameId, + pub keep_on_map_edge: NameId, + pub map_display_size: NameId, + pub map_fade_out_scale_level: NameId, + pub map_type: NameId, + pub map_visibility: NameId, + pub max_size: NameId, + pub min_size: NameId, + pub mini_map_visibility: NameId, + pub mount: NameId, + pub profession: NameId, + pub race: NameId, + pub reset_length: NameId, + pub reset_offset: NameId, + pub rotate: NameId, + pub rotate_x: NameId, + pub rotate_y: NameId, + pub rotate_z: NameId, + pub scale_on_map_with_zoom: NameId, + pub show: NameId, + pub specialization: NameId, + pub text: NameId, + pub texture: NameId, + pub tip_name: NameId, + pub tip_description: NameId, + pub title: NameId, + pub title_color: NameId, + pub toggle_category: NameId, + pub trail_data: NameId, + pub trail_scale: NameId, + pub trigger_range: NameId, + pub reset_range: NameId, + pub resetposx: NameId, + pub resetposy: NameId, + pub resetposz: NameId, + pub _source_file_name: NameId, +} + +impl XotAttributeNameIDs { + pub fn register_with_xot(tree: &mut Xot) -> Self { + Self { + // tags + overlay_data: tree.add_name("OverlayData"), + marker_category: tree.add_name("MarkerCategory"), + pois: tree.add_name("POIs"), + poi: tree.add_name("POI"), + trail: tree.add_name("Trail"), + route: tree.add_name("Route"), + // non inheritable attributes + category: tree.add_name("type"), + xpos: tree.add_name("xpos"), + ypos: tree.add_name("ypos"), + zpos: tree.add_name("zpos"), + map_id: tree.add_name("MapID"), + guid: tree.add_name("GUID"), + + // marker category specific attrs + separator: tree.add_name("IsSeparator"), + default_enabled: tree.add_name("defaulttoggle"), + display_name: tree.add_name("DisplayName"), + name: tree.add_name("name"), + capital_name: tree.add_name("Name"), + // inheritable attributes + achievement_id: tree.add_name("achievementId"), + achievement_bit: tree.add_name("achievementBit"), + alpha: tree.add_name("alpha"), + anim_speed: tree.add_name("animSpeed"), + auto_trigger: tree.add_name("autotrigger"), + behavior: tree.add_name("behavior"), + color: tree.add_name("color"), + copy: tree.add_name("copy"), + copy_message: tree.add_name("copy-message"), + fade_near: tree.add_name("fadeNear"), + fade_far: tree.add_name("fadeFar"), + festival: tree.add_name("festival"), + has_countdown: tree.add_name("hasCountdown"), + height_offset: tree.add_name("heightOffset"), + icon_file: tree.add_name("iconFile"), + icon_size: tree.add_name("iconSize"), + in_game_visibility: tree.add_name("inGameVisibility"), + info: tree.add_name("info"), + info_range: tree.add_name("infoRange"), + map_display_size: tree.add_name("mapDisplaySize"), + map_visibility: tree.add_name("mapVisibility"), + max_size: tree.add_name("maxSize"), + min_size: tree.add_name("minSize"), + mini_map_visibility: tree.add_name("miniMapVisibility"), + mount: tree.add_name("mount"), + profession: tree.add_name("profession"), + race: tree.add_name("race"), + reset_length: tree.add_name("resetLength"), + reset_offset: tree.add_name("resetOffset"), + scale_on_map_with_zoom: tree.add_name("scaleOnMapWithZoom"), + tip_name: tree.add_name("tip-name"), + tip_description: tree.add_name("tip-description"), + toggle_category: tree.add_name("togglecateogry"), + texture: tree.add_name("texture"), + trail_data: tree.add_name("trailData"), + trail_scale: tree.add_name("trailScale"), + trigger_range: tree.add_name("triggerRange"), + bounce_delay: tree.add_name("bounce-delay"), + bounce_duration: tree.add_name("bounce-duration"), + bounce_height: tree.add_name("bounce-height"), + can_fade: tree.add_name("canfade"), + cull: tree.add_name("cull"), + hide: tree.add_name("hide"), + is_wall: tree.add_name("iswall"), + invert_behavior: tree.add_name("invertbehavior"), + map_type: tree.add_name("maptype"), + rotate: tree.add_name("rotate"), + rotate_x: tree.add_name("rotate-x"), + rotate_y: tree.add_name("rotate-y"), + rotate_z: tree.add_name("rotate-z"), + show: tree.add_name("show"), + specialization: tree.add_name("specialization"), + title: tree.add_name("title"), + title_color: tree.add_name("title-color"), + text: tree.add_name("text"), + bounce: tree.add_name("bounce"), + keep_on_map_edge: tree.add_name("keepOnMapEdge"), + map_fade_out_scale_level: tree.add_name("mapFadeoutScaleLevel"), + reset_range: tree.add_name("resetrange"), + resetposx: tree.add_name("resetposx"), + resetposy: tree.add_name("resetposy"), + resetposz: tree.add_name("resetposz"), + _source_file_name: tree.add_name("_source_file_name"), + } + } +} + /// This is a onetime macro to reduce code duplication /// It basically takes the CommmonAttributes struct, adds the active_attributes and bool_attributes fields to it. /// Then, it creates a method call `inherit_if_attr_none`, which will clone fields from other struct, if its own fields are not active (set) @@ -64,6 +245,7 @@ macro_rules! common_attributes_struct_macro { } } } + /// uses the [ToString] impl of attributes to serialize them (only if the relevant active attribute flag is set) /// /// #### Args: @@ -71,12 +253,12 @@ macro_rules! common_attributes_struct_macro { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute(names.field1, ca.field1.to_string()); /// } @@ -88,6 +270,7 @@ macro_rules! set_attribute_to_ele { })+ }; } + /// true -> 1 and 0 -> false. (only if the relevant active attribute flag is set) /// /// #### Args: @@ -95,12 +278,12 @@ macro_rules! set_attribute_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_bool_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute(names.field1, /// ca @@ -126,6 +309,7 @@ macro_rules! set_attribute_bool_to_ele { })+ }; } + /// iterates over a bitflags field and joins the enabled flags (as str) with comma. (only if the relevant active attribute flag is set) /// /// #### Args: @@ -133,12 +317,12 @@ macro_rules! set_attribute_bool_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// set_attribute_bitflags_as_array_to_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if ca.active_attributes.contains(ActiveAttributes::field1) { /// ele.set_attribute( /// names.field1, @@ -156,6 +340,7 @@ macro_rules! set_attribute_bitflags_as_array_to_ele { })+ }; } + /// uses the [FromStr] impl of attributes to deserialize them (and set the relevant active attribute flag if successful) /// /// #### Args: @@ -163,12 +348,12 @@ macro_rules! set_attribute_bitflags_as_array_to_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// update_attribute_from_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(value) = ele.get_attribute(names.field1) { /// match value.trim().parse() { /// Ok(value) => { @@ -201,6 +386,24 @@ macro_rules! update_attribute_from_ele { }; } +fn parse_boolean(raw_value: &str) -> Option { + let trimmed = raw_value.trim().to_lowercase(); + match trimmed.as_ref() { + "true" => Some(true), + "false" => Some(false), + _ => { + match trimmed.parse::() { + //might entirely get rid of parsing + Ok(parsed_value) => match parsed_value { + 0 | 1 => Some(parsed_value == 1), + _ => None, + }, + Err(_e) => None, + } + } + } +} + /// deserializes an [i8] and matches that as 1 -> true and 0 -> false. /// On success, set the relevant active attribute flag. /// @@ -209,12 +412,12 @@ macro_rules! update_attribute_from_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1, f2, f3...]: an array of field identifiers which will be serialized. -/// ```rust +/// ```rust,ignore /// update_attribute_bool_from_ele!(ca, ele, names, [field1, field2, field3]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(value) = ele.get_attribute(names.field1) { /// match value.trim().parse::() { /// Ok(value) => { @@ -242,30 +445,21 @@ macro_rules! update_attribute_from_ele { macro_rules! update_attribute_bool_from_ele { ($common_attributes: ident, $ele: ident,$names: ident, [$($field: ident),+]) => { $(if let Some(value) = $ele.get_attribute($names.$field) { - match value.trim().parse::() { - Ok(value) => { - match value { - 0 | 1 => { - $common_attributes - .active_attributes - .insert(ActiveAttributes::$field); - $common_attributes.bool_attributes.set( - BoolAttributes::$field, - if value == 0 { false } else { true }, - ); - } - _ => { - info!(value, "failed to parse {}", stringify!($field)); - } - } - } - Err(e) => { - tracing::info!(?e, value, "failed to parse {}", stringify!($field)); - } + if let Some(found) = parse_boolean(value) { + $common_attributes + .active_attributes + .insert(ActiveAttributes::$field); + $common_attributes.bool_attributes.set( + BoolAttributes::$field, + found, + ); + } else { + tracing::info!(value, "failed to parse {}", stringify!($field)); } })+ }; } + /// deserializes an [i8] and matches that as 1 -> true and 0 -> false. /// On success, set the relevant active attribute flag. /// @@ -274,18 +468,18 @@ macro_rules! update_attribute_bool_from_ele { /// - ele: &[xot::Element] (xot Element to which we are serializing our fields to) /// - names: &[XotAttributeNameIDs] (which contains the name ids of our fields) /// - [f1,t1; f2,t2;...]: an array of field identifiers which will be serialized and their enum type. -/// ```rust +/// ```rust,ignore /// update_attribute_bitflags_array_from_ele!(ca, ele, names, [f1, t1; f2, t2]); /// ``` /// /// The expansion for each field is like this -/// ```rust +/// ```rust,ignore /// if let Some(field1_str) = ele.get_attribute(names.field1) { /// for value in field1_str.split(',') { /// match value.trim().parse::() { /// Ok(flag) => { /// ca -/// .active_attribus +/// .active_attributes /// .insert(ActiveAttributes::field1); /// ca.field1.set(flag); /// } @@ -313,8 +507,9 @@ macro_rules! update_attribute_bitflags_array_from_ele { })+ }; } + /// generates getters for bool attributes -/// ```rust +/// ```rust,ignore /// getters_for_bool_attributes!([field1, field2, field3]); /// ``` /// @@ -336,8 +531,9 @@ macro_rules! getters_for_bool_attributes { } }; } + /// generates setters for bool attributes -/// ```rust +/// ```rust,ignore /// setters_for_bool_attributes!([field1, field2, field3]); /// ``` /// @@ -362,10 +558,11 @@ macro_rules! setters_for_bool_attributes { } }; } + common_attributes_struct_macro!( /// the struct we use for inheritance from category/other markers. - #[derive(Debug, Clone, Default)] - pub(crate) struct CommonAttributes { + #[derive(Debug, Clone, Default, Serialize, Deserialize)] + pub struct CommonAttributes { /// An ID for an achievement from the GW2 API. Markers with the corresponding achievement ID will be hidden if the ID is marked as "done" for the API key that's entered in TacO. achievement_id: u32, /// This is similar to achievementId, but works for partially completed achievements as well, if the achievement has "bits", they can be individually referenced with this. @@ -469,7 +666,7 @@ impl CommonAttributes { mini_map_visibility, scale_on_map_with_zoom ]); - pub(crate) fn update_common_attributes_from_element( + pub fn update_common_attributes_from_element( &mut self, ele: &Element, names: &XotAttributeNameIDs, @@ -523,7 +720,7 @@ impl CommonAttributes { Ok(f) => { if let Some(x) = array.get_mut(index) { *x = f; - self.rotate = array.into(); + self.rotate = Vec3(glam::Vec3::from_array(array)); self.active_attributes.insert(ActiveAttributes::rotate); } } @@ -622,7 +819,7 @@ impl CommonAttributes { ); } - pub(crate) fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { + pub fn serialize_to_element(&self, ele: &mut Element, names: &XotAttributeNameIDs) { // color arrays if self.active_attributes.contains(ActiveAttributes::color) { ele.set_attribute(names.color, data_encoding::HEXLOWER.encode(&self.color)); @@ -640,7 +837,10 @@ impl CommonAttributes { if self.active_attributes.contains(ActiveAttributes::rotate) { ele.set_attribute( names.rotate, - format!("{},{},{}", self.rotate.x, self.rotate.y, self.rotate.z), + format!( + "{},{},{}", + self.rotate.0.x, self.rotate.0.y, self.rotate.0.z + ), ); } // spec vector @@ -767,6 +967,7 @@ pub enum BoolAttributes { /// scaling of marker on 2d map (or minimap) scale_on_map_with_zoom = 1 << 9, } + #[allow(non_camel_case_types)] #[bitflags] #[repr(u64)] @@ -831,7 +1032,8 @@ pub enum ActiveAttributes { trail_scale = 1 << 56, trigger_range = 1 << 57, } -#[derive(Debug, Clone, Copy, PartialEq, Default)] + +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] pub enum Behavior { #[default] AlwaysVisible, @@ -855,6 +1057,7 @@ pub enum Behavior { WvWObjective, WeeklyReset = 101, } + impl FromStr for Behavior { type Err = &'static str; @@ -875,66 +1078,15 @@ impl FromStr for Behavior { }) } } -/// Filter which professions the marker should be active for. if its null, its available for all professions -#[bitflags] -#[repr(u16)] -#[derive(Debug, Clone, Copy)] -pub enum Profession { - Elementalist = 1 << 0, - Engineer = 1 << 1, - Guardian = 1 << 2, - Mesmer = 1 << 3, - Necromancer = 1 << 4, - Ranger = 1 << 5, - Revenant = 1 << 6, - Thief = 1 << 7, - Warrior = 1 << 8, -} -impl FromStr for Profession { - type Err = &'static str; - fn from_str(s: &str) -> Result { - Ok(match s { - "guardian" => Profession::Guardian, - "warrior" => Profession::Warrior, - "engineer" => Profession::Engineer, - "ranger" => Profession::Ranger, - "thief" => Profession::Thief, - "elementalist" => Profession::Elementalist, - "mesmer" => Profession::Mesmer, - "necromancer" => Profession::Necromancer, - "revenant" => Profession::Revenant, - _ => return Err("invalid profession"), - }) - } -} -impl AsRef for Profession { - fn as_ref(&self) -> &str { - match self { - Profession::Guardian => "guardian", - Profession::Warrior => "warrior", - Profession::Engineer => "engineer", - Profession::Ranger => "ranger", - Profession::Thief => "thief", - Profession::Elementalist => "elementalist", - Profession::Mesmer => "mesmer", - Profession::Necromancer => "necromancer", - Profession::Revenant => "revenant", - } - } -} -impl ToString for Profession { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub enum Cull { #[default] None, ClockWise, CounterClockWise, } + impl FromStr for Cull { type Err = &'static str; @@ -949,6 +1101,7 @@ impl FromStr for Cull { }) } } + impl AsRef for Cull { fn as_ref(&self) -> &'static str { match self { @@ -958,15 +1111,17 @@ impl AsRef for Cull { } } } -impl ToString for Cull { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for Cull { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } + /// Filter for which festivals will the marker be active for #[bitflags] #[repr(u8)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Festival { DragonBash = 1 << 0, #[allow(clippy::enum_variant_names)] @@ -976,6 +1131,7 @@ pub enum Festival { SuperAdventureBox = 1 << 4, Wintersday = 1 << 5, } + impl FromStr for Festival { type Err = &'static str; @@ -991,6 +1147,7 @@ impl FromStr for Festival { }) } } + impl AsRef for Festival { fn as_ref(&self) -> &'static str { match self { @@ -1003,254 +1160,13 @@ impl AsRef for Festival { } } } -impl ToString for Festival { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} -/// Filter for which specializations (the third traitline) will the marker be active for -#[derive(Debug, Clone, Copy)] -#[repr(u8)] -pub enum Specialization { - Dueling = 0, - DeathMagic = 1, - Invocation = 2, - Strength = 3, - Druid = 4, - Explosives = 5, - Daredevil = 6, - Marksmanship = 7, - Retribution = 8, - Domination = 9, - Tactics = 10, - Salvation = 11, - Valor = 12, - Corruption = 13, - Devastation = 14, - Radiance = 15, - Water = 16, - Berserker = 17, - BloodMagic = 18, - ShadowArts = 19, - Tools = 20, - Defense = 21, - Inspiration = 22, - Illusions = 23, - NatureMagic = 24, - Earth = 25, - Dragonhunter = 26, - DeadlyArts = 27, - Alchemy = 28, - Skirmishing = 29, - Fire = 30, - BeastMastery = 31, - WildernessSurvival = 32, - Reaper = 33, - CriticalStrikes = 34, - Arms = 35, - Arcane = 36, - Firearms = 37, - Curses = 38, - Chronomancer = 39, - Air = 40, - Zeal = 41, - Scrapper = 42, - Trickery = 43, - Chaos = 44, - Virtues = 45, - Inventions = 46, - Tempest = 47, - Honor = 48, - SoulReaping = 49, - Discipline = 50, - Herald = 51, - Spite = 52, - Acrobatics = 53, - Soulbeast = 54, - Weaver = 55, - Holosmith = 56, - Deadeye = 57, - Mirage = 58, - Scourge = 59, - Spellbreaker = 60, - Firebrand = 61, - Renegade = 62, - Harbinger = 63, - Willbender = 64, - Virtuoso = 65, - Catalyst = 66, - Bladesworn = 67, - Vindicator = 68, - Mechanist = 69, - Specter = 70, - Untamed = 71, -} - -impl FromStr for Specialization { - type Err = &'static str; - fn from_str(s: &str) -> Result { - Ok(match s { - "dueling" => Self::Dueling, - "deathmagic" => Self::DeathMagic, - "invocation" => Self::Invocation, - "strength" => Self::Strength, - "druid" => Self::Druid, - "explosives" => Self::Explosives, - "daredevil" => Self::Daredevil, - "marksmanship" => Self::Marksmanship, - "retribution" => Self::Retribution, - "domination" => Self::Domination, - "tactics" => Self::Tactics, - "salvation" => Self::Salvation, - "valor" => Self::Valor, - "corruption" => Self::Corruption, - "devastation" => Self::Devastation, - "radiance" => Self::Radiance, - "water" => Self::Water, - "berserker" => Self::Berserker, - "bloodmagic" => Self::BloodMagic, - "shadowarts" => Self::ShadowArts, - "tools" => Self::Tools, - "defense" => Self::Defense, - "inspiration" => Self::Inspiration, - "illusions" => Self::Illusions, - "naturemagic" => Self::NatureMagic, - "earth" => Self::Earth, - "dragonhunter" => Self::Dragonhunter, - "deadlyarts" => Self::DeadlyArts, - "alchemy" => Self::Alchemy, - "skirmishing" => Self::Skirmishing, - "fire" => Self::Fire, - "beastmastery" => Self::BeastMastery, - "wildernesssurvival" => Self::WildernessSurvival, - "reaper" => Self::Reaper, - "criticalstrikes" => Self::CriticalStrikes, - "arms" => Self::Arms, - "arcane" => Self::Arcane, - "firearms" => Self::Firearms, - "curses" => Self::Curses, - "chronomancer" => Self::Chronomancer, - "air" => Self::Air, - "zeal" => Self::Zeal, - "scrapper" => Self::Scrapper, - "trickery" => Self::Trickery, - "chaos" => Self::Chaos, - "virtues" => Self::Virtues, - "inventions" => Self::Inventions, - "tempest" => Self::Tempest, - "honor" => Self::Honor, - "soulreaping" => Self::SoulReaping, - "discipline" => Self::Discipline, - "herald" => Self::Herald, - "spite" => Self::Spite, - "acrobatics" => Self::Acrobatics, - "soulbeast" => Self::Soulbeast, - "weaver" => Self::Weaver, - "holosmith" => Self::Holosmith, - "deadeye" => Self::Deadeye, - "mirage" => Self::Mirage, - "scourge" => Self::Scourge, - "spellbreaker" => Self::Spellbreaker, - "firebrand" => Self::Firebrand, - "renegade" => Self::Renegade, - "harbinger" => Self::Harbinger, - "willbender" => Self::Willbender, - "virtuoso" => Self::Virtuoso, - "catalyst" => Self::Catalyst, - "bladesworn" => Self::Bladesworn, - "vindicator" => Self::Vindicator, - "mechanist" => Self::Mechanist, - "specter" => Self::Specter, - "untamed" => Self::Untamed, - _ => return Err("invalid specialization"), - }) - } -} -impl AsRef for Specialization { - fn as_ref(&self) -> &str { - match self { - Self::Dueling => "dueling", - Self::DeathMagic => "deathmagic", - Self::Invocation => "invocation", - Self::Strength => "strength", - Self::Druid => "druid", - Self::Explosives => "explosives", - Self::Daredevil => "daredevil", - Self::Marksmanship => "marksmanship", - Self::Retribution => "retribution", - Self::Domination => "domination", - Self::Tactics => "tactics", - Self::Salvation => "salvation", - Self::Valor => "valor", - Self::Corruption => "corruption", - Self::Devastation => "devastation", - Self::Radiance => "radiance", - Self::Water => "water", - Self::Berserker => "berserker", - Self::BloodMagic => "bloodmagic", - Self::ShadowArts => "shadowarts", - Self::Tools => "tools", - Self::Defense => "defense", - Self::Inspiration => "inspiration", - Self::Illusions => "illusions", - Self::NatureMagic => "naturemagic", - Self::Earth => "earth", - Self::Dragonhunter => "dragonhunter", - Self::DeadlyArts => "deadlyarts", - Self::Alchemy => "alchemy", - Self::Skirmishing => "skirmishing", - Self::Fire => "fire", - Self::BeastMastery => "beastmastery", - Self::WildernessSurvival => "wildernesssurvival", - Self::Reaper => "reaper", - Self::CriticalStrikes => "criticalstrikes", - Self::Arms => "arms", - Self::Arcane => "arcane", - Self::Firearms => "firearms", - Self::Curses => "curses", - Self::Chronomancer => "chronomancer", - Self::Air => "air", - Self::Zeal => "zeal", - Self::Scrapper => "scrapper", - Self::Trickery => "trickery", - Self::Chaos => "chaos", - Self::Virtues => "virtues", - Self::Inventions => "inventions", - Self::Tempest => "tempest", - Self::Honor => "honor", - Self::SoulReaping => "soulreaping", - Self::Discipline => "discipline", - Self::Herald => "herald", - Self::Spite => "spite", - Self::Acrobatics => "acrobatics", - Self::Soulbeast => "soulbeast", - Self::Weaver => "weaver", - Self::Holosmith => "holosmith", - Self::Deadeye => "deadeye", - Self::Mirage => "mirage", - Self::Scourge => "scourge", - Self::Spellbreaker => "spellbreaker", - Self::Firebrand => "firebrand", - Self::Renegade => "renegade", - Self::Harbinger => "harbinger", - Self::Willbender => "willbender", - Self::Virtuoso => "virtuoso", - Self::Catalyst => "catalyst", - Self::Bladesworn => "bladesworn", - Self::Vindicator => "vindicator", - Self::Mechanist => "mechanist", - Self::Specter => "specter", - Self::Untamed => "untamed", - } +impl std::fmt::Display for Festival { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } -impl ToString for Specialization { - fn to_string(&self) -> String { - self.as_ref().to_string() - } -} /// Most of this data is stolen from BlishHUD. #[bitflags] #[repr(u32)] @@ -1295,22 +1211,26 @@ pub enum MapType { /// WvW lounge map type, e.g. Armistice Bastion. WvwLounge = 1 << 18, } + impl FromStr for MapType { type Err = &'static str; fn from_str(_s: &str) -> Result { unimplemented!("needs research to verify the map type values") } } + impl AsRef for MapType { fn as_ref(&self) -> &str { unimplemented!("needs research to verify the maptype values") } } -impl ToString for MapType { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for MapType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } + /// made it using multi cursor (ctrl + shift + L) by copy-pasting json from api #[allow(unused)] pub static MAP_ID_TO_NAME: phf::OrderedMap = phf::phf_ordered_map! { diff --git a/crates/joko_package_models/src/category.rs b/crates/joko_package_models/src/category.rs new file mode 100644 index 0000000..d6604f8 --- /dev/null +++ b/crates/joko_package_models/src/category.rs @@ -0,0 +1,291 @@ +use crate::{attributes::CommonAttributes, package::PackageImportReport}; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use tracing::debug; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct RawCategory { + pub guid: Uuid, + pub parent_name: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, + pub sources: IndexMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Category { + pub guid: Uuid, + pub parent: Option, + pub display_name: String, + pub relative_category_name: String, + pub full_category_name: String, + pub separator: bool, + pub default_enabled: bool, + pub props: CommonAttributes, + pub children: IndexMap, //TODO: make a branch to test if having an Vec associated with global list of categories is faster. +} + +pub fn nth_chunk(s: &str, pat: char, n: usize) -> String { + let nb_matches = s.matches(pat).count(); + assert!(nb_matches + 1 > n); + let res = s.split(pat).nth(n); + debug!("nth_chunk {} {} {:?}", s, n, res); + res.unwrap().to_string() +} + +pub fn prefix_until_nth_char(s: &str, pat: char, n: usize) -> Option { + let res = s + .match_indices(pat) + .nth(n) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_until_nth_char {} {} {:?}", s, n, res); + res +} + +pub fn prefix_parent(s: &str, pat: char) -> Option { + let n = s.matches(pat).count(); + assert!(n > 0); + let res = s + .match_indices(pat) + .nth(n - 1) + .map(|(index, _)| s.split_at(index)) + .map(|(left, _)| left.to_string()); + debug!("prefix_parent {} {} {:?}", s, n, res); + res +} + +impl Category { + // Required method + pub fn from(value: &RawCategory, parent: Option) -> Self { + Self { + guid: value.guid, + props: value.props.clone(), + separator: value.separator, + default_enabled: value.default_enabled, + display_name: value.display_name.clone(), + relative_category_name: value.relative_category_name.clone(), + full_category_name: value.full_category_name.clone(), + parent, + children: Default::default(), + } + } + fn per_route<'a>( + categories: &'a mut IndexMap, + route: &[&str], + ) -> Option<&'a mut Category> { + let mut route = route.to_owned(); + route.reverse(); + Category::_per_route(categories, &mut route) + } + fn _per_route<'a>( + categories: &'a mut IndexMap, + route: &mut Vec<&str>, + ) -> Option<&'a mut Category> { + if let Some(relative_category_name) = route.pop() { + for (_, cat) in categories { + if cat.relative_category_name == relative_category_name { + if route.is_empty() { + return Some(cat); + } else { + return Category::_per_route(&mut cat.children, route); + } + } + } + } + None + } + #[allow(dead_code)] + fn per_uuid<'a>( + categories: &'a mut IndexMap, + uuid: &Uuid, + ) -> Option<&'a mut Category> { + /* + Do a look up in the tree based on uuid. Whole tree is scanned until a match is found. + + WARNING: very inefficient in the general case. + */ + for (_, cat) in categories { + if &cat.guid == uuid { + return Some(cat); + } + let sub_res = Category::per_uuid(&mut cat.children, uuid); + if sub_res.is_some() { + return sub_res; + } + } + None + } + pub fn reassemble( + input_first_pass_categories: &IndexMap, + report: &mut PackageImportReport, + ) -> IndexMap { + let start_initialize = std::time::SystemTime::now(); + let mut first_pass_categories = input_first_pass_categories.clone(); + let mut second_pass_categories: IndexMap = Default::default(); + let mut need_a_pass: bool = true; + + let mut third_pass_categories: IndexMap = Default::default(); + let mut third_pass_categories_ref: Vec = Default::default(); + let mut root: IndexMap = Default::default(); + + let elaspsed_initialize = start_initialize.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.initialize = elaspsed_initialize.as_millis(); + + let start_multi_pass_missing_categories_creation = std::time::SystemTime::now(); + let mut nb_pass_done = 0; + while need_a_pass { + need_a_pass = false; + nb_pass_done += 1; + for (key, value) in first_pass_categories.iter() { + debug!("reassemble_categories pass #{} {:?}", nb_pass_done, value); + let mut to_insert = value.clone(); + if value.relative_category_name.matches('.').count() > 0 + && value.relative_category_name == value.full_category_name + { + let mut n = 0; + let mut last_name: Option = None; + // This is an almost duplication of code of pack/mod.rs + while let Some(parent_name) = + prefix_until_nth_char(&value.relative_category_name, '.', n) + { + debug!("{} {}", parent_name, n); + if let Some(parent_category) = first_pass_categories.get(&parent_name) { + report.found_category_late(&parent_name, parent_category.guid); + last_name = Some(parent_name.clone()); + } else if let Some(parent_category) = + second_pass_categories.get(&parent_name) + { + report.found_category_late(&parent_name, parent_category.guid); + last_name = Some(parent_name.clone()); + } else { + let new_uuid = Uuid::new_v4(); + let relative_category_name = + nth_chunk(&value.relative_category_name, '.', n); + debug!("reassemble_categories Partial create missing parent category: {} {} {} {}", parent_name, relative_category_name, n, new_uuid); + let sources: IndexMap = IndexMap::new(); + let to_insert = RawCategory { + default_enabled: value.default_enabled, + guid: new_uuid, + relative_category_name: relative_category_name.clone(), + display_name: relative_category_name.clone(), + parent_name: prefix_until_nth_char(&parent_name, '.', n - 1), + props: value.props.clone(), + separator: false, + full_category_name: parent_name.clone(), + sources, + }; + last_name = Some(to_insert.full_category_name.clone()); + report.found_category_late(&to_insert.full_category_name, new_uuid); + second_pass_categories.insert(parent_name.clone(), to_insert); + need_a_pass = true; + } + n += 1; + } + for (requester_uuid, source_file_uuid) in value.sources.iter() { + report.found_category_late_with_details( + &value.full_category_name, + value.guid, + requester_uuid, + source_file_uuid, + ); + } + report.found_category_late(&value.full_category_name, value.guid); + to_insert.relative_category_name = + nth_chunk(&value.relative_category_name, '.', n); + to_insert + .display_name + .clone_from(&to_insert.relative_category_name); + debug!( + "parent_name: {:?}, new name: {}, old name: {}", + last_name, to_insert.relative_category_name, &value.relative_category_name + ); + assert!(last_name.is_some()); + to_insert.parent_name = last_name; + } else { + to_insert.parent_name = if let Some(parent_name) = &value.parent_name { + first_pass_categories + .get(parent_name) + .map(|parent_category| parent_category.full_category_name.clone()) + } else { + None + }; + debug!("insert as is {:?}", to_insert); + } + second_pass_categories.insert(key.clone(), to_insert); + } + if need_a_pass { + std::mem::swap(&mut first_pass_categories, &mut second_pass_categories); + second_pass_categories.clear(); + } + } + let elaspsed_multi_pass_missing_categories_creation = + start_multi_pass_missing_categories_creation + .elapsed() + .unwrap_or_default(); + report + .telemetry + .categories_reassemble + .missing_categories_creation = + elaspsed_multi_pass_missing_categories_creation.as_millis(); + + debug!("nb_pass_done {}", nb_pass_done); + let start_parent_child_relationship = std::time::SystemTime::now(); + for (key, value) in second_pass_categories { + let parent = if let Some(parent_name) = &value.parent_name { + first_pass_categories + .get(parent_name) + .map(|parent_category| parent_category.guid) + } else { + None + }; + + debug!("{} parent is {:?}", key, parent); + let cat = Category::from(&value, parent); + let cat_ref = cat.guid; + if third_pass_categories.insert(cat.guid, cat).is_none() { + third_pass_categories_ref.push(cat_ref); + } + } + let elaspsed_parent_child_relationship = start_parent_child_relationship + .elapsed() + .unwrap_or_default(); + report + .telemetry + .categories_reassemble + .parent_child_relationship = elaspsed_parent_child_relationship.as_millis(); + + debug!("third_pass_categories_ref"); + let start_tree_insertion = std::time::SystemTime::now(); + for full_category_uuid in third_pass_categories_ref { + if let Some(cat) = third_pass_categories.shift_remove(&full_category_uuid) { + let mut route = Vec::from_iter(cat.full_category_name.split('.')); + route.pop(); //it is now the parent route + if let Some(parent) = cat.parent { + if let Some(parent_category) = + Category::per_route(&mut third_pass_categories, &route) + { + parent_category.children.insert(cat.guid, cat); + } else if let Some(parent_category) = Category::per_route(&mut root, &route) { + parent_category.children.insert(cat.guid, cat); + } else { + panic!("Could not find parent {} for {:?}", parent, cat); + } + } else { + root.insert(cat.guid, cat); + } + } else { + panic!("Some bad logic at works"); + } + } + let elaspsed_tree_insertion = start_tree_insertion.elapsed().unwrap_or_default(); + report.telemetry.categories_reassemble.tree_insertion = elaspsed_tree_insertion.as_millis(); + debug!("reassemble_categories end {:?}", root); + root + } +} diff --git a/crates/joko_package_models/src/lib.rs b/crates/joko_package_models/src/lib.rs new file mode 100644 index 0000000..a6f2fdf --- /dev/null +++ b/crates/joko_package_models/src/lib.rs @@ -0,0 +1,7 @@ +pub mod attributes; +pub mod category; +pub mod map; +pub mod marker; +pub mod package; +pub mod route; +pub mod trail; diff --git a/crates/joko_package_models/src/map.rs b/crates/joko_package_models/src/map.rs new file mode 100644 index 0000000..a35d361 --- /dev/null +++ b/crates/joko_package_models/src/map.rs @@ -0,0 +1,13 @@ +use crate::marker::Marker; +use crate::route::Route; +use crate::trail::Trail; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct MapData { + pub markers: IndexMap, + pub routes: IndexMap, + pub trails: IndexMap, +} diff --git a/crates/joko_package_models/src/marker.rs b/crates/joko_package_models/src/marker.rs new file mode 100644 index 0000000..5258442 --- /dev/null +++ b/crates/joko_package_models/src/marker.rs @@ -0,0 +1,15 @@ +use crate::attributes::CommonAttributes; +use joko_core::serde_glam::Vec3; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Marker { + pub guid: Uuid, + pub parent: Uuid, + pub position: Vec3, + pub map_id: u32, + pub category: String, + pub source_file_uuid: Uuid, + pub attrs: CommonAttributes, +} diff --git a/crates/joko_package_models/src/package.rs b/crates/joko_package_models/src/package.rs new file mode 100644 index 0000000..f7b4e52 --- /dev/null +++ b/crates/joko_package_models/src/package.rs @@ -0,0 +1,521 @@ +use crate::category::{prefix_until_nth_char, Category}; +use crate::map::MapData; +use crate::marker::Marker; +use crate::route::{route_to_tbin, route_to_trail, Route}; +use crate::trail::{TBin, Trail}; +use base64::Engine; +use indexmap::IndexMap; +use joko_core::RelativePath; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use tracing::{debug, trace}; +use uuid::Uuid; + +pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); + +fn serialize_reference(reference: &ElementReference, serializer: S) -> Result +where + S: Serializer, +{ + match reference { + ElementReference::Uuid(uuid) => { + let to_do = BASE64_ENGINE.encode(uuid); + serializer.serialize_str(to_do.as_str()) + } + ElementReference::Category(full_category_name) => { + serializer.serialize_str(full_category_name.as_str()) + } + } +} + +fn deserialize_reference<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let encoded_uuid_or_full_category_name = String::deserialize(deserializer)?; + if let Ok(bytes) = BASE64_ENGINE.decode(encoded_uuid_or_full_category_name.as_bytes()) { + let mut uuid_bytes: [u8; 16] = Default::default(); + uuid_bytes.copy_from_slice(bytes.as_slice()); + let res = Uuid::from_bytes(uuid_bytes); + Ok(ElementReference::Uuid(res)) + } else { + Ok(ElementReference::Category( + encoded_uuid_or_full_category_name, + )) + } +} + +fn serialize_uuid_in_base64(uuid: &Uuid, serializer: S) -> Result +where + S: Serializer, +{ + let to_do = BASE64_ENGINE.encode(uuid); + serializer.serialize_str(to_do.as_str()) +} + +fn deserialize_uuid_in_base64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let encoded = String::deserialize(deserializer)?; + if let Ok(bytes) = BASE64_ENGINE.decode(encoded.as_bytes()) { + let mut uuid_bytes: [u8; 16] = Default::default(); + uuid_bytes.copy_from_slice(bytes.as_slice()); + let res = Uuid::from_bytes(uuid_bytes); + Ok(res) + } else { + Err(serde::de::Error::custom( + "Could not parse base64 encoded uuid", + )) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PackageCategorySource { + full_category_name: String, + #[serde( + serialize_with = "serialize_uuid_in_base64", + deserialize_with = "deserialize_uuid_in_base64" + )] + requester_uuid: Uuid, + source_file_name: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +enum ElementReference { + Uuid(Uuid), + Category(String), +} +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PackageElementSource { + file_path: String, + #[serde( + serialize_with = "serialize_reference", + deserialize_with = "deserialize_reference" + )] + requester_reference: ElementReference, + source_file_name: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PackageImportStatistics { + pub categories: usize, // total number of found categories + pub missing_categories: usize, // categories that should be defined in a node + pub textures: usize, //total number of texture used (or should) + pub missing_textures: usize, // how many of the textures are missing + pub entities: usize, // total number of tracked elements: categories, trails, markers, ... + pub markers: usize, // total number of markers + pub trails: usize, // total number of trails + pub routes: usize, // total number of routes defined, they shall not count as trails even if imported as such + pub maps: usize, // total number of maps covered + pub source_files: usize, // total number of XML files +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PackageImportReassembleTelemetry { + pub total: u128, + pub initialize: u128, + pub missing_categories_creation: u128, + pub parent_child_relationship: u128, + pub tree_insertion: u128, +} +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PackageImportTelemetry { + pub total: u128, + pub texture_loading: u128, + pub categories_loading: u128, + pub categories_first_pass: u128, + pub categories_second_pass: u128, + pub categories_registering: u128, + pub categories_reassemble: PackageImportReassembleTelemetry, + pub elements_registering: u128, +} +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PackageImportReport { + #[serde(skip)] + pub uuid: Uuid, + pub number_of: PackageImportStatistics, // count everything we can think of + pub telemetry: PackageImportTelemetry, // all the time spent in which step + late_discovered_categories: IndexMap, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + missing_categories: Vec, //categories that are defined only from a marker point of view. It needs to be saved in some way or it's lost at next start. + #[serde(skip)] + _missing_categories_tracker: HashSet, // for tracking purpose to avoid duplicate + #[serde(skip)] + _missing_textures_tracker: HashSet, // for tracking purpose to avoid duplicate + missing_textures: Vec, //missing texture for display + missing_trails: Vec, //missing file for trail + source_files: bimap::BiMap, //map of all files to uuid. When exporting this shall have to be reversed. +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PackCore { + /* + PackCore is a temporary holder of data + It is moved and breaked down into a Data and Texture part. Former for background work and later for UI display. + */ + pub uuid: Uuid, + pub textures: HashMap>, + pub tbins: HashMap, + pub categories: IndexMap, + pub all_categories: HashMap, + pub entities_parents: HashMap, + pub active_source_files: BTreeMap, + pub maps: HashMap, + pub report: PackageImportReport, +} + +impl PackageImportReport { + pub const REPORT_FILE_NAME: &'static str = "import_report.json"; + + pub fn reset_counters(&mut self) { + self.number_of = Default::default(); + } + fn merge_partial(&mut self, partial_report: PackageImportReport) { + self.late_discovered_categories + .extend(partial_report.late_discovered_categories); + } + + pub fn is_category_discovered_late(&self, uuid: Uuid) -> bool { + self.late_discovered_categories.contains_key(&uuid) + } + + pub fn source_file_uuid_to_name(&self, source_file_uuid: &Uuid) -> Option<&String> { + self.source_files.get_by_right(source_file_uuid) + } + pub fn source_file_name_to_uuid(&self, source_file_name: &String) -> Option<&Uuid> { + self.source_files.get_by_left(source_file_name) + } + + pub fn found_category_late(&mut self, full_category_name: &str, category_uuid: Uuid) { + self.late_discovered_categories + .insert(category_uuid, full_category_name.to_owned()); + } + pub fn found_category_late_with_details( + &mut self, + full_category_name: &String, + category_uuid: Uuid, + requester_uuid: &Uuid, + source_file_uuid: &Uuid, + ) { + self.found_category_late(full_category_name, category_uuid); + let source_file_name = self.source_files.get_by_right(source_file_uuid).unwrap(); + + //for this to work we need to keep track of where each category was called and thus defined since late + self.missing_categories.push(PackageCategorySource { + full_category_name: full_category_name.clone(), + requester_uuid: *requester_uuid, + source_file_name: source_file_name.clone(), + }); + if !self + ._missing_categories_tracker + .contains(full_category_name) + { + self.number_of.missing_categories += 1; + self._missing_categories_tracker + .insert(full_category_name.clone()); + } + } + fn found_missing_texture(&mut self, file_path: &String) { + if !self._missing_textures_tracker.contains(file_path) { + self.number_of.missing_textures += 1; + self._missing_textures_tracker.insert(file_path.clone()); + } + } +} + +impl PackCore { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut res = Self { + all_categories: Default::default(), + categories: Default::default(), + entities_parents: Default::default(), + report: Default::default(), + maps: Default::default(), + active_source_files: Default::default(), + tbins: Default::default(), + textures: Default::default(), + uuid: Default::default(), + }; + res.uuid = Uuid::new_v4(); + res.report.uuid = res.uuid; + res + } + pub fn partial(all_categories: &HashMap) -> Self { + // When loading extra data, one MUST know ALL the already existing categories. None MUST be missing. + let mut res: Self = Self::new(); + res.all_categories.clone_from(all_categories); + res + } + + pub fn merge_partial(&mut self, partial_pack: PackCore) { + self.maps.extend(partial_pack.maps); + self.all_categories = partial_pack.all_categories; + self.report.merge_partial(partial_pack.report); + self.active_source_files + .extend(partial_pack.active_source_files); + self.tbins.extend(partial_pack.tbins); + self.entities_parents.extend(partial_pack.entities_parents); + } + pub fn category_exists(&self, full_category_name: &String) -> bool { + self.all_categories.contains_key(full_category_name) + } + + pub fn get_category_uuid(&self, full_category_name: &String) -> Option<&Uuid> { + self.all_categories.get(full_category_name) + } + + pub fn get_or_create_category_uuid( + &mut self, + full_category_name: &String, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) -> Uuid { + if let Some(category_uuid) = self.all_categories.get(full_category_name) { + *category_uuid + } else { + // If imported package is "dirty", create missing category + //TODO: default import mode is "strict" (get inspiration from HTML modes) + debug!("There is no defined category for {}", full_category_name); + + let mut n = 0; + let mut last_uuid: Option = None; + while let Some(parent_full_category_name) = + prefix_until_nth_char(full_category_name, '.', n) + { + n += 1; + if let Some(parent_uuid) = self.all_categories.get(&parent_full_category_name) { + //FIXME: might want to make the difference between impacted parents and actual missing category + self.report + .found_category_late(full_category_name, *parent_uuid); + last_uuid = Some(*parent_uuid); + } else { + let new_uuid = Uuid::new_v4(); + debug!( + "Partial create missing parent category: {} {}", + parent_full_category_name, new_uuid + ); + self.all_categories + .insert(parent_full_category_name.clone(), new_uuid); + self.report.found_category_late_with_details( + full_category_name, + new_uuid, + &requester_uuid, + source_file_uuid, + ); + last_uuid = Some(new_uuid); + } + } + trace!("{} uuid: {:?}", full_category_name, last_uuid); + assert!(last_uuid.is_some()); + last_uuid.unwrap() + } + } + + pub fn get_source_file_uuid(&mut self, source_file_name: &String) -> Uuid { + // Must always exist when called since we registered the file already. + *self + .report + .source_files + .get_by_left(source_file_name) + .unwrap() + } + + pub fn register_source_file(&mut self, source_file_name: &String) -> Uuid { + if !self.report.source_files.contains_left(source_file_name) { + let uuid_to_insert = Uuid::new_v4(); //TODO: have a uuid built from current package name and source file name + self.report + .source_files + .insert(source_file_name.clone(), uuid_to_insert); + self.report.number_of.source_files += 1; + self.active_source_files.insert(uuid_to_insert, true); + uuid_to_insert + } else { + self.get_source_file_uuid(source_file_name) + } + } + pub fn register_texture(&mut self, name: String, file_path: &RelativePath, bytes: Vec) { + assert!( + self.textures.insert(file_path.clone(), bytes).is_none(), + "duplicate image file {name}" + ); + self.report.number_of.textures += 1; + } + + pub fn register_uuid( + &mut self, + full_category_name: &String, + uuid: &Uuid, + ) -> Result { + if let Some(parent_uuid) = self.all_categories.get(full_category_name) { + let mut uuid_to_insert = *uuid; + while self.entities_parents.contains_key(&uuid_to_insert) { + trace!( + "Uuid collision detected {} for elements in {}", + uuid_to_insert, + full_category_name + ); + uuid_to_insert = Uuid::new_v4(); + } + self.entities_parents.insert(uuid_to_insert, *parent_uuid); + self.report.number_of.entities += 1; + Ok(uuid_to_insert) + } else { + // Dirty package ! We could fix it by making usage of the relative category the node is in. + Err(format!( + "Can't register world entity {} {}, no associated category found.", + full_category_name, uuid + )) + } + } + + pub fn register_marker( + &mut self, + full_category_name: String, + mut marker: Marker, + ) -> Result<(), String> { + let uuid_to_insert = self.register_uuid(&full_category_name, &marker.guid)?; + marker.guid = uuid_to_insert; + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(marker.map_id) { + e.insert(MapData::default()); + self.report.number_of.maps += 1; + } + self.maps + .get_mut(&marker.map_id) + .unwrap() + .markers + .insert(uuid_to_insert, marker); + self.report.number_of.markers += 1; + Ok(()) + } + + pub fn register_trail( + &mut self, + full_category_name: String, + mut trail: Trail, + ) -> Result<(), String> { + let uuid_to_insert = self.register_uuid(&full_category_name, &trail.guid)?; + trail.guid = uuid_to_insert; + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(trail.map_id) { + e.insert(MapData::default()); + self.report.number_of.maps += 1; + } + self.maps + .get_mut(&trail.map_id) + .unwrap() + .trails + .insert(uuid_to_insert, trail); + self.report.number_of.trails += 1; + Ok(()) + } + + pub fn register_route(&mut self, mut route: Route) -> Result<(), String> { + let file_name = format!("data/dynamic_trails/{}.trl", &route.guid); + let tbin_path: RelativePath = file_name.parse().unwrap(); + let uuid_to_insert = self.register_uuid(&route.category, &route.guid)?; + route.guid = uuid_to_insert; + let trail = route_to_trail(&route, &tbin_path); + let tbin = route_to_tbin(&route); + + self.tbins.insert(tbin_path, tbin); //there may be duplicates since we load and save each time + if let std::collections::hash_map::Entry::Vacant(e) = self.maps.entry(trail.map_id) { + e.insert(MapData::default()); + self.report.number_of.maps += 1; + } + self.maps + .get_mut(&trail.map_id) + .unwrap() + .trails + .insert(uuid_to_insert, trail); + self.maps + .get_mut(&route.map_id) + .unwrap() + .routes + .insert(uuid_to_insert, route); + self.report.number_of.routes += 1; + Ok(()) + } + + pub fn register_categories(&mut self) { + let mut entities_parents: HashMap = Default::default(); + let mut all_categories: HashMap = Default::default(); + Self::recursive_register_categories( + &mut entities_parents, + &self.categories, + &mut all_categories, + ); + self.entities_parents.extend(entities_parents); + self.report.number_of.categories = all_categories.len(); + self.all_categories = all_categories; + } + fn recursive_register_categories( + entities_parents: &mut HashMap, + categories: &IndexMap, + all_categories: &mut HashMap, + ) { + for (_, cat) in categories.iter() { + debug!( + "Register category {} {} {:?}", + cat.full_category_name, cat.guid, cat.parent + ); + all_categories.insert(cat.full_category_name.clone(), cat.guid); + if let Some(parent) = cat.parent { + entities_parents.insert(cat.guid, parent); + } + Self::recursive_register_categories(entities_parents, &cat.children, all_categories); + } + } + + pub fn found_missing_element_texture( + &mut self, + file_path: String, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) { + self.report.found_missing_texture(&file_path); + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_textures.push(PackageElementSource { + file_path, + requester_reference: ElementReference::Uuid(requester_uuid), + source_file_name: source_file_name.clone(), + }); + } + pub fn found_missing_inherited_texture( + &mut self, + file_path: String, + full_category_name: String, + source_file_uuid: &Uuid, + ) { + self.report.found_missing_texture(&file_path); + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_textures.push(PackageElementSource { + file_path, + requester_reference: ElementReference::Category(full_category_name), + source_file_name: source_file_name.clone(), + }); + } + pub fn found_missing_trail( + &mut self, + file_path: &RelativePath, + requester_uuid: Uuid, + source_file_uuid: &Uuid, + ) { + let source_file_name = self + .report + .source_file_uuid_to_name(source_file_uuid) + .unwrap(); + self.report.missing_trails.push(PackageElementSource { + file_path: file_path.as_str().to_string(), + requester_reference: ElementReference::Uuid(requester_uuid), + source_file_name: source_file_name.clone(), + }); + } +} diff --git a/crates/joko_package_models/src/route.rs b/crates/joko_package_models/src/route.rs new file mode 100644 index 0000000..f5e825a --- /dev/null +++ b/crates/joko_package_models/src/route.rs @@ -0,0 +1,45 @@ +use joko_core::{serde_glam::Vec3, RelativePath}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + attributes::CommonAttributes, + trail::{TBin, Trail}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Route { + pub category: String, + pub parent: Uuid, + pub path: Vec, + pub reset_position: Vec3, + pub reset_range: f64, + pub map_id: u32, + pub guid: Uuid, + pub name: String, + pub source_file_uuid: Uuid, +} + +pub(crate) fn route_to_tbin(route: &Route) -> TBin { + assert!(route.path.len() > 1); + TBin { + map_id: route.map_id, + version: 0, + nodes: route.path.clone(), + } +} + +pub(crate) fn route_to_trail(route: &Route, file_path: &RelativePath) -> Trail { + let mut props = CommonAttributes::default(); + props.set_texture(None); + props.set_trail_data(Some(file_path.clone())); + Trail { + map_id: route.map_id, + category: route.category.clone(), + parent: route.parent, + guid: route.guid, + props, + dynamic: true, + source_file_uuid: route.source_file_uuid, + } +} diff --git a/crates/joko_package_models/src/trail.rs b/crates/joko_package_models/src/trail.rs new file mode 100644 index 0000000..33e8398 --- /dev/null +++ b/crates/joko_package_models/src/trail.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::attributes::CommonAttributes; +use joko_core::serde_glam::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Trail { + pub guid: Uuid, + pub parent: Uuid, + pub map_id: u32, + pub category: String, + pub props: CommonAttributes, + pub dynamic: bool, + pub source_file_uuid: Uuid, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TBin { + pub map_id: u32, + pub version: u32, + pub nodes: Vec, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TBinStatus { + pub tbin: TBin, + pub iso_x: bool, + pub iso_y: bool, + pub iso_z: bool, + pub closed: bool, +} + +impl TBin {} diff --git a/crates/joko_plugin_manager/Cargo.toml b/crates/joko_plugin_manager/Cargo.toml new file mode 100644 index 0000000..ba6563c --- /dev/null +++ b/crates/joko_plugin_manager/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "joko_plugin_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +joko_component_models = { path = "../joko_component_models" } +tokio = { workspace = true } diff --git a/crates/joko_plugin_manager/src/lib.rs b/crates/joko_plugin_manager/src/lib.rs new file mode 100644 index 0000000..7ef8fec --- /dev/null +++ b/crates/joko_plugin_manager/src/lib.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +use joko_component_models::{ + default_component_result, Component, ComponentChannels, ComponentResult, +}; + +pub struct JokolayPlugin {} + +pub struct JokolayPluginManager { + #[allow(dead_code)] + path: PathBuf, +} + +impl JokolayPluginManager { + pub fn new(path: PathBuf) -> Self { + Self { path } + } + pub fn create(&mut self, _name: String) -> JokolayPlugin { + JokolayPlugin {} + } +} +impl Component for JokolayPluginManager { + fn init(&mut self) {} + fn flush_all_messages(&mut self) {} + fn tick(&mut self, _timestamp: f64) -> ComponentResult { + default_component_result() + } + fn bind(&mut self, _channels: ComponentChannels) {} +} + +impl Component for JokolayPlugin { + fn init(&mut self) { + println!("initialize dummy plugin"); + } + fn flush_all_messages(&mut self) {} + fn tick(&mut self, _timestamp: f64) -> ComponentResult { + default_component_result() + } + fn bind(&mut self, _channels: ComponentChannels) {} + fn requirements(&self) -> Vec<&str> { + vec!["back:mumble_link"] + } +} diff --git a/crates/joko_render/Cargo.toml b/crates/joko_render/Cargo.toml deleted file mode 100644 index ddbe098..0000000 --- a/crates/joko_render/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "joko_render" -version = "0.2.1" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -egui_render_three_d = { version = "*" } -egui_window_glfw_passthrough = { version = "0.5" } -bytemuck = { version = "1", default-features = false } -jokolink = { path = "../jokolink" } -glam = { workspace = true, features = ["bytemuck"] } -tracing = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -egui = { workspace = true } -raw-window-handle = { version = "0.5" } diff --git a/crates/joko_render/src/lib.rs b/crates/joko_render/src/lib.rs deleted file mode 100644 index bf710ab..0000000 --- a/crates/joko_render/src/lib.rs +++ /dev/null @@ -1,170 +0,0 @@ -pub mod billboard; -use billboard::BillBoardRenderer; -use billboard::MarkerObject; -use billboard::TrailObject; -use egui_render_three_d::three_d; -use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; -use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; -use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; -use egui_render_three_d::three_d::Camera; -use egui_render_three_d::three_d::HasContext; -use egui_render_three_d::three_d::ScissorBox; -use egui_render_three_d::three_d::Viewport; -use egui_render_three_d::ThreeDBackend; -use egui_render_three_d::ThreeDConfig; -use egui_window_glfw_passthrough::GlfwBackend; -use glam::Mat4; -use jokolink::MumbleLink; -use raw_window_handle::HasRawWindowHandle; -use std::sync::Arc; -use three_d::prelude::*; - -#[macro_export] -macro_rules! gl_error { - ($gl:expr) => {{ - let e = $gl.get_error(); - if e != egui_render_three_d::three_d::context::NO_ERROR { - tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); - } - }}; -} - -pub struct JokoRenderer { - pub view_proj: Mat4, - pub cam_pos: glam::Vec3, - pub camera: Camera, - pub viewport: Viewport, - pub link: Option>, - pub billboard_renderer: BillBoardRenderer, - pub gl: egui_render_three_d::ThreeDBackend, -} - -impl JokoRenderer { - pub fn new(glfw_backend: &mut GlfwBackend, _debug: bool) -> Self { - let glfw = glfw_backend.glfw.clone(); - let backend = ThreeDBackend::new( - ThreeDConfig { - glow_config: Default::default(), - }, - |s| glfw.get_proc_address_raw(s), - glfw_backend.window.raw_window_handle(), - glfw_backend.framebuffer_size_physical, - ); - let viewport = Viewport { - x: 0, - y: 0, - width: glfw_backend.framebuffer_size_physical[0], - height: glfw_backend.framebuffer_size_physical[1], - }; - let gl = &backend.context; - unsafe { gl_error!(gl) }; - let billboard_renderer = BillBoardRenderer::new(gl); - unsafe { gl_error!(gl) }; - Self { - viewport, - view_proj: Default::default(), - camera: Camera::new_perspective( - viewport, - [0.0, 0.0, 0.0].into(), - [0.0, 0.0, 0.0].into(), - Vector3::unit_y(), - Deg(90.0), - 1.0, - 5000.0, - ), - link: Default::default(), - gl: backend, - billboard_renderer, - cam_pos: Default::default(), - } - } - pub fn get_z_near(&self) -> f32 { - 1.0 - } - pub fn get_z_far(&self) -> f32 { - 1000.0 - } - pub fn tick(&mut self, link: Option>) { - if let Some(link) = link.as_ref() { - let center = link.cam_pos + link.f_camera_front; - let camera = Camera::new_perspective( - self.viewport, - link.cam_pos.to_array().into(), - center.to_array().into(), - Vector3::unit_y(), - Rad(link.fov), - self.get_z_near(), - self.get_z_far(), - ); - self.camera = camera; - let view = Mat4::look_at_lh(link.cam_pos, center, glam::Vec3::Y); - let proj = Mat4::perspective_lh( - link.fov, - self.viewport.aspect(), - self.get_z_near(), - self.get_z_far(), - ); - self.view_proj = proj * view; - self.cam_pos = link.cam_pos; - } - self.link = link; - } - pub fn add_billboard(&mut self, marker_object: MarkerObject) { - self.billboard_renderer.markers.push(marker_object); - } - pub fn add_trail(&mut self, trail_object: TrailObject) { - self.billboard_renderer.trails.push(trail_object); - } - pub fn prepare_frame(&mut self, latest_framebuffer_size_getter: impl FnMut() -> [u32; 2]) { - self.billboard_renderer.prepare_frame(); - self.gl.prepare_frame(latest_framebuffer_size_getter); - unsafe { - let gl = self.gl.context.clone(); - gl_error!(gl); - // self.gl.context.set_viewport(self.viewport); - self.gl.context.set_scissor(ScissorBox::new_at_origo( - self.viewport.width, - self.viewport.height, - )); - self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); - self.gl - .context - .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); - gl_error!(gl); - } - } - - pub fn render_egui( - &mut self, - meshes: Vec, - textures_delta: egui::TexturesDelta, - logical_screen_size: [f32; 2], - ) { - if let Some(link) = self.link.as_ref() { - self.billboard_renderer - .prepare_render_data(link, &self.gl.context); - self.billboard_renderer.render( - &self.gl.context, - self.cam_pos, - &self.view_proj, - &self.gl.glow_backend.painter.managed_textures, - ); - } - self.gl - .render_egui(meshes, textures_delta, logical_screen_size); - } - - pub fn present(&mut self) {} - - pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { - tracing::info!(?latest_size, "resizing framebuffer"); - - self.viewport = Viewport { - x: 0, - y: 0, - width: latest_size[0], - height: latest_size[1], - }; - self.gl.resize_framebuffer(latest_size); - } -} diff --git a/crates/joko_render_manager/Cargo.toml b/crates/joko_render_manager/Cargo.toml new file mode 100644 index 0000000..d7ca262 --- /dev/null +++ b/crates/joko_render_manager/Cargo.toml @@ -0,0 +1,25 @@ +# Define all structures that can be sent through asynchronous messages + +[package] +name = "joko_render_manager" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +bytemuck = { workspace = true } +glam = { workspace = true, features = ["bytemuck"] } +tracing = { workspace = true } +egui = { workspace = true } +egui_render_three_d = { version = "*" } +egui_window_glfw_passthrough = { version = "0.8" } +tokio = { workspace = true } + + +joko_component_models = { path = "../joko_component_models" } +joko_ui_models = { path = "../joko_ui_models" } +joko_link_models = { path = "../joko_link_models" } +joko_render_models = { path = "../joko_render_models" } + diff --git a/crates/joko_render/shaders/marker.fs b/crates/joko_render_manager/shaders/marker.fs similarity index 100% rename from crates/joko_render/shaders/marker.fs rename to crates/joko_render_manager/shaders/marker.fs diff --git a/crates/joko_render/shaders/marker.vs b/crates/joko_render_manager/shaders/marker.vs similarity index 100% rename from crates/joko_render/shaders/marker.vs rename to crates/joko_render_manager/shaders/marker.vs diff --git a/crates/joko_render/shaders/marker.wgsl b/crates/joko_render_manager/shaders/marker.wgsl similarity index 100% rename from crates/joko_render/shaders/marker.wgsl rename to crates/joko_render_manager/shaders/marker.wgsl diff --git a/crates/joko_render/shaders/player_visibility.wgsl b/crates/joko_render_manager/shaders/player_visibility.wgsl similarity index 100% rename from crates/joko_render/shaders/player_visibility.wgsl rename to crates/joko_render_manager/shaders/player_visibility.wgsl diff --git a/crates/joko_render_manager/shaders/trail.fs b/crates/joko_render_manager/shaders/trail.fs new file mode 100644 index 0000000..e8ca1d4 --- /dev/null +++ b/crates/joko_render_manager/shaders/trail.fs @@ -0,0 +1,22 @@ +#version 450 + +layout(location = 0) in vec2 vtex_coord; +layout(location = 1) in float valpha; +layout(location = 2) in vec4 vcolor; + +layout(location = 0) out vec4 ocolor; + +layout(location = 1) uniform sampler2D sam; // wrap_s = "REPEAT" wrap_t = "REPEAT" +layout(location = 3) uniform vec2 scroll_texture; + +void main() { + //vec4 color = texture(sam, vec2 (vtex_coord.x + scroll_texture.x, vtex_coord.y + scroll_texture.y), -2.0); + vec4 color = texture(sam, vec2 (vtex_coord.x + 0.0, vtex_coord.y + scroll_texture.y), -2.0); + //vec4 color = texture(sam, vtex_coord + scroll_texture); + //vec4 color = texture(sam, vtex_coord, -2.0);//original working + color.a = color.a * valpha; + if (color.a < 0.01) { + discard; + } + ocolor = color; +} diff --git a/crates/joko_render_manager/shaders/trail.vs b/crates/joko_render_manager/shaders/trail.vs new file mode 100644 index 0000000..c8f04b6 --- /dev/null +++ b/crates/joko_render_manager/shaders/trail.vs @@ -0,0 +1,37 @@ +#version 450 + +layout(location = 0) in vec4 position; +layout(location = 1) in float alpha; +layout(location = 2) in vec2 tex_coord; +layout(location = 3) in vec2 fade_near_far; +layout(location = 4) in vec4 color; + + +layout(location = 0) out vec2 vtex_coord; +layout(location = 1) out float valpha; +layout(location = 2) out vec4 vcolor; + +layout(location = 0) uniform vec3 camera_pos; +// location 1 is for sampler in frag shader +layout(location = 2) uniform mat4 transform; +// location 3 is for scroll_texture + +void main( +) { + valpha = alpha; + vtex_coord = tex_coord; + gl_Position = transform * position; + vcolor = color; + + float dist = distance(camera_pos, position.xyz); + if (fade_near_far.x > 0.0 && dist >= fade_near_far.x) { + // if distance is exactly fade_near, we will multiply with 1.0 + // if its more, then we will multiply with how far we are in between fade_near and fade_far + float ratio = 1.0 - (abs(dist - fade_near_far.x) / abs(fade_near_far.y - fade_near_far.x)); + // The actual alpha + valpha *= ratio; + } + if (fade_near_far.y > 0.0 && dist >= fade_near_far.y) { + valpha = 0.0; + } +} diff --git a/crates/joko_render/src/billboard.rs b/crates/joko_render_manager/src/billboard.rs similarity index 51% rename from crates/joko_render/src/billboard.rs rename to crates/joko_render_manager/src/billboard.rs index 16880d7..273175e 100644 --- a/crates/joko_render/src/billboard.rs +++ b/crates/joko_render_manager/src/billboard.rs @@ -1,12 +1,14 @@ -use std::sync::Arc; - use egui::ahash::HashMap; use egui_render_three_d::{ three_d::{context::*, Context, HasContext}, GpuTexture, }; -use glam::{Vec2, Vec3}; -use tracing::{error, info, warn}; +use glam::Vec2; +use joko_render_models::{ + marker::{MarkerObject, MarkerVertex}, + trail::TrailObject, +}; +use tracing::{error, info, trace, warn}; use crate::gl_error; @@ -14,68 +16,85 @@ const MARKER_VERTEX_STRIDE: i32 = std::mem::size_of::() as _; pub struct BillBoardRenderer { pub markers: Vec, pub trails: Vec, + pub markers_wip: Vec, //work in progress: this is where the markers are inserted + pub trails_wip: Vec, //work in progress: this is where the markers are inserted marker_program: NativeProgram, - vao: NativeVertexArray, - vb: NativeBuffer, - trail_buffers: Vec, -} -pub struct TrailObject { - pub vertices: Arc<[MarkerVertex]>, - pub texture: u64, + marker_vertex_buffer: NativeBuffer, + marker_vertex_array: NativeVertexArray, + + trail_program: NativeProgram, + trail_vertex_buffers: Vec, + trail_vertex_arrays: Vec, } -const MARKER_VS: &str = include_str!("../shaders/marker.vs"); -const MARKER_FS: &str = include_str!("../shaders/marker.fs"); +const MARKER_VERTEX_SHADER: &str = include_str!("../shaders/marker.vs"); +const MARKER_FRAGMENT_SHADER: &str = include_str!("../shaders/marker.fs"); +const TRAIL_VERTEX_SHADER: &str = include_str!("../shaders/trail.vs"); +const TRAIL_FRAGMENT_SHADER: &str = include_str!("../shaders/trail.fs"); + impl BillBoardRenderer { pub fn new(gl: &Context) -> Self { unsafe { - let marker_program = new_program(gl, MARKER_VS, MARKER_FS, None); - let vb = create_marker_buffer(gl); - let vao = gl.create_vertex_array().expect("failed to create egui vao"); - gl.bind_vertex_array(Some(vao)); - gl.bind_vertex_buffer(0, Some(vb), 0, MARKER_VERTEX_STRIDE); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 0); - gl.vertex_array_attrib_format_f32(vao, 0, 3, FLOAT, false, 0); - gl.vertex_array_attrib_binding_f32(vao, 0, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 1); - gl.vertex_array_attrib_format_f32(vao, 1, 1, FLOAT, false, 12); - gl.vertex_array_attrib_binding_f32(vao, 1, 0); + let marker_program = + new_program(gl, MARKER_VERTEX_SHADER, MARKER_FRAGMENT_SHADER, None); gl_error!(gl); - gl.enable_vertex_array_attrib(vao, 2); - gl.vertex_array_attrib_format_f32(vao, 2, 2, FLOAT, false, 16); - gl.vertex_array_attrib_binding_f32(vao, 2, 0); + let trail_shift_program = + new_program(gl, TRAIL_VERTEX_SHADER, TRAIL_FRAGMENT_SHADER, None); gl_error!(gl); - gl.enable_vertex_array_attrib(vao, 3); - gl.vertex_array_attrib_format_f32(vao, 3, 2, FLOAT, false, 24); - gl.vertex_array_attrib_binding_f32(vao, 3, 0); - gl_error!(gl); - - gl.enable_vertex_array_attrib(vao, 4); - gl.vertex_array_attrib_format_f32(vao, 4, 4, UNSIGNED_BYTE, true, 32); - gl.vertex_array_attrib_binding_f32(vao, 4, 0); + let marker_vertex_buffer = create_buffer(gl); + let marker_vertex_array = create_marker_array(gl, marker_vertex_buffer); gl_error!(gl); Self { markers: Vec::new(), + markers_wip: Vec::new(), + marker_program, - vb, + marker_vertex_buffer, + marker_vertex_array, + trails: Vec::new(), - trail_buffers: Default::default(), - vao, + trails_wip: Vec::new(), + + trail_program: trail_shift_program, + trail_vertex_buffers: Default::default(), + trail_vertex_arrays: Default::default(), } } } - pub fn prepare_frame(&mut self) { - self.markers.clear(); - self.trails.clear(); + + pub fn begin(&mut self) { + trace!("Begin with a fresh list of markers and trails"); + self.markers_wip.clear(); + self.trails_wip.clear(); + } + pub fn flush(&mut self) { + trace!( + "Flush UI to display {} markers, {} trails", + self.markers_wip.len(), + self.trails_wip.len() + ); + self.markers.clone_from(&self.markers_wip); + self.trails.clone_from(&self.trails_wip); + } + pub fn swap(&mut self) { + trace!( + "swap UI to display {} markers, {} trails", + self.markers_wip.len(), + self.trails_wip.len() + ); + self.markers = std::mem::take(&mut self.markers_wip); + self.trails = std::mem::take(&mut self.trails_wip); } - pub fn prepare_render_data(&mut self, _link: &jokolink::MumbleLink, gl: &Context) { + + pub fn prepare_render_data(&mut self, gl: &Context) { + /* + TODO: map view (view from above) + trim down the trails too far ? + fatten them ? + */ unsafe { gl_error!(gl); } @@ -90,25 +109,27 @@ impl BillBoardRenderer { let len = (trail.vertices.len() * std::mem::size_of::()) as u64; required_size_in_bytes = required_size_in_bytes.max(len); } - let mut vb = vec![]; - vb.reserve(self.markers.len() * 6 * std::mem::size_of::()); + let mut vb: Vec = Vec::with_capacity(self.markers.len() * 6); for marker_object in self.markers.iter() { vb.extend_from_slice(&marker_object.vertices); } unsafe { gl_error!(gl); - gl.bind_buffer(ARRAY_BUFFER, Some(self.vb)); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); gl.buffer_data_u8_slice(ARRAY_BUFFER, bytemuck::cast_slice(&vb), DYNAMIC_DRAW); gl_error!(gl); } - if self.trails.len() > self.trail_buffers.len() { - let needs = self.trails.len() - self.trail_buffers.len(); + if self.trails.len() > self.trail_vertex_buffers.len() { + let needs = self.trails.len() - self.trail_vertex_buffers.len(); for _ in 0..needs { - self.trail_buffers.push(unsafe { create_marker_buffer(gl) }); + let vb = unsafe { create_buffer(gl) }; + self.trail_vertex_buffers.push(vb); + let trail_vertex_array = unsafe { create_trail_array(gl, vb, 1) }; + self.trail_vertex_arrays.push(trail_vertex_array); } } - for (trail, trail_buffer) in self.trails.iter().zip(self.trail_buffers.iter()) { + for (trail, trail_buffer) in self.trails.iter().zip(self.trail_vertex_buffers.iter()) { unsafe { gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); gl.buffer_data_u8_slice( @@ -128,33 +149,69 @@ impl BillBoardRenderer { cam_pos: glam::Vec3, view_proj: &glam::Mat4, textures: &HashMap, + latest_time: f64, ) { unsafe { gl_error!(gl); gl.disable(SCISSOR_TEST); - gl.use_program(Some(self.marker_program)); - gl.bind_vertex_array(Some(self.vao)); + gl.use_program(Some(self.trail_program)); + gl_error!(gl); gl.active_texture(TEXTURE0); + gl_error!(gl); + let scroll_texture: Vec2 = Vec2 { + x: 0.0, + y: (latest_time as f32 % 2.0) - 1.0, + }; //TODO: manage speed in some configurations. per trail ? - gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); - gl.uniform_matrix_4_f32_slice( - Some(&NativeUniformLocation(2)), - false, - view_proj.to_cols_array().as_ref(), - ); - for (trail, trail_buffer) in self.trails.iter().zip(self.trail_buffers.iter()) { + gl.uniform_2_f32_slice(Some(&NativeUniformLocation(3)), scroll_texture.as_ref()); + //https://stackoverflow.com/questions/27771902/opengl-changing-texture-coordinates-on-the-fly + //https://www.khronos.org/opengl/wiki/Uniform_(GLSL) + for ((trail, trail_buffer), trail_array) in self + .trails + .iter() + .zip(self.trail_vertex_buffers.iter()) + .zip(self.trail_vertex_arrays.iter()) + { if let Some(texture) = textures.get(&trail.texture) { + gl.bind_vertex_array(Some(*trail_array)); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + gl.bind_vertex_buffer(0, Some(*trail_buffer), 0, MARKER_VERTEX_STRIDE); gl.bind_buffer(ARRAY_BUFFER, Some(*trail_buffer)); gl.bind_texture(TEXTURE_2D, Some(texture.handle)); gl.bind_sampler(0, Some(texture.sampler)); + gl_error!(gl); + gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl_error!(gl); + + /* + gl.polygon_mode(FRONT_AND_BACK, LINE); gl.draw_arrays(TRIANGLES, 0, trail.vertices.len() as _); + gl.polygon_mode(FRONT_AND_BACK, FILL); + gl_error!(gl); + */ } } - gl.bind_vertex_buffer(0, Some(self.vb), 0, MARKER_VERTEX_STRIDE); - - gl.bind_buffer(ARRAY_BUFFER, Some(self.vb)); + gl.use_program(Some(self.marker_program)); + gl_error!(gl); + gl.bind_vertex_array(Some(self.marker_vertex_array)); + gl_error!(gl); + gl.uniform_3_f32_slice(Some(&NativeUniformLocation(0)), cam_pos.as_ref()); + gl.uniform_matrix_4_f32_slice( + Some(&NativeUniformLocation(2)), + false, + view_proj.to_cols_array().as_ref(), + ); + gl_error!(gl); + gl.bind_vertex_buffer(0, Some(self.marker_vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl.bind_buffer(ARRAY_BUFFER, Some(self.marker_vertex_buffer)); for (index, mo) in self.markers.iter().enumerate() { let index: u32 = index.try_into().unwrap(); if let Some(texture) = textures.get(&mo.texture) { @@ -169,27 +226,6 @@ impl BillBoardRenderer { } } -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub struct MarkerVertex { - pub position: Vec3, - pub alpha: f32, - pub texture_coordinates: Vec2, - pub fade_near_far: Vec2, - pub color: [u8; 4], -} - -pub struct MarkerObject { - /// The six vertices that make up the marker quad - pub vertices: [MarkerVertex; 6], - /// The (managed) texture id from egui data - pub texture: u64, - /// The distance from camera - /// As markers have transparency, we need to render them from far -> near order - /// So, we will sort them using this distance just before rendering - pub distance: f32, -} - /// takes in strings containing vertex/fragment shaders and returns a Shaderprogram with them attached #[tracing::instrument(skip(gl))] pub fn new_program( @@ -198,6 +234,7 @@ pub fn new_program( fragment_shader_source: &str, _geometry_shader_source: Option<&str>, ) -> NativeProgram { + //https://www.khronos.org/opengl/wiki/Shader_Compilation#Program_setup unsafe { gl_error!(gl); @@ -268,7 +305,7 @@ pub fn new_program( program } } -unsafe fn create_marker_buffer(gl: &Context) -> NativeBuffer { +unsafe fn create_buffer(gl: &Context) -> NativeBuffer { gl_error!(gl); let vb = gl.create_buffer().expect("failed to create vb for markers"); gl_error!(gl); @@ -281,3 +318,58 @@ unsafe fn create_marker_buffer(gl: &Context) -> NativeBuffer { gl_error!(gl); vb } + +unsafe fn create_marker_array(gl: &Context, vertex_buffer: NativeBuffer) -> NativeVertexArray { + create_array(gl, vertex_buffer, 1) +} + +unsafe fn create_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { + let marker_vertex_array = gl.create_vertex_array().expect("failed to create egui vao"); + gl.bind_vertex_array(Some(marker_vertex_array)); + gl.bind_vertex_buffer(binding_index, Some(vertex_buffer), 0, MARKER_VERTEX_STRIDE); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 0); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 0, 3, FLOAT, false, 0); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 0, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 1); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 1, 1, FLOAT, false, 12); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 1, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 2); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 2, 2, FLOAT, false, 16); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 2, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 3); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 3, 2, FLOAT, false, 24); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 3, 0); + gl_error!(gl); + + gl.enable_vertex_array_attrib(marker_vertex_array, 4); + gl.vertex_array_attrib_format_f32(marker_vertex_array, 4, 4, UNSIGNED_BYTE, true, 32); + gl.vertex_array_attrib_binding_f32(marker_vertex_array, 4, 0); + gl_error!(gl); + marker_vertex_array +} + +unsafe fn create_trail_array( + gl: &Context, + vertex_buffer: NativeBuffer, + binding_index: u32, +) -> NativeVertexArray { + let trail_vertex_array = create_array(gl, vertex_buffer, binding_index); + gl.enable_vertex_array_attrib(trail_vertex_array, 5); + gl.vertex_array_attrib_format_f32(trail_vertex_array, 5, 2, FLOAT, false, 36); + gl.vertex_array_attrib_binding_f32(trail_vertex_array, 5, 0); + gl_error!(gl); + + trail_vertex_array +} diff --git a/crates/joko_render_manager/src/gl.rs b/crates/joko_render_manager/src/gl.rs new file mode 100644 index 0000000..8ac9626 --- /dev/null +++ b/crates/joko_render_manager/src/gl.rs @@ -0,0 +1,9 @@ +#[macro_export] +macro_rules! gl_error { + ($gl:expr) => {{ + let e = $gl.get_error(); + if e != egui_render_three_d::three_d::context::NO_ERROR { + tracing::error!("glerror {} at {} {} {}", e, file!(), line!(), column!()); + } + }}; +} diff --git a/crates/joko_render_manager/src/lib.rs b/crates/joko_render_manager/src/lib.rs new file mode 100644 index 0000000..9050354 --- /dev/null +++ b/crates/joko_render_manager/src/lib.rs @@ -0,0 +1,3 @@ +pub mod billboard; +pub mod gl; +pub mod renderer; diff --git a/crates/joko_render_manager/src/renderer.rs b/crates/joko_render_manager/src/renderer.rs new file mode 100644 index 0000000..9d15d8e --- /dev/null +++ b/crates/joko_render_manager/src/renderer.rs @@ -0,0 +1,481 @@ +use std::sync::Arc; +use std::sync::RwLock; + +use crate::billboard::BillBoardRenderer; +use crate::gl_error; +use egui_render_three_d::three_d; +use egui_render_three_d::three_d::context::COLOR_BUFFER_BIT; +use egui_render_three_d::three_d::context::DEPTH_BUFFER_BIT; +use egui_render_three_d::three_d::context::STENCIL_BUFFER_BIT; +use egui_render_three_d::three_d::Camera; +use egui_render_three_d::three_d::HasContext; +use egui_render_three_d::three_d::ScissorBox; +use egui_render_three_d::three_d::Viewport; +use egui_render_three_d::ThreeDBackend; +use egui_render_three_d::ThreeDConfig; +use egui_window_glfw_passthrough::glfw::Context; +use egui_window_glfw_passthrough::GlfwBackend; +use glam::Mat4; +use joko_component_models::default_component_result; +use joko_component_models::from_broadcast; +use joko_component_models::from_data; +use joko_component_models::Component; +use joko_component_models::ComponentChannels; +use joko_component_models::ComponentMessage; +use joko_component_models::ComponentResult; +use joko_link_models::MumbleLink; +use joko_link_models::UIState; +use joko_render_models::messages::MessageToRenderer; +use joko_ui_models::UIArea; +use joko_ui_models::UIPanel; +use three_d::prelude::*; + +use joko_render_models::{marker::MarkerObject, trail::TrailObject}; + +struct JokoRendererChannels { + notification_receiver: tokio::sync::mpsc::Receiver, + subscription_mumble_link: tokio::sync::broadcast::Receiver, +} +pub struct JokoRenderer { + pub view_proj: Mat4, + pub cam_pos: glam::Vec3, + pub camera: Camera, + pub viewport: Viewport, + pub has_link: bool, + pub is_map_open: bool, + nb_swap: u128, + pub billboard_renderer: BillBoardRenderer, + glfw_backend: Arc>, + egui_context: egui::Context, + pub gl: egui_render_three_d::ThreeDBackend, + channels: Option, + link: Option, +} + +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for JokoRenderer {} +unsafe impl Sync for JokoRenderer {} + +impl JokoRenderer { + pub fn new(glfw_backend: Arc>, egui_context: egui::Context) -> Self { + let framebuffer_size_physical = glfw_backend.read().unwrap().framebuffer_size_physical; + let backend = ThreeDBackend::new( + ThreeDConfig { + glow_config: Default::default(), + }, + |s| glfw_backend.read().unwrap().glfw.get_proc_address_raw(s), + framebuffer_size_physical, + ); + let viewport = Viewport { + x: 0, + y: 0, + width: framebuffer_size_physical[0], + height: framebuffer_size_physical[1], + }; + let gl = &backend.context; + unsafe { gl_error!(gl) }; + let billboard_renderer = BillBoardRenderer::new(gl); + unsafe { gl_error!(gl) }; + Self { + viewport, + view_proj: Default::default(), + camera: Camera::new_perspective( + viewport, + [0.0, 0.0, 0.0].into(), + [0.0, 0.0, 0.0].into(), + Vector3::unit_y(), + Deg(90.0), + 1.0, + 5000.0, + ), + has_link: false, + is_map_open: false, + nb_swap: 0, + gl: backend, + egui_context, + billboard_renderer, + glfw_backend, + cam_pos: Default::default(), + channels: None, + link: Default::default(), + } + } + + /* + CRect GetMinimapRectangle() + { + int w = mumbleLink.miniMap.compassWidth; + int h = mumbleLink.miniMap.compassHeight; + + CRect pos; + CRect size = App->GetRoot()->GetClientRect(); + float scale = GetWindowTooSmallScale(); + + pos.x1 = int( size.Width() - w * scale ); + pos.x2 = size.Width(); + + + if ( mumbleLink.isMinimapTopRight ) + { + pos.y1 = 1; + pos.y2 = int( h * scale + 1 ); + } + else + { + int delta = 37; + if ( mumbleLink.uiSize == 0 ) + delta = 33; + if ( mumbleLink.uiSize == 2 ) + delta = 41; + if ( mumbleLink.uiSize == 3 ) + delta = 45; + + pos.y1 = int( size.Height() - h * scale - delta * scale ); + pos.y2 = int( size.Height() - delta * scale ); + } + + return pos; + } + */ + pub fn get_z_near() -> f32 { + 1.0 + } + pub fn get_z_far() -> f32 { + 1000.0 + } + + pub fn begin(&mut self) { + self.billboard_renderer.begin(); + } + pub fn flush(&mut self) { + self.billboard_renderer.flush(); + } + pub fn swap(&mut self) { + self.nb_swap += 1; + self.billboard_renderer.swap(); + } + /* + //https://wiki.guildwars2.com/wiki/API:1/event_details#Coordinate_recalculation + fn _scale_coords(continent_rect, map_rect, coords){ + continent_width = continent_rect[1].x - continent_rect[0].x; + continent_height = continent_rect[1].y - continent_rect[0].y; + map_width = map_rect[1].x - map_rect[0].x; + map_height = map_rect[1].y - map_rect[0].y; + position_on_map_x = coords.x - map_rect[0].x; + position_on_map_y = coords.y - map_rect[1].y; + return [ + Math.round( continent_rect[0].x + ( 1 * position_on_map_x / map_width * continent_width ) ), + Math.round( continent_rect[0].y + (-1 * position_on_map_y / map_height * continent_height ) ) + ]; + } + */ + fn handle_message(&mut self, msg: MessageToRenderer) { + match msg { + MessageToRenderer::BulkMarkerObject(marker_objects) => { + tracing::debug!( + "Handling of MessageToRenderer::BulkMarkerObject {}", + marker_objects.len() + ); + self.extend_markers(marker_objects); + } + MessageToRenderer::BulkTrailObject(trail_objects) => { + tracing::debug!( + "Handling of MessageToRenderer::BulkTrailObject {}", + trail_objects.len() + ); + self.extend_trails(trail_objects); + } + MessageToRenderer::MarkerObject(mo) => { + tracing::trace!("Handling of MessageToRenderer::MarkerObject"); + self.add_billboard(*mo); + } + MessageToRenderer::TrailObject(to) => { + tracing::trace!("Handling of MessageToRenderer::TrailObject"); + self.add_trail(*to); + } + MessageToRenderer::RenderBegin => { + tracing::trace!("Handling of MessageToRenderer::RenderBegin"); + self.begin(); + } + MessageToRenderer::RenderFlush => { + tracing::trace!("Handling of MessageToRenderer::RenderFlush"); + self.flush(); + } + MessageToRenderer::RenderSwapChain => { + tracing::trace!( + "Handling of MessageToRenderer::RenderSwapChain {}", + self.nb_swap + ); + self.swap(); + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling MessageToRenderer has not been implemented yet"); + } + } + } + + pub fn extend_markers(&mut self, marker_objects: Vec) { + self.billboard_renderer.markers_wip.extend(marker_objects); + } + pub fn add_billboard(&mut self, marker_object: MarkerObject) { + self.billboard_renderer.markers_wip.push(marker_object); + } + + pub fn extend_trails(&mut self, trail_objects: Vec) { + self.billboard_renderer.trails_wip.extend(trail_objects); + } + pub fn add_trail(&mut self, trail_object: TrailObject) { + self.billboard_renderer.trails_wip.push(trail_object); + } + + pub fn prepare_frame(&mut self) { + let latest_framebuffer_size_getter = || Self::frame_size(Arc::clone(&self.glfw_backend)); + self.gl.prepare_frame(latest_framebuffer_size_getter); + unsafe { + let gl = self.gl.context.clone(); + gl_error!(gl); + // self.gl.context.set_viewport(self.viewport); + self.gl.context.set_scissor(ScissorBox::new_at_origo( + self.viewport.width, + self.viewport.height, + )); + self.gl.context.clear_color(0.0, 0.0, 0.0, 0.0); + self.gl + .context + .clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT | STENCIL_BUFFER_BIT); + gl_error!(gl); + } + } + + pub fn render_egui( + &mut self, + meshes: Vec, + textures_delta: egui::TexturesDelta, + logical_screen_size: [f32; 2], + latest_time: f64, + ) { + if self.has_link && !self.is_map_open { + self.billboard_renderer + .prepare_render_data(&self.gl.context); + self.billboard_renderer.render( + &self.gl.context, + self.cam_pos, + &self.view_proj, + &self.gl.glow_backend.painter.managed_textures, + latest_time, + ); + } + self.gl + .render_egui(meshes, textures_delta, logical_screen_size); + } + + pub fn present(&mut self) {} + + pub fn resize_framebuffer(&mut self, latest_size: [u32; 2]) { + tracing::info!(?latest_size, "resizing framebuffer"); + + self.viewport = Viewport { + x: 0, + y: 0, + width: latest_size[0], + height: latest_size[1], + }; + self.gl.resize_framebuffer(latest_size); + } + + fn frame_size(glfw_backend: Arc>) -> [u32; 2] { + let mut glfw_backend = glfw_backend.write().unwrap(); + let latest_size = glfw_backend.window.get_framebuffer_size(); + + let latest_size = [latest_size.0 as _, latest_size.1 as _]; + + glfw_backend.framebuffer_size_physical = latest_size; + glfw_backend.window_size_logical = [ + latest_size[0] as f32 / glfw_backend.scale, + latest_size[1] as f32 / glfw_backend.scale, + ]; + glfw_backend.resized_event_pending = false; + latest_size + } + fn _window_tick(&mut self) { + let resized_event_pending = { self.glfw_backend.read().unwrap().resized_event_pending }; + if resized_event_pending { + let latest_size = Self::frame_size(Arc::clone(&self.glfw_backend)); + self.resize_framebuffer(latest_size); + } + + self.prepare_frame(); + } +} + +impl Component for JokoRenderer { + fn init(&mut self) {} + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = JokoRendererChannels { + notification_receiver: channels.input_notification.unwrap(), + subscription_mumble_link: channels.requirements.remove(&0).unwrap(), + }; + self.channels = Some(channels); + } + fn accept_notifications(&self) -> bool { + true + } + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + + //two steps reading due to self mutability required by channel + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + + let channels = self.channels.as_mut().unwrap(); + let raw_link = channels.subscription_mumble_link.blocking_recv().unwrap(); + let link: Option = from_broadcast(&raw_link); + self.link = link; + default_component_result() + } +} + +impl UIPanel for JokoRenderer { + fn init(&mut self) {} + fn areas(&self) -> Vec { + vec![UIArea { + id: "overlay".to_string(), + name: String::new(), + is_open: true, // N/A + }] + } + + fn gui(&mut self, _is_open: &mut bool, _area_id: &str, latest_time: f64) { + self._window_tick(); + if let Some(link) = &self.link { + //trace!("JokoRenderer {:?} {:?}", link.player_pos, link.cam_pos); + //x positive => east + //y positive => ascention + //z positive => north + self.is_map_open = if let Some(ui_state) = link.ui_state { + ui_state.contains(UIState::IsMapOpen) + } else { + false + }; + + //TODO: change perspective is map is open + let center = link.cam_pos.0 + link.f_camera_front.0; + let cam_pos = link.cam_pos; + /* + let map_pos_x = (link.player_x - link.map_center_x) / 1.64; + let map_pos_y = (link.map_center_y - link.player_y) / 1.64; + let center = if self.is_map_open { + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 100.0, + z: link.player_pos.z - map_pos_y, + } + } else { + link.cam_pos + link.f_camera_front //default old one + }; + + let client_width = (link.client_size.x) as f32; + let client_height = (link.client_size.y) as f32; + + let cam_pos = if self.is_map_open { + //TODO: validate values + glam::Vec3{ + x: link.player_pos.x - map_pos_x, + y: link.player_pos.y + 101.0, + z: link.player_pos.z - map_pos_y, + } + }else { + link.cam_pos //default old one + };*/ + let camera = Camera::new_perspective( + self.viewport, + cam_pos.0.to_array().into(), + center.to_array().into(), + Vector3::unit_y(), + Rad(link.fov), + Self::get_z_near(), + Self::get_z_far(), + ); + self.camera = camera; + /* + is_map_open: + target camera direction: 0 -20 1 + have trails seen from further + have trails fatter drawing + + println!("client: {} {} {} {}", client_width, client_height, client_width.div(client_height), client_height.div(client_width)); + println!("map scale: {}", link.map_scale); + println!("map position: {} {}", map_pos_x, map_pos_y); + println!("cam: {} {} {}", cam_pos.x, cam_pos.y, cam_pos.z); + println!("center: {} {} {}", center.x, center.y, center.z); + println!("H: {}", cam_pos.y - center.y); + println!("player: {} {} {}", link.player_pos.x, link.player_pos.y, link.player_pos.z); + */ + + let view = Mat4::look_at_lh(cam_pos.0, center, glam::Vec3::Y); + let proj = Mat4::perspective_lh( + link.fov, + self.viewport.aspect(), + Self::get_z_near(), + Self::get_z_far(), + ); + self.view_proj = proj * view; + self.cam_pos = cam_pos.0; + self.has_link = true; + } else { + self.has_link = false; + } + + self.egui_context.request_repaint(); + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + .. + } = self.egui_context.end_frame(); + if !platform_output.copied_text.is_empty() { + self.glfw_backend + .write() + .unwrap() + .window + .set_clipboard_string(&platform_output.copied_text); + } + + // if it doesn't require either keyboard or pointer, set passthrough to true + self.glfw_backend + .write() + .unwrap() + .window + .set_mouse_passthrough( + !(self.egui_context.wants_keyboard_input() + || self.egui_context.wants_pointer_input()), + ); + + let meshes = self + .egui_context + .tessellate(shapes, self.egui_context.pixels_per_point()); + let window_size_logical = self.glfw_backend.read().unwrap().window_size_logical; + self.render_egui(meshes, textures_delta, window_size_logical, latest_time); + self.present(); + self.glfw_backend.write().unwrap().window.swap_buffers(); + } +} diff --git a/crates/joko_render_models/Cargo.toml b/crates/joko_render_models/Cargo.toml new file mode 100644 index 0000000..d94e66c --- /dev/null +++ b/crates/joko_render_models/Cargo.toml @@ -0,0 +1,18 @@ +# Define all structures that can be sent through asynchronous messages + +[package] +name = "joko_render_models" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +bytemuck = { workspace = true } +glam = { workspace = true, features = ["bytemuck"] } +serde = { workspace = true } + +joko_core = { path = "../joko_core" } + + diff --git a/crates/joko_render_models/src/lib.rs b/crates/joko_render_models/src/lib.rs new file mode 100644 index 0000000..fc275d3 --- /dev/null +++ b/crates/joko_render_models/src/lib.rs @@ -0,0 +1,3 @@ +pub mod marker; +pub mod messages; +pub mod trail; diff --git a/crates/joko_render_models/src/marker.rs b/crates/joko_render_models/src/marker.rs new file mode 100644 index 0000000..cc630d4 --- /dev/null +++ b/crates/joko_render_models/src/marker.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +use joko_core::serde_glam::*; + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize)] +pub struct MarkerVertex { + pub position: Vec3, + pub alpha: f32, + pub texture_coordinates: Vec2, + pub fade_near_far: Vec2, + pub color: [u8; 4], +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MarkerObject { + /// The six vertices that make up the marker quad + pub vertices: [MarkerVertex; 6], + /// The (managed) texture id from egui data + pub texture: u64, + /// The distance from camera + /// As markers have transparency, we need to render them from far -> near order + /// So, we will sort them using this distance just before rendering + pub distance: f32, +} diff --git a/crates/joko_render_models/src/messages.rs b/crates/joko_render_models/src/messages.rs new file mode 100644 index 0000000..5b20064 --- /dev/null +++ b/crates/joko_render_models/src/messages.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::{marker::MarkerObject, trail::TrailObject}; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToRenderer { + BulkMarkerObject(Vec), + BulkTrailObject(Vec), + //Present,// a render loop is finished and we can present it + MarkerObject(Box), + RenderBegin, // There is a change in what to display, reset current build + RenderSwapChain, // The list of elements to display was changed. Or camera or position was changed. + RenderFlush, // Force whatever is being constructed to be kept and be what to display + TrailObject(Box), +} diff --git a/crates/joko_render_models/src/trail.rs b/crates/joko_render_models/src/trail.rs new file mode 100644 index 0000000..5ae46e3 --- /dev/null +++ b/crates/joko_render_models/src/trail.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::marker::MarkerVertex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrailObject { + pub vertices: Arc<[MarkerVertex]>, + pub texture: u64, +} diff --git a/crates/joko_ui_models/Cargo.toml b/crates/joko_ui_models/Cargo.toml new file mode 100644 index 0000000..369ebf9 --- /dev/null +++ b/crates/joko_ui_models/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "joko_ui_models" +version = "0.2.1" +edition = "2021" +[lib] +crate-type = ["cdylib", "lib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +egui = {workspace = true} diff --git a/crates/joko_ui_models/README.md b/crates/joko_ui_models/README.md new file mode 100644 index 0000000..5962a47 --- /dev/null +++ b/crates/joko_ui_models/README.md @@ -0,0 +1,58 @@ +# jokolink +A crate to extract info from Guild Wars 2 MumbleLink and copy it to a file /dev/shm in linux for native linux apps (primarily jokolay). + +it will also get the x11 window id of the gw2 window and paste it at the end of the mumblelink data in /dev/shm. the format is simply 1193 bytes of useful mumblelink data AND an isize (for x11 window id of gw2). will sleep for 5 ms every frame (configurable), so will copy upto 200 times per second. + +## Precaution +This jokolink binary is ONLY for linux users to get the `MumbleLink` data from guild wars 2 in wine to `/dev/shm`, so that linux native clients can read that. eg: `Jokolay`. + +> WARNING: Guild Wars 2 doesn't update MumbleLink Data during character select screen or map loading screens. So, until you load into a map with a character, there is nothing for jokolink to write to `/dev/shm/MumbleLink` + +## Installation +1. Just run `cargo build -p jokolink --release` to build the `jokolink.dll` (or download it ) +2. copy the `jokolink.dll` into `Guild Wars 2` folder right beside `Gw2-64.exe` +3. If you don't use arcdps, then rename `jokolink.dll` to `d3d11.dll`, so that gw2 will load the dll when it starts +4. If you use arcdps, then you can rename `jokolink.dll` to `arcdps_jokolink.dll`. All dlls whose names start with `arcdps` will be loaded by arcdps. + + +## Configuration +Jokolink configuration is stored in json format and a default config file will be created in the same directory as the dll. + + * loglevel: + default: "info" + type: string + possible_values: ["trace", "debug", "info", "warn", "error"] + help: the log level of the application. + + * logdir: + default: "." // current working directory + type: directory path + help: a path to a directory, where jokolink will create jokolink.log file + + * mumble_link_name: + default: "MumbleLink" + type: string + help: names of mumble link to copy data from and to. useful if you provide `-mumble` option to Guild Wars 2 for custom link name + + * interval + default: 5 + type: unsigned integer (positive integer) + help: the interval to sleep after updating mumble link data. in milliseconds. 5 milliseconds is roughly 200 times per second which should be enough. + + * copy_dest_dir: + default: "z:\\dev\\shm" + type: directory path + help: the directory under which we will create files with the provided `mumble_link_names` and write the mumble data from the shared memory inside wine. lutris uses "z" drive to represent linux root "/". and /dev/shm is an in memory directory, so writing to files is basically just writing bytes to ram (not wrriten to ssd/hdd -> really fast copying). + + +## Verification : +1. start Guild Wars 2 and you should see a file at `/dev/shm/MumbleLink`. If you use a custom link name by editing the config, then the path will be `/dev/shm/custom_link_name`. +2. The jokolink dll is basically copying gw2 data to this file. you can either do `cat /dev/shm/MumbleLink` or use a hex editor to browse the data. If you are playing in a PvE map, then you should see the currently logged in player name easily. +3. if you can't find any such file, it means jokolink probably failed to start, you can go check the `Guild Wars 2` folder for `jokolink.log` and raise an issue with that log. +4. If you right click the game in lutris and select `show logs`, you can see lines printed by jokolink when it is loaded/unloaded and initialized. + + + +## Cross Compilation +To compile for windows on linux, install `x86_64-pc-windows-gnu` target with rustup and `mingw` package on your distro. +`.cargo/config.toml` already sets the linker settings for mingw toolchain. diff --git a/crates/joko_ui_models/src/lib.rs b/crates/joko_ui_models/src/lib.rs new file mode 100644 index 0000000..0f816fa --- /dev/null +++ b/crates/joko_ui_models/src/lib.rs @@ -0,0 +1,14 @@ +use egui::Ui; + +pub struct UIArea { + pub is_open: bool, + pub name: String, + /// if empty, no option shall be displayed in the menu + pub id: String, +} +pub trait UIPanel { + fn init(&mut self); + fn gui(&mut self, is_open: &mut bool, area_id: &str, latest_time: f64); + fn menu_ui(&mut self, _ui: &mut Ui) {} + fn areas(&self) -> Vec; +} diff --git a/crates/jokoapi/src/end_point.rs b/crates/jokoapi/src/end_point.rs index cf942e6..affabf9 100644 --- a/crates/jokoapi/src/end_point.rs +++ b/crates/jokoapi/src/end_point.rs @@ -14,7 +14,9 @@ pub use serde::{Deserialize, Serialize}; // pub mod quaggans; // pub mod races; pub mod mounts; +pub mod professions; pub mod races; +pub mod specializations; pub mod worlds; const AUTHORIZATION_HEADER_NAME: &str = "Authorization"; diff --git a/crates/jokoapi/src/end_point/mounts/mod.rs b/crates/jokoapi/src/end_point/mounts/mod.rs index cc6feef..374410b 100644 --- a/crates/jokoapi/src/end_point/mounts/mod.rs +++ b/crates/jokoapi/src/end_point/mounts/mod.rs @@ -70,8 +70,9 @@ impl AsRef for Mount { } } } -impl ToString for Mount { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for Mount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/crates/jokoapi/src/end_point/professions/mod.rs b/crates/jokoapi/src/end_point/professions/mod.rs index e69de29..ad5d358 100644 --- a/crates/jokoapi/src/end_point/professions/mod.rs +++ b/crates/jokoapi/src/end_point/professions/mod.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +use crate::prelude::*; + +/// Filter which professions the marker should be active for. if its null, its available for all professions +#[bitflags] +#[repr(u16)] +#[derive(Debug, Clone, Copy)] +pub enum Profession { + Elementalist = 1 << 0, + Engineer = 1 << 1, + Guardian = 1 << 2, + Mesmer = 1 << 3, + Necromancer = 1 << 4, + Ranger = 1 << 5, + Revenant = 1 << 6, + Thief = 1 << 7, + Warrior = 1 << 8, +} + +impl FromStr for Profession { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "guardian" => Profession::Guardian, + "warrior" => Profession::Warrior, + "engineer" => Profession::Engineer, + "ranger" => Profession::Ranger, + "thief" => Profession::Thief, + "elementalist" => Profession::Elementalist, + "mesmer" => Profession::Mesmer, + "necromancer" => Profession::Necromancer, + "revenant" => Profession::Revenant, + _ => return Err("invalid profession"), + }) + } +} + +impl AsRef for Profession { + fn as_ref(&self) -> &str { + match self { + Profession::Guardian => "guardian", + Profession::Warrior => "warrior", + Profession::Engineer => "engineer", + Profession::Ranger => "ranger", + Profession::Thief => "thief", + Profession::Elementalist => "elementalist", + Profession::Mesmer => "mesmer", + Profession::Necromancer => "necromancer", + Profession::Revenant => "revenant", + } + } +} + +impl std::fmt::Display for Profession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) + } +} diff --git a/crates/jokoapi/src/end_point/races/mod.rs b/crates/jokoapi/src/end_point/races/mod.rs index 575661d..202605c 100644 --- a/crates/jokoapi/src/end_point/races/mod.rs +++ b/crates/jokoapi/src/end_point/races/mod.rs @@ -4,25 +4,40 @@ use crate::prelude::*; #[bitflags] #[repr(u8)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Race { - ASURA = 1 << 0, - CHARR = 1 << 2, - HUMAN = 1 << 3, - NORN = 1 << 4, - SYLVARI = 1 << 5, + Unknown = 1 << 1, + Asura = 1 << 2, + Charr = 1 << 3, + Human = 1 << 4, + Norn = 1 << 5, + Sylvari = 1 << 6, +} + +impl Race { + #[allow(dead_code)] + fn from_link_id(race_id: u32) -> Race { + match race_id { + 0 => Race::Asura, + 1 => Race::Charr, + 2 => Race::Human, + 3 => Race::Norn, + 4 => Race::Sylvari, + _ => Race::Unknown, + } + } } impl FromStr for Race { type Err = &'static str; fn from_str(s: &str) -> std::result::Result { Ok(match s { - "asura" => Self::ASURA, - "charr" => Self::CHARR, - "human" => Self::HUMAN, - "norn" => Self::NORN, - "sylvari" => Self::SYLVARI, - _ => return Err("invalid race string"), + "Asura" => Self::Asura, + "Charr" => Self::Charr, + "Human" => Self::Human, + "Norn" => Self::Norn, + "Sylvari" => Self::Sylvari, + _ => Self::Unknown, }) } } @@ -30,16 +45,18 @@ impl FromStr for Race { impl AsRef for Race { fn as_ref(&self) -> &'static str { match self { - Self::ASURA => "asura", - Self::CHARR => "charr", - Self::HUMAN => "human", - Self::NORN => "norn", - Self::SYLVARI => "sylvari", + Self::Asura => "Asura", + Self::Charr => "Charr", + Self::Human => "Human", + Self::Norn => "Norn", + Self::Sylvari => "Sylvari", + Self::Unknown => "Unknown", } } } -impl ToString for Race { - fn to_string(&self) -> String { - self.as_ref().to_string() + +impl std::fmt::Display for Race { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/crates/jokoapi/src/end_point/specializations/mod.rs b/crates/jokoapi/src/end_point/specializations/mod.rs index e69de29..a9058c8 100644 --- a/crates/jokoapi/src/end_point/specializations/mod.rs +++ b/crates/jokoapi/src/end_point/specializations/mod.rs @@ -0,0 +1,248 @@ +use std::str::FromStr; + +use crate::prelude::*; + +/// Filter for which specializations (the third traitline) will the marker be active for +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[repr(u8)] +pub enum Specialization { + Dueling = 0, + DeathMagic = 1, + Invocation = 2, + Strength = 3, + Druid = 4, + Explosives = 5, + Daredevil = 6, + Marksmanship = 7, + Retribution = 8, + Domination = 9, + Tactics = 10, + Salvation = 11, + Valor = 12, + Corruption = 13, + Devastation = 14, + Radiance = 15, + Water = 16, + Berserker = 17, + BloodMagic = 18, + ShadowArts = 19, + Tools = 20, + Defense = 21, + Inspiration = 22, + Illusions = 23, + NatureMagic = 24, + Earth = 25, + Dragonhunter = 26, + DeadlyArts = 27, + Alchemy = 28, + Skirmishing = 29, + Fire = 30, + BeastMastery = 31, + WildernessSurvival = 32, + Reaper = 33, + CriticalStrikes = 34, + Arms = 35, + Arcane = 36, + Firearms = 37, + Curses = 38, + Chronomancer = 39, + Air = 40, + Zeal = 41, + Scrapper = 42, + Trickery = 43, + Chaos = 44, + Virtues = 45, + Inventions = 46, + Tempest = 47, + Honor = 48, + SoulReaping = 49, + Discipline = 50, + Herald = 51, + Spite = 52, + Acrobatics = 53, + Soulbeast = 54, + Weaver = 55, + Holosmith = 56, + Deadeye = 57, + Mirage = 58, + Scourge = 59, + Spellbreaker = 60, + Firebrand = 61, + Renegade = 62, + Harbinger = 63, + Willbender = 64, + Virtuoso = 65, + Catalyst = 66, + Bladesworn = 67, + Vindicator = 68, + Mechanist = 69, + Specter = 70, + Untamed = 71, +} + +impl FromStr for Specialization { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(match s { + "dueling" => Self::Dueling, + "deathmagic" => Self::DeathMagic, + "invocation" => Self::Invocation, + "strength" => Self::Strength, + "druid" => Self::Druid, + "explosives" => Self::Explosives, + "daredevil" => Self::Daredevil, + "marksmanship" => Self::Marksmanship, + "retribution" => Self::Retribution, + "domination" => Self::Domination, + "tactics" => Self::Tactics, + "salvation" => Self::Salvation, + "valor" => Self::Valor, + "corruption" => Self::Corruption, + "devastation" => Self::Devastation, + "radiance" => Self::Radiance, + "water" => Self::Water, + "berserker" => Self::Berserker, + "bloodmagic" => Self::BloodMagic, + "shadowarts" => Self::ShadowArts, + "tools" => Self::Tools, + "defense" => Self::Defense, + "inspiration" => Self::Inspiration, + "illusions" => Self::Illusions, + "naturemagic" => Self::NatureMagic, + "earth" => Self::Earth, + "dragonhunter" => Self::Dragonhunter, + "deadlyarts" => Self::DeadlyArts, + "alchemy" => Self::Alchemy, + "skirmishing" => Self::Skirmishing, + "fire" => Self::Fire, + "beastmastery" => Self::BeastMastery, + "wildernesssurvival" => Self::WildernessSurvival, + "reaper" => Self::Reaper, + "criticalstrikes" => Self::CriticalStrikes, + "arms" => Self::Arms, + "arcane" => Self::Arcane, + "firearms" => Self::Firearms, + "curses" => Self::Curses, + "chronomancer" => Self::Chronomancer, + "air" => Self::Air, + "zeal" => Self::Zeal, + "scrapper" => Self::Scrapper, + "trickery" => Self::Trickery, + "chaos" => Self::Chaos, + "virtues" => Self::Virtues, + "inventions" => Self::Inventions, + "tempest" => Self::Tempest, + "honor" => Self::Honor, + "soulreaping" => Self::SoulReaping, + "discipline" => Self::Discipline, + "herald" => Self::Herald, + "spite" => Self::Spite, + "acrobatics" => Self::Acrobatics, + "soulbeast" => Self::Soulbeast, + "weaver" => Self::Weaver, + "holosmith" => Self::Holosmith, + "deadeye" => Self::Deadeye, + "mirage" => Self::Mirage, + "scourge" => Self::Scourge, + "spellbreaker" => Self::Spellbreaker, + "firebrand" => Self::Firebrand, + "renegade" => Self::Renegade, + "harbinger" => Self::Harbinger, + "willbender" => Self::Willbender, + "virtuoso" => Self::Virtuoso, + "catalyst" => Self::Catalyst, + "bladesworn" => Self::Bladesworn, + "vindicator" => Self::Vindicator, + "mechanist" => Self::Mechanist, + "specter" => Self::Specter, + "untamed" => Self::Untamed, + _ => return Err("invalid specialization"), + }) + } +} + +impl AsRef for Specialization { + fn as_ref(&self) -> &str { + match self { + Self::Dueling => "dueling", + Self::DeathMagic => "deathmagic", + Self::Invocation => "invocation", + Self::Strength => "strength", + Self::Druid => "druid", + Self::Explosives => "explosives", + Self::Daredevil => "daredevil", + Self::Marksmanship => "marksmanship", + Self::Retribution => "retribution", + Self::Domination => "domination", + Self::Tactics => "tactics", + Self::Salvation => "salvation", + Self::Valor => "valor", + Self::Corruption => "corruption", + Self::Devastation => "devastation", + Self::Radiance => "radiance", + Self::Water => "water", + Self::Berserker => "berserker", + Self::BloodMagic => "bloodmagic", + Self::ShadowArts => "shadowarts", + Self::Tools => "tools", + Self::Defense => "defense", + Self::Inspiration => "inspiration", + Self::Illusions => "illusions", + Self::NatureMagic => "naturemagic", + Self::Earth => "earth", + Self::Dragonhunter => "dragonhunter", + Self::DeadlyArts => "deadlyarts", + Self::Alchemy => "alchemy", + Self::Skirmishing => "skirmishing", + Self::Fire => "fire", + Self::BeastMastery => "beastmastery", + Self::WildernessSurvival => "wildernesssurvival", + Self::Reaper => "reaper", + Self::CriticalStrikes => "criticalstrikes", + Self::Arms => "arms", + Self::Arcane => "arcane", + Self::Firearms => "firearms", + Self::Curses => "curses", + Self::Chronomancer => "chronomancer", + Self::Air => "air", + Self::Zeal => "zeal", + Self::Scrapper => "scrapper", + Self::Trickery => "trickery", + Self::Chaos => "chaos", + Self::Virtues => "virtues", + Self::Inventions => "inventions", + Self::Tempest => "tempest", + Self::Honor => "honor", + Self::SoulReaping => "soulreaping", + Self::Discipline => "discipline", + Self::Herald => "herald", + Self::Spite => "spite", + Self::Acrobatics => "acrobatics", + Self::Soulbeast => "soulbeast", + Self::Weaver => "weaver", + Self::Holosmith => "holosmith", + Self::Deadeye => "deadeye", + Self::Mirage => "mirage", + Self::Scourge => "scourge", + Self::Spellbreaker => "spellbreaker", + Self::Firebrand => "firebrand", + Self::Renegade => "renegade", + Self::Harbinger => "harbinger", + Self::Willbender => "willbender", + Self::Virtuoso => "virtuoso", + Self::Catalyst => "catalyst", + Self::Bladesworn => "bladesworn", + Self::Vindicator => "vindicator", + Self::Mechanist => "mechanist", + Self::Specter => "specter", + Self::Untamed => "untamed", + } + } +} + +impl std::fmt::Display for Specialization { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) + } +} diff --git a/crates/jokoapi/src/lib.rs b/crates/jokoapi/src/lib.rs index a57a903..8567c08 100644 --- a/crates/jokoapi/src/lib.rs +++ b/crates/jokoapi/src/lib.rs @@ -23,7 +23,7 @@ pub(crate) mod prelude { pub type HttpClient = ureq::Agent; pub use crate::end_point::EndPoint; pub use enumflags2::bitflags; - pub use miette::{IntoDiagnostic, Result, WrapErr}; + pub use miette::{IntoDiagnostic, Result}; pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; pub use std::fmt::Display; const API_BASE_URL: &str = "https://api.guildwars2.com"; diff --git a/crates/jokolay/Cargo.toml b/crates/jokolay/Cargo.toml index a6e6cc4..d61b300 100644 --- a/crates/jokolay/Cargo.toml +++ b/crates/jokolay/Cargo.toml @@ -8,37 +8,50 @@ default-run = "jokolay" name = "jokolay" path = "src/main.rs" + [features] # will not work because wayland won't allow us to get global cursor position wayland = ["egui_window_glfw_passthrough/wayland"] + + [dependencies] -joko_core = { path = "../joko_core" } -joko_render = { path = "../joko_render" } -jmf = { path = "../joko_marker_format", package = "joko_marker_format" } -jokolink = { path = "../jokolink" } -url = { workspace = true, features = ["serde"] } -egui_window_glfw_passthrough = { version = "0.5" } +enumflags2 = { workspace = true } +joko_component_manager = { path = "../joko_component_manager" } +joko_component_models = { path = "../joko_component_models" } +joko_ui_models = { path = "../joko_ui_models" } +joko_plugin_manager = { path = "../joko_plugin_manager" } +joko_render_manager = { path = "../joko_render_manager" } +joko_package_manager = { path = "../joko_package_manager" } +joko_link_manager = { path = "../joko_link_manager" } +joko_link_ui_manager = { path = "../joko_link_ui_manager" } +joko_link_models = { path = "../joko_link_models" } +egui_window_glfw_passthrough = { version = "0.8" } # we use this instead of cap-dirs because we want to debug/show the jokolay path to users # and `Dir` from cap-dirs doesn't allow us to get the path. -cap-directories = { version = "*" } +cap-directories = { workspace = true } cap-std = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "time", -] } # for ErrorLayer -tracing-appender = { version = "*" } +tracing-appender = { workspace = true } +tracing-subscriber = { workspace = true } miette = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +indexmap = { workspace = true } +ringbuffer = { workspace = true } -egui = { workspace = true, features = ["serde"] } -egui_extras = { workspace = true } -ringbuffer = { workspace = true } rayon = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } -indexmap = { workspace = true } rfd = { workspace = true } glam = { workspace = true } -# sea-orm ={ version = "*", features = ["sqlx-sqlite"]} +scopeguard = "1.2.0" +smol_str = { workspace = true } + +egui = { workspace = true, features = ["serde"] } +egui_extras = { workspace = true } + +uuid = { workspace = true } +toml = "0.8.12" +directories-next = "2.0.0" + diff --git a/crates/jokolay/src/app/init.rs b/crates/jokolay/src/app/init.rs index 78a09bd..d26f821 100644 --- a/crates/jokolay/src/app/init.rs +++ b/crates/jokolay/src/app/init.rs @@ -1,15 +1,23 @@ use cap_std::{ambient_authority, fs_utf8::camino::Utf8PathBuf, fs_utf8::Dir}; use miette::{Context, IntoDiagnostic, Result}; + /// Jokolay Configuration /// We will read a path from env `JOKOLAY_DATA_DIR` or create a folder at data_local_dir/jokolay, where data_local_dir is platform specific /// Inside this directory, we will store all of jokolay's data like configuration files, themes, logs etc.. +pub fn get_jokolay_path() -> Result { + if let Some(project_dir) = directories_next::ProjectDirs::from("com.jokolay", "", "jokolay") { + Ok(project_dir.data_local_dir().to_path_buf()) + } else { + Err(miette::miette!( + "getting project path failed for some reason" + )) + } +} + pub fn get_jokolay_dir() -> Result { let authoratah = ambient_authority(); let jdir = if let Ok(env_dir) = std::env::var("JOKOLAY_DATA_DIR") { - let jkl_path = Utf8PathBuf::try_from(&env_dir) - .into_diagnostic() - .wrap_err(env_dir) - .wrap_err("failed to parse JOKOLAY_DATA_DIR")?; + let jkl_path = Utf8PathBuf::from(&env_dir); //may still be an invalid path cap_std::fs_utf8::Dir::create_ambient_dir_all(&jkl_path, authoratah) .into_diagnostic() @@ -20,7 +28,9 @@ pub fn get_jokolay_dir() -> Result { .wrap_err(jkl_path) .wrap_err("failed to open jokolay data dir")? } else { - let dir = cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah) + let project_dir = + cap_directories::ProjectDirs::from("com.jokolay", "", "jokolay", authoratah); + let dir = project_dir .ok_or(miette::miette!( "getting project dirs failed for some reason" ))? diff --git a/crates/jokolay/src/app/menu.rs b/crates/jokolay/src/app/menu.rs new file mode 100644 index 0000000..aee7f08 --- /dev/null +++ b/crates/jokolay/src/app/menu.rs @@ -0,0 +1,294 @@ +use std::sync::{Arc, RwLock}; + +use egui_window_glfw_passthrough::GlfwBackend; +use joko_component_models::{ + default_component_result, from_broadcast, Component, ComponentChannels, ComponentResult, +}; +use joko_link_models::{MumbleLink, UISize}; +use joko_ui_models::{UIArea, UIPanel}; +use tracing::info; + +use super::window::{MINIMAL_WINDOW_HEIGHT, MINIMAL_WINDOW_WIDTH}; + +struct MenuPanel { + panel: Arc>, + areas: Vec, + nb_draw: u128, + draw_time: std::time::Duration, +} + +struct MenuPanelManagerChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, +} + +/// Guild Wars 2 has an array of menu icons on top left corner of the game. +/// Its size is affected by four different factors +/// 1. UISZ: +/// This is a setting in graphics options of gw2 and it comes in 4 variants +/// small, normal, large and larger. +/// This is something we can get from mumblelink's context. +/// 2. DPI scaling +/// This is a setting in graphics options too. When scaling is enabled, sizes of menu become bigger according to the dpi of gw2 window +/// This is something we get from gw2's config file in AppData/Roaming and store in mumble link as dpi scaling +/// We also get dpi of gw2 window and store it in mumble link. +/// 3. Dimensions of the gw2 window +/// This is something we get from mumble link and win32 api. We store this as client pos/size in mumble link +/// It is not just the width or height, but their ratio to the 1024x768 resolution +/// +/// 1. By default, with dpi 96 (scale 1.0), at resolution 1024x768 these are the sizes of menu at different uisz settings +/// UISZ -> WIDTH HEIGHT +/// small -> 288 27 +/// normal -> 319 31 +/// large -> 355 34 +/// larger -> 391 38 +/// all units are in raw pixels. +/// +/// If we think of small uisz as the default. Then, we can express the rest of the sizes as ratio to small. +/// small = 1.0 +/// normal = 1.1 +/// large = 1.23 +/// larger = 1.35 +/// +/// So, just multiply small (288) with these ratios to get the actual pixels of each uisz. +/// 2. When dpi doubles, so do the sizes. 288 -> 576, 319 -> 638 etc.. So, when dpi scaling is enabled, we must multiply the above uisz ratio with dpi scale ratio to get the combined scaling ratio. +/// 3. The dimensions thing is a little complicated. So, i will just list the actual steps here. +/// 1. take gw2's actual width in raw pixels. lets call this gw2_width. +/// 2. take 1024 as reference minimum width. If dpi scaling is enabled, multiply 1024 * dpi scaling ratio. lets call this reference_width. +/// 3. Now, get the smaller value out of the two. lets call this minimum_width. +/// 4. finally, do (minimum_width / reference_width) to get "width scaling ratio". +/// 5. repeat steps 1 - 4, but for height. use 768 as the reference width (with approapriate dpi scaling). +/// 6. now just take the minimum of "width scaling ratio" and "height scaling ratio". lets call this "aspect ratio scaling". +/// +/// Finally, just multiply the width 288 or height 27 with these three values. +/// eg: menu width = 288 * uisz_ratio * dpi_scaling_ratio * aspect_ratio_scaling; +/// do the same with 288 replaced by 27 for height. +pub struct MenuPanelManager { + pub pos: egui::Pos2, + pub ui_scaling_factor: f32, + pub show_tracing_window: bool, + glfw_backend: Arc>, + egui_context: egui::Context, + menus: Vec, + channels: Option, +} + +unsafe impl Send for MenuPanelManager {} +unsafe impl Sync for MenuPanelManager {} + +impl MenuPanelManager { + pub const WIDTH: f32 = 288.0; + pub const HEIGHT: f32 = 27.0; + + pub fn new(glfw_backend: Arc>, egui_context: egui::Context) -> Self { + Self { + glfw_backend, + egui_context, + pos: Default::default(), + show_tracing_window: Default::default(), + ui_scaling_factor: Default::default(), + menus: Default::default(), + channels: None, + } + } + + pub fn register(&mut self, component: Arc>) { + self.menus.push(MenuPanel { + panel: component.clone(), + areas: component.read().unwrap().areas(), + nb_draw: 0, + draw_time: Default::default(), + }) + } + + pub fn gui(&mut self, latest_time: f64) { + //let mut glfw_backend = self.glfw_backend.(); + // do the gui stuff now + egui::Area::new("menu panel") + .fixed_pos(self.pos) + .interactable(true) + .order(egui::Order::Foreground) + .show(&self.egui_context, |ui| { + ui.style_mut().visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT; + ui.horizontal(|ui| { + ui.menu_button( + egui::RichText::new("JKL") + .size((MenuPanelManager::HEIGHT - 2.0) * self.ui_scaling_factor) + .background_color(egui::Color32::TRANSPARENT), + |ui: &mut egui::Ui| { + let mut any_open = false; + for panel in self.menus.iter_mut() { + for area in panel.areas.iter_mut() { + if area.name.is_empty() { + continue; + } + ui.checkbox(&mut area.is_open, &area.name); + any_open = any_open || area.is_open; + } + } + //ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); + if any_open && ui.button("Close all panels").clicked() { + for panel in self.menus.iter_mut() { + for area in panel.areas.iter_mut() { + area.is_open = false; + } + } + } + if ui.button("exit").clicked() { + info!("exiting jokolay"); + self.glfw_backend + .write() + .unwrap() + .window + .set_should_close(true); + } + }, + ); + for panel in self.menus.iter_mut() { + let handle = &mut panel.panel.write().unwrap(); + handle.menu_ui(ui); + } + }); + }); + for panel in self.menus.iter_mut() { + let handle = &mut panel.panel.write().unwrap(); + let start = std::time::SystemTime::now(); + for area in panel.areas.iter_mut() { + handle.gui(&mut area.is_open, &area.id, latest_time); + } + panel.nb_draw += 1; + panel.draw_time += start.elapsed().unwrap(); + } + } +} + +fn convert_uisz_to_scale(uisize: UISize) -> f32 { + const SMALL: f32 = 288.0; + const NORMAL: f32 = 319.0; + const LARGE: f32 = 355.0; + const LARGER: f32 = 391.0; + const SMALL_SCALING_RATIO: f32 = 1.0; + const NORMAL_SCALING_RATIO: f32 = NORMAL / SMALL; + const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; + const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; + match uisize { + UISize::Small => SMALL_SCALING_RATIO, + UISize::Normal => NORMAL_SCALING_RATIO, + UISize::Large => LARGE_SCALING_RATIO, + UISize::Larger => LARGER_SCALING_RATIO, + } +} +/* +Just some random measurements to verify in the future (or write tests for :)) +with dpi enabled, there's some math involved it seems. +Linux -> +width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled +small -> 540 53 +normal -> 599 59 +large -> 667 65 +larger -> 734 72 + + +Windows -> +width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled. +small -> 540 53 +normal -> 599 59 +large -> 667 65 +larger -> 734 72 + +width 1914 pixels. height 2072 pixels. ratio 0.92. fov 1.01. scaling 3.0. dpi enabled. dpi 288 +small -> 538 52 +normal -> 598 58 +large -> 665 65 +larger -> 731 72 + +width 3840. height 2160. ratio 1.78. scaling 3. dpi true. dpi 288 (windowed fullscreen) +small -> 810 80 +normal -> 900 89 +large -> 1000 99 +larger -> 1100 109 + +width 1916 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 1.5. dpi enabled. dpi 144 +small -> 432 42 +normal -> 480 47 +large -> 533 52 +larger -> 586 57 + +width 1000 pixels. height 1000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. +small -> 281 26 +normal -> 312 29 +large -> 347 33 +larger -> 382 36 + +width 2000 pixels. height 1000 pixels. ratio 2. fov 1.01. scaling 2.0. dpi enabled. +small -> 375 36 +normal -> 416 40 +large -> 463 45 +larger -> 509 49 + +width 2000 pixels. height 2000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. +small -> 562 55 +normal -> 624 61 +large -> 694 68 +larger -> 764 75 + + +*/ + +impl Component for MenuPanelManager { + fn init(&mut self) {} + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = MenuPanelManagerChannels { + subscription_mumblelink: channels.requirements.remove(&0).unwrap(), + }; + self.channels = Some(channels); + } + fn accept_notifications(&self) -> bool { + false + } + fn flush_all_messages(&mut self) {} + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let egui_context = &self.egui_context; + let raw_link = { + let channels = self.channels.as_mut().unwrap(); + channels.subscription_mumblelink.try_recv().unwrap() + }; + let link: Option = from_broadcast(&raw_link); + + let mut ui_scaling_factor = 1.0; + if let Some(link) = link.as_ref() { + let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { + (if link.dpi == 0 { 96.0 } else { link.dpi as f32 }) / 96.0 + } else { + 1.0 + }; + + ui_scaling_factor *= gw2_scale; + let uisz_scale = convert_uisz_to_scale(link.uisz); + ui_scaling_factor *= uisz_scale; + + let min_width = MINIMAL_WINDOW_WIDTH as f32 * gw2_scale; + let min_height = MINIMAL_WINDOW_HEIGHT as f32 * gw2_scale; + let gw2_width = link.client_size.0.x.max(MINIMAL_WINDOW_WIDTH) as f32; + let gw2_height = link.client_size.0.y.max(MINIMAL_WINDOW_HEIGHT) as f32; + let min_width_ratio = min_width.min(gw2_width) / min_width; + let min_height_ratio = min_height.min(gw2_height) / min_height; + + let min_ratio = min_height_ratio.min(min_width_ratio); + ui_scaling_factor *= min_ratio; + + let egui_scale = egui_context.pixels_per_point(); + ui_scaling_factor /= egui_scale; + } + + self.pos.x = ui_scaling_factor * (Self::WIDTH + 8.0); // add 8 pixels padding just for some space + self.ui_scaling_factor = ui_scaling_factor; + default_component_result() + } +} diff --git a/crates/jokolay/src/app/messages.rs b/crates/jokolay/src/app/messages.rs new file mode 100644 index 0000000..3a24aa1 --- /dev/null +++ b/crates/jokolay/src/app/messages.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToApplicationBack { + SaveUIConfiguration(String), +} diff --git a/crates/jokolay/src/app/mod.rs b/crates/jokolay/src/app/mod.rs index fef6821..d2117d9 100644 --- a/crates/jokolay/src/app/mod.rs +++ b/crates/jokolay/src/app/mod.rs @@ -1,39 +1,118 @@ -use std::sync::Arc; +use std::{ + path::PathBuf, + sync::{Arc, RwLock}, + thread, +}; use cap_std::fs_utf8::Dir; -use egui_window_glfw_passthrough::{glfw::Context as _, GlfwBackend, GlfwConfig}; +use egui_window_glfw_passthrough::{GlfwBackend, GlfwConfig}; +use joko_link_ui_manager::MumbleUIManager; +use joko_plugin_manager::JokolayPluginManager; mod init; -mod wm; -use init::get_jokolay_dir; -use jmf::MarkerManager; -use joko_core::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; -use joko_render::JokoRenderer; -use jokolink::{MumbleChanges, MumbleManager}; -use miette::{Context, Result}; -use tracing::{error, info}; +mod menu; +mod ui_parameters; +mod window; + +use crate::manager::{theme::ThemeManager, trace::JokolayTracingLayer}; +use init::{get_jokolay_dir, get_jokolay_path}; +use joko_component_manager::{ComponentExecutor, ComponentManager}; +use joko_package_manager::{PackageDataManager, PackageUIManager}; + +use joko_link_manager::MumbleManager; +use joko_package_manager::jokolay_to_editable_path; +use joko_render_manager::renderer::JokoRenderer; +use miette::{Context, IntoDiagnostic, Result}; +use tracing::{error, info_span}; + +use self::{menu::MenuPanelManager, window::WindowManager}; + +struct JokolayGui { + menu_panel: Arc>, + egui_context: egui::Context, + glfw_backend: Arc>, +} #[allow(unused)] pub struct Jokolay { - frame_stats: wm::WindowStatistics, - jdir: Arc, - menu_panel: MenuPanel, - mumble_manager: MumbleManager, - marker_manager: MarkerManager, - theme_manager: ThemeManager, - joko_renderer: JokoRenderer, - egui_context: egui::Context, - glfw_backend: GlfwBackend, + gui: JokolayGui, + app: ComponentManager, } + impl Jokolay { - pub fn new(jdir: Arc) -> Result { - let mumble = - MumbleManager::new("MumbleLink", None).wrap_err("failed to create mumble manager")?; - let marker_manager = - MarkerManager::new(&jdir).wrap_err("failed to create marker manager")?; - let mut theme_manager = - ThemeManager::new(&jdir).wrap_err("failed to create theme manager")?; + pub fn new(root_dir: Arc, root_path: std::path::PathBuf) -> Result { + /* + We have two mumble_managers, one for UI, one for backend, each keeping its own copy + this avoid transmition between threads to read same data from system + It happens anyway when the UI start the edit mode of the mumble link. + */ + + let mut component_manager = ComponentManager::new(); + + let _ = component_manager.register( + "ui:mumble_link", + Arc::new(RwLock::new( + MumbleManager::new("MumbleLink", true) + .wrap_err("failed to create mumble manager")?, + )), + ); + let _ = component_manager.register( + "back:mumble_link", + Arc::new(RwLock::new( + MumbleManager::new("MumbleLink", false) + .wrap_err("failed to create mumble manager")?, + )), + ); let egui_context = egui::Context::default(); - theme_manager.init_egui(&egui_context); - let mut glfw_backend = GlfwBackend::new(GlfwConfig { + let mumble_ui = Arc::new(RwLock::new(MumbleUIManager::new(egui_context.clone()))); + + component_manager + .register("ui:mumble_ui", mumble_ui.clone()) + .unwrap(); + + /* + components can be migrated to plugins + root_path/ + ui.toml + components/ + mumble_link/ + ... + theme_manager/ + ... + package_ui/ + ... + package_data/ + ... + plugins/ + plugin1 + plugin2 + ... + */ + + let plugin_manager = Arc::new(RwLock::new(JokolayPluginManager::new(PathBuf::from( + "plugins", + )))); + let dummy_plugin = Arc::new(RwLock::new( + plugin_manager + .as_ref() + .write() + .unwrap() + .create("dummy plugin".to_string()), + )); + let _ = component_manager.register("dummy plugin", dummy_plugin); + + let _ = component_manager.register( + "back:jokolay_package_manager", + Arc::new(RwLock::new(PackageDataManager::new( + &root_path, //TODO: when given to a plugin, root MUST be unique to the plugin and cannot be global to jokolay + )?)), + ); + + let theme_manager = Arc::new(RwLock::new( + ThemeManager::new(Arc::clone(&root_dir), egui_context.clone()) + .wrap_err("failed to create theme manager")?, + )); + + #[allow(clippy::arc_with_non_send_sync)] + let glfw_backend = Arc::new(RwLock::new(GlfwBackend::new(GlfwConfig { glfw_callback: Box::new(|glfw_context| { glfw_context.window_hint( egui_window_glfw_passthrough::glfw::WindowHint::SRgbCapable(true), @@ -49,203 +128,189 @@ impl Jokolay { transparent_window: Some(true), window_title: "Jokolay".to_string(), ..Default::default() - }); - glfw_backend.window.set_floating(true); - glfw_backend.window.set_decorated(false); - let joko_renderer = JokoRenderer::new(&mut glfw_backend, Default::default()); - Ok(Self { - mumble_manager: mumble, - marker_manager, - frame_stats: wm::WindowStatistics::new(glfw_backend.glfw.get_time() as _), - joko_renderer, + }))); + let _ = component_manager.register( + "ui:window_manager", + Arc::new(RwLock::new(WindowManager::new(Arc::clone(&glfw_backend)))), + ); + + let package_manager_ui = Arc::new(RwLock::new(PackageUIManager::new( + egui_context.clone(), + JokoRenderer::get_z_near(), + ))); + let _ = + component_manager.register("ui:jokolay_package_manager", package_manager_ui.clone()); + + let renderer_ui = Arc::new(RwLock::new(JokoRenderer::new( + Arc::clone(&glfw_backend), + egui_context.clone(), + ))); + let _ = component_manager.register("ui:jokolay_renderer", renderer_ui.clone()); + + let editable_path = jokolay_to_editable_path(&root_path) + .to_str() + .unwrap() + .to_string(); + + let configuration_ui = Arc::new(RwLock::new(ui_parameters::JokolayUIConfiguration::new( + Arc::clone(&glfw_backend), + egui_context.clone(), + editable_path.clone(), + root_path.to_str().unwrap().to_owned(), + ))); + let _ = component_manager.register("ui:configuration", configuration_ui.clone()); + + let _ = component_manager.register( + "back:configuration", + Arc::new(RwLock::new(ui_parameters::JokolayConfiguration::new( + Arc::clone(&root_dir), + ))), + ); + + let menu_panel = Arc::new(RwLock::new(MenuPanelManager::new( + Arc::clone(&glfw_backend), + egui_context.clone(), + ))); + + let _ = component_manager.register("ui:menu_panel", menu_panel.clone()); + + match component_manager.build_routes() { + Ok(_) => {} + Err(e) => { + panic!("Could not build component routes. {}", e); + } + } + + /* + Configuration + Themes + Mumble Manager + Package Manager + File Manader => where ? + + close all + exit + */ + if let Ok(mut menu_panel) = menu_panel.write() { + menu_panel.register(configuration_ui); + menu_panel.register(theme_manager); + menu_panel.register(mumble_ui); + menu_panel.register(package_manager_ui); + menu_panel.register(renderer_ui); + } + + let gui = JokolayGui { glfw_backend, - jdir, egui_context, - theme_manager, - menu_panel: MenuPanel::default(), + menu_panel, + }; + //let gui = Mutex::new(gui); + //let gui = Arc::new(gui); + //let gui = Box::new(gui); + Ok(Self { + gui, + app: component_manager, }) } - pub fn enter_event_loop(mut self) { + + fn start_background_loop(mut executor: ComponentExecutor) { + let _background_thread = std::thread::spawn(move || { + tracing::info!("Initialize the background components"); + executor.init(); + let _ = Self::background_loop(executor); + }); + } + + fn background_loop(mut executor: ComponentExecutor) -> Result<()> { + tracing::info!("entering background event loop"); + let _span_guard = info_span!("background event loop").entered(); + let mut loop_index: u128 = 0; + let start = std::time::SystemTime::now(); + loop { + tracing::trace!("background loop tick: {}", loop_index); + let latest_time = start.elapsed().into_diagnostic()?.as_secs_f64(); + executor.tick(latest_time); + + thread::sleep(std::time::Duration::from_millis(10)); + loop_index += 1; + } + #[allow(unreachable_code)] + { + drop(_span_guard); + unreachable!("Program broke out a never ending loop !") + } + } + + pub fn enter_event_loop(&mut self) { + // do all the non-gui stuff + Self::start_background_loop(self.app.executor("back")); + tracing::info!("entering glfw event loop"); - self.menu_panel.show_theme_window = true; - self.menu_panel.show_marker_manager_window = true; + let span_guard = info_span!("glfw event loop").entered(); + let mut ui_executor = self.app.executor("ui"); + + ui_executor.init(); + loop { - let Self { - frame_stats, - jdir: _, + let JokolayGui { menu_panel, - mumble_manager, - marker_manager, - theme_manager, - joko_renderer, egui_context, glfw_backend, - } = &mut self; - let etx = egui_context.clone(); - - // gather events - glfw_backend.glfw.poll_events(); - glfw_backend.tick(); + } = &mut self.gui; + + let latest_time = { + let mut glfw_backend = glfw_backend.write().unwrap(); + let latest_time = glfw_backend.glfw.get_time(); + + // gather events + glfw_backend.glfw.poll_events(); + glfw_backend.tick(); + if glfw_backend.window.should_close() { + tracing::warn!("should close is true. So, exiting event loop"); + break; + } + let mut input = glfw_backend.take_raw_input(); + input.time = Some(latest_time); - if glfw_backend.window.should_close() { - tracing::warn!("should close is true. So, exiting event loop"); - break; - } + egui_context.begin_frame(input); + latest_time + }; + ui_executor.tick(latest_time); - if glfw_backend.resized_event_pending { - let latest_size = glfw_backend.window.get_framebuffer_size(); - let latest_size = [latest_size.0 as _, latest_size.1 as _]; - - glfw_backend.framebuffer_size_physical = latest_size; - glfw_backend.window_size_logical = [ - latest_size[0] as f32 / glfw_backend.scale, - latest_size[1] as f32 / glfw_backend.scale, - ]; - joko_renderer.resize_framebuffer(latest_size); - glfw_backend.resized_event_pending = false; + if let Ok(mut menu_panel) = menu_panel.write() { + menu_panel.gui(latest_time); + JokolayTracingLayer::gui(egui_context, &mut menu_panel.show_tracing_window); + } else { + println!("cannot update GUI due to lock issues"); } - joko_renderer.prepare_frame(|| { - let latest_size = glfw_backend.window.get_framebuffer_size(); - tracing::info!( - ?latest_size, - "failed to get surface texture, so calling latest framebuffer size" - ); - let latest_size = [latest_size.0 as _, latest_size.1 as _]; - glfw_backend.framebuffer_size_physical = latest_size; - glfw_backend.window_size_logical = [ - latest_size[0] as f32 / glfw_backend.scale, - latest_size[1] as f32 / glfw_backend.scale, - ]; - latest_size - }); - - let latest_time = glfw_backend.glfw.get_time(); - let mut input = glfw_backend.take_raw_input(); - input.time = Some(latest_time); - - etx.begin_frame(input); - // do all the non-gui stuff first - frame_stats.tick(latest_time); - let link = match mumble_manager.tick() { - Ok(ml) => ml, - Err(e) => { - error!(?e, "mumble manager tick error"); - None - } - }; - joko_renderer.tick(link.clone()); - marker_manager.tick(&etx, latest_time, joko_renderer, &link); - menu_panel.tick(&etx, link.clone().as_ref().map(|m| m.as_ref())); - - // do the gui stuff now - egui::Area::new("menu panel") - .fixed_pos(menu_panel.pos) - .interactable(true) - .order(egui::Order::Foreground) - .show(&etx, |ui| { - ui.style_mut().visuals.widgets.inactive.weak_bg_fill = - egui::Color32::TRANSPARENT; - ui.horizontal(|ui| { - ui.menu_button( - egui::RichText::new("JKL") - .size((MenuPanel::HEIGHT - 2.0) * menu_panel.ui_scaling_factor) - .background_color(egui::Color32::TRANSPARENT), - |ui| { - ui.checkbox( - &mut menu_panel.show_window_manager, - "Show Window Manager", - ); - ui.checkbox( - &mut menu_panel.show_marker_manager_window, - "Show Marker Manager", - ); - ui.checkbox( - &mut menu_panel.show_mumble_manager_winodw, - "Show Mumble Manager", - ); - ui.checkbox( - &mut menu_panel.show_theme_window, - "Show Theme Manager", - ); - ui.checkbox(&mut menu_panel.show_tracing_window, "Show Logs"); - if ui.button("exit").clicked() { - info!("exiting jokolay"); - glfw_backend.window.set_should_close(true); - } - }, - ); - marker_manager.menu_ui(ui); - }); - }); - marker_manager.gui(&etx, &mut menu_panel.show_marker_manager_window); - mumble_manager.gui(&etx, &mut menu_panel.show_mumble_manager_winodw); - JokolayTracingLayer::gui(&etx, &mut menu_panel.show_tracing_window); - theme_manager.gui(&etx, &mut menu_panel.show_theme_window); - frame_stats.gui(&etx, glfw_backend, &mut menu_panel.show_window_manager); // show notifications - JokolayTracingLayer::show_notifications(&etx); + JokolayTracingLayer::show_notifications(egui_context); // end gui stuff - // check if we need to change window position or size. - if let Some(link) = link.as_ref() { - if link.changes.contains(MumbleChanges::WindowPosition) - || link.changes.contains(MumbleChanges::WindowSize) - { - info!( - ?link.client_pos, ?link.client_size, - "resizing/repositioning to match gw2 window dimensions" - ); - - glfw_backend - .window - .set_pos(link.client_pos.x, link.client_pos.y); - // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. - // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) - // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 - glfw_backend - .window - .set_size(link.client_size.x - 1, link.client_size.y - 1); - } - } - etx.request_repaint(); - - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - .. - } = etx.end_frame(); - - if !platform_output.copied_text.is_empty() { - glfw_backend - .window - .set_clipboard_string(&platform_output.copied_text); - } + //egui_context.request_repaint(); - // if it doesn't require either keyboard or pointer, set passthrough to true - glfw_backend - .window - .set_mouse_passthrough(!(etx.wants_keyboard_input() || etx.wants_pointer_input())); - joko_renderer.render_egui( - etx.tessellate(shapes), - textures_delta, - glfw_backend.window_size_logical, - ); - joko_renderer.present(); - glfw_backend.window.swap_buffers(); + /* + let animation_time = if ui_configuration.display_parameters.animate { + latest_time + } else { + 0.0 + };*/ } + drop(span_guard); } } pub fn start_jokolay() { - let jdir = match get_jokolay_dir() { + let jokolay_dir = match get_jokolay_dir() { Ok(jdir) => jdir, Err(e) => { eprintln!("failed to create jokolay dir: {e:#?}"); panic!("failed to create jokolay_dir: {e:#?}"); } }; - let log_file_flush_guard = match JokolayTracingLayer::install_tracing(&jdir) { + let jokolay_path = get_jokolay_path().unwrap(); + + let log_file_flush_guard = match JokolayTracingLayer::install_tracing(&jokolay_dir) { Ok(g) => g, Err(e) => { eprintln!("failed to install tracing: {e:#?}"); @@ -265,8 +330,8 @@ pub fn start_jokolay() { ); } - match Jokolay::new(jdir.into()) { - Ok(jokolay) => { + match Jokolay::new(jokolay_dir.into(), jokolay_path) { + Ok(mut jokolay) => { jokolay.enter_event_loop(); } Err(e) => { @@ -275,164 +340,3 @@ pub fn start_jokolay() { }; std::mem::drop(log_file_flush_guard); } -/// Guild Wars 2 has an array of menu icons on top left corner of the game. -/// Its size is affected by four different factors -/// 1. UISZ: -/// This is a setting in graphics options of gw2 and it comes in 4 variants -/// small, normal, large and larger. -/// This is something we can get from mumblelink's context. -/// 2. DPI scaling -/// This is a setting in graphics options too. When scaling is enabled, sizes of menu become bigger according to the dpi of gw2 window -/// This is something we get from gw2's config file in AppData/Roaming and store in mumble link as dpi scaling -/// We also get dpi of gw2 window and store it in mumble link. -/// 3. Dimensions of the gw2 window -/// This is something we get from mumble link and win32 api. We store this as client pos/size in mumble link -/// It is not just the width or height, but their ratio to the 1024x768 resolution - -/// -/// 1. By default, with dpi 96 (scale 1.0), at resolution 1024x768 these are the sizes of menu at different uisz settings -/// UISZ -> WIDTH HEIGHT -/// small -> 288 27 -/// normal -> 319 31 -/// large -> 355 34 -/// larger -> 391 38 -/// all units are in raw pixels. -/// -/// If we think of small uisz as the default. Then, we can express the rest of the sizes as ratio to small. -/// small = 1.0 -/// normal = 1.1 -/// large = 1.23 -/// larger = 1.35 -/// -/// So, just multiply small (288) with these ratios to get the actual pixels of each uisz. -/// 2. When dpi doubles, so do the sizes. 288 -> 576, 319 -> 638 etc.. So, when dpi scaling is enabled, we must multiply the above uisz ratio with dpi scale ratio to get the combined scaling ratio. -/// 3. The dimensions thing is a little complicated. So, i will just list the actual steps here. -/// 1. take gw2's actual width in raw pixels. lets call this gw2_width. -/// 2. take 1024 as reference minimum width. If dpi scaling is enabled, multiply 1024 * dpi scaling ratio. lets call this reference_width. -/// 3. Now, get the smaller value out of the two. lets call this minimum_width. -/// 4. finally, do (minimum_width / reference_width) to get "width scaling ratio". -/// 5. repeat steps 1 - 4, but for height. use 768 as the reference width (with approapriate dpi scaling). -/// 6. now just take the minimum of "width scaling ratio" and "height scaling ratio". lets call this "aspect ratio scaling". -/// -/// Finally, just multiply the width 288 or height 27 with these three values. -/// eg: menu width = 288 * uisz_ratio * dpi_scaling_ratio * aspect_ratio_scaling; -/// do the same with 288 replaced by 27 for height. -#[derive(Debug, Default)] -pub struct MenuPanel { - pub pos: egui::Pos2, - pub ui_scaling_factor: f32, - show_tracing_window: bool, - show_theme_window: bool, - // show_settings_window: bool, - show_marker_manager_window: bool, - show_mumble_manager_winodw: bool, - show_window_manager: bool, -} - -impl MenuPanel { - pub const WIDTH: f32 = 288.0; - pub const HEIGHT: f32 = 27.0; - pub fn tick(&mut self, etx: &egui::Context, link: Option<&jokolink::MumbleLink>) { - let mut ui_scaling_factor = 1.0; - if let Some(link) = link.as_ref() { - let gw2_scale: f32 = if link.dpi_scaling == 1 || link.dpi_scaling == -1 { - (if link.dpi == 0 { 96.0 } else { link.dpi as f32 }) / 96.0 - } else { - 1.0 - }; - - ui_scaling_factor *= gw2_scale; - let uisz_scale = convert_uisz_to_scale(link.uisz); - ui_scaling_factor *= uisz_scale; - - let min_width = 1024.0 * gw2_scale; - let min_height = 768.0 * gw2_scale; - let gw2_width = link.client_size.x as f32; - let gw2_height = link.client_size.y as f32; - let min_width_ratio = min_width.min(gw2_width) / min_width; - let min_height_ratio = min_height.min(gw2_height) / min_height; - - let min_ratio = min_height_ratio.min(min_width_ratio); - ui_scaling_factor *= min_ratio; - - let egui_scale = etx.pixels_per_point(); - ui_scaling_factor /= egui_scale; - } - - self.pos.x = ui_scaling_factor * (Self::WIDTH + 8.0); // add 8 pixels padding just for some space - self.ui_scaling_factor = ui_scaling_factor; - } -} - -fn convert_uisz_to_scale(uisize: jokolink::UISize) -> f32 { - const SMALL: f32 = 288.0; - const NORMAL: f32 = 319.0; - const LARGE: f32 = 355.0; - const LARGER: f32 = 391.0; - const SMALL_SCALING_RATIO: f32 = 1.0; - const NORMAL_SCALING_RATIO: f32 = NORMAL / SMALL; - const LARGE_SCALING_RATIO: f32 = LARGE / SMALL; - const LARGER_SCALING_RATIO: f32 = LARGER / SMALL; - match uisize { - jokolink::UISize::Small => SMALL_SCALING_RATIO, - jokolink::UISize::Normal => NORMAL_SCALING_RATIO, - jokolink::UISize::Large => LARGE_SCALING_RATIO, - jokolink::UISize::Larger => LARGER_SCALING_RATIO, - } -} -/* -Just some random measurements to verify in the future (or write tests for :)) -with dpi enabled, there's some math involved it seems. -Linux -> -width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled -small -> 540 53 -normal -> 599 59 -large -> 667 65 -larger -> 734 72 - - -Windows -> -width 1920 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 2.0. dpi enabled. -small -> 540 53 -normal -> 599 59 -large -> 667 65 -larger -> 734 72 - -width 1914 pixels. height 2072 pixels. ratio 0.92. fov 1.01. scaling 3.0. dpi enabled. dpi 288 -small -> 538 52 -normal -> 598 58 -large -> 665 65 -larger -> 731 72 - -width 3840. height 2160. ratio 1.78. scaling 3. dpi true. dpi 288 (windowed fullscreen) -small -> 810 80 -normal -> 900 89 -large -> 1000 99 -larger -> 1100 109 - -width 1916 pixels. height 2113 pixels. ratio 0.91. fov 1.01. scaling 1.5. dpi enabled. dpi 144 -small -> 432 42 -normal -> 480 47 -large -> 533 52 -larger -> 586 57 - -width 1000 pixels. height 1000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. -small -> 281 26 -normal -> 312 29 -large -> 347 33 -larger -> 382 36 - -width 2000 pixels. height 1000 pixels. ratio 2. fov 1.01. scaling 2.0. dpi enabled. -small -> 375 36 -normal -> 416 40 -large -> 463 45 -larger -> 509 49 - -width 2000 pixels. height 2000 pixels. ratio 1. fov 1.01. scaling 2.0. dpi enabled. -small -> 562 55 -normal -> 624 61 -large -> 694 68 -larger -> 764 75 - - -*/ diff --git a/crates/jokolay/src/app/mumble.rs b/crates/jokolay/src/app/mumble.rs new file mode 100644 index 0000000..163f7e2 --- /dev/null +++ b/crates/jokolay/src/app/mumble.rs @@ -0,0 +1,289 @@ +use egui::DragValue; +use joko_link_models::{MessageToMumbleLinkBack, MumbleLink}; + +pub fn mumble_gui( + u2mb_sender: &tokio::sync::mpsc::Sender, + etx: &egui::Context, + open: &mut bool, + editable_mumble: &mut bool, + link: &mut MumbleLink, +) { + egui::Window::new("Mumble Manager") + .open(open) + .show(etx, |ui| { + ui.horizontal(|ui| { + if ui.selectable_label(!*editable_mumble, "live").clicked() { + *editable_mumble = false; + let _ = u2mb_sender.blocking_send(MessageToMumbleLinkBack::Autonomous); + } + if ui.selectable_label(*editable_mumble, "editable").clicked() { + *editable_mumble = true; + let _ = u2mb_sender.blocking_send(MessageToMumbleLinkBack::BindedOnUI); + } + }); + if *editable_mumble { + ui.label( + egui::RichText::new("Mumble is not live, values need to be manually updated.") + .color(egui::Color32::RED), + ); + editable_mumble_ui(ui, link); + } else { + let link: MumbleLink = link.clone(); + live_mumble_ui(ui, link); + } + }); +} + +fn live_mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + ui.label("ui state"); + if let Some(ui_state) = link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.compass_height)); + ui.add(DragValue::new(&mut link.compass_width)); + ui.add(DragValue::new(&mut link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.horizontal(|ui| { + ui.label(&link.name); + ui.label(format!("{:?}", link.race)); + }); + ui.end_row(); + + ui.label("map id"); + ui.add(DragValue::new(&mut link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut link.map_type)); + ui.end_row(); + ui.label("world position"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut link.map_center_x)); + ui.add(DragValue::new(&mut link.map_center_y)); + ui.add(DragValue::new(&mut link.map_scale)); + }); + ui.end_row(); + + ui.label("address"); + ui.label(format!("{}", link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut link.dpi)); + ui.end_row(); + }); +} + +fn editable_mumble_ui(ui: &mut egui::Ui, dummy_link: &mut MumbleLink) { + egui::Grid::new("link grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("ui tick"); + ui.add(DragValue::new(&mut dummy_link.ui_tick)); + ui.end_row(); + ui.label("player position"); + ui.horizontal(|ui| { + let player_pos = &mut dummy_link.player_pos.0; + ui.add(DragValue::new(&mut player_pos.x)); + ui.add(DragValue::new(&mut player_pos.y)); + ui.add(DragValue::new(&mut player_pos.z)); + }); + ui.end_row(); + ui.label("player direction"); + ui.horizontal(|ui| { + let f_avatar_front = &mut dummy_link.f_avatar_front.0; + ui.add(DragValue::new(&mut f_avatar_front.x)); + ui.add(DragValue::new(&mut f_avatar_front.y)); + ui.add(DragValue::new(&mut f_avatar_front.z)); + }); + ui.end_row(); + ui.label("camera position"); + ui.horizontal(|ui| { + let cam_pos = &mut dummy_link.cam_pos.0; + ui.add(DragValue::new(&mut cam_pos.x)); + ui.add(DragValue::new(&mut cam_pos.y)); + ui.add(DragValue::new(&mut cam_pos.z)); + }); + ui.end_row(); + ui.label("camera direction"); + ui.horizontal(|ui| { + let f_camera_front = &mut dummy_link.f_camera_front.0; + ui.add(DragValue::new(&mut f_camera_front.x)); + ui.add(DragValue::new(&mut f_camera_front.y)); + ui.add(DragValue::new(&mut f_camera_front.z)); + }); + ui.end_row(); + + ui.label("ui state"); + if let Some(ui_state) = dummy_link.ui_state { + ui.label(ui_state.to_string()); + } else { + ui.label("None"); + } + + ui.end_row(); + ui.label("compass"); + ui.horizontal(|ui| { + ui.add(DragValue::new(&mut dummy_link.compass_height)); + ui.add(DragValue::new(&mut dummy_link.compass_width)); + ui.add(DragValue::new(&mut dummy_link.compass_rotation)); + }); + ui.end_row(); + + ui.label("fov"); + ui.add(DragValue::new(&mut dummy_link.fov)); + ui.end_row(); + ui.label("w/h ratio"); + let ratio = dummy_link.client_size.0.as_vec2(); + let mut ratio = ratio.x / ratio.y; + ui.add(DragValue::new(&mut ratio)); + ui.end_row(); + ui.label("character"); + ui.label(&dummy_link.name); + ui.end_row(); + ui.label("map id"); + ui.add(DragValue::new(&mut dummy_link.map_id)); + ui.end_row(); + ui.label("map type"); + ui.add(DragValue::new(&mut dummy_link.map_type)); + ui.end_row(); + ui.label("address"); + ui.label(format!("{}", dummy_link.server_address)); + ui.end_row(); + ui.label("instance"); + ui.add(DragValue::new(&mut dummy_link.instance)); + ui.end_row(); + ui.label("shard id"); + ui.add(DragValue::new(&mut dummy_link.shard_id)); + ui.end_row(); + ui.label("mount"); + ui.label(format!("{:?}", dummy_link.mount)); + ui.end_row(); + ui.label("client pos"); + ui.horizontal(|ui| { + let client_pos = &mut dummy_link.client_pos.0; + ui.add(DragValue::new(&mut client_pos.x)); + ui.add(DragValue::new(&mut client_pos.y)); + }); + ui.end_row(); + ui.label("client size"); + ui.horizontal(|ui| { + let client_size = &mut dummy_link.client_size.0; + ui.add(DragValue::new(&mut client_size.x)); + ui.add(DragValue::new(&mut client_size.y)); + }); + ui.end_row(); + ui.label("dpi scaling"); + ui.add(DragValue::new(&mut dummy_link.dpi_scaling)); + ui.end_row(); + ui.label("dpi"); + ui.add(DragValue::new(&mut dummy_link.dpi)); + ui.end_row(); + + // ui.label("position"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos.x)); + // ui.add(DragValue::new(&mut link.window_pos.y)); + // }); + // ui.end_row(); + // ui.label("size"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size.x)); + // ui.add(DragValue::new(&mut link.window_size.y)); + // }); + // ui.end_row(); + // ui.label("position_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); + // }); + // ui.end_row(); + // ui.label("size_nb"); + // ui.horizontal(|ui| { + // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); + // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); + // }); + // ui.end_row(); + }); +} diff --git a/crates/jokolay/src/app/ui_parameters.rs b/crates/jokolay/src/app/ui_parameters.rs new file mode 100644 index 0000000..a86a585 --- /dev/null +++ b/crates/jokolay/src/app/ui_parameters.rs @@ -0,0 +1,270 @@ +use cap_std::fs_utf8::Dir; +use egui_window_glfw_passthrough::GlfwBackend; +use std::{ + io::Write, + sync::{Arc, RwLock}, +}; + +use joko_component_models::{ + default_component_result, from_data, to_data, Component, ComponentMessage, ComponentResult, +}; +use joko_ui_models::{UIArea, UIPanel}; +use miette::IntoDiagnostic; +use serde::{Deserialize, Serialize}; +use tracing::error; + +pub const UI_PARAMETERS_FILE_NAME: &str = "ui.toml"; + +#[derive(Clone, Serialize, Deserialize)] +pub enum MessageToApplicationBack { + SaveUIConfiguration(String), +} + +#[derive(Serialize, Deserialize)] +pub struct JokolayUIParameters { + pub visible_borders: bool, + pub animate: bool, //FIXME: not linked to animation anymore + pub editable_path: String, + pub root_path: String, + //TODO: save configuration into a file + make backups of configuration +} + +struct JokolayUIConfigurationChannels { + back_end_notifier: tokio::sync::mpsc::Sender, +} +struct JokolayConfigurationChannels { + notification_receiver: tokio::sync::mpsc::Receiver, +} + +pub struct JokolayUIConfiguration { + pub fps_last_reset: f64, + pub frame_count: u32, + pub total_frame_count: u32, + pub average_fps: u32, + pub display_parameters: JokolayUIParameters, + glfw_backend: Arc>, + egui_context: egui::Context, + channels: Option, +} + +pub struct JokolayConfiguration { + root_dir: Arc, + channels: Option, +} + +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for JokolayUIConfiguration {} +unsafe impl Sync for JokolayUIConfiguration {} + +impl JokolayUIConfiguration { + pub fn new( + glfw_backend: Arc>, + egui_context: egui::Context, + editable_path: String, + root_path: String, + ) -> Self { + let fps_last_reset: f64 = { glfw_backend.read().unwrap().glfw.get_time() as _ }; + Self { + fps_last_reset, + frame_count: 0, + total_frame_count: 0, + average_fps: 0, + display_parameters: JokolayUIParameters { + visible_borders: false, + animate: true, + editable_path, + root_path, + }, + glfw_backend, + egui_context, + channels: None, + } + } +} + +impl Component for JokolayUIConfiguration { + fn accept_notifications(&self) -> bool { + true + } + fn bind(&mut self, mut channels: joko_component_models::ComponentChannels) { + let back_end_notifier = channels.notify.remove(&0).unwrap(); + let channels = JokolayUIConfigurationChannels { back_end_notifier }; + self.channels = Some(channels) + } + fn flush_all_messages(&mut self) {} + + fn init(&mut self) {} + + fn tick(&mut self, current_time: f64) -> ComponentResult { + self.total_frame_count += 1; + self.frame_count += 1; + if current_time - self.fps_last_reset > 1.0 { + self.average_fps = self.frame_count; + self.frame_count = 0; + self.fps_last_reset = current_time; + } + default_component_result() + } + fn notify(&self) -> Vec<&str> { + vec!["back:configuration"] + } +} + +impl JokolayConfiguration { + pub fn new(root_dir: Arc) -> Self { + Self { + root_dir, + channels: None, + } + } + fn handle_message(&mut self, msg: MessageToApplicationBack) { + let root_dir = &self.root_dir; + match msg { + MessageToApplicationBack::SaveUIConfiguration(serialized_string) => { + match root_dir.create(UI_PARAMETERS_FILE_NAME) { + Ok(mut file) => { + match file.write(serialized_string.as_bytes()).into_diagnostic() { + Ok(_) => {} + Err(e) => { + error!(?e, "failed to save UI configuration"); + } + } + } + Err(e) => { + error!(?e, "failed to open UI configuration file"); + } + } + } + #[allow(unreachable_patterns)] + _ => { + unimplemented!("Handling BackToUIMessage has not been implemented yet"); + } + } + } +} + +impl Component for JokolayConfiguration { + fn accept_notifications(&self) -> bool { + true + } + fn init(&mut self) {} + fn flush_all_messages(&mut self) { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + let mut messages = Vec::new(); + while let Ok(msg) = channels.notification_receiver.try_recv() { + messages.push(from_data(&msg)); + } + for msg in messages { + self.handle_message(msg); + } + } + fn bind(&mut self, channels: joko_component_models::ComponentChannels) { + let channels = JokolayConfigurationChannels { + notification_receiver: channels.input_notification.unwrap(), + }; + self.channels = Some(channels); + } + fn tick(&mut self, _latest_time: f64) -> joko_component_models::ComponentResult { + default_component_result() + } +} + +impl UIPanel for JokolayUIConfiguration { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Configuration".to_string(), + id: "configuration_ui".to_string(), + }] + } + fn init(&mut self) {} + + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { + let channels = self.channels.as_mut().unwrap(); + let u2b_sender = &channels.back_end_notifier; + let glfw_backend = Arc::clone(&self.glfw_backend); + let mut glfw_backend = glfw_backend.as_ref().write().unwrap(); + let mut need_to_save = false; + egui::Window::new("Configuration") + .open(is_open) + .show(&self.egui_context, |ui| { + egui::Grid::new("frame details") + .num_columns(2) + .show(ui, |ui| { + ui.label("FPS"); + ui.label(&format!("{}", self.average_fps)); + ui.end_row(); + ui.label("Frame count"); + ui.label(&format!("{}", self.total_frame_count)); + ui.end_row(); + ui.label("Overlay position"); + ui.label(&format!( + "x: {}; y: {}", + glfw_backend.window_position[0], glfw_backend.window_position[1] + )); + ui.end_row(); + ui.label("Overlay size"); + ui.label(&format!( + "width: {}, height: {}", + glfw_backend.framebuffer_size_physical[0], glfw_backend.framebuffer_size_physical[1] + )); + ui.end_row(); + + ui.label("Decorations (borders)") + .on_hover_text("Should the jokolay overlay window boreders be displayed"); + let is_decorated = glfw_backend.window.is_decorated(); + ui.horizontal(|ui|{ + let result = is_decorated; + if ui.selectable_label(result, "Visible").clicked() { + glfw_backend.window.set_decorated(true); + self.display_parameters.visible_borders = true; + need_to_save = true; + } + if ui.selectable_label(!result, "Hidden").clicked() { + glfw_backend.window.set_decorated(false); + self.display_parameters.visible_borders = false; + need_to_save = true; + } + }); + ui.end_row(); + + ui.label("Animation") + .on_hover_text("As an example, this toggle the animation of trails"); + ui.horizontal(|ui|{ + if ui.selectable_label(self.display_parameters.animate, "Enable").clicked() { + self.display_parameters.animate = true; + need_to_save = true; + } + if ui.selectable_label(!self.display_parameters.animate, "Disable").clicked() { + self.display_parameters.animate = false; + need_to_save = true; + } + }); + ui.end_row(); + ui.label("All files and preferences are saved into:"); + ui.label(&self.display_parameters.root_path); + ui.end_row(); + + ui.label("Editable package directory") + .on_hover_text_at_pointer("This is where you can manually edit a package and have it regularly imported for validation."); + ui.text_edit_singleline(&mut self.display_parameters.editable_path); + }); + }); + if need_to_save { + match toml::to_string(&self.display_parameters) { + Ok(serialized_string) => { + let _ = u2b_sender.blocking_send(to_data( + MessageToApplicationBack::SaveUIConfiguration(serialized_string), + )); + } + Err(e) => { + tracing::error!(?e, "failed to serialize UI configuration"); + } + } + } + } +} diff --git a/crates/jokolay/src/app/window.rs b/crates/jokolay/src/app/window.rs new file mode 100644 index 0000000..cdb20e1 --- /dev/null +++ b/crates/jokolay/src/app/window.rs @@ -0,0 +1,127 @@ +use std::sync::{Arc, RwLock}; + +use egui_window_glfw_passthrough::GlfwBackend; +use joko_component_models::{ + default_component_result, from_broadcast, Component, ComponentChannels, ComponentResult, +}; +use joko_link_models::{MumbleChanges, MumbleLink}; + +pub(crate) const MINIMAL_WINDOW_WIDTH: u32 = 640; +pub(crate) const MINIMAL_WINDOW_HEIGHT: u32 = 480; +pub(crate) const MINIMAL_WINDOW_POSITION_X: i32 = 0; +pub(crate) const MINIMAL_WINDOW_POSITION_Y: i32 = 0; + +struct WindowManagerChannels { + subscription_mumblelink: tokio::sync::broadcast::Receiver, +} +pub(crate) struct WindowManager { + glfw_backend: Arc>, + window_changed: bool, + maximal_window_width: u32, + maximal_window_height: u32, + last_known_link: Option, + channels: Option, +} + +impl WindowManager { + pub fn new(glfw_backend: Arc>) -> Self { + //retrieve current screen resolution + let video_mode = glfw_backend + .write() + .unwrap() + .glfw + .with_primary_monitor(|_, m| { + if let Some(m) = m { + m.get_video_mode() + } else { + None + } + }); + let maximal_window_width = video_mode.unwrap().width; + let maximal_window_height = video_mode.unwrap().height; + + glfw_backend.write().unwrap().window.set_floating(true); + glfw_backend.write().unwrap().window.set_decorated(false); + + Self { + glfw_backend, + window_changed: true, + maximal_window_width, + maximal_window_height, + last_known_link: None, + channels: None, + } + } +} + +/// Necessary lies for GlfwBackend, which despite not moved (Arc + Mutex) shall prevent compilation +unsafe impl Send for WindowManager {} +unsafe impl Sync for WindowManager {} + +impl Component for WindowManager { + fn accept_notifications(&self) -> bool { + true + } + fn bind(&mut self, mut channels: ComponentChannels) { + let channels = WindowManagerChannels { + subscription_mumblelink: channels.requirements.remove(&0).unwrap(), + }; + + self.channels = Some(channels); + } + fn flush_all_messages(&mut self) {} + fn init(&mut self) {} + fn requirements(&self) -> Vec<&str> { + vec!["ui:mumble_link"] // is it ? + } + fn tick(&mut self, _latest_time: f64) -> ComponentResult { + assert!( + self.channels.is_some(), + "channels must be initialized before interacting with component." + ); + let channels = self.channels.as_mut().unwrap(); + if let Ok(data) = channels.subscription_mumblelink.try_recv() { + let link: Option = from_broadcast(&data); + match link { + Some(link) => { + if link.changes.contains(MumbleChanges::WindowPosition) + || link.changes.contains(MumbleChanges::WindowSize) + { + self.window_changed = true; + } + self.last_known_link = Some(link); + } + _ => { + //error!("WindowManager manager tick error, MumbleLink link data, nothing found"); + } + } + } else { + println!("WindowManager: No data from mumble"); + } + if let Some(last_known_link) = &mut self.last_known_link { + if self.window_changed { + let client_pos = &last_known_link.client_pos.0; + let client_size = &last_known_link.client_size.0; + let mut glfw_backend = self.glfw_backend.write().unwrap(); + glfw_backend.window.set_pos( + client_pos.x.max(MINIMAL_WINDOW_POSITION_X), + client_pos.y.max(MINIMAL_WINDOW_POSITION_Y), + ); + // if gw2 is in windowed fullscreen mode, then the size is full resolution of the screen/monitor. + // But if we set that size, when you focus jokolay, the screen goes blank on win11 (some kind of fullscreen optimization maybe?) + // so we remove a pixel from right/bottom edges. mostly indistinguishable, but makes sure that transparency works even in windowed fullscrene mode of gw2 + let client_size_x = MINIMAL_WINDOW_WIDTH + .max(client_size.x) + .min(self.maximal_window_width); + let client_size_y = MINIMAL_WINDOW_HEIGHT + .max(client_size.y) + .min(self.maximal_window_height); + glfw_backend + .window + .set_size((client_size_x - 1) as i32, (client_size_y - 1) as i32); + } + self.window_changed = false; + } + default_component_result() + } +} diff --git a/crates/jokolay/src/app/wm.rs b/crates/jokolay/src/app/wm.rs deleted file mode 100644 index 4b99466..0000000 --- a/crates/jokolay/src/app/wm.rs +++ /dev/null @@ -1,75 +0,0 @@ -use egui_window_glfw_passthrough::GlfwBackend; - -pub struct WindowStatistics { - pub fps_last_reset: f64, - pub frame_count: u32, - pub total_frame_count: u32, - pub average_fps: u32, -} - -impl WindowStatistics { - pub fn new(current_time: f64) -> Self { - Self { - fps_last_reset: current_time, - frame_count: 0, - total_frame_count: 0, - average_fps: 0, - } - } - - pub fn tick(&mut self, current_time: f64) { - self.total_frame_count += 1; - self.frame_count += 1; - if current_time - self.fps_last_reset > 1.0 { - self.average_fps = self.frame_count; - self.frame_count = 0; - self.fps_last_reset = current_time; - } - } - - pub fn gui(&mut self, etx: &egui::Context, wb: &mut GlfwBackend, open: &mut bool) { - egui::Window::new("Window Manager") - .open(open) - .show(etx, |ui| { - egui::Grid::new("frame details") - .num_columns(2) - .show(ui, |ui| { - ui.label("fps"); - ui.label(&format!("{}", self.average_fps)); - ui.end_row(); - ui.label("frame count"); - ui.label(&format!("{}", self.total_frame_count)); - ui.end_row(); - ui.label("jokolay pos"); - ui.label(&format!( - "x: {}; y: {}", - wb.window_position[0], wb.window_position[1] - )); - ui.end_row(); - ui.label("jokolay size"); - ui.label(&format!( - "width: {}, height: {}", - wb.framebuffer_size_physical[0], wb.framebuffer_size_physical[1] - )); - ui.end_row(); - ui.label("decorations (borders)"); - let is_decorated = wb.window.is_decorated(); - let mut result = is_decorated; - if ui - .checkbox( - &mut result, - if is_decorated { - "borders visible" - } else { - "borders hidden" - }, - ) - .changed() - { - wb.window.set_decorated(result); - } - ui.end_row(); - }); - }); - } -} diff --git a/crates/jokolay/src/lib.rs b/crates/jokolay/src/lib.rs index 33beac7..5c4ccf5 100644 --- a/crates/jokolay/src/lib.rs +++ b/crates/jokolay/src/lib.rs @@ -1,3 +1,4 @@ mod app; +mod manager; pub use app::start_jokolay; diff --git a/crates/joko_core/src/manager/mod.rs b/crates/jokolay/src/manager/mod.rs similarity index 100% rename from crates/joko_core/src/manager/mod.rs rename to crates/jokolay/src/manager/mod.rs diff --git a/crates/joko_core/src/manager/theme/mod.rs b/crates/jokolay/src/manager/theme/mod.rs similarity index 86% rename from crates/joko_core/src/manager/theme/mod.rs rename to crates/jokolay/src/manager/theme/mod.rs index 1759e2a..25c67f8 100644 --- a/crates/joko_core/src/manager/theme/mod.rs +++ b/crates/jokolay/src/manager/theme/mod.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, io::Read, sync::Arc}; use cap_std::fs_utf8::Dir; use egui::Style; +use joko_ui_models::{UIArea, UIPanel}; use miette::{Context, IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; @@ -13,6 +14,7 @@ pub struct ThemeManager { fonts: BTreeMap>, config: ThemeManagerConfig, ui_data: ThemeUIData, + egui_context: egui::Context, } #[derive(Debug, Default)] @@ -46,39 +48,42 @@ impl Default for ThemeManagerConfig { } impl ThemeManager { - const THEME_MANAGER_DIR_NAME: &str = "theme_manager"; - const THEMES_DIR_NAME: &str = "themes"; - const FONTS_DIR_NAME: &str = "fonts"; - const DEFAULT_FONT_NAME: &str = "default"; - const DEFAULT_THEME_NAME: &str = "default"; - const THEME_MANAGER_CONFIG_NAME: &str = "theme_manager_config"; - pub fn new(jdir: &Dir) -> Result { - jdir.create_dir_all(Self::THEME_MANAGER_DIR_NAME) + const THEME_MANAGER_DIR_NAME: &'static str = "theme_manager"; + const THEMES_DIR_NAME: &'static str = "themes"; + const FONTS_DIR_NAME: &'static str = "fonts"; + const DEFAULT_FONT_NAME: &'static str = "default"; + const DEFAULT_THEME_NAME: &'static str = "default"; + const THEME_MANAGER_CONFIG_NAME: &'static str = "theme_manager_config"; + pub fn new(jokolay_dir: Arc, egui_context: egui::Context) -> Result { + jokolay_dir + .create_dir_all(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() .wrap_err("failed to create theme manager dir")?; - let dir: Arc = jdir + let theme_manager_dir: Arc = jokolay_dir .open_dir(Self::THEME_MANAGER_DIR_NAME) .into_diagnostic() .wrap_err("failed to open theme_manager dir")? .into(); - dir.create_dir_all(Self::THEMES_DIR_NAME) + theme_manager_dir + .create_dir_all(Self::THEMES_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; - let themes_dir: Arc = dir + let themes_dir: Arc = theme_manager_dir .open_dir(Self::THEMES_DIR_NAME) .into_diagnostic() .wrap_err("failed to open themes dir")? .into(); - dir.create_dir_all(Self::FONTS_DIR_NAME) + theme_manager_dir + .create_dir_all(Self::FONTS_DIR_NAME) .into_diagnostic() .wrap_err("failed to create themes dir")?; - let fonts_dir: Arc = dir + let fonts_dir: Arc = theme_manager_dir .open_dir(Self::FONTS_DIR_NAME) .into_diagnostic() .wrap_err("failed to open themes dir")? .into(); - if !fonts_dir.exists(&format!("{}.ttf", Self::DEFAULT_FONT_NAME)) { + if !fonts_dir.exists(format!("{}.ttf", Self::DEFAULT_FONT_NAME)) { fonts_dir .write( format!("{}.ttf", Self::DEFAULT_FONT_NAME), @@ -87,7 +92,7 @@ impl ThemeManager { .into_diagnostic() .wrap_err("failed to write roboto/default font file to fonts dir")?; } - if !themes_dir.exists(&format!("{}.json", Self::DEFAULT_THEME_NAME)) { + if !themes_dir.exists(format!("{}.json", Self::DEFAULT_THEME_NAME)) { themes_dir .write( format!("{}.json", Self::DEFAULT_THEME_NAME), @@ -167,34 +172,48 @@ impl ThemeManager { fonts.insert(theme_name, font_bytes); } } - if !dir.exists(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) { - dir.write( - format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME), - serde_json::to_vec_pretty(&ThemeManagerConfig::default()) - .into_diagnostic() - .wrap_err("failed to serialize theme manager config")?, - ) - .into_diagnostic() - .wrap_err("failed to write theme manager config to the theme manager dir")?; + if !theme_manager_dir.exists(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) { + theme_manager_dir + .write( + format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME), + serde_json::to_vec_pretty(&ThemeManagerConfig::default()) + .into_diagnostic() + .wrap_err("failed to serialize theme manager config")?, + ) + .into_diagnostic() + .wrap_err("failed to write theme manager config to the theme manager dir")?; } let config = serde_json::from_str( - &dir.read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) + &theme_manager_dir + .read_to_string(format!("{}.json", Self::THEME_MANAGER_CONFIG_NAME)) .into_diagnostic() .wrap_err("failed to read theme manager config file")?, ) .into_diagnostic() .wrap_err("failed to deserialize theme manager config file")?; Ok(Self { - dir, + dir: theme_manager_dir, themes_dir, fonts_dir, themes, fonts, config, ui_data: Default::default(), + egui_context, }) } - pub fn init_egui(&mut self, etx: &egui::Context) { +} + +impl UIPanel for ThemeManager { + fn areas(&self) -> Vec { + vec![UIArea { + is_open: false, + name: "Themes".to_string(), + id: "themes_ui".to_string(), + }] + } + fn init(&mut self) { + let egui_context = &mut self.egui_context; let mut fonts = egui::FontDefinitions::default(); for (name, font_data) in self.fonts.iter() { fonts.font_data.insert( @@ -202,18 +221,19 @@ impl ThemeManager { egui::FontData::from_owned(font_data.to_owned()), ); } - etx.set_fonts(fonts); + egui_context.set_fonts(fonts); if let Some(theme) = self.themes.get(&self.config.default_theme) { - etx.set_style(theme.style.clone()); + egui_context.set_style(theme.style.clone()); } else { error!(%self.config.default_theme, "failed to find the default theme in the loaded themes :("); } } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { + fn gui(&mut self, is_open: &mut bool, _area_id: &str, _latest_time: f64) { + let egui_context = &mut self.egui_context; egui::Window::new("Theme Manager") - .open(open) + .open(is_open) .scroll2([false, true]) - .show(etx, |ui| { + .show(egui_context, |ui| { ui.horizontal(|ui| { ui.selectable_value( &mut self.ui_data.tab, @@ -234,7 +254,7 @@ impl ThemeManager { .on_hover_text("save this theme with the above name") .clicked() { - let style = etx.style().as_ref().clone(); + let style = egui_context.style().as_ref().clone(); let theme = Theme { style }; let theme_name = self.ui_data.theme_name.clone(); match serde_json::to_string_pretty(&theme) { @@ -270,7 +290,7 @@ impl ThemeManager { } self.themes.insert(theme_name, theme); } - etx.style_ui(ui); + egui_context.style_ui(ui); }); } ThemeUITab::Config => { @@ -292,7 +312,7 @@ impl ThemeManager { .clicked() && !checked { - self.config.default_theme = theme_name.clone(); + self.config.default_theme.clone_from(theme_name); } } }); @@ -309,7 +329,7 @@ impl ThemeManager { .clicked() && !checked { - etx.set_style(theme.style.clone()); + egui_context.set_style(theme.style.clone()); } } }); diff --git a/crates/joko_core/src/manager/theme/roboto.ttf b/crates/jokolay/src/manager/theme/roboto.ttf similarity index 100% rename from crates/joko_core/src/manager/theme/roboto.ttf rename to crates/jokolay/src/manager/theme/roboto.ttf diff --git a/crates/joko_core/src/manager/trace/mod.rs b/crates/jokolay/src/manager/trace/mod.rs similarity index 90% rename from crates/joko_core/src/manager/trace/mod.rs rename to crates/jokolay/src/manager/trace/mod.rs index f92ad36..932f1d4 100644 --- a/crates/joko_core/src/manager/trace/mod.rs +++ b/crates/jokolay/src/manager/trace/mod.rs @@ -16,19 +16,39 @@ impl JokolayTracingLayer { ) -> Result { use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; + + std::panic::set_hook(Box::new(|info: &std::panic::PanicInfo| { + use std::fs::File; + use std::io::Write; + let backtrace = std::backtrace::Backtrace::force_capture(); + let output = if let Some(string) = info.payload().downcast_ref::() { + string.to_string() + } else if let Some(str) = info.payload().downcast_ref::<&'static str>() { + str.to_string() + } else { + format!("{info:?}") + }; + eprintln!("{output}"); + eprintln!("Backtrace: {backtrace:}"); + let mut w = File::create("jokolay.error").unwrap(); + writeln!(&mut w, "{output}").unwrap(); + writeln!(&mut w, "Backtrace: {backtrace:}").unwrap(); + })); + // get the log level let filter_layer = EnvFilter::try_from_env("JOKOLAY_LOG") .or_else(|_| EnvFilter::try_new("info,wgpu=warn,naga=warn")) .into_diagnostic() .wrap_err("failed to parse log filter levels from env")?; // create log file in the data dir. This will also serve as a check that the directory is "writeable" by us - let writer = std::io::BufWriter::new( + let log_writer = std::io::BufWriter::new( jokolay_dir .create("jokolay.log") .into_diagnostic() .wrap_err("failed to create jokolay.log file")?, ); - let (nb, guard) = tracing_appender::non_blocking(writer); + + let (nb, guard) = tracing_appender::non_blocking(log_writer); let fmt_layer = fmt::layer() .with_ansi(false) .with_target(false) @@ -60,9 +80,8 @@ impl JokolayTracingLayer { .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .column(Column::exact(40.0)) - .column(Column::initial(100.0).range(40.0..=300.0).clip(true)) - .column(Column::exact(40.0)) - .column(Column::initial(100.0).clip(true)) + .column(Column::initial(200.0).range(40.0..=300.0).clip(true)) + .column(Column::initial(400.0).clip(true)) .min_scrolled_height(0.0) .header(20.0, |mut header| { header.col(|ui| { @@ -71,17 +90,14 @@ impl JokolayTracingLayer { header.col(|ui| { ui.strong("target"); }); - header.col(|ui| { - ui.strong("line"); - }); header.col(|ui| { ui.strong("message"); }); }) .body(|body| { let events = &JKL_TRACING_DATA.get().unwrap().lock().unwrap().buffer; - body.rows(20.0, events.len(), |index, mut row| { - let ev = events.get(index as _).unwrap(); + body.rows(20.0, events.len(), |mut row| { + let ev = events.get(row.index() as _).unwrap(); ev.ui_row(&mut row); }); }); diff --git a/crates/jokolink/src/lib.rs b/crates/jokolink/src/lib.rs deleted file mode 100644 index 3d64eec..0000000 --- a/crates/jokolink/src/lib.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! Jokolink is a crate to deal with Mumble Link data exposed by games/apps on windows via shared memory - -//! Joko link is designed to primarily get the MumbleLink or the window size -//! of the GW2 window for Jokolay (an crossplatform overlay for Guild Wars 2). -//! on windows, you can use it to create/open shared memory. -//! and on linux, you can run jokolink binary in wine, which will create/open shared memory and copy-paste it into /dev/shm. -//! then, you can easily read the /dev/shm file from a any number of linux native applications. -//! along with mumblelink data, it also copies the x11 window id of gw2. you can use this to get the size of gw2 window. -//! - -mod mumble; -use egui::DragValue; -use enumflags2::BitFlags; -use glam::IVec2; -use jokoapi::end_point::mounts::Mount; -use miette::{IntoDiagnostic, Result, WrapErr}; -pub use mumble::*; -use serde_json::from_str; -use std::sync::Arc; -use tracing::error; - -/// The default mumble link name. can only be changed by passing the `-mumble` options to gw2 for multiboxing -pub const DEFAULT_MUMBLELINK_NAME: &str = "MumbleLink"; -#[cfg(target_os = "linux")] -pub mod linux; -#[cfg(target_os = "windows")] -pub mod win; - -#[cfg(target_os = "linux")] -use linux::MumbleLinuxImpl as MumblePlatformImpl; -#[cfg(target_os = "windows")] -use win::MumbleWinImpl as MumblePlatformImpl; -// Useful link size is only [ctypes::USEFUL_C_MUMBLE_LINK_SIZE] . And we add 100 more bytes so that jokolink can put some extra stuff in there -// pub(crate) const JOKOLINK_MUMBLE_BUFFER_SIZE: usize = ctypes::USEFUL_C_MUMBLE_LINK_SIZE + 100; -/// This primarily manages the mumble backend. -/// the purpose of `MumbleBackend` is to get mumble link data and window dimensions when asked. -/// Manager also caches the previous mumble link details like window dimensions or mapid etc.. -/// and every frame gets the latest mumble link data, and compares with the previous frame. -/// if any of the changed this frame, it will set the relevant changed flags so that plugins -/// or other parts of program which care can run the relevant code. -pub struct MumbleManager { - /// This abstracts over the windows and linux impl of mumble link functionality. - /// we use this to get the latest mumble link and latest window dimensions of the current mumble link - backend: MumblePlatformImpl, - /// latest mumble link - link: Arc, -} -impl MumbleManager { - pub fn new(name: &str, _jokolay_window_id: Option) -> Result { - let backend = MumblePlatformImpl::new(name)?; - Ok(Self { - backend, - link: Arc::new(Default::default()), - }) - } - pub fn tick(&mut self) -> Result>> { - if let Err(e) = self.backend.tick() { - error!(?e, "mumble backend tick error"); - return Ok(None); - } - - if !self.backend.is_alive() { - // reset link - if self.link.ui_tick != 0 { - self.link = Arc::new(Default::default()); - } - return Ok(None); - } - // backend is alive and tick is successful. time to get link - let cml: ctypes::CMumbleLink = self.backend.get_cmumble_link(); - if cml.ui_tick == 0 && self.link.ui_tick != 0 { - self.link = Arc::new(Default::default()); - } - - if cml.ui_tick == 0 || cml.context.client_pos_size == [0; 4] { - return Ok(None); - } - let mut changes: BitFlags = Default::default(); - // safety. as the link is valid, we can use as_ref - let json_string = widestring::U16CStr::from_slice_truncate(&cml.identity) - .into_diagnostic() - .wrap_err("failed to get widestring out of cml identity")? - .to_string() - .into_diagnostic() - .wrap_err("failed to convert widestring to cstring")?; - - let identity: ctypes::CIdentity = from_str(&json_string) - .into_diagnostic() - .wrap_err("failed to deserialize identity from json string")?; - let uisz = identity - .get_uisz() - .ok_or(miette::miette!("uisz is invalid"))?; - let server_address = if cml.context.server_address[0] == 2 { - let addr = cml.context.server_address; - std::net::Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7]).into() - } else { - std::net::Ipv4Addr::UNSPECIFIED.into() - }; - if self.link.ui_tick != cml.ui_tick { - changes.insert(MumbleChanges::UiTick); - } - if self.link.name != identity.name { - changes.insert(MumbleChanges::Character); - } - if self.link.map_id != cml.context.map_id { - changes.insert(MumbleChanges::Map); - } - // let window_pos = IVec2::new( - // cml.context.window_pos_size[0], - // cml.context.window_pos_size[1], - // ); - // let window_size = IVec2::new( - // cml.context.window_pos_size[2], - // cml.context.window_pos_size[3], - // ); - // let window_pos_without_borders = IVec2::new( - // cml.context.window_pos_size_without_borders[0], - // cml.context.window_pos_size_without_borders[1], - // ); - // let window_size_without_borders = IVec2::new( - // cml.context.window_pos_size_without_borders[2], - // cml.context.window_pos_size_without_borders[3], - // ); - let client_pos = IVec2::new( - cml.context.client_pos_size[0], - cml.context.client_pos_size[1], - ); - let client_size = IVec2::new( - cml.context.client_pos_size[2], - cml.context.client_pos_size[3], - ); - - if self.link.client_pos != client_pos { - changes.insert(MumbleChanges::WindowPosition); - } - if self.link.client_size != client_size { - changes.insert(MumbleChanges::WindowSize); - } - let link = Arc::new(MumbleLink { - ui_tick: cml.ui_tick, - player_pos: cml.f_avatar_position.into(), - f_avatar_front: cml.f_avatar_front.into(), - cam_pos: cml.f_camera_position.into(), - f_camera_front: cml.f_camera_front.into(), - name: identity.name, - map_id: cml.context.map_id, - fov: identity.fov, - uisz, - // window_pos, - // window_size, - changes, - // window_pos_without_borders, - // window_size_without_borders, - dpi_scaling: cml.context.dpi_scaling, - dpi: cml.context.dpi, - client_pos, - client_size, - map_type: cml.context.map_type, - server_address, - shard_id: cml.context.shard_id, - instance: cml.context.instance, - build_id: cml.context.build_id, - ui_state: cml.context.ui_state, - compass_width: cml.context.compass_width, - compass_height: cml.context.compass_height, - compass_rotation: cml.context.compass_rotation, - player_x: cml.context.player_x, - player_y: cml.context.player_y, - map_center_x: cml.context.map_center_x, - map_center_y: cml.context.map_center_y, - map_scale: cml.context.map_scale, - process_id: cml.context.process_id, - mount: Mount::try_from_mumble_link(cml.context.mount_index), - }); - self.link = link.clone(); - Ok(if self.link.ui_tick == 0 { - None - } else { - Some(link) - }) - } - pub fn gui(&mut self, etx: &egui::Context, open: &mut bool) { - egui::Window::new("Mumble Manager") - .open(open) - .show(etx, |ui| { - if self.link.ui_tick == 0 { - ui.label("Mumble is not initialized"); - } else { - let link: MumbleLink = self.link.as_ref().clone(); - mumble_ui(ui, link); - } - }); - } -} - -fn mumble_ui(ui: &mut egui::Ui, mut link: MumbleLink) { - egui::Grid::new("link grid") - .num_columns(2) - .striped(true) - .show(ui, |ui| { - ui.label("ui tick"); - ui.add(DragValue::new(&mut link.ui_tick)); - ui.end_row(); - ui.label("player position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.player_pos.x)); - ui.add(DragValue::new(&mut link.player_pos.y)); - ui.add(DragValue::new(&mut link.player_pos.z)); - }); - ui.end_row(); - ui.label("player direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_avatar_front.x)); - ui.add(DragValue::new(&mut link.f_avatar_front.y)); - ui.add(DragValue::new(&mut link.f_avatar_front.z)); - }); - ui.end_row(); - ui.label("camera position"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.cam_pos.x)); - ui.add(DragValue::new(&mut link.cam_pos.y)); - ui.add(DragValue::new(&mut link.cam_pos.z)); - }); - ui.end_row(); - ui.label("camera direction"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.f_camera_front.x)); - ui.add(DragValue::new(&mut link.f_camera_front.y)); - ui.add(DragValue::new(&mut link.f_camera_front.z)); - }); - ui.end_row(); - - ui.label("fov"); - ui.add(DragValue::new(&mut link.fov)); - ui.end_row(); - ui.label("w/h ratio"); - let ratio = link.client_size.as_vec2(); - let mut ratio = ratio.x / ratio.y; - ui.add(DragValue::new(&mut ratio)); - ui.end_row(); - ui.label("character"); - ui.label(&link.name); - ui.end_row(); - ui.label("map id"); - ui.add(DragValue::new(&mut link.map_id)); - ui.end_row(); - ui.label("map type"); - ui.add(DragValue::new(&mut link.map_type)); - ui.end_row(); - ui.label("address"); - ui.label(format!("{}", link.server_address)); - ui.end_row(); - ui.label("instance"); - ui.add(DragValue::new(&mut link.instance)); - ui.end_row(); - ui.label("shard id"); - ui.add(DragValue::new(&mut link.shard_id)); - ui.end_row(); - ui.label("mount"); - ui.label(format!("{:?}", link.mount)); - ui.end_row(); - ui.label("client pos"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_pos.x)); - ui.add(DragValue::new(&mut link.client_pos.y)); - }); - ui.end_row(); - ui.label("client size"); - ui.horizontal(|ui| { - ui.add(DragValue::new(&mut link.client_size.x)); - ui.add(DragValue::new(&mut link.client_size.y)); - }); - ui.end_row(); - ui.label("dpi scaling"); - ui.add(DragValue::new(&mut link.dpi_scaling)); - ui.end_row(); - ui.label("dpi"); - ui.add(DragValue::new(&mut link.dpi)); - ui.end_row(); - - // ui.label("position"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_pos.x)); - // ui.add(DragValue::new(&mut link.window_pos.y)); - // }); - // ui.end_row(); - // ui.label("size"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_size.x)); - // ui.add(DragValue::new(&mut link.window_size.y)); - // }); - // ui.end_row(); - // ui.label("position_nb"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_pos_without_borders.x)); - // ui.add(DragValue::new(&mut link.window_pos_without_borders.y)); - // }); - // ui.end_row(); - // ui.label("size_nb"); - // ui.horizontal(|ui| { - // ui.add(DragValue::new(&mut link.window_size_without_borders.x)); - // ui.add(DragValue::new(&mut link.window_size_without_borders.y)); - // }); - // ui.end_row(); - }); -} diff --git a/documentation/diagrams/category_change.dotuml b/documentation/diagrams/category_change.dotuml new file mode 100644 index 0000000..f64b530 --- /dev/null +++ b/documentation/diagrams/category_change.dotuml @@ -0,0 +1,45 @@ +SequenceDiagram { + actor user + lifeline package_data + control choice_of_category_changed + lifeline package_ui + control list_of_textures_changed + collection categories + collection currently_used_files + collection active_markers + lifeline renderer + + user -a-> package_data "CategorySetAll" + package_data --> choice_of_category_changed "activate" + activate choice_of_category_changed + choice_of_category_changed --> package_data "trigger update" + + + activate package_data + package_data -a-> package_ui "TextureBegin" + package_ui --> active_markers "clear" + package_data -a-> package_ui "per package per marker: MarkerTexture" + package_data -a-> package_ui "per package per trail: TrailTexture" + package_data -a-> package_ui "per package: PackageActiveElements" + package_data -a-> package_ui "CurrentlyUsedFiles" + package_ui --> currently_used_files "replace" + package_data -a-> package_ui "ActiveElements" + package_ui --> categories "update each package" + package_data -a-> package_ui "TextureSwapChain" + package_ui --> list_of_textures_changed "activate" + activate list_of_textures_changed + package_data --> choice_of_category_changed "deactivate" + deactivate choice_of_category_changed + deactivate package_data + + activate package_ui + package_ui --> active_markers "insert" + list_of_textures_changed --> package_ui "trigger update" + package_ui -a-> renderer "RenderBegin" + package_ui -a-> renderer "per package: BulkMarkerObject" + package_ui -a-> renderer "per package: BulkTrailObject" + package_ui -a-> renderer "RenderSwapChain" + package_ui --> list_of_textures_changed "deactivate" + deactivate list_of_textures_changed + deactivate package_ui +} \ No newline at end of file diff --git a/documentation/diagrams/components_generated.dotuml b/documentation/diagrams/components_generated.dotuml new file mode 100644 index 0000000..c51e44d --- /dev/null +++ b/documentation/diagrams/components_generated.dotuml @@ -0,0 +1,22 @@ +digraph { + 0 [ label = "back:mumble_link" ] + 1 [ label = "back:jokolay_package_manager" ] + 2 [ label = "ui:window_manager" ] + 3 [ label = "ui:configuration" ] + 4 [ label = "ui:mumble_link" ] + 5 [ label = "ui:jokolay_renderer" ] + 6 [ label = "back:configuration" ] + 7 [ label = "ui:jokolay_package_manager" ] + 8 [ label = "ui:menu_panel" ] + 1 -> 7 [ label = "Peer" ] + 7 -> 1 [ label = "Peer" ] + 1 -> 0 [ label = "Requires" ] + 2 -> 4 [ label = "Requires" ] + 3 -> 6 [ label = "Notify" ] + 4 -> 0 [ label = "Notify" ] + 5 -> 4 [ label = "Requires" ] + 7 -> 4 [ label = "Requires" ] + 7 -> 5 [ label = "Notify" ] + 8 -> 4 [ label = "Requires" ] +} + diff --git a/documentation/mumble_editable.dotuml b/documentation/mumble_editable.dotuml new file mode 100644 index 0000000..f7d4ca5 --- /dev/null +++ b/documentation/mumble_editable.dotuml @@ -0,0 +1,39 @@ +SequenceDiagram { + actor user + lifeline ui_interface + control ui_editable_mumble + lifeline mumble_back + control back_repeat_last_value + lifeline mumble_ui + control ui_repeat_last_value + + user -a-> ui_interface "select editable" + ui_interface --> ui_editable_mumble "activate" + activate ui_editable_mumble + + ui_interface -a-> mumble_back "BindedOnUI" + mumble_back --> back_repeat_last_value "activate" + activate back_repeat_last_value + back_repeat_last_value --> mumble_back "trigger last value repetition" + + mumble_back -a-> mumble_ui "BindedOnUI" + mumble_ui --> ui_repeat_last_value "activate" + activate ui_repeat_last_value + ui_repeat_last_value --> mumble_ui "trigger last value repetition" + + ui_editable_mumble --> ui_interface "trigger last value propagation (from form)" + ui_interface -a-> mumble_back "Value" + + user -a-> ui_interface "select live" + ui_interface --> ui_editable_mumble "deactivate" + deactivate ui_editable_mumble + ui_interface -a-> mumble_back "Autonomous" + mumble_back --> back_repeat_last_value "deactivate" + deactivate back_repeat_last_value + + mumble_back -a-> mumble_ui "Autonomous" + mumble_ui --> ui_repeat_last_value "deactivate" + deactivate ui_repeat_last_value + + +} \ No newline at end of file