├── .cargo └── config.toml ├── .github └── workflows │ ├── check.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── integration │ └── FSD │ │ └── Content │ │ ├── _AssemblyStorm │ │ └── ModIntegration │ │ │ ├── BPL_MINT_Utility.uasset │ │ │ ├── BPL_MINT_Utility.uexp │ │ │ ├── JSON │ │ │ ├── JSON.uasset │ │ │ ├── JSON.uexp │ │ │ ├── JSONSortOrder.uasset │ │ │ ├── JSONSortOrder.uexp │ │ │ ├── JSONSortOrders.uasset │ │ │ ├── JSONSortOrders.uexp │ │ │ ├── JSONStream.uasset │ │ │ ├── JSONStream.uexp │ │ │ ├── JSONToken.uasset │ │ │ ├── JSONToken.uexp │ │ │ ├── JSONTokenType.uasset │ │ │ ├── JSONTokenType.uexp │ │ │ ├── JSONType.uasset │ │ │ ├── JSONType.uexp │ │ │ ├── JSONValue.uasset │ │ │ └── JSONValue.uexp │ │ │ ├── MI_Replication_v1.uasset │ │ │ ├── MI_Replication_v1.uexp │ │ │ ├── MI_SpawnMods.uasset │ │ │ ├── MI_SpawnMods.uexp │ │ │ ├── MI_UI.uasset │ │ │ ├── MI_UI.uexp │ │ │ ├── RebuiltAssets │ │ │ ├── ITM_Modding_ToolTip_Entry.uasset │ │ │ ├── ITM_Modding_ToolTip_Entry.uexp │ │ │ ├── TOOLTIP_ServerEntry_Mods.uasset │ │ │ └── TOOLTIP_ServerEntry_Mods.uexp │ │ │ ├── Sorting │ │ │ ├── BP_MINT_Sorter_ModAuthor_Asc.uasset │ │ │ ├── BP_MINT_Sorter_ModAuthor_Asc.uexp │ │ │ ├── BP_MINT_Sorter_ModAuthor_Desc.uasset │ │ │ ├── BP_MINT_Sorter_ModAuthor_Desc.uexp │ │ │ ├── BP_MINT_Sorter_ModCategory_Asc.uasset │ │ │ ├── BP_MINT_Sorter_ModCategory_Asc.uexp │ │ │ ├── BP_MINT_Sorter_ModCategory_Desc.uasset │ │ │ ├── BP_MINT_Sorter_ModCategory_Desc.uexp │ │ │ ├── BP_MINT_Sorter_ModName_Asc.uasset │ │ │ ├── BP_MINT_Sorter_ModName_Asc.uexp │ │ │ ├── BP_MINT_Sorter_ModName_Desc.uasset │ │ │ ├── BP_MINT_Sorter_ModName_Desc.uexp │ │ │ ├── BP_MINT_Sorter_ModRequired_Asc.uasset │ │ │ ├── BP_MINT_Sorter_ModRequired_Asc.uexp │ │ │ ├── BP_MINT_Sorter_ModRequired_Desc.uasset │ │ │ ├── BP_MINT_Sorter_ModRequired_Desc.uexp │ │ │ ├── BP_MINT_Sorter_ModURL_Asc.uasset │ │ │ ├── BP_MINT_Sorter_ModURL_Asc.uexp │ │ │ ├── BP_MINT_Sorter_ModURL_Desc.uasset │ │ │ ├── BP_MINT_Sorter_ModURL_Desc.uexp │ │ │ ├── BP_MINT_Sorter_PlayerEntry.uasset │ │ │ ├── BP_MINT_Sorter_PlayerEntry.uexp │ │ │ ├── IMINT_ObjectSorter.uasset │ │ │ ├── IMINT_ObjectSorter.uexp │ │ │ ├── MINTLib_Sorting.uasset │ │ │ └── MINTLib_Sorting.uexp │ │ │ ├── UI │ │ │ ├── EMINT_Compare_Availability.uasset │ │ │ ├── EMINT_Compare_Availability.uexp │ │ │ ├── MENU_MINT.uasset │ │ │ ├── MENU_MINT.uexp │ │ │ ├── WMINT_ModList.uasset │ │ │ ├── WMINT_ModList.uexp │ │ │ ├── WMINT_ModListOptions.uasset │ │ │ ├── WMINT_ModListOptions.uexp │ │ │ ├── WMINT_ModList_Entry.uasset │ │ │ ├── WMINT_ModList_Entry.uexp │ │ │ ├── WMINT_MultiModList.uasset │ │ │ ├── WMINT_MultiModList.uexp │ │ │ ├── WMINT_MultiModList_Entry.uasset │ │ │ ├── WMINT_MultiModList_Entry.uexp │ │ │ ├── WMINT_PlayerList.uasset │ │ │ ├── WMINT_PlayerList.uexp │ │ │ ├── WMINT_PlayerList_Entry.uasset │ │ │ ├── WMINT_PlayerList_Entry.uexp │ │ │ ├── WMINT_RadioButton.uasset │ │ │ ├── WMINT_RadioButton.uexp │ │ │ ├── WMINT_ScrollBar.uasset │ │ │ ├── WMINT_ScrollBar.uexp │ │ │ ├── WMINT_SingleModList.uasset │ │ │ ├── WMINT_SingleModList.uexp │ │ │ ├── WMINT_SlideInNotification.uasset │ │ │ ├── WMINT_SlideInNotification.uexp │ │ │ ├── WMINT_SlideInNotification_Container.uasset │ │ │ ├── WMINT_SlideInNotification_Container.uexp │ │ │ ├── WMINT_SlideIn_HookWarning.uasset │ │ │ ├── WMINT_SlideIn_HookWarning.uexp │ │ │ ├── WMINT_SlideIn_VersionWarning.uasset │ │ │ ├── WMINT_SlideIn_VersionWarning.uexp │ │ │ ├── WMINT_TextInput.uasset │ │ │ ├── WMINT_TextInput.uexp │ │ │ ├── WMINT_UpdateBanner.uasset │ │ │ └── WMINT_UpdateBanner.uexp │ │ │ ├── github-mark.uasset │ │ │ └── github-mark.uexp │ │ └── _mint │ │ ├── BPL_MINT.uasset │ │ └── BPL_MINT.uexp └── modio-cog-blue.png ├── flake.lock ├── flake.nix ├── hook ├── Cargo.toml └── src │ ├── hooks │ ├── mod.rs │ └── server_list.rs │ ├── lib.rs │ └── ue │ ├── array.rs │ ├── kismet.rs │ ├── malloc.rs │ ├── map.rs │ ├── mod.rs │ ├── name.rs │ ├── object.rs │ └── string.rs ├── hook_resolvers ├── Cargo.toml └── src │ └── lib.rs ├── mint_lib ├── Cargo.toml ├── build.rs └── src │ ├── error.rs │ ├── lib.rs │ ├── mod_info.rs │ └── update.rs ├── release.toml ├── rust-toolchain.toml ├── src ├── gui │ ├── find_string.rs │ ├── message.rs │ ├── mod.rs │ ├── named_combobox.rs │ ├── request_counter.rs │ └── toggle_switch.rs ├── integrate.rs ├── lib.rs ├── main.rs ├── mod_lints │ ├── archive_multiple_paks.rs │ ├── archive_only_non_pak_files.rs │ ├── asset_register_bin.rs │ ├── conflicting_mods.rs │ ├── empty_archive.rs │ ├── mod.rs │ ├── non_asset_files.rs │ ├── outdated_pak_version.rs │ ├── shader_files.rs │ ├── split_asset_pairs.rs │ └── unmodified_game_assets.rs ├── providers │ ├── cache.rs │ ├── file.rs │ ├── http.rs │ ├── mod.rs │ ├── mod_store.rs │ └── modio.rs └── state │ ├── config.rs │ └── mod.rs ├── test_assets └── lints │ ├── A.pak │ ├── A │ └── FSD │ │ └── Content │ │ ├── A.uexp │ │ ├── AssetRegistry.bin │ │ ├── B.uexp │ │ └── C.ushaderbytecode │ ├── B.pak │ ├── B │ └── FSD │ │ └── Content │ │ ├── A.uexp │ │ └── C.uexp │ ├── empty_archive.zip │ ├── multiple_pak_files.zip │ ├── multiple_paks.zip │ ├── non_asset_files.pak │ ├── non_asset_files │ └── never_gonna_give_you_up.txt │ ├── only_non_pak_files.zip │ ├── only_non_pak_files │ └── never_gonna_give_you_up.txt │ ├── outdated_pak_version.pak │ ├── outdated_pak_version │ └── foo.uexp │ ├── reference.pak │ ├── reference │ ├── a.uasset │ └── a.uexp │ ├── split_asset_pairs.pak │ ├── split_asset_pairs │ ├── missing_uasset │ │ └── a.uexp │ ├── missing_uexp │ │ └── b.uasset │ └── not_missing │ │ ├── c.uasset │ │ └── c.uexp │ ├── unmodified_game_assets.pak │ └── unmodified_game_assets │ ├── a.uasset │ └── a.uexp ├── tests ├── lint │ └── mod.rs └── tests.rs └── workspace_hack ├── Cargo.toml └── src └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [unstable] 2 | bindeps = true 3 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Run checks 3 | 4 | jobs: 5 | check: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Install packages 10 | run: sudo apt-get update && sudo apt-get install libgtk-3-dev gcc-mingw-w64 11 | 12 | - uses: dtolnay/rust-toolchain@nightly 13 | with: 14 | targets: x86_64-unknown-linux-gnu, x86_64-pc-windows-gnu 15 | components: clippy, rustfmt 16 | 17 | - name: Get versions 18 | run: | 19 | cargo fmt --version 20 | cargo clippy --version 21 | 22 | - uses: actions-rs/clippy-check@v1 23 | name: Check clippy 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | args: --all-features --all-targets -- -D warnings 27 | 28 | - name: Check rustfmt 29 | run: cargo fmt -- --check 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # CI that: 2 | # 3 | # * checks for a Git Tag that looks like a release 4 | # * creates a Github Release™ and fills in its text 5 | # * builds artifacts with cargo-dist (executable-zips, installers) 6 | # * uploads those artifacts to the Github Release™ 7 | # 8 | # Note that the Github Release™ will be created before the artifacts, 9 | # so there will be a few minutes where the release has no artifacts 10 | # and then they will slowly trickle in, possibly failing. To make 11 | # this more pleasant we mark the release as a "draft" until all 12 | # artifacts have been successfully uploaded. This allows you to 13 | # choose what to do with partial successes and avoids spamming 14 | # anyone with notifications before the release is actually ready. 15 | name: Release 16 | 17 | permissions: 18 | contents: write 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc. 22 | # The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version. 25 | # 26 | # If PACKAGE_NAME is specified, then we will create a Github Release™ for that 27 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all 30 | # (cargo-dist-able) packages in the workspace with that version (this is mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent Github Release™ for each one. 36 | # 37 | # If there's a prerelease-style suffix to the version then the Github Release™ 38 | # will be marked as a prerelease. 39 | on: 40 | push: 41 | tags: 42 | - '*-?v[0-9]+*' 43 | 44 | jobs: 45 | # Create the Github Release™ so the packages have something to be uploaded to 46 | create-release: 47 | runs-on: ubuntu-latest 48 | outputs: 49 | has-releases: ${{ steps.create-release.outputs.has-releases }} 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Install Rust 55 | run: rustup update nightly --no-self-update && rustup default nightly 56 | - name: Install cargo-dist 57 | run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 58 | - id: create-release 59 | run: | 60 | cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json 61 | echo "dist plan ran successfully" 62 | cat dist-manifest.json 63 | 64 | # Create the Github Release™ based on what cargo-dist thinks it should be 65 | ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json) 66 | IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json) 67 | jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md 68 | gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md 69 | echo "created announcement!" 70 | 71 | # Upload the manifest to the Github Release™ 72 | gh release upload ${{ github.ref_name }} dist-manifest.json 73 | echo "uploaded manifest!" 74 | 75 | # Disable all the upload-artifacts tasks if we have no actual releases 76 | HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json) 77 | echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT" 78 | 79 | # Build and packages all the things 80 | upload-artifacts: 81 | # Let the initial task tell us to not run (currently very blunt) 82 | needs: create-release 83 | if: ${{ needs.create-release.outputs.has-releases == 'true' }} 84 | strategy: 85 | matrix: 86 | # For these target platforms 87 | include: 88 | - os: ubuntu-20.04 89 | dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu 90 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 91 | - os: windows-2019 92 | dist-args: --artifacts=local --target=x86_64-pc-windows-msvc 93 | install-dist: irm https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.ps1 | iex 94 | 95 | runs-on: ${{ matrix.os }} 96 | env: 97 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | steps: 99 | - uses: actions/checkout@v3 100 | - name: Install Rust 101 | run: rustup update nightly --no-self-update && rustup default nightly 102 | - name: Install x86_64-pc-windows-gnu target 103 | if: runner.os == 'Linux' || runner.os == 'macOS' 104 | run: rustup target add x86_64-pc-windows-gnu 105 | - name: Cache Dependencies 106 | uses: Swatinem/rust-cache@v2 107 | - name: Install cargo-dist 108 | run: ${{ matrix.install-dist }} 109 | - name: Install packages (Linux) 110 | if: runner.os == 'Linux' 111 | run: sudo apt-get update && sudo apt-get install libgtk-3-dev gcc-mingw-w64 112 | - name: Install packages (MacOS) 113 | if: runner.os == 'macOS' 114 | run: | 115 | NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 116 | brew install mingw-w64 117 | - name: Run cargo-dist 118 | # This logic is a bit janky because it's trying to be a polyglot between 119 | # powershell and bash since this will run on windows, macos, and linux! 120 | # The two platforms don't agree on how to talk about env vars but they 121 | # do agree on 'cat' and '$()' so we use that to marshal values between commands. 122 | run: | 123 | # Actually do builds and make zips and whatnot 124 | cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json 125 | echo "dist ran successfully" 126 | cat dist-manifest.json 127 | 128 | # Parse out what we just built and upload it to the Github Release™ 129 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt 130 | echo "uploading..." 131 | cat uploads.txt 132 | gh release upload ${{ github.ref_name }} $(cat uploads.txt) 133 | echo "uploaded!" 134 | 135 | # Mark the Github Release™ as a non-draft now that everything has succeeded! 136 | publish-release: 137 | # Only run after all the other tasks, but it's ok if upload-artifacts was skipped 138 | needs: [create-release, upload-artifacts] 139 | if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }} 140 | runs-on: ubuntu-latest 141 | env: 142 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 143 | steps: 144 | - uses: actions/checkout@v3 145 | - name: mark release as non-draft 146 | run: | 147 | gh release edit ${{ github.ref_name }} --draft=false 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /hook/target 3 | /.idea 4 | /result 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | 5 | ## [Unreleased] - ReleaseDate 6 | 7 | ### General 8 | 9 | - Fix unintentionally linking to libssl on Linux. This used to prevent some users on various Linux 10 | distros from being able to launch mint at all. 11 | 12 | ### User Interface 13 | 14 | - Add light/dark mode toggle to settings menu 15 | - Replace escape menu modding tab with new modding menu 16 | - Show mint mods in public server list 17 | - Show time since last action 18 | - Make mod URL searchable for mods without cache data 19 | - Implement load priority to no longer rely on implicit ordering 20 | - Add mod list sorting 21 | - Slightly improved error reporting; mint should now indicate the mod that caused a failure 22 | - Various GUI improvements 23 | 24 | ### Core Functionality 25 | 26 | - Implement Asset Registry handling. This should be sufficient for most mods that were previously 27 | not usable with mint due to the lack of Asset Registry handling. 28 | - Implement self-update 29 | - Fix mod url resolution 30 | - Fix mods sometimes integrating in incorrect order 31 | - Add patch to fix gas clouds not exploding sometimes 32 | - Some mod save file fixes for Windows store version 33 | 34 | ### Internal Changes 35 | 36 | - Significantly optimize cache updates (first update will still be a full update) 37 | - Allow overriding appdata dir via CLI flag 38 | - Rename cache and config directories from `drg-mod-integration` to `mint` and default to legacy 39 | if they exist 40 | - Fix Windows console being full of garbage characters 41 | - Disable LTO for dev builds 42 | - Add nix flake 43 | - Improved testing 44 | - Various CI improvements 45 | - Reduce release size by removing debug symbols 46 | - Package Linux target with zip instead of tar 47 | - Compress bundled mod pak 48 | 49 | ## [0.2.10] - 2023-08-18 50 | 51 | - Many small improvements to the GUI 52 | - Add simple in game UI to show local and remote integration version and active mods 53 | - Add experimental mod linting to assist with common mod issues such as conflicts ([#55](https://github.com/trumank/mint/pull/55)) 54 | - Microsoft Store: Fix mods being unable to write custom save files ([#58](https://github.com/trumank/mint/issues/58)) 55 | - Fix `profile` CLI command not respecting mod's `enable` flag 56 | 57 | ## [0.2.9] - 2023-08-11 58 | 59 | - Update `egui_dnd` which makes dragging and re-ordering mods significantly smoother 60 | - Restore modding subsystem config upon uninstalling to prevent all mods getting enabled and kicking the user to sandbox 61 | - Fix regression introduced by case sensitive path fix ([#36](https://github.com/trumank/mint/issues/36)) 62 | 63 | ## [0.2.8] - 2023-08-05 64 | 65 | - Fix `*.ushaderbytecode` files not being filtered out and causing crash on load 66 | - Fix including same asset paths with different casings causing Unreal Engine to load neither ([#29](https://github.com/trumank/mint/issues/29)) 67 | 68 | 69 | [Unreleased]: https://github.com/trumank/mint/compare/v0.2.10...HEAD 70 | [0.2.10]: https://github.com/trumank/mint/compare/v0.2.9...v0.2.10 71 | [0.2.9]: https://github.com/trumank/mint/compare/v0.2.8...v0.2.9 72 | [0.2.8]: https://github.com/trumank/mint/compare/v0.2.7...v0.2.8 73 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "mint_lib", 4 | "hook", 5 | "hook_resolvers", 6 | "workspace_hack", 7 | ] 8 | 9 | [workspace.package] 10 | repository = "https://github.com/trumank/mint" 11 | authors = ["trumank", "jieyouxu"] 12 | license = "MIT OR Apache-2.0" 13 | version = "0.2.10" 14 | edition = "2021" 15 | 16 | [workspace.dependencies] 17 | anyhow = { version = "1.0.97", features = ["backtrace"] } 18 | patternsleuth = { git = "https://github.com/trumank/patternsleuth" } 19 | steamlocate = "2.0.1" 20 | repak = { git = "https://github.com/trumank/repak" } 21 | serde = { version = "1.0.219", features = ["derive"] } 22 | serde_json = "1.0.140" 23 | itertools = "0.14.0" 24 | postcard = { version = "1.1.1", default-features = false, features = ["alloc"] } 25 | fs-err = "3.1.0" 26 | tracing = { version = "0.1.41", features = ["attributes"] } 27 | tracing-appender = "0.2.3" 28 | tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter", "std", "registry"] } 29 | tokio = "1.44.2" 30 | reqwest = { version = "0.11.27", default-features = false, features = ["blocking", "rustls", "json"] } 31 | snafu = "0.8.5" 32 | 33 | [package] 34 | name = "mint" 35 | repository.workspace = true 36 | authors.workspace = true 37 | license.workspace = true 38 | version.workspace = true 39 | edition.workspace = true 40 | 41 | [features] 42 | default = ["hook", "oodle_platform_dependent"] 43 | hook = ["dep:hook"] 44 | oodle = ["repak/oodle_implicit_dynamic"] 45 | oodle_platform_dependent = ["workspace_hack/oodle_platform_dependent"] 46 | debug = ["egui/deadlock_detection"] 47 | 48 | [dependencies] 49 | workspace_hack = { path = "workspace_hack" } 50 | ansi_term = "0.12.1" 51 | anyhow.workspace = true 52 | async-trait = "0.1.88" 53 | clap = { version = "4.5.36", features = ["derive"] } 54 | dialoguer = "0.11.0" 55 | directories = "6.0.0" 56 | eframe = "0.31.1" 57 | egui = "0.31.1" 58 | egui_commonmark = "0.20.0" 59 | futures = "0.3.31" 60 | hex = "0.4.3" 61 | image = { version = "0.25.6", default-features = false, features = ["png"] } 62 | indexmap = { version = "2.9.0", features = ["serde"] } 63 | inventory = "0.3.20" 64 | mint_lib = { path = "mint_lib" } 65 | modio = { git = "https://github.com/trumank/modio-rs.git", branch = "dev", default-features = false, features = ["rustls-tls"] } 66 | obake = { version = "1.0.5", features = ["serde"] } 67 | opener = "0.7.2" 68 | path-slash = "0.2.1" 69 | rayon = "1.10.0" 70 | regex = "1.11.1" 71 | reqwest.workspace = true 72 | reqwest-middleware = "0.2.5" 73 | rfd = "0.15.3" 74 | rust-ini = "0.21.1" 75 | self_update = { version = "0.42.0", default-features = false, features = ["archive-zip", "rustls"] } 76 | semver = "1.0.26" 77 | serde.workspace = true 78 | serde_json.workspace = true 79 | sha2 = "0.10.8" 80 | steamlocate.workspace = true 81 | task-local-extensions = "0.1.4" 82 | tempfile = "3.19.1" 83 | thiserror = "2.0.12" 84 | tokio = { workspace = true, features = ["full"] } 85 | tracing.workspace = true 86 | typetag = "0.2.20" 87 | uasset_utils = { git = "https://github.com/trumank/uasset_utils" } 88 | unreal_asset = { git = "https://github.com/trumank/unrealmodding", branch = "patches" } 89 | url = "2.5.4" 90 | zip = { version = "2.6.1", default-features = false, features = ["aes-crypto", "deflate", "time"] } 91 | repak.workspace = true 92 | include_dir = "0.7.4" 93 | postcard.workspace = true 94 | fs-err.workspace = true 95 | snafu.workspace = true 96 | strum = { version = "0.27", features = ["derive"] } 97 | itertools.workspace = true 98 | egui_dnd = "0.12.0" 99 | 100 | [target.'cfg(target_env = "msvc")'.dependencies] 101 | hook = { path = "hook", artifact = "cdylib", optional = true, target = "x86_64-pc-windows-msvc"} 102 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 103 | hook = { path = "hook", artifact = "cdylib", optional = true, target = "x86_64-pc-windows-gnu"} 104 | 105 | # generated by 'cargo dist init' 106 | [profile.dist] 107 | inherits = "release" 108 | debug = false 109 | strip = true 110 | 111 | [profile.dev] 112 | opt-level = 3 113 | lto = "off" 114 | 115 | # Config for 'cargo dist' 116 | [workspace.metadata.dist] 117 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 118 | cargo-dist-version = "0.0.7" 119 | # The preferred Rust toolchain to use in CI (rustup toolchain syntax) 120 | rust-toolchain-version = "1.70.0" 121 | # CI backends to support (see 'cargo dist generate-ci') 122 | ci = ["github"] 123 | # The installers to generate for each app 124 | installers = [] 125 | # Target platforms to build apps for (Rust target-triple syntax) 126 | targets = ["x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 127 | unix-archive = ".zip" 128 | 129 | [dev-dependencies] 130 | mockall = "0.13.1" 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Truman Kilen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mint 2 | 3 | 3rd party mod integration tool for Deep Rock Galactic to download and integrate mods completely 4 | externally of the game. This enables more stable mod usage as well as offline mod usage. Works for 5 | both Steam and Microsoft Store versions. 6 | 7 | Graphical User Interface 8 | 9 | Mods are added via URL to a .pak or .zip containing a .pak. Mods can also be pulled from mod.io. 10 | Examples: 11 | 12 | - `C:\Path\To\Local\Mod.zip` 13 | - `https://example.org/some-online-mod-repository/public-mod.pak` 14 | - `https://mod.io/g/drg/m/sandbox-utilities` 15 | 16 | Mods from mod.io will require an OAuth token which can be obtained from 17 | when prompted. 18 | 19 | Most mods work just as if they were loaded via the official integration, but there are still some 20 | behavioural differences. If a mod is crashing or otherwise behaving differently than when using the 21 | official integration, *please* create an 22 | [issue](https://github.com/trumank/mint/issues/new) so it can be addressed. 23 | 24 | For more details, please consult our [user guide](https://github.com/trumank/mint/wiki). 25 | 26 | ## Usage 27 | 28 | This section assumes that you are on Windows and is using the steam version of DRG, working with 29 | either local `.pak`s or mod.io mods. 30 | 31 | First, download the [latest release](https://github.com/trumank/mint/releases/latest) 32 | compatible with your architecture. For windows, this will be the 33 | `mint-x86_64-pc-windows-msvc.zip`. Extract this to anywhere you'd like to keep the 34 | executable. 35 | 36 | Then, we'll need to perform some first-time setup. 37 | 38 | ### First Time Setup 39 | 40 | We need to provide the tool with the path to `FSD-WindowsNoEditor.pak` and a mod.io OAuth token if 41 | you want to use mod.io mods. These can be configured in the settings menu (cogwheel located in the 42 | bottom toolbar). 43 | 44 | Settings menu 45 | 46 | #### Locating the DRG `FSD-WindowsNoEditor.pak` 47 | 48 | If the tool fails to detect your DRG installation, then you can manually browse to add the path to 49 | `FSD-WindowsNoEditor.pak`. 50 | 51 | This file is located under the `FSD` folder inside your DRG installation directory, e.g. 52 | 53 | ``` 54 | E:\SteamLibrary\steamapps\common\Deep Rock Galactic\FSD\FSD-WindowsNoEditor.pak 55 | ``` 56 | 57 | #### Adding a mod.io OAuth Token 58 | 59 | Inside the settings menu, there is a modio setting (cogwheel). If you click on that, it will prompt 60 | for an mod.io OAuth token. 61 | 62 | To generate a mod.io OAuth token, you'll need to visit . You'll need to 63 | accept the API terms and conditions. 64 | 65 | mod.io Access page 66 | 67 | Then, you'll need to add a new client under OAuth Access, call it e.g. "DRG Mod Integration". 68 | 69 | For that client, create a new token named e.g. "modio-access" with Read-only scope. Copy the token 70 | into the integration tool's prompt. 71 | 72 | ### Adding Mods 73 | 74 | After these steps, you can now add local mods or mod.io mods. 75 | 76 | #### Adding mod.io mods 77 | 78 | Copy the URL to the mod into the "Add mods..." field and hit enter. 79 | 80 | You can obtain a list of your subscribed mods list using the "Copy Mod URLs" 81 | button via [A Better Modding Menu](https://mod.io/g/drg/m/a-better-modding-menu) 82 | in game: 83 | 84 | ![Copy Mod URLs](https://github.com/trumank/mint/assets/1144160/375f441f-4762-4549-a241-1b54ed391b2f) 85 | 86 | #### Adding a local mod 87 | 88 | You can either drag and drop a local `.pak` file on to the tool window, or add the path to the 89 | local `.pak` in the same "Add mods..." field. 90 | 91 | ### Updating Cache 92 | 93 | The versioned mod.io mods are *cached*. If you want to update to the latest version of your mods, 94 | you'll need to press the "Update cache" button. 95 | 96 | ### Installing/uninstalling mods 97 | 98 | Once you are happy with your mod profile, you can install the mods by pressing the "Install mods" 99 | button, and uninstall them with the "Uninstall mods" button. **This must be done while the game is 100 | closed.** 101 | 102 | ## Using integrated mod support again 103 | 104 | If you want to go back to the integrated mod support again, you must uninstall the mods installed by 105 | mint. Then, launch the game normally. 106 | -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/BPL_MINT_Utility.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/BPL_MINT_Utility.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/BPL_MINT_Utility.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/BPL_MINT_Utility.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSON.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSON.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSON.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSON.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrder.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrder.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrder.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrder.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrders.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrders.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrders.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONSortOrders.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONStream.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONStream.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONStream.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONStream.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONToken.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONToken.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONToken.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONToken.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONTokenType.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONTokenType.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONTokenType.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONTokenType.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONType.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONType.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONType.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONType.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONValue.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONValue.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONValue.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/JSON/JSONValue.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_Replication_v1.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_Replication_v1.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_Replication_v1.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_Replication_v1.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_SpawnMods.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_SpawnMods.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_SpawnMods.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_SpawnMods.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_UI.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_UI.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_UI.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/MI_UI.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/ITM_Modding_ToolTip_Entry.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/ITM_Modding_ToolTip_Entry.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/ITM_Modding_ToolTip_Entry.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/ITM_Modding_ToolTip_Entry.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/TOOLTIP_ServerEntry_Mods.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/TOOLTIP_ServerEntry_Mods.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/TOOLTIP_ServerEntry_Mods.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/RebuiltAssets/TOOLTIP_ServerEntry_Mods.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Asc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Asc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Asc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Asc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Desc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Desc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Desc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModAuthor_Desc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Asc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Asc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Asc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Asc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Desc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Desc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Desc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModCategory_Desc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Asc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Asc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Asc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Asc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Desc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Desc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Desc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModName_Desc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Asc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Asc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Asc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Asc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Desc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Desc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Desc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModRequired_Desc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Asc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Asc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Asc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Asc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Desc.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Desc.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Desc.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_ModURL_Desc.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_PlayerEntry.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_PlayerEntry.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_PlayerEntry.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/BP_MINT_Sorter_PlayerEntry.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/IMINT_ObjectSorter.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/IMINT_ObjectSorter.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/IMINT_ObjectSorter.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/IMINT_ObjectSorter.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/MINTLib_Sorting.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/MINTLib_Sorting.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/MINTLib_Sorting.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/Sorting/MINTLib_Sorting.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/EMINT_Compare_Availability.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/EMINT_Compare_Availability.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/EMINT_Compare_Availability.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/EMINT_Compare_Availability.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/MENU_MINT.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/MENU_MINT.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/MENU_MINT.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/MENU_MINT.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModListOptions.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModListOptions.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModListOptions.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModListOptions.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList_Entry.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList_Entry.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList_Entry.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ModList_Entry.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList_Entry.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList_Entry.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList_Entry.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_MultiModList_Entry.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList_Entry.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList_Entry.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList_Entry.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_PlayerList_Entry.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_RadioButton.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_RadioButton.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_RadioButton.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_RadioButton.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ScrollBar.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ScrollBar.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ScrollBar.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_ScrollBar.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SingleModList.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SingleModList.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SingleModList.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SingleModList.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification_Container.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification_Container.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification_Container.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideInNotification_Container.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_HookWarning.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_HookWarning.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_HookWarning.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_HookWarning.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_VersionWarning.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_VersionWarning.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_VersionWarning.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_SlideIn_VersionWarning.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_TextInput.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_TextInput.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_TextInput.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_TextInput.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_UpdateBanner.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_UpdateBanner.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_UpdateBanner.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/UI/WMINT_UpdateBanner.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/github-mark.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/github-mark.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/github-mark.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_AssemblyStorm/ModIntegration/github-mark.uexp -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_mint/BPL_MINT.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_mint/BPL_MINT.uasset -------------------------------------------------------------------------------- /assets/integration/FSD/Content/_mint/BPL_MINT.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/integration/FSD/Content/_mint/BPL_MINT.uexp -------------------------------------------------------------------------------- /assets/modio-cog-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/assets/modio-cog-blue.png -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1746188796, 6 | "narHash": "sha256-ZZ38C6Wy11lqVNKV/N5DOM4hnVyJNwS0cb03yD3ZTqk=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "4be4d7a7758d92d60a092a503c93a381f95c71b1", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "rust-overlay": "rust-overlay" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ] 30 | }, 31 | "locked": { 32 | "lastModified": 1746239644, 33 | "narHash": "sha256-wMvMBMlpS1H8CQdSSgpLeoCWS67ciEkN/GVCcwk7Apc=", 34 | "owner": "oxalica", 35 | "repo": "rust-overlay", 36 | "rev": "bd32e88bef6da0e021a42fb4120a8df2150e9b8c", 37 | "type": "github" 38 | }, 39 | "original": { 40 | "owner": "oxalica", 41 | "repo": "rust-overlay", 42 | "type": "github" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Mint development shell"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | 10 | outputs = 11 | { nixpkgs, rust-overlay, ... }: 12 | let 13 | system = "x86_64-linux"; 14 | 15 | lib = nixpkgs.lib; 16 | overlays = [ (import rust-overlay) ]; 17 | pkgs = import nixpkgs { 18 | inherit system overlays; 19 | }; 20 | 21 | rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { 22 | extensions = [ 23 | "rust-src" 24 | "rust-analyzer" 25 | ]; 26 | }; 27 | rustPlatform = pkgs.makeRustPlatform { 28 | cargo = rustToolchain; 29 | rustc = rustToolchain; 30 | }; 31 | 32 | mingwPkgs = pkgs.pkgsCross.mingwW64; 33 | mingwCompiler = mingwPkgs.buildPackages.gcc; 34 | mingwRustflags = "-L ${mingwPkgs.windows.pthreads}/lib"; 35 | mingwTool = name: "${mingwCompiler}/bin/${mingwCompiler.targetPrefix}${name}"; 36 | 37 | libs = with pkgs; [ 38 | gtk3 39 | libGL 40 | openssl 41 | atk 42 | libxkbcommon 43 | wayland 44 | ]; 45 | 46 | buildTools = with pkgs; [ 47 | rustToolchain 48 | pkg-config 49 | mingwCompiler 50 | makeWrapper 51 | ]; 52 | 53 | libraryPath = lib.makeLibraryPath libs; 54 | 55 | manifest = lib.importTOML ./Cargo.toml; 56 | packageName = manifest.package.name; 57 | packageVersion = manifest.workspace.package.version; 58 | 59 | package = rustPlatform.buildRustPackage { 60 | nativeBuildInputs = buildTools; 61 | buildInputs = libs; 62 | 63 | pname = packageName; 64 | version = packageVersion; 65 | src = lib.cleanSource ./.; 66 | 67 | verbose = true; 68 | 69 | cargoLock = { 70 | lockFile = ./Cargo.lock; 71 | allowBuiltinFetchGit = true; 72 | }; 73 | 74 | doCheck = false; 75 | 76 | preConfigure = '' 77 | export LD_LIBRARY_PATH="${libraryPath}" 78 | export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="${mingwRustflags}"; 79 | ''; 80 | 81 | postInstall = '' 82 | wrapProgram $out/bin/${packageName} \ 83 | --prefix LD_LIBRARY_PATH : "${libraryPath}" \ 84 | --prefix XDG_DATA_DIRS : "${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}" 85 | ''; 86 | 87 | meta = with lib; { 88 | description = "Deep Rock Galactic mod loader and integration"; 89 | license = licenses.mit; 90 | homepage = "https://github.com/trumank/mint"; 91 | mainProgram = packageName; 92 | }; 93 | }; 94 | 95 | devShell = pkgs.mkShell { 96 | name = "mint"; 97 | 98 | buildInputs = buildTools ++ libs; 99 | 100 | LD_LIBRARY_PATH = libraryPath; 101 | CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS = mingwRustflags; 102 | 103 | # Necessary for cross compiled build scripts, otherwise it will build as ELF format 104 | # https://docs.rs/cc/latest/cc/#external-configuration-via-environment-variables 105 | CC_x86_64_pc_windows_gnu = mingwTool "cc"; 106 | AR_x86_64_pc_windows_gnu = mingwTool "ar"; 107 | }; 108 | in 109 | { 110 | packages.${system} = { 111 | ${packageName} = package; 112 | default = package; 113 | }; 114 | 115 | devShells.${system}.default = devShell; 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /hook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hook" 3 | repository.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | 9 | [lib] 10 | path = "src/lib.rs" 11 | crate-type = ["cdylib"] 12 | 13 | [dependencies] 14 | anyhow.workspace = true 15 | repak.workspace = true 16 | serde.workspace = true 17 | serde_json.workspace = true 18 | postcard.workspace = true 19 | fs-err.workspace = true 20 | tracing.workspace = true 21 | patternsleuth = { workspace = true, features = ["process-internal", "image-pe"] } 22 | retour = { git = "https://github.com/Hpmason/retour-rs", features = ["static-detour"] } 23 | hook_resolvers = { path = "../hook_resolvers" } 24 | windows = { version = "0.61.1", features = [ 25 | "Win32_Foundation", 26 | "Win32_System_SystemServices", 27 | "Win32_System_LibraryLoader", 28 | "Win32_System_Memory", 29 | "Win32_System_Threading", 30 | ] } 31 | mint_lib = { path = "../mint_lib" } 32 | bitflags = "2.9.0" 33 | widestring = "1.2.0" 34 | tokio = { workspace = true, features = ["full"] } 35 | tracing-appender = "0.2.3" 36 | proxy_dll = { git = "https://github.com/trumank/proxy_dll.git" } 37 | -------------------------------------------------------------------------------- /hook/src/hooks/server_list.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::globals; 7 | use crate::hooks::ExecFn; 8 | use crate::ue::{self, FName, FString, TArray, TMap}; 9 | 10 | retour::static_detour! { 11 | static GetServerName: unsafe extern "system" fn(*const c_void, *const c_void) -> *const ue::FString; 12 | static USessionHandlingFSDFillSessionSetting: unsafe extern "system" fn(*const c_void, *mut c_void, bool, *mut c_void, *mut c_void); 13 | } 14 | 15 | pub fn kismet_hooks() -> &'static [(&'static str, ExecFn)] { 16 | &[( 17 | "/Script/FSD.SessionHandling:FSDGetModsInstalled", 18 | exec_get_mods_installed as ExecFn, 19 | )] 20 | } 21 | 22 | pub unsafe fn init_hooks() -> Result<()> { 23 | if let Ok(server_name) = &globals().resolution.server_name { 24 | GetServerName 25 | .initialize( 26 | std::mem::transmute(server_name.get_server_name.0), 27 | detour_get_server_name, 28 | )? 29 | .enable()?; 30 | } 31 | 32 | if let Ok(server_mods) = &globals().resolution.server_mods { 33 | USessionHandlingFSDFillSessionSetting 34 | .initialize( 35 | std::mem::transmute(server_mods.fill_session_setting.0), 36 | detour_fill_session_setting, 37 | )? 38 | .enable()?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | fn detour_get_server_name(a: *const c_void, b: *const c_void) -> *const ue::FString { 45 | unsafe { 46 | let name = GetServerName.call(a, b).cast_mut().as_mut().unwrap(); 47 | 48 | let mut new_name = widestring::U16String::new(); 49 | new_name.push_slice([0x5b, 0x4d, 0x4f, 0x44, 0x44, 0x45, 0x44, 0x5d, 0x20]); 50 | new_name.push_slice(name.as_slice()); 51 | 52 | name.clear(); 53 | name.extend_from_slice(new_name.as_slice()); 54 | name.push(0); 55 | 56 | name 57 | } 58 | } 59 | 60 | fn detour_fill_session_setting( 61 | world: *const c_void, 62 | game_settings: *mut c_void, 63 | full_server: bool, 64 | unknown1: *mut c_void, 65 | unknown2: *mut c_void, 66 | ) { 67 | unsafe { 68 | USessionHandlingFSDFillSessionSetting.call( 69 | world, 70 | game_settings, 71 | full_server, 72 | unknown1, 73 | unknown2, 74 | ); 75 | 76 | let name = globals().meta.to_server_list_string(); 77 | 78 | let s: FString = serde_json::to_string(&vec![JsonMod { 79 | name, 80 | version: "mint".into(), 81 | category: 0, 82 | }]) 83 | .unwrap() 84 | .as_str() 85 | .into(); 86 | 87 | type Fn = unsafe extern "system" fn(*const c_void, ue::FName, *const ue::FString, u32); 88 | 89 | let f: Fn = std::mem::transmute( 90 | globals() 91 | .resolution 92 | .server_mods 93 | .as_ref() 94 | .unwrap() 95 | .set_fstring 96 | .0, 97 | ); 98 | 99 | f(game_settings, ue::FName::new(&"Mods".into()), &s, 3); 100 | } 101 | } 102 | 103 | #[derive(Debug, Serialize, Deserialize)] 104 | struct JsonMod { 105 | name: String, 106 | version: String, 107 | category: i32, 108 | } 109 | 110 | #[derive(Debug)] 111 | #[repr(C)] 112 | struct FBlueprintSessionResult { 113 | online_result: FOnlineSessionSearchResult, 114 | } 115 | #[derive(Debug)] 116 | #[repr(C)] 117 | struct FOnlineSessionSearchResult { 118 | session: FOnlineSession, 119 | ping_in_ms: i32, 120 | } 121 | #[derive(Debug)] 122 | #[repr(C)] 123 | struct FOnlineSession { 124 | vtable: u64, 125 | owning_user_id: TSharedPtr, // TSharedPtr OwningUserId; 126 | owning_user_name: FString, 127 | session_settings: FOnlineSessionSettings, 128 | session_info: TSharedPtr, //class TSharedPtr SessionInfo; 129 | num_open_private_connections: i32, 130 | num_open_public_connections: i32, 131 | } 132 | 133 | #[derive(Debug)] 134 | #[repr(C)] 135 | struct FOnlineSessionSettings { 136 | vtable: u64, 137 | num_public_connections: i32, 138 | num_private_connections: i32, 139 | b_should_advertise: u8, 140 | b_allow_join_in_progress: u8, 141 | b_is_lan_match: u8, 142 | b_is_dedicated: u8, 143 | b_uses_stats: u8, 144 | b_allow_invites: u8, 145 | b_uses_presence: u8, 146 | b_allow_join_via_presence: u8, 147 | b_allow_join_via_presence_friends_only: u8, 148 | b_anti_cheat_protected: u8, 149 | build_unique_id: i32, 150 | settings: TMap, 151 | member_settings: [u64; 10], 152 | } 153 | 154 | #[derive(Debug)] 155 | #[repr(C)] 156 | struct FOnlineSessionSetting { 157 | data: FVariantData, 158 | padding: [u32; 2], 159 | } 160 | 161 | #[derive(Debug)] 162 | #[repr(u32)] 163 | #[allow(unused)] 164 | enum EOnlineKeyValuePairDataType { 165 | Empty, 166 | Int32, 167 | UInt32, 168 | Int64, 169 | UInt64, 170 | Double, 171 | String, 172 | Float, 173 | Blob, 174 | Bool, 175 | Json, 176 | #[allow(clippy::upper_case_acronyms)] 177 | MAX, 178 | } 179 | 180 | #[repr(C)] 181 | struct FVariantData { 182 | type_: EOnlineKeyValuePairDataType, 183 | value: FVariantDataValue, 184 | } 185 | impl std::fmt::Debug for FVariantData { 186 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 187 | let mut dbg = f.debug_struct("FVariantData"); 188 | dbg.field("type", &self.type_); 189 | unsafe { 190 | match self { 191 | Self { 192 | type_: EOnlineKeyValuePairDataType::String, 193 | value: FVariantDataValue { as_tchar }, 194 | } => { 195 | dbg.field("data", &widestring::U16CStr::from_ptr_str(*as_tchar)); 196 | } 197 | Self { 198 | type_: EOnlineKeyValuePairDataType::UInt32, 199 | value: FVariantDataValue { as_uint }, 200 | } => { 201 | dbg.field("data", &as_uint); 202 | } 203 | Self { 204 | type_: EOnlineKeyValuePairDataType::Int32, 205 | value: FVariantDataValue { as_int }, 206 | } => { 207 | dbg.field("data", &as_int); 208 | } 209 | _ => { 210 | dbg.field("data", &""); 211 | } 212 | } 213 | } 214 | dbg.finish() 215 | } 216 | } 217 | 218 | #[repr(C)] 219 | union FVariantDataValue { 220 | as_bool: bool, 221 | as_int: i32, 222 | as_uint: u32, 223 | as_float: f32, 224 | as_int64: i64, 225 | as_uint64: u64, 226 | as_double: f64, 227 | as_tchar: *const u16, 228 | as_blob: std::mem::ManuallyDrop, 229 | } 230 | 231 | #[repr(C)] 232 | struct FVariantDataValueBlob { 233 | blob_data: *const u8, 234 | blob_size: u32, 235 | } 236 | 237 | #[cfg(test)] 238 | mod test { 239 | use super::*; 240 | const _: [u8; 0x20] = [0; std::mem::size_of::()]; 241 | const _: [u8; 0x18] = [0; std::mem::size_of::()]; 242 | const _: [u8; 0x10] = [0; std::mem::size_of::()]; 243 | const _: [u8; 0x10] = [0; std::mem::size_of::()]; 244 | } 245 | 246 | #[derive(Debug)] 247 | #[repr(C)] 248 | struct TSharedPtr { 249 | a: u64, 250 | b: u64, 251 | } 252 | 253 | unsafe extern "system" fn exec_get_mods_installed( 254 | _context: *mut ue::UObject, 255 | stack: *mut ue::kismet::FFrame, 256 | result: *mut c_void, 257 | ) { 258 | let stack = stack.as_mut().unwrap(); 259 | 260 | let session: FBlueprintSessionResult = stack.arg(); 261 | let _exclude_verified_mods: bool = stack.arg(); 262 | 263 | let result = &mut *(result as *mut TArray); 264 | result.clear(); 265 | 266 | let settings = &session.online_result.session.session_settings.settings; 267 | 268 | let mods = settings.find(FName::new(&"Mods".into())); 269 | if let Some(mods) = mods { 270 | if let FVariantData { 271 | type_: EOnlineKeyValuePairDataType::String, 272 | value: FVariantDataValue { as_tchar }, 273 | } = mods.data 274 | { 275 | if let Ok(string) = widestring::U16CStr::from_ptr_str(as_tchar).to_string() { 276 | if let Ok(mods) = serde_json::from_str::>(&string) { 277 | for m in mods { 278 | result.push(m.name.as_str().into()); 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | // TODO figure out lifetimes of structs from kismet params 286 | std::mem::forget(session); 287 | 288 | if !stack.code.is_null() { 289 | stack.code = stack.code.add(1); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /hook/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod hooks; 2 | mod ue; 3 | 4 | use std::{io::BufReader, path::Path}; 5 | 6 | use anyhow::{Context, Result}; 7 | use fs_err as fs; 8 | use hooks::{FnLoadGameFromMemory, FnSaveGameToMemory}; 9 | use mint_lib::mod_info::Meta; 10 | use tracing::{info, warn}; 11 | 12 | proxy_dll::proxy_dll!([x3daudio1_7, d3d9], init); 13 | 14 | fn init() { 15 | unsafe { 16 | patch().ok(); 17 | } 18 | } 19 | 20 | static mut GLOBALS: Option = None; 21 | thread_local! { 22 | static LOG_GUARD: std::cell::RefCell> = None.into(); 23 | } 24 | 25 | pub struct Globals { 26 | resolution: hook_resolvers::HookResolution, 27 | meta: Meta, 28 | } 29 | 30 | impl Globals { 31 | pub fn gmalloc(&self) -> &ue::FMalloc { 32 | unsafe { 33 | &**(self.resolution.core.as_ref().unwrap().gmalloc.0 as *const *const ue::FMalloc) 34 | } 35 | } 36 | pub fn fframe_step(&self) -> ue::FnFFrameStep { 37 | unsafe { std::mem::transmute(self.resolution.core.as_ref().unwrap().fframe_step.0) } 38 | } 39 | pub fn fframe_step_explicit_property(&self) -> ue::FnFFrameStepExplicitProperty { 40 | unsafe { 41 | std::mem::transmute( 42 | self.resolution 43 | .core 44 | .as_ref() 45 | .unwrap() 46 | .fframe_step_explicit_property 47 | .0, 48 | ) 49 | } 50 | } 51 | pub fn fname_to_string(&self) -> ue::FnFNameToString { 52 | unsafe { std::mem::transmute(self.resolution.core.as_ref().unwrap().fnametostring.0) } 53 | } 54 | pub fn fname_ctor_wchar(&self) -> ue::FnFNameCtorWchar { 55 | unsafe { std::mem::transmute(self.resolution.core.as_ref().unwrap().fname_ctor_wchar.0) } 56 | } 57 | pub fn uobject_base_utility_get_path_name(&self) -> ue::FnUObjectBaseUtilityGetPathName { 58 | unsafe { 59 | std::mem::transmute( 60 | self.resolution 61 | .core 62 | .as_ref() 63 | .unwrap() 64 | .uobject_base_utility_get_path_name 65 | .0, 66 | ) 67 | } 68 | } 69 | pub fn save_game_to_memory(&self) -> FnSaveGameToMemory { 70 | unsafe { 71 | std::mem::transmute( 72 | self.resolution 73 | .save_game 74 | .as_ref() 75 | .unwrap() 76 | .save_game_to_memory 77 | .0, 78 | ) 79 | } 80 | } 81 | pub fn load_game_from_memory(&self) -> FnLoadGameFromMemory { 82 | unsafe { 83 | std::mem::transmute( 84 | self.resolution 85 | .save_game 86 | .as_ref() 87 | .unwrap() 88 | .load_game_from_memory 89 | .0, 90 | ) 91 | } 92 | } 93 | } 94 | 95 | pub fn globals() -> &'static Globals { 96 | #[allow(static_mut_refs)] 97 | unsafe { 98 | GLOBALS.as_ref().unwrap() 99 | } 100 | } 101 | 102 | unsafe fn patch() -> Result<()> { 103 | let exe_path = std::env::current_exe().ok(); 104 | let bin_dir = exe_path.as_deref().and_then(Path::parent); 105 | 106 | let guard = bin_dir 107 | .and_then(|bin_dir| mint_lib::setup_logging(bin_dir.join("mint_hook.log"), "hook").ok()); 108 | if guard.is_none() { 109 | warn!("failed to set up logging"); 110 | } 111 | 112 | let pak_path = bin_dir 113 | .and_then(Path::parent) 114 | .and_then(Path::parent) 115 | .map(|p| p.join("Content/Paks/mods_P.pak")) 116 | .context("could not determine pak path")?; 117 | 118 | let mut pak_reader = BufReader::new(fs::File::open(pak_path)?); 119 | let pak = repak::PakBuilder::new().reader(&mut pak_reader)?; 120 | 121 | let meta_buf = pak.get("meta", &mut pak_reader)?; 122 | let meta: Meta = postcard::from_bytes(&meta_buf)?; 123 | 124 | let image = patternsleuth::process::internal::read_image()?; 125 | let resolution = image.resolve(hook_resolvers::HookResolution::resolver())?; 126 | info!("PS scan: {:#x?}", resolution); 127 | 128 | GLOBALS = Some(Globals { resolution, meta }); 129 | LOG_GUARD.with_borrow_mut(|g| *g = guard); 130 | 131 | hooks::initialize()?; 132 | 133 | info!("hook initialized"); 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /hook/src/ue/array.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use crate::globals; 4 | 5 | #[derive(Debug)] 6 | #[repr(C)] 7 | pub struct TArray { 8 | data: *mut T, 9 | num: i32, 10 | max: i32, 11 | } 12 | impl TArray { 13 | pub fn new() -> Self { 14 | Self { 15 | data: std::ptr::null_mut(), 16 | num: 0, 17 | max: 0, 18 | } 19 | } 20 | pub fn as_ptr(&self) -> *const T { 21 | self.data 22 | } 23 | pub fn as_mut_ptr(&mut self) -> *mut T { 24 | self.data 25 | } 26 | } 27 | impl Drop for TArray { 28 | fn drop(&mut self) { 29 | unsafe { 30 | std::ptr::drop_in_place(std::ptr::slice_from_raw_parts_mut( 31 | self.data, 32 | self.num as usize, 33 | )) 34 | } 35 | globals().gmalloc().free(self.data.cast()); 36 | } 37 | } 38 | impl Default for TArray { 39 | fn default() -> Self { 40 | Self { 41 | data: std::ptr::null_mut(), 42 | num: 0, 43 | max: 0, 44 | } 45 | } 46 | } 47 | impl TArray { 48 | pub fn with_capacity(capacity: usize) -> Self { 49 | Self { 50 | data: globals().gmalloc().malloc( 51 | capacity * std::mem::size_of::(), 52 | std::mem::align_of::() as u32, 53 | ) as *mut _, 54 | num: 0, 55 | max: capacity as i32, 56 | } 57 | } 58 | pub fn len(&self) -> usize { 59 | self.num as usize 60 | } 61 | pub fn capacity(&self) -> usize { 62 | self.max as usize 63 | } 64 | pub fn is_empty(&self) -> bool { 65 | self.len() == 0 66 | } 67 | pub fn as_slice(&self) -> &[T] { 68 | if self.num == 0 { 69 | &[] 70 | } else { 71 | unsafe { std::slice::from_raw_parts(self.data, self.num as usize) } 72 | } 73 | } 74 | pub fn as_mut_slice(&mut self) -> &mut [T] { 75 | if self.num == 0 { 76 | &mut [] 77 | } else { 78 | unsafe { std::slice::from_raw_parts_mut(self.data as *mut _, self.num as usize) } 79 | } 80 | } 81 | pub fn clear(&mut self) { 82 | let elems: *mut [T] = self.as_mut_slice(); 83 | 84 | unsafe { 85 | self.num = 0; 86 | std::ptr::drop_in_place(elems); 87 | } 88 | } 89 | pub fn reserve(&mut self, additional: usize) { 90 | if self.num + additional as i32 >= self.max { 91 | self.max = u32::next_power_of_two((self.max + additional as i32) as u32) as i32; 92 | let new = globals().gmalloc().realloc( 93 | self.data as *mut c_void, 94 | self.max as usize * std::mem::size_of::(), 95 | std::mem::align_of::() as u32, 96 | ) as *mut _; 97 | self.data = new; 98 | } 99 | } 100 | pub fn push(&mut self, new_value: T) { 101 | self.reserve(1); 102 | unsafe { 103 | std::ptr::write(self.data.add(self.num as usize), new_value); 104 | } 105 | self.num += 1; 106 | } 107 | pub fn extend_from_slice(&mut self, other: &[T]) 108 | where 109 | T: Copy, 110 | { 111 | self.reserve(other.len()); 112 | // TODO optimize 113 | for elm in other { 114 | self.push(*elm); 115 | } 116 | } 117 | } 118 | 119 | impl From<&[T]> for TArray 120 | where 121 | T: Copy, 122 | { 123 | fn from(value: &[T]) -> Self { 124 | let mut new = Self::with_capacity(value.len()); 125 | new.extend_from_slice(value); 126 | new 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /hook/src/ue/kismet.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | 3 | use super::*; 4 | 5 | #[derive(Debug)] 6 | #[repr(C)] 7 | pub struct FFrame { 8 | pub base: FOutputDevice, 9 | pub node: *const c_void, 10 | pub object: *mut UObject, 11 | pub code: *const c_void, 12 | pub locals: *const c_void, 13 | pub most_recent_property: *const FProperty, 14 | pub most_recent_property_address: *const c_void, 15 | pub flow_stack: [u8; 0x30], 16 | pub previous_frame: *const FFrame, 17 | pub out_parms: *const c_void, 18 | pub property_chain_for_compiled_in: *const FField, 19 | pub current_native_function: *const c_void, 20 | pub b_array_context_failed: bool, 21 | } 22 | 23 | impl FFrame { 24 | pub unsafe fn arg(self: &mut FFrame) -> T { 25 | let mut value: MaybeUninit = MaybeUninit::zeroed(); 26 | let ptr = value.as_mut_ptr() as *mut _; 27 | 28 | if self.code.is_null() { 29 | let cur = self.property_chain_for_compiled_in; 30 | self.property_chain_for_compiled_in = (*cur).next; 31 | (globals().fframe_step_explicit_property())(self, ptr, cur as *const FProperty); 32 | } else { 33 | (globals().fframe_step())(self, self.object, ptr); 34 | } 35 | 36 | value.assume_init() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hook/src/ue/malloc.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | #[derive(Debug)] 4 | #[repr(C)] 5 | pub struct FMalloc { 6 | vtable: *const FMallocVTable, 7 | } 8 | unsafe impl Sync for FMalloc {} 9 | unsafe impl Send for FMalloc {} 10 | impl FMalloc { 11 | pub fn malloc(&self, count: usize, alignment: u32) -> *mut c_void { 12 | unsafe { ((*self.vtable).malloc)(self, count, alignment) } 13 | } 14 | pub fn realloc(&self, original: *mut c_void, count: usize, alignment: u32) -> *mut c_void { 15 | unsafe { ((*self.vtable).realloc)(self, original, count, alignment) } 16 | } 17 | pub fn free(&self, original: *mut c_void) { 18 | unsafe { ((*self.vtable).free)(self, original) } 19 | } 20 | } 21 | 22 | #[derive(Debug)] 23 | #[repr(C)] 24 | pub struct FMallocVTable { 25 | pub __vec_del_dtor: *const (), 26 | pub exec: *const (), 27 | pub malloc: 28 | unsafe extern "system" fn(this: &FMalloc, count: usize, alignment: u32) -> *mut c_void, 29 | pub try_malloc: 30 | unsafe extern "system" fn(this: &FMalloc, count: usize, alignment: u32) -> *mut c_void, 31 | pub realloc: unsafe extern "system" fn( 32 | this: &FMalloc, 33 | original: *mut c_void, 34 | count: usize, 35 | alignment: u32, 36 | ) -> *mut c_void, 37 | pub try_realloc: unsafe extern "system" fn( 38 | this: &FMalloc, 39 | original: *mut c_void, 40 | count: usize, 41 | alignment: u32, 42 | ) -> *mut c_void, 43 | pub free: unsafe extern "system" fn(this: &FMalloc, original: *mut c_void), 44 | pub quantize_size: *const (), 45 | pub get_allocation_size: *const (), 46 | pub trim: *const (), 47 | pub setup_tls_caches_on_current_thread: *const (), 48 | pub clear_and_disable_tlscaches_on_current_thread: *const (), 49 | pub initialize_stats_metadata: *const (), 50 | pub update_stats: *const (), 51 | pub get_allocator_stats: *const (), 52 | pub dump_allocator_stats: *const (), 53 | pub is_internally_thread_safe: *const (), 54 | pub validate_heap: *const (), 55 | pub get_descriptive_name: *const (), 56 | } 57 | -------------------------------------------------------------------------------- /hook/src/ue/map.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use super::TArray; 4 | 5 | pub trait UEHash { 6 | fn ue_hash(&self) -> u32; 7 | } 8 | 9 | #[derive(Default, Debug)] 10 | #[repr(C)] 11 | struct TSetElement { 12 | value: V, 13 | hash_next_id: FSetElementId, 14 | hash_index: i32, 15 | } 16 | impl UEHash for TSetElement> { 17 | fn ue_hash(&self) -> u32 { 18 | self.value.a.ue_hash() 19 | } 20 | } 21 | 22 | #[derive(Default, Clone, Copy)] 23 | #[repr(C)] 24 | pub struct FSetElementId { 25 | index: i32, 26 | } 27 | impl FSetElementId { 28 | pub fn is_valid(self) -> bool { 29 | self.index != -1 30 | } 31 | } 32 | impl Debug for FSetElementId { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | write!(f, "FSetElementId({:?})", self.index) 35 | } 36 | } 37 | 38 | #[derive(Default, Debug)] 39 | #[repr(C)] 40 | struct TTuple { 41 | a: A, 42 | b: B, 43 | } 44 | 45 | #[repr(C)] 46 | union TSparseArrayElementOrFreeListLink { 47 | element: std::mem::ManuallyDrop, 48 | list_link: ListLink, 49 | } 50 | 51 | #[derive(Debug, Clone, Copy)] 52 | #[repr(C)] 53 | struct ListLink { 54 | next_free_index: i32, 55 | prev_free_index: i32, 56 | } 57 | 58 | #[derive(Debug)] 59 | #[repr(C)] 60 | struct TInlineAllocator { 61 | inline_data: [V; N], 62 | secondary_data: *const V, // TSizedHeapAllocator<32>::ForElementType, 63 | } 64 | impl Default for TInlineAllocator { 65 | fn default() -> Self { 66 | Self { 67 | inline_data: [Default::default(); N], 68 | secondary_data: std::ptr::null(), 69 | } 70 | } 71 | } 72 | 73 | impl TInlineAllocator { 74 | fn get_allocation(&self) -> *const V { 75 | if !self.secondary_data.is_null() { 76 | self.secondary_data 77 | } else { 78 | self.inline_data.as_ptr() 79 | } 80 | } 81 | } 82 | 83 | #[derive(Default, Debug)] 84 | #[repr(C)] 85 | struct TBitArray { 86 | allocator_instance: TInlineAllocator<4, u32>, 87 | num_bits: i32, 88 | max_bits: i32, 89 | } 90 | impl TBitArray { 91 | fn get_data(&self) -> *const u32 { 92 | self.allocator_instance.get_allocation() 93 | } 94 | 95 | fn index(&self, index: usize) -> FBitReference<'_> { 96 | assert!(index < self.num_bits as usize); 97 | let num_bits_per_dword = 32; 98 | FBitReference { 99 | data: unsafe { &*self.get_data().add(index / num_bits_per_dword) }, 100 | mask: 1 << (index & (num_bits_per_dword - 1)), 101 | } 102 | } 103 | } 104 | 105 | #[derive(Debug, Clone, Copy)] 106 | #[repr(C)] 107 | struct FBitReference<'data> { 108 | data: &'data u32, 109 | mask: u32, 110 | } 111 | impl FBitReference<'_> { 112 | fn bool(self) -> bool { 113 | (self.data & self.mask) != 0 114 | } 115 | } 116 | 117 | #[repr(C)] 118 | struct TSparseArray { 119 | data: TArray>, 120 | allocation_flags: TBitArray, 121 | first_free_index: i32, 122 | num_free_indices: i32, 123 | } 124 | impl Default for TSparseArray { 125 | fn default() -> Self { 126 | Self { 127 | data: Default::default(), 128 | allocation_flags: Default::default(), 129 | first_free_index: 0, 130 | num_free_indices: 0, 131 | } 132 | } 133 | } 134 | impl TSparseArray { 135 | fn index(&self, index: usize) -> &E { 136 | assert!(index < self.data.len() && index < self.allocation_flags.num_bits as usize); 137 | assert!(self.allocation_flags.index(index).bool()); 138 | unsafe { &self.data.as_slice()[index].element } 139 | } 140 | } 141 | 142 | struct DbgTSparseArrayData<'a, E>(&'a TSparseArray); 143 | impl Debug for DbgTSparseArrayData<'_, E> { 144 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 145 | let mut dbg = f.debug_list(); 146 | for i in 0..(self.0.allocation_flags.num_bits as usize) { 147 | if self.0.allocation_flags.index(i).bool() { 148 | dbg.entry(self.0.index(i)); 149 | } 150 | } 151 | dbg.finish() 152 | } 153 | } 154 | 155 | impl Debug for TSparseArray { 156 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 157 | f.debug_struct("TSparseArray") 158 | .field("data", &DbgTSparseArrayData(self)) 159 | .field("allocation_flags", &self.allocation_flags) 160 | .field("first_free_index", &self.first_free_index) 161 | .field("num_free_indices", &self.num_free_indices) 162 | .finish() 163 | } 164 | } 165 | 166 | #[repr(C)] 167 | pub struct TMap { 168 | elements: TSparseArray>>, 169 | hash: TInlineAllocator<1, FSetElementId>, 170 | hash_size: i32, 171 | } 172 | impl TMap { 173 | fn hash(&self) -> &[FSetElementId] { 174 | unsafe { std::slice::from_raw_parts(self.hash.get_allocation(), self.hash_size as usize) } 175 | } 176 | } 177 | impl Default for TMap { 178 | fn default() -> Self { 179 | Self { 180 | elements: Default::default(), 181 | hash: Default::default(), 182 | hash_size: 0, 183 | } 184 | } 185 | } 186 | impl Debug for TMap { 187 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 188 | f.debug_struct("TMap") 189 | .field("elements", &self.elements) 190 | .field("hash", &self.hash()) 191 | .finish() 192 | } 193 | } 194 | 195 | impl TMap { 196 | pub fn find(&self, key: K) -> Option<&V> { 197 | let id = self.find_id(key); 198 | if id.is_valid() { 199 | Some(&self.elements.index(id.index as usize).value.b) 200 | } else { 201 | None 202 | } 203 | } 204 | pub fn find_id(&self, key: K) -> FSetElementId { 205 | if self.elements.data.len() != self.elements.num_free_indices as usize { 206 | let key_hash = key.ue_hash(); 207 | let hash = &self.hash(); 208 | 209 | let mut i: FSetElementId = 210 | hash[(((self.hash_size as i64) - 1) & (key_hash as i64)) as usize]; 211 | 212 | if i.is_valid() { 213 | loop { 214 | let elm = self.elements.index(i.index as usize); 215 | 216 | if elm.value.a == key { 217 | return i; 218 | } 219 | 220 | i = elm.hash_next_id; 221 | if !i.is_valid() { 222 | break; 223 | } 224 | } 225 | } 226 | } 227 | 228 | FSetElementId { index: -1 } 229 | } 230 | } 231 | 232 | #[cfg(test)] 233 | mod test { 234 | use crate::ue::FName; 235 | 236 | use super::*; 237 | const _: [u8; 0x50] = [0; std::mem::size_of::>()]; 238 | const _: [u8; 0x38] = 239 | [0; std::mem::size_of::>>>()]; 240 | const _: [u8; 0x10] = [0; std::mem::size_of::>()]; 241 | } 242 | -------------------------------------------------------------------------------- /hook/src/ue/mod.rs: -------------------------------------------------------------------------------- 1 | mod array; 2 | pub mod kismet; 3 | mod malloc; 4 | mod map; 5 | mod name; 6 | mod object; 7 | mod string; 8 | 9 | pub use array::*; 10 | pub use malloc::*; 11 | pub use map::*; 12 | pub use name::*; 13 | pub use object::*; 14 | pub use string::*; 15 | 16 | use std::ffi::c_void; 17 | 18 | use crate::globals; 19 | 20 | pub type FnFFrameStep = 21 | unsafe extern "system" fn(stack: &mut kismet::FFrame, *mut UObject, result: *mut c_void); 22 | pub type FnFFrameStepExplicitProperty = unsafe extern "system" fn( 23 | stack: &mut kismet::FFrame, 24 | result: *mut c_void, 25 | property: *const FProperty, 26 | ); 27 | pub type FnFNameToString = unsafe extern "system" fn(&FName, &mut FString); 28 | pub type FnFNameCtorWchar = unsafe extern "system" fn(&mut FName, *const u16, EFindName); 29 | 30 | pub type FnUObjectBaseUtilityGetPathName = 31 | unsafe extern "system" fn(&UObjectBase, Option<&UObject>, &mut FString); 32 | 33 | #[derive(Debug, Default)] 34 | #[repr(C)] 35 | pub struct FVector { 36 | pub x: f32, 37 | pub y: f32, 38 | pub z: f32, 39 | } 40 | 41 | #[derive(Debug, Default)] 42 | #[repr(C)] 43 | pub struct FLinearColor { 44 | pub r: f32, 45 | pub g: f32, 46 | pub b: f32, 47 | pub a: f32, 48 | } 49 | -------------------------------------------------------------------------------- /hook/src/ue/name.rs: -------------------------------------------------------------------------------- 1 | use crate::{globals, ue::FString}; 2 | 3 | use super::UEHash; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | #[repr(u32)] 7 | pub enum EFindName { 8 | Find, 9 | Add, 10 | ReplaceNotSafeForThreading, 11 | } 12 | 13 | #[derive(Default, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] 14 | #[repr(C)] 15 | pub struct FName { 16 | pub comparison_index: FNameEntryId, 17 | pub number: u32, 18 | } 19 | impl FName { 20 | pub fn new(string: &FString) -> FName { 21 | let mut ret = FName::default(); 22 | unsafe { globals().fname_ctor_wchar()(&mut ret, string.as_ptr(), EFindName::Add) }; 23 | ret 24 | } 25 | pub fn find(string: &FString) -> FName { 26 | let mut ret = FName::default(); 27 | unsafe { globals().fname_ctor_wchar()(&mut ret, string.as_ptr(), EFindName::Find) }; 28 | ret 29 | } 30 | } 31 | impl std::fmt::Debug for FName { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | write!(f, "FName({self})") 34 | } 35 | } 36 | 37 | #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] 38 | #[repr(C)] 39 | pub struct FNameEntryId { 40 | pub value: u32, 41 | } 42 | 43 | impl std::fmt::Display for FName { 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | let mut string = FString::new(); 46 | unsafe { 47 | (globals().fname_to_string())(self, &mut string); 48 | }; 49 | write!(f, "{string}") 50 | } 51 | } 52 | impl UEHash for FNameEntryId { 53 | fn ue_hash(&self) -> u32 { 54 | let value = self.value; 55 | (value >> 4) + value.wrapping_mul(0x10001) + (value >> 0x10).wrapping_mul(0x80001) 56 | } 57 | } 58 | 59 | impl UEHash for FName { 60 | fn ue_hash(&self) -> u32 { 61 | self.comparison_index.ue_hash() + self.number 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /hook/src/ue/object.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | bitflags::bitflags! { 4 | #[derive(Debug, Clone)] 5 | pub struct EObjectFlags: u32 { 6 | const RF_NoFlags = 0x0000; 7 | const RF_Public = 0x0001; 8 | const RF_Standalone = 0x0002; 9 | const RF_MarkAsNative = 0x0004; 10 | const RF_Transactional = 0x0008; 11 | const RF_ClassDefaultObject = 0x0010; 12 | const RF_ArchetypeObject = 0x0020; 13 | const RF_Transient = 0x0040; 14 | const RF_MarkAsRootSet = 0x0080; 15 | const RF_TagGarbageTemp = 0x0100; 16 | const RF_NeedInitialization = 0x0200; 17 | const RF_NeedLoad = 0x0400; 18 | const RF_KeepForCooker = 0x0800; 19 | const RF_NeedPostLoad = 0x1000; 20 | const RF_NeedPostLoadSubobjects = 0x2000; 21 | const RF_NewerVersionExists = 0x4000; 22 | const RF_BeginDestroyed = 0x8000; 23 | const RF_FinishDestroyed = 0x00010000; 24 | const RF_BeingRegenerated = 0x00020000; 25 | const RF_DefaultSubObject = 0x00040000; 26 | const RF_WasLoaded = 0x00080000; 27 | const RF_TextExportTransient = 0x00100000; 28 | const RF_LoadCompleted = 0x00200000; 29 | const RF_InheritableComponentTemplate = 0x00400000; 30 | const RF_DuplicateTransient = 0x00800000; 31 | const RF_StrongRefOnFrame = 0x01000000; 32 | const RF_NonPIEDuplicateTransient = 0x02000000; 33 | const RF_Dynamic = 0x04000000; 34 | const RF_WillBeLoaded = 0x08000000; 35 | } 36 | } 37 | bitflags::bitflags! { 38 | #[derive(Debug, Clone)] 39 | pub struct EFunctionFlags: u32 { 40 | const FUNC_None = 0x0000; 41 | const FUNC_Final = 0x0001; 42 | const FUNC_RequiredAPI = 0x0002; 43 | const FUNC_BlueprintAuthorityOnly = 0x0004; 44 | const FUNC_BlueprintCosmetic = 0x0008; 45 | const FUNC_Net = 0x0040; 46 | const FUNC_NetReliable = 0x0080; 47 | const FUNC_NetRequest = 0x0100; 48 | const FUNC_Exec = 0x0200; 49 | const FUNC_Native = 0x0400; 50 | const FUNC_Event = 0x0800; 51 | const FUNC_NetResponse = 0x1000; 52 | const FUNC_Static = 0x2000; 53 | const FUNC_NetMulticast = 0x4000; 54 | const FUNC_UbergraphFunction = 0x8000; 55 | const FUNC_MulticastDelegate = 0x00010000; 56 | const FUNC_Public = 0x00020000; 57 | const FUNC_Private = 0x00040000; 58 | const FUNC_Protected = 0x00080000; 59 | const FUNC_Delegate = 0x00100000; 60 | const FUNC_NetServer = 0x00200000; 61 | const FUNC_HasOutParms = 0x00400000; 62 | const FUNC_HasDefaults = 0x00800000; 63 | const FUNC_NetClient = 0x01000000; 64 | const FUNC_DLLImport = 0x02000000; 65 | const FUNC_BlueprintCallable = 0x04000000; 66 | const FUNC_BlueprintEvent = 0x08000000; 67 | const FUNC_BlueprintPure = 0x10000000; 68 | const FUNC_EditorOnly = 0x20000000; 69 | const FUNC_Const = 0x40000000; 70 | const FUNC_NetValidate = 0x80000000; 71 | const FUNC_AllFlags = 0xffffffff; 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | #[repr(C)] 77 | pub struct UObjectBase { 78 | pub vtable: *const c_void, 79 | pub object_flags: EObjectFlags, 80 | pub internal_index: i32, 81 | pub class_private: *const UClass, 82 | pub name_private: FName, 83 | pub outer_private: *const UObject, 84 | } 85 | 86 | #[derive(Debug)] 87 | #[repr(C)] 88 | pub struct UObjectBaseUtility { 89 | pub uobject_base: UObjectBase, 90 | } 91 | 92 | #[derive(Debug)] 93 | #[repr(C)] 94 | pub struct UObject { 95 | pub uobject_base_utility: UObjectBaseUtility, 96 | } 97 | 98 | #[derive(Debug)] 99 | #[repr(C)] 100 | pub struct FOutputDevice { 101 | vtable: *const c_void, 102 | b_suppress_event_tag: bool, 103 | b_auto_emit_line_terminator: bool, 104 | } 105 | 106 | #[derive(Debug)] 107 | #[repr(C)] 108 | pub struct UField { 109 | pub uobject: UObject, 110 | pub next: *const UField, 111 | } 112 | 113 | #[derive(Debug)] 114 | #[repr(C)] 115 | pub struct FStructBaseChain { 116 | pub struct_base_chain_array: *const *const FStructBaseChain, 117 | pub num_struct_bases_in_chain_minus_one: i32, 118 | } 119 | 120 | #[derive(Debug)] 121 | #[repr(C)] 122 | pub struct FFieldClass { 123 | // TODO 124 | name: FName, 125 | } 126 | 127 | #[derive(Debug)] 128 | #[repr(C)] 129 | pub struct FFieldVariant { 130 | container: *const c_void, 131 | b_is_uobject: bool, 132 | } 133 | 134 | #[derive(Debug)] 135 | #[repr(C)] 136 | pub struct FField { 137 | pub class_private: *const FFieldClass, 138 | pub owner: FFieldVariant, 139 | pub next: *const FField, 140 | pub name_private: FName, 141 | pub flags_private: EObjectFlags, 142 | } 143 | 144 | pub struct FProperty { 145 | // TODO 146 | } 147 | 148 | #[derive(Debug)] 149 | #[repr(C)] 150 | pub struct UStruct { 151 | pub ufield: UField, 152 | pub fstruct_base_chain: FStructBaseChain, 153 | pub super_struct: *const UStruct, 154 | pub children: *const UField, 155 | pub child_properties: *const FField, 156 | pub properties_size: i32, 157 | pub min_alignment: i32, 158 | pub script: TArray, 159 | pub property_link: *const FProperty, 160 | pub ref_link: *const FProperty, 161 | pub destructor_link: *const FProperty, 162 | pub post_construct_link: *const FProperty, 163 | pub script_and_property_object_references: TArray<*const UObject>, 164 | pub unresolved_script_properties: *const (), //TODO pub TArray,int>,TSizedDefaultAllocator<32> >* 165 | pub unversioned_schema: *const (), //TODO const FUnversionedStructSchema* 166 | } 167 | 168 | #[derive(Debug)] 169 | #[repr(C)] 170 | pub struct UFunction { 171 | pub ustruct: UStruct, 172 | pub function_flags: EFunctionFlags, 173 | pub num_parms: u8, 174 | pub parms_size: u16, 175 | pub return_value_offset: u16, 176 | pub rpc_id: u16, 177 | pub rpc_response_id: u16, 178 | pub first_property_to_init: *const FProperty, 179 | pub event_graph_function: *const UFunction, 180 | pub event_graph_call_offset: i32, 181 | pub func: unsafe extern "system" fn(*mut UObject, *mut kismet::FFrame, *mut c_void), 182 | } 183 | 184 | #[derive(Debug)] 185 | #[repr(C)] 186 | pub struct UClass { 187 | pub ustruct: UStruct, 188 | } 189 | 190 | impl UObjectBase { 191 | pub fn get_path_name(&self, stop_outer: Option<&UObject>) -> String { 192 | let mut string = FString::new(); 193 | unsafe { 194 | (globals().uobject_base_utility_get_path_name())(self, stop_outer, &mut string); 195 | } 196 | string.to_string() 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /hook/src/ue/string.rs: -------------------------------------------------------------------------------- 1 | use super::TArray; 2 | 3 | pub type FString = TArray; 4 | 5 | impl From<&str> for FString { 6 | fn from(value: &str) -> Self { 7 | Self::from( 8 | widestring::U16CString::from_str(value) 9 | .unwrap() 10 | .as_slice_with_nul(), 11 | ) 12 | } 13 | } 14 | 15 | impl std::fmt::Display for FString { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | let slice = self.as_slice(); 18 | let last = slice.len() 19 | - slice 20 | .iter() 21 | .cloned() 22 | .rev() 23 | .position(|c| c != 0) 24 | .unwrap_or_default(); 25 | write!( 26 | f, 27 | "{}", 28 | widestring::U16Str::from_slice(&slice[..last]) 29 | .to_string() 30 | .unwrap() 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /hook_resolvers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hook_resolvers" 3 | repository.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | 9 | [dependencies] 10 | patternsleuth.workspace = true 11 | serde = { workspace = true, optional = true } 12 | 13 | [features] 14 | serde-resolvers = ["dep:serde"] 15 | -------------------------------------------------------------------------------- /hook_resolvers/src/lib.rs: -------------------------------------------------------------------------------- 1 | use patternsleuth::resolvers::futures::future::join_all; 2 | use patternsleuth::resolvers::unreal::blueprint_library::UFunctionBind; 3 | use patternsleuth::resolvers::unreal::fname::{FNameCtorWchar, FNameToString}; 4 | use patternsleuth::resolvers::unreal::game_loop::Main; 5 | use patternsleuth::resolvers::unreal::gmalloc::GMalloc; 6 | use patternsleuth::resolvers::unreal::kismet::{FFrameStep, FFrameStepExplicitProperty}; 7 | use patternsleuth::resolvers::unreal::save_game::{ 8 | UGameplayStaticsDoesSaveGameExist, UGameplayStaticsLoadGameFromMemory, 9 | UGameplayStaticsLoadGameFromSlot, UGameplayStaticsSaveGameToMemory, 10 | UGameplayStaticsSaveGameToSlot, 11 | }; 12 | use patternsleuth::resolvers::unreal::*; 13 | use patternsleuth::resolvers::*; 14 | use patternsleuth::scanner::Pattern; 15 | use patternsleuth::MemoryAccessorTrait; 16 | 17 | #[cfg(feature = "serde-resolvers")] 18 | use serde::{Deserialize, Serialize}; 19 | 20 | #[derive(Debug, PartialEq)] 21 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 22 | pub struct GetServerName(pub usize); 23 | impl_resolver_singleton!(collect, GetServerName); 24 | impl_resolver_singleton!(PEImage, GetServerName, |ctx| async { 25 | let patterns = [ 26 | "48 89 5C 24 10 48 89 6C 24 18 48 89 74 24 20 57 41 56 41 57 48 83 EC 30 45 33 FF 4C 8B F2 48 8B D9 44 89 7C 24 50 41 8B FF" 27 | ]; 28 | 29 | let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; 30 | 31 | // matches two but the first is the one we need 32 | // could potentially get number of xrefs if this becomes problematic 33 | Ok(Self( 34 | res.into_iter() 35 | .flatten() 36 | .next() 37 | .context("expected at least one")?, 38 | )) 39 | }); 40 | 41 | #[derive(Debug, PartialEq)] 42 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 43 | pub struct FOnlineSessionSettingsSetFString(pub usize); 44 | impl_resolver_singleton!(collect, FOnlineSessionSettingsSetFString); 45 | impl_resolver_singleton!(PEImage, FOnlineSessionSettingsSetFString, |ctx| async { 46 | let patterns = ["48 89 5C 24 ?? 48 89 54 24 ?? 55 56 57 48 83 EC 40 49 8B F8 48 8D 69"]; 47 | let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; 48 | Ok(Self(ensure_one(res.into_iter().flatten())?)) 49 | }); 50 | 51 | #[derive(Debug, PartialEq)] 52 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 53 | pub struct USessionHandlingFSDFillSessionSetttingInner(pub usize); 54 | impl_resolver_singleton!(collect, USessionHandlingFSDFillSessionSetttingInner); 55 | impl_resolver_singleton!( 56 | PEImage, 57 | USessionHandlingFSDFillSessionSetttingInner, 58 | |ctx| async { 59 | let patterns = [ 60 | "48 89 5C 24 ?? 48 89 4C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 8B EC 48 81 EC 80 00 00 00 4C 8B FA", 61 | "48 89 5C 24 ?? 4C 89 4C 24 ?? 48 89 4C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 8B EC 48 83 EC 70", 62 | ]; 63 | let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; 64 | Ok(Self(ensure_one(res.into_iter().flatten())?)) 65 | } 66 | ); 67 | 68 | #[derive(Debug, PartialEq)] 69 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 70 | pub struct ModsFName(pub usize); 71 | impl_resolver_singleton!(collect, ModsFName); 72 | impl_resolver_singleton!(PEImage, ModsFName, |ctx| async { 73 | let strings = ctx 74 | .scan( 75 | Pattern::from_bytes("Mods\0".encode_utf16().flat_map(u16::to_le_bytes).collect()) 76 | .unwrap(), 77 | ) 78 | .await; 79 | 80 | let refs = join_all(strings.iter().map(|s| { 81 | ctx.scan( 82 | Pattern::new(format!( 83 | "41 b8 01 00 00 00 48 8d 15 X0x{s:X} 48 8d 0d | ?? ?? ?? ?? e9 ?? ?? ?? ??" 84 | )) 85 | .unwrap(), 86 | ) 87 | })) 88 | .await; 89 | 90 | Ok(Self(try_ensure_one( 91 | refs.iter() 92 | .flatten() 93 | .map(|a| Ok(ctx.image().memory.rip4(*a)?)), 94 | )?)) 95 | }); 96 | 97 | #[derive(Debug, PartialEq)] 98 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 99 | pub struct Disable(pub usize); 100 | impl_resolver_singleton!(collect, Disable); 101 | impl_resolver_singleton!(PEImage, Disable, |ctx| async { 102 | let patterns = ["4C 8B B4 24 48 01 00 00 0F 84"]; 103 | 104 | let res = join_all(patterns.iter().map(|p| ctx.scan(Pattern::new(p).unwrap()))).await; 105 | 106 | Ok(Self(ensure_one(res.into_iter().flatten())?)) 107 | }); 108 | 109 | impl_try_collector! { 110 | #[derive(Debug, PartialEq)] 111 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 112 | pub struct ServerModsResolution { 113 | pub set_fstring: FOnlineSessionSettingsSetFString, 114 | pub fill_session_setting: USessionHandlingFSDFillSessionSetttingInner, 115 | pub mods_fname: ModsFName, 116 | } 117 | } 118 | 119 | impl_try_collector! { 120 | #[derive(Debug, PartialEq)] 121 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 122 | pub struct ServerNameResolution { 123 | pub get_server_name: GetServerName, 124 | } 125 | } 126 | 127 | impl_try_collector! { 128 | #[derive(Debug, PartialEq)] 129 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 130 | pub struct SaveGameResolution { 131 | pub save_game_to_memory: UGameplayStaticsSaveGameToMemory, 132 | pub save_game_to_slot: UGameplayStaticsSaveGameToSlot, 133 | pub load_game_from_memory: UGameplayStaticsLoadGameFromMemory, 134 | pub load_game_from_slot: UGameplayStaticsLoadGameFromSlot, 135 | pub does_save_game_exist: UGameplayStaticsDoesSaveGameExist, 136 | } 137 | } 138 | 139 | impl_try_collector! { 140 | #[derive(Debug, PartialEq)] 141 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 142 | pub struct CoreResolution { 143 | pub gmalloc: GMalloc, 144 | pub main: Main, 145 | pub fnametostring: FNameToString, 146 | pub fname_ctor_wchar: FNameCtorWchar, 147 | pub uobject_base_utility_get_path_name: UObjectBaseUtilityGetPathName, 148 | pub ufunction_bind: UFunctionBind, 149 | pub fframe_step: FFrameStep, 150 | pub fframe_step_explicit_property: FFrameStepExplicitProperty, 151 | } 152 | } 153 | 154 | impl_collector! { 155 | #[derive(Debug, PartialEq)] 156 | #[cfg_attr(feature = "serde-resolvers", derive(Serialize, Deserialize))] 157 | pub struct HookResolution { 158 | pub disable: Disable, 159 | pub server_name: ServerNameResolution, 160 | pub server_mods: ServerModsResolution, 161 | pub save_game: SaveGameResolution, 162 | pub core: CoreResolution, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /mint_lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mint_lib" 3 | repository.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | build = "build.rs" 9 | 10 | [dependencies] 11 | anyhow.workspace = true 12 | steamlocate.workspace = true 13 | repak.workspace = true 14 | serde.workspace = true 15 | itertools.workspace = true 16 | fs-err.workspace = true 17 | tracing.workspace = true 18 | tracing-appender.workspace = true 19 | tracing-subscriber.workspace = true 20 | reqwest.workspace = true 21 | snafu.workspace = true 22 | 23 | [build-dependencies] 24 | built = { version = "0.8.0", features = ["semver", "git2"] } 25 | -------------------------------------------------------------------------------- /mint_lib/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | built::write_built_file().expect("Failed to acquire build-time information"); 3 | } 4 | -------------------------------------------------------------------------------- /mint_lib/src/error.rs: -------------------------------------------------------------------------------- 1 | use snafu::Snafu; 2 | 3 | #[derive(Debug, Snafu)] 4 | #[snafu(display("mint encountered an error: {msg}"))] 5 | pub struct GenericError { 6 | pub msg: String, 7 | } 8 | 9 | pub trait ResultExt { 10 | fn generic(self, msg: String) -> Result; 11 | fn with_generic(self, f: F) -> Result 12 | where 13 | F: FnOnce(E) -> String; 14 | } 15 | 16 | impl ResultExt for Result { 17 | fn generic(self, msg: String) -> Result { 18 | match self { 19 | Ok(ok) => Ok(ok), 20 | Err(_) => Err(GenericError { msg }), 21 | } 22 | } 23 | fn with_generic(self, f: F) -> Result 24 | where 25 | F: FnOnce(E) -> String, 26 | { 27 | match self { 28 | Ok(ok) => Ok(ok), 29 | Err(e) => Err(GenericError { msg: f(e) }), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mint_lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod mod_info; 3 | pub mod update; 4 | 5 | use std::{ 6 | io::BufWriter, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use anyhow::{bail, Context, Result}; 11 | use fs_err as fs; 12 | use tracing::*; 13 | use tracing_subscriber::fmt::format::FmtSpan; 14 | 15 | pub mod built_info { 16 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 17 | 18 | pub fn version() -> &'static str { 19 | GIT_VERSION.unwrap() 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum DRGInstallationType { 25 | Steam, 26 | Xbox, 27 | } 28 | 29 | impl DRGInstallationType { 30 | pub fn from_exe_path() -> Result { 31 | let exe_name = std::env::current_exe() 32 | .context("could not determine running exe")? 33 | .file_name() 34 | .context("failed to get exe path")? 35 | .to_string_lossy() 36 | .to_lowercase(); 37 | Ok(match exe_name.as_str() { 38 | "fsd-win64-shipping.exe" => Self::Steam, 39 | "fsd-wingdk-shipping.exe" => Self::Xbox, 40 | _ => bail!("unrecognized exe file name: {exe_name}"), 41 | }) 42 | } 43 | } 44 | 45 | impl DRGInstallationType { 46 | pub fn from_pak_path>(pak: P) -> Result { 47 | let pak_name = pak 48 | .as_ref() 49 | .file_name() 50 | .context("failed to get pak file name")? 51 | .to_string_lossy() 52 | .to_lowercase(); 53 | Ok(match pak_name.as_str() { 54 | "fsd-windowsnoeditor.pak" => Self::Steam, 55 | "fsd-wingdk.pak" => Self::Xbox, 56 | _ => bail!("unrecognized pak file name: {pak_name}"), 57 | }) 58 | } 59 | pub fn binaries_directory_name(&self) -> &'static str { 60 | match self { 61 | Self::Steam => "Win64", 62 | Self::Xbox => "WinGDK", 63 | } 64 | } 65 | pub fn main_pak_name(&self) -> &'static str { 66 | match self { 67 | Self::Steam => "FSD-WindowsNoEditor.pak", 68 | Self::Xbox => "FSD-WinGDK.pak", 69 | } 70 | } 71 | pub fn hook_dll_name(&self) -> &'static str { 72 | match self { 73 | Self::Steam => "x3daudio1_7.dll", 74 | Self::Xbox => "d3d9.dll", 75 | } 76 | } 77 | } 78 | 79 | #[derive(Debug)] 80 | pub struct DRGInstallation { 81 | pub root: PathBuf, 82 | pub installation_type: DRGInstallationType, 83 | } 84 | 85 | impl DRGInstallation { 86 | /// Returns first DRG installation found. Only supports Steam version 87 | /// TODO locate Xbox version 88 | pub fn find() -> Option { 89 | steamlocate::SteamDir::locate() 90 | .ok() 91 | .and_then(|steamdir| { 92 | steamdir 93 | .find_app(548430) 94 | .ok() 95 | .flatten() 96 | .map(|(app, library)| { 97 | library 98 | .resolve_app_dir(&app) 99 | .join("FSD/Content/Paks/FSD-WindowsNoEditor.pak") 100 | }) 101 | }) 102 | .and_then(|path| Self::from_pak_path(path).ok()) 103 | } 104 | pub fn from_pak_path>(pak: P) -> Result { 105 | let root = pak 106 | .as_ref() 107 | .parent() 108 | .and_then(Path::parent) 109 | .and_then(Path::parent) 110 | .context("failed to get pak parent directory")? 111 | .to_path_buf(); 112 | Ok(Self { 113 | root, 114 | installation_type: DRGInstallationType::from_pak_path(pak)?, 115 | }) 116 | } 117 | pub fn binaries_directory(&self) -> PathBuf { 118 | self.root 119 | .join("Binaries") 120 | .join(self.installation_type.binaries_directory_name()) 121 | } 122 | pub fn paks_path(&self) -> PathBuf { 123 | self.root.join("Content").join("Paks") 124 | } 125 | pub fn main_pak(&self) -> PathBuf { 126 | self.root 127 | .join("Content") 128 | .join("Paks") 129 | .join(self.installation_type.main_pak_name()) 130 | } 131 | pub fn modio_directory(&self) -> Option { 132 | match self.installation_type { 133 | DRGInstallationType::Steam => { 134 | #[cfg(target_os = "windows")] 135 | { 136 | Some(PathBuf::from("C:\\Users\\Public\\mod.io\\2475")) 137 | } 138 | #[cfg(target_os = "linux")] 139 | { 140 | steamlocate::SteamDir::locate() 141 | .map(|s| { 142 | s.path().join( 143 | "steamapps/compatdata/548430/pfx/drive_c/users/Public/mod.io/2475", 144 | ) 145 | }) 146 | .ok() 147 | } 148 | #[cfg(not(any(target_os = "windows", target_os = "linux")))] 149 | { 150 | None // TODO 151 | } 152 | } 153 | DRGInstallationType::Xbox => None, 154 | } 155 | } 156 | } 157 | 158 | pub fn setup_logging>( 159 | log_path: P, 160 | target: &str, 161 | ) -> Result { 162 | use tracing::metadata::LevelFilter; 163 | use tracing_subscriber::prelude::*; 164 | use tracing_subscriber::{ 165 | field::RecordFields, 166 | filter, 167 | fmt::{ 168 | self, 169 | format::{Pretty, Writer}, 170 | FormatFields, 171 | }, 172 | EnvFilter, 173 | }; 174 | 175 | /// Workaround for . 176 | struct NewType(Pretty); 177 | 178 | impl<'writer> FormatFields<'writer> for NewType { 179 | fn format_fields( 180 | &self, 181 | writer: Writer<'writer>, 182 | fields: R, 183 | ) -> core::fmt::Result { 184 | self.0.format_fields(writer, fields) 185 | } 186 | } 187 | 188 | let f = fs::File::create(log_path.as_ref())?; 189 | let writer = BufWriter::new(f); 190 | let (log_file_appender, guard) = tracing_appender::non_blocking(writer); 191 | let debug_file_log = fmt::layer() 192 | .with_writer(log_file_appender) 193 | .fmt_fields(NewType(Pretty::default())) 194 | .with_ansi(false) 195 | .with_filter(filter::Targets::new().with_target(target, Level::DEBUG)); 196 | let stderr_log = fmt::layer() 197 | .with_writer(std::io::stderr) 198 | .event_format(tracing_subscriber::fmt::format().without_time()) 199 | .with_span_events(FmtSpan::CLOSE) 200 | .with_filter( 201 | EnvFilter::builder() 202 | .with_default_directive(LevelFilter::INFO.into()) 203 | .from_env_lossy(), 204 | ); 205 | let subscriber = tracing_subscriber::registry() 206 | .with(stderr_log) 207 | .with(debug_file_log); 208 | 209 | tracing::subscriber::set_global_default(subscriber)?; 210 | 211 | debug!("tracing subscriber setup"); 212 | info!("writing logs to {:?}", log_path.as_ref().display()); 213 | info!("version: {}", built_info::version()); 214 | 215 | Ok(guard) 216 | } 217 | -------------------------------------------------------------------------------- /mint_lib/src/mod_info.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Tags from mod.io. 6 | #[derive(Debug, Clone)] 7 | pub struct ModioTags { 8 | pub qol: bool, 9 | pub gameplay: bool, 10 | pub audio: bool, 11 | pub visual: bool, 12 | pub framework: bool, 13 | pub versions: BTreeSet, 14 | pub required_status: RequiredStatus, 15 | pub approval_status: ApprovalStatus, 16 | } 17 | 18 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 19 | pub enum RequiredStatus { 20 | RequiredByAll, 21 | Optional, 22 | } 23 | 24 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 25 | pub enum ApprovalStatus { 26 | Verified, 27 | Approved, 28 | Sandbox, 29 | } 30 | 31 | /// Whether a mod can be resolved by clients or not 32 | #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)] 33 | pub enum ResolvableStatus { 34 | Unresolvable(String), 35 | Resolvable, 36 | } 37 | 38 | /// Returned from ModStore 39 | #[derive(Debug, Clone)] 40 | pub struct ModInfo { 41 | pub provider: &'static str, 42 | pub name: String, 43 | pub spec: ModSpecification, // unpinned version 44 | pub versions: Vec, // pinned versions TODO make this a different type 45 | pub resolution: ModResolution, 46 | pub suggested_require: bool, 47 | pub suggested_dependencies: Vec, // ModResponse 48 | pub modio_tags: Option, // only available for mods from mod.io 49 | pub modio_id: Option, // only available for mods from mod.io 50 | } 51 | 52 | /// Returned from ModProvider 53 | #[derive(Debug, Clone)] 54 | pub enum ModResponse { 55 | Redirect(ModSpecification), 56 | Resolve(ModInfo), 57 | } 58 | 59 | /// Points to a mod, optionally a specific version 60 | #[derive( 61 | Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, 62 | )] 63 | pub struct ModSpecification { 64 | pub url: String, 65 | } 66 | 67 | impl ModSpecification { 68 | pub fn new(url: String) -> Self { 69 | Self { url } 70 | } 71 | pub fn satisfies_dependency(&self, other: &ModSpecification) -> bool { 72 | // TODO this hack works surprisingly well but is still a complete hack and should be replaced 73 | self.url.starts_with(&other.url) || other.url.starts_with(&self.url) 74 | } 75 | } 76 | 77 | /// Points to a specific version of a specific mod 78 | #[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)] 79 | pub struct ModResolution { 80 | pub url: ModIdentifier, 81 | pub status: ResolvableStatus, 82 | } 83 | 84 | impl ModResolution { 85 | pub fn resolvable(url: ModIdentifier) -> Self { 86 | Self { 87 | url, 88 | status: ResolvableStatus::Resolvable, 89 | } 90 | } 91 | pub fn unresolvable(url: ModIdentifier, name: String) -> Self { 92 | Self { 93 | url, 94 | status: ResolvableStatus::Unresolvable(name), 95 | } 96 | } 97 | /// Used to get the URL if resolvable or just return the mod name if not 98 | pub fn get_resolvable_url_or_name(&self) -> &str { 99 | match &self.status { 100 | ResolvableStatus::Resolvable => &self.url.0, 101 | ResolvableStatus::Unresolvable(name) => name, 102 | } 103 | } 104 | } 105 | 106 | /// Mod identifier used for tracking gameplay affecting status. 107 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 108 | pub struct ModIdentifier(pub String); 109 | 110 | impl ModIdentifier { 111 | pub fn new(s: String) -> Self { 112 | Self(s) 113 | } 114 | } 115 | impl From for ModIdentifier { 116 | fn from(value: String) -> Self { 117 | Self::new(value) 118 | } 119 | } 120 | impl From<&str> for ModIdentifier { 121 | fn from(value: &str) -> Self { 122 | Self::new(value.to_owned()) 123 | } 124 | } 125 | 126 | /// Stripped down mod info stored in the mod pak to be used in game 127 | #[derive(Debug, Serialize, Deserialize)] 128 | pub struct Meta { 129 | pub version: String, 130 | pub mods: Vec, 131 | pub config: MetaConfig, 132 | } 133 | #[derive(Debug, Serialize, Deserialize)] 134 | pub struct MetaConfig {} 135 | #[derive(Debug, Serialize, Deserialize)] 136 | pub struct MetaMod { 137 | pub name: String, 138 | pub version: String, 139 | pub url: String, 140 | pub author: String, 141 | pub approval: ApprovalStatus, 142 | pub required: bool, 143 | } 144 | impl Meta { 145 | pub fn to_server_list_string(&self) -> String { 146 | use itertools::Itertools; 147 | 148 | ["mint".into(), self.version.to_string()] 149 | .into_iter() 150 | .chain( 151 | self.mods 152 | .iter() 153 | .sorted_by_key(|m| (std::cmp::Reverse(m.approval), &m.name)) 154 | .flat_map(|m| { 155 | [ 156 | match m.approval { 157 | ApprovalStatus::Verified => 'V', 158 | ApprovalStatus::Approved => 'A', 159 | ApprovalStatus::Sandbox => 'S', 160 | } 161 | .into(), 162 | m.name.replace(';', ""), 163 | ] 164 | }), 165 | ) 166 | .join(";") 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /mint_lib/src/update.rs: -------------------------------------------------------------------------------- 1 | use crate::error::GenericError; 2 | use crate::error::ResultExt; 3 | 4 | pub const GITHUB_RELEASE_URL: &str = "https://api.github.com/repos/trumank/mint/releases/latest"; 5 | pub const GITHUB_REQ_USER_AGENT: &str = "trumank/mint"; 6 | 7 | #[derive(Debug, serde::Deserialize)] 8 | pub struct GitHubRelease { 9 | pub html_url: String, 10 | pub tag_name: String, 11 | pub body: String, 12 | } 13 | 14 | pub async fn get_latest_release() -> Result { 15 | reqwest::Client::builder() 16 | .user_agent(GITHUB_REQ_USER_AGENT) 17 | .build() 18 | .generic("failed to construct reqwest client".to_string())? 19 | .get(GITHUB_RELEASE_URL) 20 | .send() 21 | .await 22 | .generic("check self update request failed".to_string())? 23 | .json::() 24 | .await 25 | .generic("check self update response is error".to_string()) 26 | } 27 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["master"] 2 | publish = false 3 | pre-release-replacements = [ 4 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, 5 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 6 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, 7 | {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased] - ReleaseDate", exactly=1}, 8 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/trumank/mint/compare/{{tag_name}}...HEAD", exactly=1}, 9 | ] 10 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | targets = [ 4 | "x86_64-unknown-linux-gnu", 5 | "x86_64-pc-windows-gnu", 6 | ] 7 | -------------------------------------------------------------------------------- /src/gui/find_string.rs: -------------------------------------------------------------------------------- 1 | use egui::{text::LayoutJob, Color32, TextFormat}; 2 | 3 | pub(crate) struct FindString<'data> { 4 | pub(crate) string: &'data str, 5 | pub(crate) string_lower: String, 6 | pub(crate) needle: &'data str, 7 | pub(crate) needle_lower: String, 8 | pub(crate) curr: usize, 9 | pub(crate) curr_match: bool, 10 | pub(crate) finished: bool, 11 | } 12 | 13 | impl<'data> FindString<'data> { 14 | pub(crate) fn new(string: &'data str, needle: &'data str) -> Self { 15 | Self { 16 | string, 17 | string_lower: string.to_lowercase(), 18 | needle, 19 | needle_lower: needle.to_lowercase(), 20 | curr: 0, 21 | curr_match: false, 22 | finished: false, 23 | } 24 | } 25 | pub(crate) fn next_internal(&mut self) -> Option<(bool, &'data str)> { 26 | if self.finished { 27 | None 28 | } else if self.needle.is_empty() { 29 | self.finished = true; 30 | Some((false, self.string)) 31 | } else if self.curr_match { 32 | self.curr_match = false; 33 | Some((true, &self.string[self.curr - self.needle.len()..self.curr])) 34 | } else if let Some(index) = self.string_lower[self.curr..].find(&self.needle_lower) { 35 | let next = self.curr + index; 36 | let chunk = &self.string[self.curr..next]; 37 | self.curr = next + self.needle.len(); 38 | self.curr_match = true; 39 | Some((false, chunk)) 40 | } else { 41 | self.finished = true; 42 | Some((false, &self.string[self.curr..])) 43 | } 44 | } 45 | } 46 | 47 | impl<'data> Iterator for FindString<'data> { 48 | type Item = (bool, &'data str); 49 | 50 | fn next(&mut self) -> Option { 51 | if self.string.is_empty() { 52 | return None; 53 | } 54 | // skip empty chunks 55 | while let Some(chunk) = self.next_internal() { 56 | if !chunk.1.is_empty() { 57 | return Some(chunk); 58 | } 59 | } 60 | None 61 | } 62 | } 63 | 64 | pub(crate) struct SearchJob { 65 | pub(crate) job: LayoutJob, 66 | pub(crate) is_match: bool, 67 | } 68 | 69 | pub(crate) fn searchable_text(text: &str, search_string: &str, format: TextFormat) -> SearchJob { 70 | let mut job = LayoutJob::default(); 71 | let mut is_match = false; 72 | if !search_string.is_empty() { 73 | for (m, chunk) in FindString::new(text, search_string) { 74 | let background = if m { 75 | is_match = true; 76 | TextFormat { 77 | background: Color32::YELLOW, 78 | ..format.clone() 79 | } 80 | } else { 81 | format.clone() 82 | }; 83 | job.append(chunk, 0.0, background); 84 | } 85 | } else { 86 | job.append(text, 0.0, format); 87 | } 88 | SearchJob { job, is_match } 89 | } 90 | -------------------------------------------------------------------------------- /src/gui/named_combobox.rs: -------------------------------------------------------------------------------- 1 | use super::{colors, custom_popup_above_or_below_widget, is_committed}; 2 | 3 | use crate::state::{ModData_v0_1_0 as ModData, ModProfile_v0_1_0 as ModProfile}; 4 | 5 | #[derive(Debug, Clone)] 6 | struct NamePopup { 7 | buffer_needs_prefill_and_focus: bool, 8 | buffer: String, 9 | } 10 | 11 | impl Default for NamePopup { 12 | fn default() -> Self { 13 | Self { 14 | buffer_needs_prefill_and_focus: true, 15 | buffer: String::new(), 16 | } 17 | } 18 | } 19 | 20 | pub trait NamedEntries { 21 | fn len(&self) -> usize; 22 | fn contains(&self, name: &str) -> bool; 23 | fn select(&mut self, name: String); 24 | fn selected_name(&self) -> &str; 25 | fn add_new(&mut self, name: &str); 26 | fn remove_selected(&mut self); 27 | fn rename_selected(&mut self, new_name: String); 28 | fn duplicate_selected(&mut self, new_name: String); 29 | fn entries<'s>(&'s mut self) -> Box + 's>; 30 | } 31 | 32 | impl NamedEntries for ModData { 33 | fn len(&self) -> usize { 34 | self.profiles.len() 35 | } 36 | fn contains(&self, name: &str) -> bool { 37 | self.profiles.contains_key(name) 38 | } 39 | fn select(&mut self, name: String) { 40 | self.active_profile = name; 41 | } 42 | fn selected_name(&self) -> &str { 43 | &self.active_profile 44 | } 45 | fn add_new(&mut self, name: &str) { 46 | self.profiles.insert(name.to_owned(), Default::default()); 47 | self.active_profile = name.to_string(); 48 | } 49 | fn remove_selected(&mut self) { 50 | self.remove_active_profile(); 51 | } 52 | fn rename_selected(&mut self, new_name: String) { 53 | let tmp = self.profiles.remove(&self.active_profile).unwrap(); 54 | self.profiles.insert(new_name.clone(), tmp); 55 | self.active_profile = new_name; 56 | } 57 | fn duplicate_selected(&mut self, new_name: String) { 58 | let new = self.get_active_profile().clone(); 59 | self.profiles.insert(new_name.clone(), new); 60 | self.active_profile = new_name; 61 | } 62 | fn entries<'s>(&'s mut self) -> Box + 's> { 63 | Box::new(self.profiles.iter()) 64 | } 65 | } 66 | 67 | /// Render and return whether any changes were made 68 | pub(crate) fn ui( 69 | ui: &mut egui::Ui, 70 | name: &str, 71 | entries: &mut N, 72 | additional_ui: Option, 73 | ) -> bool 74 | where 75 | N: NamedEntries, 76 | { 77 | let mut modified = false; 78 | ui.push_id(name, |ui| { 79 | ui.horizontal(|ui| { 80 | mk_add(ui, name, entries, &mut modified); 81 | mk_delete(ui, name, entries, &mut modified); 82 | mk_rename(ui, name, entries, &mut modified); 83 | 84 | ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { 85 | mk_duplicate(ui, name, entries, &mut modified); 86 | 87 | if let Some(additional_ui) = additional_ui { 88 | additional_ui(ui, entries); 89 | } 90 | 91 | ui.with_layout(ui.layout().with_main_justify(true), |ui| { 92 | mk_dropdown(ui, name, entries, &mut modified); 93 | }); 94 | }); 95 | }); 96 | }); 97 | modified 98 | } 99 | 100 | fn mk_delete(ui: &mut egui::Ui, name: &str, entries: &mut N, modified: &mut bool) 101 | where 102 | N: NamedEntries, 103 | { 104 | ui.add_enabled_ui(entries.len() > 1, |ui| { 105 | ui.scope(|ui| { 106 | ui.visuals_mut().widgets.hovered.weak_bg_fill = colors::DARK_RED; 107 | ui.visuals_mut().widgets.active.weak_bg_fill = colors::DARKER_RED; 108 | if ui 109 | .button(" 🗑 ") 110 | .on_hover_text_at_pointer(format!("Delete {name}")) 111 | .clicked() 112 | { 113 | entries.remove_selected(); 114 | *modified = true; 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | fn mk_add(ui: &mut egui::Ui, name: &str, entries: &mut N, modified: &mut bool) 121 | where 122 | N: NamedEntries, 123 | { 124 | ui.add_enabled_ui(true, |ui| { 125 | let response = ui 126 | .scope(|ui| { 127 | ui.visuals_mut().widgets.hovered.weak_bg_fill = colors::DARK_GREEN; 128 | ui.visuals_mut().widgets.active.weak_bg_fill = colors::DARKER_GREEN; 129 | ui.button(" ➕ ") 130 | .on_hover_text_at_pointer(format!("Add new {name}")) 131 | }) 132 | .inner; 133 | 134 | let popup_id = ui.make_persistent_id(format!("add-{name}")); 135 | if response.clicked() { 136 | ui.memory_mut(|mem| mem.open_popup(popup_id)); 137 | } 138 | mk_name_popup( 139 | entries, 140 | ui, 141 | name, 142 | popup_id, 143 | response, 144 | |_state| String::new(), 145 | |entries, name| { 146 | entries.add_new(&name); 147 | *modified = true; 148 | }, 149 | ); 150 | }); 151 | } 152 | 153 | fn mk_rename(ui: &mut egui::Ui, name: &str, entries: &mut N, modified: &mut bool) 154 | where 155 | N: NamedEntries, 156 | { 157 | ui.add_enabled_ui(true, |ui| { 158 | let response = ui 159 | .button("Rename") 160 | .on_hover_text_at_pointer(format!("Rename {name}")); 161 | let popup_id = ui.make_persistent_id(format!("rename-{name}")); 162 | if response.clicked() { 163 | ui.memory_mut(|mem| mem.open_popup(popup_id)); 164 | } 165 | mk_name_popup( 166 | entries, 167 | ui, 168 | name, 169 | popup_id, 170 | response, 171 | |entries| entries.selected_name().to_string(), 172 | |entries, name| { 173 | entries.rename_selected(name); 174 | *modified = true; 175 | }, 176 | ); 177 | }); 178 | } 179 | 180 | fn mk_duplicate(ui: &mut egui::Ui, name: &str, entries: &mut N, modified: &mut bool) 181 | where 182 | N: NamedEntries, 183 | { 184 | let response = ui 185 | .button("🗐") 186 | .on_hover_text_at_pointer(format!("Duplicate {name}")); 187 | let popup_id = ui.make_persistent_id(format!("duplicate-{name}")); 188 | if response.clicked() { 189 | ui.memory_mut(|mem| mem.open_popup(popup_id)); 190 | } 191 | mk_name_popup( 192 | entries, 193 | ui, 194 | name, 195 | popup_id, 196 | response, 197 | |state| format!("{} - Copy", state.selected_name()), 198 | |state, name| { 199 | state.duplicate_selected(name); 200 | *modified = true; 201 | }, 202 | ); 203 | } 204 | 205 | fn mk_dropdown(ui: &mut egui::Ui, name: &str, entries: &mut N, modified: &mut bool) 206 | where 207 | N: NamedEntries, 208 | { 209 | let mut selected = entries.selected_name().to_owned(); 210 | 211 | egui::ComboBox::from_id_salt(format!("dropdown-{name}")) 212 | .width(ui.available_width()) 213 | .selected_text(selected.clone()) 214 | .show_ui(ui, |ui| { 215 | entries.entries().for_each(|(k, _)| { 216 | ui.selectable_value(&mut selected, k.to_owned(), k); 217 | }) 218 | }); 219 | 220 | if selected != entries.selected_name() { 221 | entries.select(selected); 222 | *modified = true; 223 | } 224 | } 225 | 226 | #[allow(clippy::too_many_arguments)] 227 | fn mk_name_popup( 228 | entries: &mut N, 229 | ui: &egui::Ui, 230 | name: &str, 231 | popup_id: egui::Id, 232 | response: egui::Response, 233 | default_name: impl Fn(&mut N) -> String, 234 | mut accept: impl FnMut(&mut N, String), 235 | ) where 236 | N: NamedEntries, 237 | { 238 | let data_id = popup_id.with("data"); 239 | let mut popup: NamePopup = ui.data(|data| data.get_temp(data_id)).unwrap_or_default(); 240 | popup.buffer_needs_prefill_and_focus = custom_popup_above_or_below_widget( 241 | ui, 242 | popup_id, 243 | &response, 244 | egui::AboveOrBelow::Below, 245 | |ui| { 246 | ui.set_min_width(200.0); 247 | ui.vertical(|ui| { 248 | if popup.buffer_needs_prefill_and_focus { 249 | popup.buffer = default_name(entries); 250 | } 251 | 252 | let res = ui.add( 253 | egui::TextEdit::singleline(&mut popup.buffer) 254 | .hint_text(format!("Enter new {name} name")), 255 | ); 256 | if popup.buffer_needs_prefill_and_focus { 257 | res.request_focus(); 258 | } 259 | 260 | ui.horizontal(|ui| { 261 | if ui.button("Cancel").clicked() { 262 | ui.memory_mut(|mem| mem.close_popup()); 263 | } 264 | 265 | let invalid_name = popup.buffer.is_empty() || entries.contains(&popup.buffer); 266 | let clicked = ui 267 | .add_enabled(!invalid_name, egui::Button::new("OK")) 268 | .clicked(); 269 | if !invalid_name && (clicked || is_committed(&res)) { 270 | ui.memory_mut(|mem| mem.close_popup()); 271 | accept(entries, std::mem::take(&mut popup.buffer)); 272 | } 273 | }); 274 | }); 275 | }, 276 | ) 277 | .is_none(); 278 | 279 | ui.data_mut(|data| data.insert_temp(data_id, popup)); 280 | } 281 | -------------------------------------------------------------------------------- /src/gui/request_counter.rs: -------------------------------------------------------------------------------- 1 | /// Simple counter that returns a new ID each time it is called 2 | #[derive(Default)] 3 | pub struct RequestCounter(u32); 4 | 5 | impl RequestCounter { 6 | /// Get next ID 7 | pub fn next(&mut self) -> RequestID { 8 | let id = self.0; 9 | self.0 += 1; 10 | RequestID { id } 11 | } 12 | } 13 | 14 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 15 | pub struct RequestID { 16 | id: u32, 17 | } 18 | -------------------------------------------------------------------------------- /src/gui/toggle_switch.rs: -------------------------------------------------------------------------------- 1 | // Adapted from 2 | // . 3 | 4 | fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { 5 | let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); 6 | let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); 7 | if response.clicked() { 8 | *on = !*on; 9 | response.mark_changed(); 10 | } 11 | response.widget_info(|| { 12 | egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "") 13 | }); 14 | 15 | if ui.is_rect_visible(rect) { 16 | let how_on = ui.ctx().animate_bool(response.id, *on); 17 | let visuals = ui.style().interact_selectable(&response, *on); 18 | let rect = rect.expand(visuals.expansion); 19 | let radius = 0.5 * rect.height(); 20 | ui.painter().rect( 21 | rect, 22 | radius, 23 | visuals.bg_fill, 24 | visuals.bg_stroke, 25 | egui::StrokeKind::Inside, 26 | ); 27 | let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); 28 | let center = egui::pos2(circle_x, rect.center().y); 29 | ui.painter() 30 | .circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke); 31 | } 32 | 33 | response 34 | } 35 | 36 | #[allow(clippy::needless_pass_by_ref_mut)] 37 | pub fn toggle_switch(on: &mut bool) -> impl egui::Widget + '_ { 38 | move |ui: &mut egui::Ui| toggle_ui(ui, on) 39 | } 40 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains)] 2 | #![feature(if_let_guard)] 3 | 4 | pub mod gui; 5 | pub mod integrate; 6 | pub mod mod_lints; 7 | pub mod providers; 8 | pub mod state; 9 | 10 | use std::ops::Deref; 11 | use std::{ 12 | collections::HashSet, 13 | path::{Path, PathBuf}, 14 | }; 15 | 16 | use directories::ProjectDirs; 17 | use fs_err as fs; 18 | use integrate::IntegrationError; 19 | use providers::{ModResolution, ModSpecification, ProviderError, ProviderFactory}; 20 | use snafu::prelude::*; 21 | use state::{State, StateError}; 22 | use tracing::*; 23 | 24 | #[derive(Debug, Snafu)] 25 | pub enum MintError { 26 | #[snafu(transparent)] 27 | IoError { source: std::io::Error }, 28 | #[snafu(transparent)] 29 | RepakError { source: repak::Error }, 30 | #[snafu(transparent)] 31 | ProviderError { source: ProviderError }, 32 | #[snafu(transparent)] 33 | IntegrationError { source: IntegrationError }, 34 | #[snafu(transparent)] 35 | GenericError { 36 | source: mint_lib::error::GenericError, 37 | }, 38 | #[snafu(transparent)] 39 | StateError { source: StateError }, 40 | #[snafu(display("invalid DRG pak path: {path}"))] 41 | InvalidDrgPak { path: String }, 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct Dirs { 46 | pub config_dir: PathBuf, 47 | pub cache_dir: PathBuf, 48 | pub data_dir: PathBuf, 49 | } 50 | 51 | impl Dirs { 52 | pub fn default_xdg() -> Result { 53 | let legacy_dirs = ProjectDirs::from("", "", "drg-mod-integration") 54 | .expect("failed to construct project dirs"); 55 | 56 | let project_dirs = 57 | ProjectDirs::from("", "", "mint").expect("failed to construct project dirs"); 58 | 59 | Self::from_paths( 60 | Some(legacy_dirs.config_dir()) 61 | .filter(|p| p.exists()) 62 | .unwrap_or(project_dirs.config_dir()), 63 | Some(legacy_dirs.cache_dir()) 64 | .filter(|p| p.exists()) 65 | .unwrap_or(project_dirs.cache_dir()), 66 | Some(legacy_dirs.data_dir()) 67 | .filter(|p| p.exists()) 68 | .unwrap_or(project_dirs.data_dir()), 69 | ) 70 | } 71 | 72 | pub fn from_path>(path: P) -> Result { 73 | Self::from_paths( 74 | path.as_ref().join("config"), 75 | path.as_ref().join("cache"), 76 | path.as_ref().join("data"), 77 | ) 78 | } 79 | 80 | fn from_paths>( 81 | config_dir: P, 82 | cache_dir: P, 83 | data_dir: P, 84 | ) -> Result { 85 | fs::create_dir_all(&config_dir)?; 86 | fs::create_dir_all(&cache_dir)?; 87 | fs::create_dir_all(&data_dir)?; 88 | 89 | Ok(Self { 90 | config_dir: config_dir.as_ref().to_path_buf(), 91 | cache_dir: cache_dir.as_ref().to_path_buf(), 92 | data_dir: data_dir.as_ref().to_path_buf(), 93 | }) 94 | } 95 | } 96 | 97 | pub fn is_drg_pak>(path: P) -> Result<(), MintError> { 98 | let mut reader = std::io::BufReader::new(fs::File::open(path.as_ref())?); 99 | let pak = repak::PakBuilder::new().reader(&mut reader)?; 100 | pak.get("FSD/FSD.uproject", &mut reader)?; 101 | Ok(()) 102 | } 103 | 104 | pub async fn resolve_unordered_and_integrate>( 105 | game_path: P, 106 | state: &State, 107 | mod_specs: &[ModSpecification], 108 | update: bool, 109 | ) -> Result<(), IntegrationError> { 110 | let mods = state.store.resolve_mods(mod_specs, update).await?; 111 | 112 | let mods_set = mod_specs 113 | .iter() 114 | .flat_map(|m| [&mods[m].spec.url, &mods[m].resolution.url.0]) 115 | .collect::>(); 116 | 117 | // TODO need more rebust way of detecting whether dependencies are missing 118 | let missing_deps = mod_specs 119 | .iter() 120 | .flat_map(|m| { 121 | mods[m] 122 | .suggested_dependencies 123 | .iter() 124 | .filter_map(|m| (!mods_set.contains(&m.url)).then_some(&m.url)) 125 | }) 126 | .collect::>(); 127 | if !missing_deps.is_empty() { 128 | warn!("the following dependencies are missing:"); 129 | for d in missing_deps { 130 | warn!(" {d}"); 131 | } 132 | } 133 | 134 | let to_integrate = mod_specs 135 | .iter() 136 | .map(|u| mods[u].clone()) 137 | .collect::>(); 138 | let urls = to_integrate 139 | .iter() 140 | .map(|m| &m.resolution) 141 | .collect::>(); 142 | 143 | info!("fetching mods..."); 144 | let paths = state.store.fetch_mods(&urls, update, None).await?; 145 | 146 | integrate::integrate( 147 | game_path, 148 | state.config.deref().into(), 149 | to_integrate.into_iter().zip(paths).collect(), 150 | ) 151 | } 152 | 153 | async fn resolve_into_urls( 154 | state: &State, 155 | mod_specs: &[ModSpecification], 156 | ) -> Result, MintError> { 157 | let mods = state.store.resolve_mods(mod_specs, false).await?; 158 | 159 | let mods_set = mod_specs 160 | .iter() 161 | .flat_map(|m| [&mods[m].spec.url, &mods[m].resolution.url.0]) 162 | .collect::>(); 163 | 164 | // TODO need more rebust way of detecting whether dependencies are missing 165 | let missing_deps = mod_specs 166 | .iter() 167 | .flat_map(|m| { 168 | mods[m] 169 | .suggested_dependencies 170 | .iter() 171 | .filter_map(|m| (!mods_set.contains(&m.url)).then_some(&m.url)) 172 | }) 173 | .collect::>(); 174 | if !missing_deps.is_empty() { 175 | warn!("the following dependencies are missing:"); 176 | for d in missing_deps { 177 | warn!(" {d}"); 178 | } 179 | } 180 | 181 | let urls = mod_specs 182 | .iter() 183 | .map(|u| mods[u].clone()) 184 | .map(|m| m.resolution) 185 | .collect::>(); 186 | 187 | Ok(urls) 188 | } 189 | 190 | pub async fn resolve_ordered( 191 | state: &State, 192 | mod_specs: &[ModSpecification], 193 | ) -> Result, MintError> { 194 | let urls = resolve_into_urls(state, mod_specs).await?; 195 | Ok(state 196 | .store 197 | .fetch_mods(&urls.iter().collect::>(), false, None) 198 | .await?) 199 | } 200 | 201 | pub async fn resolve_unordered_and_integrate_with_provider_init( 202 | game_path: P, 203 | state: &mut State, 204 | mod_specs: &[ModSpecification], 205 | update: bool, 206 | init: F, 207 | ) -> Result<(), MintError> 208 | where 209 | P: AsRef, 210 | F: Fn(&mut State, String, &ProviderFactory) -> Result<(), MintError>, 211 | { 212 | loop { 213 | match resolve_unordered_and_integrate(&game_path, state, mod_specs, update).await { 214 | Ok(()) => return Ok(()), 215 | Err(ref e) 216 | if let IntegrationError::ProviderError { ref source } = e 217 | && let ProviderError::NoProvider { ref url, factory } = source => 218 | { 219 | init(state, url.clone(), factory)? 220 | } 221 | Err(e) => Err(e)?, 222 | } 223 | } 224 | } 225 | 226 | #[allow(clippy::needless_pass_by_ref_mut)] 227 | pub async fn resolve_ordered_with_provider_init( 228 | state: &mut State, 229 | mod_specs: &[ModSpecification], 230 | init: F, 231 | ) -> Result, MintError> 232 | where 233 | F: Fn(&mut State, String, &ProviderFactory) -> Result<(), MintError>, 234 | { 235 | loop { 236 | match resolve_ordered(state, mod_specs).await { 237 | Ok(mod_paths) => return Ok(mod_paths), 238 | Err(ref e) 239 | if let MintError::IntegrationError { ref source } = e 240 | && let IntegrationError::ProviderError { ref source } = source 241 | && let ProviderError::NoProvider { ref url, factory } = source => 242 | { 243 | init(state, url.clone(), factory)? 244 | } 245 | Err(e) => Err(e)?, 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::path::PathBuf; 3 | 4 | use anyhow::{anyhow, Context, Result}; 5 | use clap::{Parser, Subcommand}; 6 | use tracing::{debug, info}; 7 | 8 | use mint::mod_lints::{run_lints, LintId}; 9 | use mint::providers::ProviderFactory; 10 | use mint::{gui::gui, providers::ModSpecification, state::State}; 11 | use mint::{ 12 | resolve_ordered_with_provider_init, resolve_unordered_and_integrate_with_provider_init, Dirs, 13 | MintError, 14 | }; 15 | 16 | /// Command line integration tool. 17 | #[derive(Parser, Debug)] 18 | struct ActionIntegrate { 19 | /// Path to FSD-WindowsNoEditor.pak (FSD-WinGDK.pak for Microsoft Store version) located 20 | /// inside the "Deep Rock Galactic" installation directory under FSD/Content/Paks. Only 21 | /// necessary if it cannot be found automatically. 22 | #[arg(short, long)] 23 | fsd_pak: Option, 24 | 25 | /// Update mods. By default all mods and metadata are cached offline so this is necessary to 26 | /// check for updates. 27 | #[arg(short, long)] 28 | update: bool, 29 | 30 | /// Paths of mods to integrate 31 | /// 32 | /// Can be a file path or URL to a .pak or .zip file or a URL to a mod on https://mod.io/g/drg 33 | /// Examples: 34 | /// ./local/path/test-mod.pak 35 | /// https://mod.io/g/drg/m/custom-difficulty 36 | /// https://example.org/some-online-mod-repository/public-mod.zip 37 | #[arg(short, long, num_args=0.., verbatim_doc_comment)] 38 | mods: Vec, 39 | } 40 | 41 | /// Integrate a profile 42 | #[derive(Parser, Debug)] 43 | struct ActionIntegrateProfile { 44 | /// Path to FSD-WindowsNoEditor.pak (FSD-WinGDK.pak for Microsoft Store version) located 45 | /// inside the "Deep Rock Galactic" installation directory under FSD/Content/Paks. Only 46 | /// necessary if it cannot be found automatically. 47 | #[arg(short, long)] 48 | fsd_pak: Option, 49 | 50 | /// Update mods. By default all mods and metadata are cached offline so this is necessary to 51 | /// check for updates. 52 | #[arg(short, long)] 53 | update: bool, 54 | 55 | /// Profile to integrate. 56 | profile: String, 57 | } 58 | 59 | /// Launch via steam 60 | #[derive(Parser, Debug)] 61 | struct ActionLaunch { 62 | args: Vec, 63 | } 64 | 65 | /// Lint the mod bundle that would be created for a profile. 66 | #[derive(Parser, Debug)] 67 | struct ActionLint { 68 | /// Path to FSD-WindowsNoEditor.pak (FSD-WinGDK.pak for Microsoft Store version) located 69 | /// inside the "Deep Rock Galactic" installation directory under FSD/Content/Paks. Only 70 | /// necessary if it cannot be found automatically. 71 | #[arg(short, long)] 72 | fsd_pak: Option, 73 | 74 | /// Profile to lint. 75 | profile: String, 76 | } 77 | 78 | #[derive(Subcommand, Debug)] 79 | enum Action { 80 | Integrate(ActionIntegrate), 81 | Profile(ActionIntegrateProfile), 82 | Launch(ActionLaunch), 83 | Lint(ActionLint), 84 | } 85 | 86 | #[derive(Parser, Debug)] 87 | #[command(author, version=mint_lib::built_info::GIT_VERSION.unwrap())] 88 | struct Args { 89 | #[command(subcommand)] 90 | action: Option, 91 | 92 | /// Location to store configs and data 93 | #[arg(long)] 94 | appdata: Option, 95 | } 96 | 97 | fn main() -> Result<()> { 98 | #[cfg(target_os = "windows")] 99 | { 100 | // Try to enable ANSI code support on Windows 10 for console. If it fails, then whatever 101 | // *shrugs*. 102 | let _res = ansi_term::enable_ansi_support(); 103 | } 104 | 105 | let args = Args::parse(); 106 | 107 | let dirs = args 108 | .appdata 109 | .as_ref() 110 | .map(Dirs::from_path) 111 | .unwrap_or_else(Dirs::default_xdg)?; 112 | 113 | std::env::set_var("RUST_BACKTRACE", "1"); 114 | 115 | let _guard = mint_lib::setup_logging(dirs.data_dir.join("mint.log"), "mint")?; 116 | debug!("logging setup complete"); 117 | 118 | info!("config dir = {}", dirs.config_dir.display()); 119 | info!("cache dir = {}", dirs.cache_dir.display()); 120 | info!("data dir = {}", dirs.data_dir.display()); 121 | 122 | let rt = tokio::runtime::Runtime::new().expect("Unable to create Runtime"); 123 | debug!("tokio runtime created"); 124 | let _enter = rt.enter(); 125 | 126 | debug!(?args); 127 | 128 | match args.action { 129 | Some(Action::Integrate(action)) => rt.block_on(async { 130 | action_integrate(dirs, action).await?; 131 | Ok(()) 132 | }), 133 | Some(Action::Profile(action)) => rt.block_on(async { 134 | action_integrate_profile(dirs, action).await?; 135 | Ok(()) 136 | }), 137 | Some(Action::Launch(action)) => { 138 | std::thread::spawn(move || { 139 | rt.block_on(std::future::pending::<()>()); 140 | }); 141 | gui(dirs, Some(action.args))?; 142 | Ok(()) 143 | } 144 | Some(Action::Lint(action)) => rt.block_on(async { 145 | action_lint(dirs, action).await?; 146 | Ok(()) 147 | }), 148 | None => { 149 | std::thread::spawn(move || { 150 | rt.block_on(std::future::pending::<()>()); 151 | }); 152 | gui(dirs, None)?; 153 | Ok(()) 154 | } 155 | } 156 | } 157 | 158 | #[tracing::instrument(skip(state))] 159 | fn init_provider( 160 | state: &mut State, 161 | url: String, 162 | factory: &ProviderFactory, 163 | ) -> Result<(), MintError> { 164 | info!("initializing provider for {:?}", url); 165 | 166 | let params = state 167 | .config 168 | .provider_parameters 169 | .entry(factory.id.to_owned()) 170 | .or_default(); 171 | for p in factory.parameters { 172 | if !params.contains_key(p.name) { 173 | // this blocks but since we're calling it on the main thread it'll be fine 174 | let value = 175 | dialoguer::Password::with_theme(&dialoguer::theme::ColorfulTheme::default()) 176 | .with_prompt(p.description) 177 | .interact() 178 | .unwrap(); 179 | params.insert(p.id.to_owned(), value); 180 | } 181 | } 182 | Ok(state.store.add_provider(factory, params)?) 183 | } 184 | 185 | fn get_pak_path(state: &State, arg: &Option) -> Result { 186 | arg.as_ref() 187 | .or_else(|| state.config.drg_pak_path.as_ref()) 188 | .cloned() 189 | .context("Could not find DRG pak file, please specify manually with the --fsd_pak flag") 190 | } 191 | 192 | async fn action_integrate(dirs: Dirs, action: ActionIntegrate) -> Result<()> { 193 | let mut state = State::init(dirs)?; 194 | let game_pak_path = get_pak_path(&state, &action.fsd_pak)?; 195 | debug!(?game_pak_path); 196 | 197 | let mod_specs = action 198 | .mods 199 | .into_iter() 200 | .map(ModSpecification::new) 201 | .collect::>(); 202 | 203 | resolve_unordered_and_integrate_with_provider_init( 204 | game_pak_path, 205 | &mut state, 206 | &mod_specs, 207 | action.update, 208 | init_provider, 209 | ) 210 | .await 211 | .map_err(|e| anyhow!("{}", e)) 212 | } 213 | 214 | async fn action_integrate_profile(dirs: Dirs, action: ActionIntegrateProfile) -> Result<()> { 215 | let mut state = State::init(dirs)?; 216 | let game_pak_path = get_pak_path(&state, &action.fsd_pak)?; 217 | debug!(?game_pak_path); 218 | 219 | let mut mods = Vec::new(); 220 | state.mod_data.for_each_enabled_mod(&action.profile, |mc| { 221 | mods.push(mc.spec.clone()); 222 | }); 223 | 224 | resolve_unordered_and_integrate_with_provider_init( 225 | game_pak_path, 226 | &mut state, 227 | &mods, 228 | action.update, 229 | init_provider, 230 | ) 231 | .await 232 | .map_err(|e| anyhow!("{}", e)) 233 | } 234 | 235 | async fn action_lint(dirs: Dirs, action: ActionLint) -> Result<()> { 236 | let mut state = State::init(dirs)?; 237 | let game_pak_path = get_pak_path(&state, &action.fsd_pak)?; 238 | debug!(?game_pak_path); 239 | 240 | let mut mods = Vec::new(); 241 | state.mod_data.for_each_mod(&action.profile, |mc| { 242 | mods.push(mc.spec.clone()); 243 | }); 244 | 245 | let mod_paths = resolve_ordered_with_provider_init(&mut state, &mods, init_provider).await?; 246 | 247 | let report = tokio::task::spawn_blocking(move || { 248 | run_lints( 249 | &BTreeSet::from([ 250 | LintId::ARCHIVE_WITH_ONLY_NON_PAK_FILES, 251 | LintId::ASSET_REGISTRY_BIN, 252 | LintId::CONFLICTING, 253 | LintId::EMPTY_ARCHIVE, 254 | LintId::OUTDATED_PAK_VERSION, 255 | LintId::SHADER_FILES, 256 | LintId::ARCHIVE_WITH_MULTIPLE_PAKS, 257 | LintId::NON_ASSET_FILES, 258 | LintId::SPLIT_ASSET_PAIRS, 259 | ]), 260 | mods.into_iter().zip(mod_paths).collect(), 261 | Some(game_pak_path), 262 | ) 263 | }) 264 | .await??; 265 | println!("{report:#?}"); 266 | Ok(()) 267 | } 268 | -------------------------------------------------------------------------------- /src/mod_lints/archive_multiple_paks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct ArchiveMultiplePaksLint; 9 | 10 | impl Lint for ArchiveMultiplePaksLint { 11 | type Output = BTreeSet; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut archive_multiple_paks_mods = BTreeSet::new(); 15 | lcx.for_each_mod( 16 | |_, _, _| Ok(()), 17 | None::, 18 | None::, 19 | Some(|mod_spec| { 20 | archive_multiple_paks_mods.insert(mod_spec); 21 | }), 22 | )?; 23 | Ok(archive_multiple_paks_mods) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mod_lints/archive_only_non_pak_files.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct ArchiveOnlyNonPakFilesLint; 9 | 10 | impl Lint for ArchiveOnlyNonPakFilesLint { 11 | type Output = BTreeSet; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut archive_only_non_pak_files_mods = BTreeSet::new(); 15 | lcx.for_each_mod( 16 | |_, _, _| Ok(()), 17 | None::, 18 | Some(|mod_spec| { 19 | archive_only_non_pak_files_mods.insert(mod_spec); 20 | }), 21 | None::, 22 | )?; 23 | Ok(archive_only_non_pak_files_mods) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mod_lints/asset_register_bin.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct AssetRegisterBinLint; 9 | 10 | impl Lint for AssetRegisterBinLint { 11 | type Output = BTreeMap>; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut asset_register_bin_mods = BTreeMap::new(); 15 | 16 | lcx.for_each_mod_file(|mod_spec, _, _, raw_path, normalized_path| { 17 | if let Some(filename) = raw_path.file_name() 18 | && filename == "AssetRegistry.bin" 19 | { 20 | asset_register_bin_mods 21 | .entry(mod_spec.clone()) 22 | .and_modify(|paths: &mut BTreeSet| { 23 | paths.insert(normalized_path.clone()); 24 | }) 25 | .or_insert_with(|| [normalized_path.clone()].into()); 26 | } 27 | 28 | Ok(()) 29 | })?; 30 | 31 | Ok(asset_register_bin_mods) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/mod_lints/conflicting_mods.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use indexmap::IndexSet; 4 | 5 | use crate::providers::ModSpecification; 6 | 7 | use super::{Lint, LintCtxt, LintError}; 8 | 9 | #[derive(Default)] 10 | pub struct ConflictingModsLint; 11 | 12 | const CONFLICTING_MODS_LINT_WHITELIST: [&str; 1] = ["fsd/content/_interop"]; 13 | 14 | impl Lint for ConflictingModsLint { 15 | type Output = BTreeMap>; 16 | 17 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 18 | let mut per_path_modifiers = BTreeMap::new(); 19 | 20 | lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { 21 | per_path_modifiers 22 | .entry(normalized_path) 23 | .and_modify(|modifiers: &mut IndexSet| { 24 | modifiers.insert(mod_spec.clone()); 25 | }) 26 | .or_insert_with(|| [mod_spec.clone()].into()); 27 | Ok(()) 28 | })?; 29 | 30 | let conflicting_mods = per_path_modifiers 31 | .into_iter() 32 | .filter(|(p, _)| { 33 | for whitelisted_path in CONFLICTING_MODS_LINT_WHITELIST { 34 | if p.starts_with(whitelisted_path) { 35 | return false; 36 | } 37 | } 38 | true 39 | }) 40 | .filter(|(_, modifiers)| modifiers.len() > 1) 41 | .collect::>>(); 42 | 43 | Ok(conflicting_mods) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/mod_lints/empty_archive.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct EmptyArchiveLint; 9 | 10 | impl Lint for EmptyArchiveLint { 11 | type Output = BTreeSet; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut empty_archive_mods = BTreeSet::new(); 15 | 16 | lcx.for_each_mod( 17 | |_, _, _| Ok(()), 18 | Some(|mod_spec| { 19 | empty_archive_mods.insert(mod_spec); 20 | }), 21 | None::, 22 | None::, 23 | )?; 24 | 25 | Ok(empty_archive_mods) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/mod_lints/non_asset_files.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct NonAssetFilesLint; 9 | 10 | const ENDS_WITH_WHITE_LIST: [&str; 7] = [ 11 | ".uexp", 12 | ".uasset", 13 | ".ubulk", 14 | ".ufont", 15 | ".locres", 16 | ".ushaderbytecode", 17 | "assetregistry.bin", 18 | ]; 19 | 20 | impl Lint for NonAssetFilesLint { 21 | type Output = BTreeMap>; 22 | 23 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 24 | let mut non_asset_files = BTreeMap::new(); 25 | 26 | lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { 27 | let is_unreal_asset = ENDS_WITH_WHITE_LIST 28 | .iter() 29 | .any(|end| normalized_path.ends_with(end)); 30 | if !is_unreal_asset { 31 | non_asset_files 32 | .entry(mod_spec) 33 | .and_modify(|files: &mut BTreeSet| { 34 | files.insert(normalized_path.clone()); 35 | }) 36 | .or_insert_with(|| [normalized_path].into()); 37 | } 38 | Ok(()) 39 | })?; 40 | 41 | Ok(non_asset_files) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/mod_lints/outdated_pak_version.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct OutdatedPakVersionLint; 9 | 10 | impl Lint for OutdatedPakVersionLint { 11 | type Output = BTreeMap; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut outdated_pak_version_mods = BTreeMap::new(); 15 | 16 | lcx.for_each_mod( 17 | |mod_spec, _, pak_reader| { 18 | if pak_reader.version() < repak::Version::V11 { 19 | outdated_pak_version_mods.insert(mod_spec.clone(), pak_reader.version()); 20 | } 21 | Ok(()) 22 | }, 23 | None::, 24 | None::, 25 | None::, 26 | )?; 27 | 28 | Ok(outdated_pak_version_mods) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/mod_lints/shader_files.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use crate::providers::ModSpecification; 4 | 5 | use super::{Lint, LintCtxt, LintError}; 6 | 7 | #[derive(Default)] 8 | pub struct ShaderFilesLint; 9 | 10 | impl Lint for ShaderFilesLint { 11 | type Output = BTreeMap>; 12 | 13 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 14 | let mut shader_file_mods = BTreeMap::new(); 15 | 16 | lcx.for_each_mod_file(|mod_spec, _, _, raw_path, normalized_path| { 17 | if raw_path.extension().and_then(std::ffi::OsStr::to_str) == Some("ushaderbytecode") { 18 | shader_file_mods 19 | .entry(mod_spec) 20 | .and_modify(|paths: &mut BTreeSet| { 21 | paths.insert(normalized_path.clone()); 22 | }) 23 | .or_insert_with(|| [normalized_path].into()); 24 | } 25 | Ok(()) 26 | })?; 27 | 28 | Ok(shader_file_mods) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/mod_lints/split_asset_pairs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use tracing::trace; 4 | 5 | use crate::providers::ModSpecification; 6 | 7 | use super::{Lint, LintCtxt, LintError}; 8 | 9 | #[derive(Default)] 10 | pub struct SplitAssetPairsLint; 11 | 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 13 | pub enum SplitAssetPair { 14 | MissingUexp, 15 | MissingUasset, 16 | } 17 | 18 | impl Lint for SplitAssetPairsLint { 19 | type Output = BTreeMap>; 20 | 21 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 22 | let mut per_mod_path_without_final_ext_to_exts_map = BTreeMap::new(); 23 | 24 | lcx.for_each_mod_file(|mod_spec, _, _, _, normalized_path| { 25 | let mut iter = normalized_path.rsplit('.').take(2); 26 | let Some(final_ext) = iter.next() else { 27 | return Ok(()); 28 | }; 29 | let Some(path_without_final_ext) = iter.next() else { 30 | return Ok(()); 31 | }; 32 | 33 | per_mod_path_without_final_ext_to_exts_map 34 | .entry(mod_spec) 35 | .and_modify(|map: &mut BTreeMap>| { 36 | map.entry(path_without_final_ext.to_string()) 37 | .and_modify(|exts: &mut BTreeSet| { 38 | exts.insert(final_ext.to_string()); 39 | }) 40 | .or_insert_with(|| [final_ext.to_string()].into()); 41 | }) 42 | .or_insert_with(|| { 43 | [( 44 | path_without_final_ext.to_string(), 45 | [final_ext.to_string()].into(), 46 | )] 47 | .into() 48 | }); 49 | 50 | Ok(()) 51 | })?; 52 | 53 | let mut split_asset_pairs_mods = BTreeMap::new(); 54 | 55 | for (mod_spec, map) in per_mod_path_without_final_ext_to_exts_map { 56 | for (path_without_final_ext, final_exts) in map { 57 | split_asset_pairs_mods 58 | .entry(mod_spec.clone()) 59 | .and_modify(|map: &mut BTreeMap| { 60 | match (final_exts.contains("uexp"), final_exts.contains("uasset")) { 61 | (true, false) => { 62 | map.insert( 63 | format!("{path_without_final_ext}.uexp"), 64 | SplitAssetPair::MissingUasset, 65 | ); 66 | } 67 | (false, true) => { 68 | map.insert( 69 | format!("{path_without_final_ext}.uasset"), 70 | SplitAssetPair::MissingUexp, 71 | ); 72 | } 73 | _ => {} 74 | } 75 | }) 76 | .or_insert_with(|| { 77 | match (final_exts.contains("uexp"), final_exts.contains("uasset")) { 78 | (true, false) => [( 79 | format!("{path_without_final_ext}.uexp"), 80 | SplitAssetPair::MissingUasset, 81 | )] 82 | .into(), 83 | (false, true) => [( 84 | format!("{path_without_final_ext}.uasset"), 85 | SplitAssetPair::MissingUexp, 86 | )] 87 | .into(), 88 | _ => BTreeMap::default(), 89 | } 90 | }); 91 | } 92 | } 93 | 94 | split_asset_pairs_mods.retain(|_, map| !map.is_empty()); 95 | 96 | trace!("split_asset_pairs_mods:\n{:#?}", split_asset_pairs_mods); 97 | 98 | Ok(split_asset_pairs_mods) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/mod_lints/unmodified_game_assets.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::{BTreeMap, BTreeSet}; 3 | use std::io::BufReader; 4 | use std::path::PathBuf; 5 | 6 | use fs_err as fs; 7 | use path_slash::PathExt; 8 | use rayon::prelude::*; 9 | use sha2::Digest; 10 | use tracing::trace; 11 | 12 | use crate::providers::ModSpecification; 13 | 14 | use super::{InvalidGamePathSnafu, Lint, LintCtxt, LintError}; 15 | 16 | #[derive(Default)] 17 | pub struct UnmodifiedGameAssetsLint; 18 | 19 | impl Lint for UnmodifiedGameAssetsLint { 20 | type Output = BTreeMap>; 21 | 22 | fn check_mods(&mut self, lcx: &LintCtxt) -> Result { 23 | let Some(game_pak_path) = &lcx.fsd_pak_path else { 24 | InvalidGamePathSnafu.fail()? 25 | }; 26 | 27 | // Adapted from 28 | // . 29 | let mut reader = BufReader::new(fs::File::open(game_pak_path)?); 30 | let pak = repak::PakBuilder::new().reader(&mut reader)?; 31 | 32 | let mount_point = PathBuf::from(pak.mount_point()); 33 | 34 | let full_paths = pak 35 | .files() 36 | .into_iter() 37 | .map(|f| (mount_point.join(&f), f)) 38 | .collect::>(); 39 | let stripped = full_paths 40 | .iter() 41 | .map(|(full_path, _path)| full_path.strip_prefix("../../../")) 42 | .collect::, _>>()?; 43 | 44 | let game_file_hashes: std::sync::Arc< 45 | std::sync::Mutex, Vec>>, 46 | > = Default::default(); 47 | 48 | full_paths.par_iter().zip(stripped).try_for_each_init( 49 | || (game_file_hashes.clone(), fs::File::open(game_pak_path)), 50 | |(hashes, file), ((_full_path, path), stripped)| -> Result<(), repak::Error> { 51 | let mut hasher = sha2::Sha256::new(); 52 | pak.read_file( 53 | path, 54 | &mut BufReader::new(file.as_ref().unwrap()), 55 | &mut hasher, 56 | )?; 57 | let hash = hasher.finalize(); 58 | hashes 59 | .lock() 60 | .unwrap() 61 | .insert(stripped.to_slash_lossy(), hash.to_vec()); 62 | Ok(()) 63 | }, 64 | )?; 65 | 66 | let mut unmodified_game_assets = BTreeMap::new(); 67 | 68 | lcx.for_each_mod_file( 69 | |mod_spec, mut pak_read_seek, pak_reader, _, normalized_path| { 70 | if let Some(reference_hash) = game_file_hashes 71 | .lock() 72 | .unwrap() 73 | .get(&Cow::Owned(normalized_path.clone())) 74 | { 75 | let mut hasher = sha2::Sha256::new(); 76 | pak_reader.read_file(&normalized_path, &mut pak_read_seek, &mut hasher)?; 77 | let mod_file_hash = hasher.finalize().to_vec(); 78 | 79 | if &mod_file_hash == reference_hash { 80 | unmodified_game_assets 81 | .entry(mod_spec) 82 | .and_modify(|paths: &mut BTreeSet| { 83 | paths.insert(normalized_path.clone()); 84 | }) 85 | .or_insert_with(|| [normalized_path].into()); 86 | } 87 | } 88 | 89 | Ok(()) 90 | }, 91 | )?; 92 | 93 | trace!("unmodified_game_assets:\n{:#?}", unmodified_game_assets); 94 | 95 | Ok(unmodified_game_assets) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/providers/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::{Deref, DerefMut}; 3 | use std::path::{Path, PathBuf}; 4 | use std::sync::{Arc, RwLock}; 5 | 6 | use fs_err as fs; 7 | use serde::{Deserialize, Serialize}; 8 | use snafu::prelude::*; 9 | 10 | use crate::state::config::ConfigWrapper; 11 | 12 | pub type ProviderCache = Arc>>; 13 | 14 | #[typetag::serde(tag = "type")] 15 | pub trait ModProviderCache: Sync + Send + std::fmt::Debug { 16 | fn new() -> Self 17 | where 18 | Self: Sized; 19 | fn as_any(&self) -> &dyn std::any::Any; 20 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any; 21 | } 22 | 23 | #[obake::versioned] 24 | #[obake(version("0.0.0"))] 25 | #[derive(Debug, Default, Serialize, Deserialize)] 26 | pub struct Cache { 27 | pub(super) cache: HashMap>, 28 | } 29 | 30 | impl Cache { 31 | pub(super) fn has(&self, id: &str) -> bool { 32 | self.cache 33 | .get(id) 34 | .and_then(|c| c.as_any().downcast_ref::()) 35 | .is_none() 36 | } 37 | 38 | pub(super) fn get(&self, id: &str) -> Option<&T> { 39 | self.cache 40 | .get(id) 41 | .and_then(|c| c.as_any().downcast_ref::()) 42 | } 43 | 44 | pub(super) fn get_mut(&mut self, id: &str) -> &mut T { 45 | if self.has::(id) { 46 | self.cache.insert(id.to_owned(), Box::new(T::new())); 47 | } 48 | self.cache 49 | .get_mut(id) 50 | .and_then(|c| c.as_any_mut().downcast_mut::()) 51 | .unwrap() 52 | } 53 | } 54 | 55 | #[derive(Debug, Serialize, Deserialize)] 56 | #[serde(tag = "version")] 57 | pub enum VersionAnnotatedCache { 58 | #[serde(rename = "0.0.0")] 59 | V0_0_0(Cache!["0.0.0"]), 60 | } 61 | 62 | impl Default for VersionAnnotatedCache { 63 | fn default() -> Self { 64 | VersionAnnotatedCache::V0_0_0(Default::default()) 65 | } 66 | } 67 | 68 | impl Deref for VersionAnnotatedCache { 69 | type Target = Cache!["0.0.0"]; 70 | 71 | fn deref(&self) -> &Self::Target { 72 | match self { 73 | VersionAnnotatedCache::V0_0_0(c) => c, 74 | } 75 | } 76 | } 77 | 78 | impl DerefMut for VersionAnnotatedCache { 79 | fn deref_mut(&mut self) -> &mut Self::Target { 80 | match self { 81 | VersionAnnotatedCache::V0_0_0(c) => c, 82 | } 83 | } 84 | } 85 | 86 | #[derive(Debug, Serialize, Deserialize)] 87 | #[serde(untagged)] 88 | pub enum MaybeVersionedCache { 89 | Versioned(VersionAnnotatedCache), 90 | Legacy(Cache!["0.0.0"]), 91 | } 92 | 93 | impl Default for MaybeVersionedCache { 94 | fn default() -> Self { 95 | MaybeVersionedCache::Versioned(Default::default()) 96 | } 97 | } 98 | 99 | #[derive(Debug, Snafu)] 100 | pub enum CacheError { 101 | #[snafu(display("failed to read cache.json with provided path {}", search_path.display()))] 102 | CacheJsonReadFailed { 103 | source: std::io::Error, 104 | search_path: PathBuf, 105 | }, 106 | #[snafu(display("failed to deserialize cache.json into dynamic JSON value: {reason}"))] 107 | DeserializeJsonFailed { 108 | #[snafu(source(false))] 109 | source: Option, 110 | reason: &'static str, 111 | }, 112 | #[snafu(display("failed attempt to deserialize as legacy cache format"))] 113 | DeserializeLegacyCacheFailed { source: serde_json::Error }, 114 | #[snafu(display("failed to deserialize as cache {version} format"))] 115 | DeserializeVersionedCacheFailed { 116 | source: serde_json::Error, 117 | version: &'static str, 118 | }, 119 | } 120 | 121 | pub(crate) fn read_cache_metadata_or_default( 122 | cache_metadata_path: &PathBuf, 123 | ) -> Result { 124 | let cache: MaybeVersionedCache = match fs::read(cache_metadata_path) { 125 | Ok(buf) => { 126 | let mut dyn_value = match serde_json::from_slice::(&buf) { 127 | Ok(dyn_value) => dyn_value, 128 | Err(e) => { 129 | return Err(CacheError::DeserializeJsonFailed { 130 | source: Some(e), 131 | reason: "malformed JSON", 132 | }) 133 | } 134 | }; 135 | let Some(obj_map) = dyn_value.as_object_mut() else { 136 | return Err(CacheError::DeserializeJsonFailed { 137 | source: None, 138 | reason: "failed to deserialize into object map", 139 | }); 140 | }; 141 | let version = obj_map.remove("version"); 142 | if let Some(v) = version 143 | && let serde_json::Value::String(vs) = v 144 | { 145 | match vs.as_str() { 146 | "0.0.0" => { 147 | // HACK: workaround a serde issue relating to flattening with tags 148 | // involving numeric keys in hashmaps, see 149 | // . 150 | match serde_json::from_slice::(&buf) { 151 | Ok(c) => { 152 | MaybeVersionedCache::Versioned(VersionAnnotatedCache::V0_0_0(c)) 153 | } 154 | Err(e) => Err(e).context(DeserializeVersionedCacheFailedSnafu { 155 | version: "v0.0.0", 156 | })?, 157 | } 158 | } 159 | _ => unimplemented!(), 160 | } 161 | } else { 162 | // HACK: workaround a serde issue relating to flattening with tags involving 163 | // numeric keys in hashmaps, see . 164 | match serde_json::from_slice::>>(&buf) { 165 | Ok(c) => MaybeVersionedCache::Legacy(Cache_v0_0_0 { cache: c }), 166 | Err(e) => Err(e).context(DeserializeLegacyCacheFailedSnafu)?, 167 | } 168 | } 169 | } 170 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => MaybeVersionedCache::default(), 171 | Err(e) => Err(e).context(CacheJsonReadFailedSnafu { 172 | search_path: cache_metadata_path.to_owned(), 173 | })?, 174 | }; 175 | 176 | let cache: VersionAnnotatedCache = match cache { 177 | MaybeVersionedCache::Versioned(v) => match v { 178 | VersionAnnotatedCache::V0_0_0(v) => VersionAnnotatedCache::V0_0_0(v), 179 | }, 180 | MaybeVersionedCache::Legacy(legacy) => VersionAnnotatedCache::V0_0_0(legacy), 181 | }; 182 | 183 | Ok(cache) 184 | } 185 | 186 | #[derive(Debug, Serialize, Deserialize)] 187 | pub struct BlobRef(String); 188 | 189 | #[derive(Debug, Snafu)] 190 | #[snafu(display("blob cache {kind} failed"))] 191 | pub struct BlobCacheError { 192 | source: std::io::Error, 193 | kind: &'static str, 194 | } 195 | 196 | #[derive(Debug, Clone)] 197 | pub struct BlobCache { 198 | path: PathBuf, 199 | } 200 | 201 | impl BlobCache { 202 | pub(super) fn new>(path: P) -> Self { 203 | fs::create_dir(&path).ok(); 204 | Self { 205 | path: path.as_ref().to_path_buf(), 206 | } 207 | } 208 | 209 | pub(super) fn write(&self, blob: &[u8]) -> Result { 210 | use sha2::{Digest, Sha256}; 211 | 212 | let mut hasher = Sha256::new(); 213 | hasher.update(blob); 214 | let hash = hex::encode(hasher.finalize()); 215 | 216 | let tmp = self.path.join(format!(".{hash}")); 217 | fs::write(&tmp, blob).context(BlobCacheSnafu { kind: "write" })?; 218 | fs::rename(tmp, self.path.join(&hash)).context(BlobCacheSnafu { kind: "rename" })?; 219 | 220 | Ok(BlobRef(hash)) 221 | } 222 | 223 | pub(super) fn get_path(&self, blob: &BlobRef) -> Option { 224 | let path = self.path.join(&blob.0); 225 | path.exists().then_some(path) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/providers/file.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::{Path, PathBuf}; 3 | use std::sync::Arc; 4 | 5 | use tokio::sync::mpsc::Sender; 6 | 7 | use super::{ 8 | BlobCache, FetchProgress, ModInfo, ModProvider, ModResolution, ModResponse, ModSpecification, 9 | ProviderCache, ProviderError, 10 | }; 11 | 12 | inventory::submit! { 13 | super::ProviderFactory { 14 | id: FILE_PROVIDER_ID, 15 | new: FileProvider::new_provider, 16 | can_provide: |url| Path::new(url).exists(), 17 | parameters: &[], 18 | } 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct FileProvider {} 23 | 24 | impl FileProvider { 25 | pub fn new_provider( 26 | _parameters: &HashMap, 27 | ) -> Result, ProviderError> { 28 | Ok(Arc::new(Self::new())) 29 | } 30 | 31 | pub fn new() -> Self { 32 | Self {} 33 | } 34 | } 35 | 36 | const FILE_PROVIDER_ID: &str = "file"; 37 | 38 | #[async_trait::async_trait] 39 | impl ModProvider for FileProvider { 40 | async fn resolve_mod( 41 | &self, 42 | spec: &ModSpecification, 43 | _update: bool, 44 | _cache: ProviderCache, 45 | ) -> Result { 46 | let path = Path::new(&spec.url); 47 | let name = path 48 | .file_name() 49 | .map(|s| s.to_string_lossy().to_string()) 50 | .unwrap_or_else(|| spec.url.to_string()); 51 | Ok(ModResponse::Resolve(ModInfo { 52 | provider: FILE_PROVIDER_ID, 53 | name, 54 | spec: spec.clone(), 55 | versions: vec![], 56 | resolution: ModResolution::unresolvable( 57 | spec.url.clone().into(), 58 | path.file_name() 59 | .map(|p| p.to_string_lossy().to_string()) 60 | .unwrap_or_else(|| "unknown".to_string()), 61 | ), 62 | suggested_require: false, 63 | suggested_dependencies: vec![], 64 | modio_tags: None, 65 | modio_id: None, 66 | })) 67 | } 68 | 69 | async fn fetch_mod( 70 | &self, 71 | res: &ModResolution, 72 | _update: bool, 73 | _cache: ProviderCache, 74 | _blob_cache: &BlobCache, 75 | tx: Option>, 76 | ) -> Result { 77 | if let Some(tx) = tx { 78 | tx.send(FetchProgress::Complete { 79 | resolution: res.clone(), 80 | }) 81 | .await 82 | .unwrap(); 83 | } 84 | Ok(PathBuf::from(&res.url.0)) 85 | } 86 | 87 | async fn update_cache(&self, _cache: ProviderCache) -> Result<(), ProviderError> { 88 | Ok(()) 89 | } 90 | 91 | async fn check(&self) -> Result<(), ProviderError> { 92 | Ok(()) 93 | } 94 | 95 | fn get_mod_info(&self, spec: &ModSpecification, _cache: ProviderCache) -> Option { 96 | let path = Path::new(&spec.url); 97 | let name = path 98 | .file_name() 99 | .map(|s| s.to_string_lossy().to_string()) 100 | .unwrap_or_else(|| spec.url.to_string()); 101 | Some(ModInfo { 102 | provider: FILE_PROVIDER_ID, 103 | name, 104 | spec: spec.clone(), 105 | versions: vec![], 106 | resolution: ModResolution::unresolvable( 107 | spec.url.clone().into(), 108 | path.file_name() 109 | .map(|p| p.to_string_lossy().to_string()) 110 | .unwrap_or_else(|| "unknown".to_string()), 111 | ), 112 | suggested_require: false, 113 | suggested_dependencies: vec![], 114 | modio_tags: None, 115 | modio_id: None, 116 | }) 117 | } 118 | 119 | fn is_pinned(&self, _spec: &ModSpecification, _cache: ProviderCache) -> bool { 120 | true 121 | } 122 | 123 | fn get_version_name(&self, _spec: &ModSpecification, _cache: ProviderCache) -> Option { 124 | Some("latest".to_string()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/providers/http.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use tracing::info; 5 | 6 | use crate::providers::*; 7 | 8 | inventory::submit! { 9 | super::ProviderFactory { 10 | id: "http", 11 | new: HttpProvider::new_provider, 12 | can_provide: |url| -> bool { 13 | re_mod() 14 | .captures(url) 15 | .and_then(|c| c.name("hostname")) 16 | .is_some_and(|h| !["mod.io", "drg.mod.io", "drg.old.mod.io"].contains(&h.as_str())) 17 | }, 18 | parameters: &[], 19 | } 20 | } 21 | 22 | #[derive(Debug, Default, Serialize, Deserialize)] 23 | pub struct HttpProviderCache { 24 | url_blobs: HashMap, 25 | } 26 | 27 | #[typetag::serde] 28 | impl ModProviderCache for HttpProviderCache { 29 | fn new() -> Self { 30 | Default::default() 31 | } 32 | 33 | fn as_any(&self) -> &dyn std::any::Any { 34 | self 35 | } 36 | 37 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { 38 | self 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct HttpProvider { 44 | client: reqwest::Client, 45 | } 46 | 47 | impl HttpProvider { 48 | pub fn new_provider( 49 | _parameters: &HashMap, 50 | ) -> Result, ProviderError> { 51 | Ok(Arc::new(Self::new())) 52 | } 53 | 54 | pub fn new() -> Self { 55 | Self { 56 | client: reqwest::Client::new(), 57 | } 58 | } 59 | } 60 | 61 | static RE_MOD: OnceLock = OnceLock::new(); 62 | fn re_mod() -> &'static regex::Regex { 63 | RE_MOD.get_or_init(|| regex::Regex::new(r"^https?://(?P[^/]+)(/|$)").unwrap()) 64 | } 65 | 66 | const HTTP_PROVIDER_ID: &str = "http"; 67 | 68 | #[async_trait::async_trait] 69 | impl ModProvider for HttpProvider { 70 | async fn resolve_mod( 71 | &self, 72 | spec: &ModSpecification, 73 | _update: bool, 74 | _cache: ProviderCache, 75 | ) -> Result { 76 | let Ok(url) = url::Url::parse(&spec.url) else { 77 | return Err(ProviderError::InvalidUrl { 78 | url: spec.url.to_string(), 79 | }); 80 | }; 81 | 82 | let name = url 83 | .path_segments() 84 | .and_then(|mut s| s.next_back()) 85 | .map(|s| s.to_string()) 86 | .unwrap_or_else(|| url.to_string()); 87 | 88 | Ok(ModResponse::Resolve(ModInfo { 89 | provider: HTTP_PROVIDER_ID, 90 | name, 91 | spec: spec.clone(), 92 | versions: vec![], 93 | resolution: ModResolution::resolvable(spec.url.as_str().into()), 94 | suggested_require: false, 95 | suggested_dependencies: vec![], 96 | modio_tags: None, 97 | modio_id: None, 98 | })) 99 | } 100 | 101 | async fn fetch_mod( 102 | &self, 103 | res: &ModResolution, 104 | update: bool, 105 | cache: ProviderCache, 106 | blob_cache: &BlobCache, 107 | tx: Option>, 108 | ) -> Result { 109 | let url = &res.url; 110 | Ok( 111 | if let Some(path) = if update { 112 | None 113 | } else { 114 | cache 115 | .read() 116 | .unwrap() 117 | .get::(HTTP_PROVIDER_ID) 118 | .and_then(|c| c.url_blobs.get(&url.0)) 119 | .and_then(|r| blob_cache.get_path(r)) 120 | } { 121 | if let Some(tx) = tx { 122 | tx.send(FetchProgress::Complete { 123 | resolution: res.clone(), 124 | }) 125 | .await 126 | .unwrap(); 127 | } 128 | path 129 | } else { 130 | info!("downloading mod {url:?}..."); 131 | let response = self 132 | .client 133 | .get(&url.0) 134 | .send() 135 | .await 136 | .context(RequestFailedSnafu { 137 | url: url.0.to_string(), 138 | })? 139 | .error_for_status() 140 | .context(ResponseSnafu { 141 | url: url.0.to_string(), 142 | })?; 143 | let size = response.content_length(); // TODO will be incorrect if compressed 144 | if let Some(mime) = response 145 | .headers() 146 | .get(reqwest::header::HeaderName::from_static("content-type")) 147 | { 148 | let content_type = mime.to_str().context(InvalidMimeSnafu { 149 | url: url.0.to_string(), 150 | })?; 151 | ensure!( 152 | ["application/zip", "application/octet-stream"].contains(&content_type), 153 | UnexpectedContentTypeSnafu { 154 | found_content_type: content_type.to_string(), 155 | url: url.0.to_string(), 156 | } 157 | ); 158 | } 159 | 160 | use futures::stream::TryStreamExt; 161 | use tokio::io::AsyncWriteExt; 162 | 163 | let mut cursor = std::io::Cursor::new(vec![]); 164 | let mut stream = response.bytes_stream(); 165 | while let Some(bytes) = stream.try_next().await.with_context(|_| FetchSnafu { 166 | url: url.0.to_string(), 167 | })? { 168 | cursor 169 | .write_all(&bytes) 170 | .await 171 | .with_context(|_| BufferIoSnafu { 172 | url: url.0.to_string(), 173 | })?; 174 | if let Some(size) = size 175 | && let Some(tx) = &tx 176 | { 177 | tx.send(FetchProgress::Progress { 178 | resolution: res.clone(), 179 | progress: cursor.get_ref().len() as u64, 180 | size, 181 | }) 182 | .await 183 | .unwrap(); 184 | } 185 | } 186 | 187 | let blob = blob_cache.write(&cursor.into_inner())?; 188 | let path = blob_cache.get_path(&blob).unwrap(); 189 | cache 190 | .write() 191 | .unwrap() 192 | .get_mut::(HTTP_PROVIDER_ID) 193 | .url_blobs 194 | .insert(url.0.to_owned(), blob); 195 | 196 | if let Some(tx) = tx { 197 | tx.send(FetchProgress::Complete { 198 | resolution: res.clone(), 199 | }) 200 | .await 201 | .unwrap(); 202 | } 203 | path 204 | }, 205 | ) 206 | } 207 | 208 | async fn update_cache(&self, _cache: ProviderCache) -> Result<(), ProviderError> { 209 | Ok(()) 210 | } 211 | 212 | async fn check(&self) -> Result<(), ProviderError> { 213 | Ok(()) 214 | } 215 | 216 | fn get_mod_info(&self, spec: &ModSpecification, _cache: ProviderCache) -> Option { 217 | let url = url::Url::parse(&spec.url).ok()?; 218 | let name = url 219 | .path_segments() 220 | .and_then(|mut s| s.next_back()) 221 | .map(|s| s.to_string()) 222 | .unwrap_or_else(|| url.to_string()); 223 | Some(ModInfo { 224 | provider: HTTP_PROVIDER_ID, 225 | name, 226 | spec: spec.clone(), 227 | versions: vec![], 228 | resolution: ModResolution::resolvable(spec.url.as_str().into()), 229 | suggested_require: false, 230 | suggested_dependencies: vec![], 231 | modio_tags: None, 232 | modio_id: None, 233 | }) 234 | } 235 | 236 | fn is_pinned(&self, _spec: &ModSpecification, _cache: ProviderCache) -> bool { 237 | true 238 | } 239 | 240 | fn get_version_name(&self, _spec: &ModSpecification, _cache: ProviderCache) -> Option { 241 | Some("latest".to_string()) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/providers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod http; 3 | pub mod modio; 4 | #[macro_use] 5 | pub mod cache; 6 | pub mod mod_store; 7 | 8 | use snafu::prelude::*; 9 | use tokio::sync::mpsc::Sender; 10 | 11 | use std::collections::HashMap; 12 | use std::io::{Read, Seek}; 13 | use std::path::PathBuf; 14 | use std::sync::{Arc, RwLock}; 15 | 16 | pub use cache::*; 17 | pub use mint_lib::mod_info::*; 18 | pub use mod_store::*; 19 | 20 | use self::modio::DrgModioError; 21 | 22 | type Providers = RwLock>>; 23 | 24 | pub trait ReadSeek: Read + Seek + Send {} 25 | impl ReadSeek for T {} 26 | 27 | #[derive(Debug)] 28 | pub enum FetchProgress { 29 | Progress { 30 | resolution: ModResolution, 31 | progress: u64, 32 | size: u64, 33 | }, 34 | Complete { 35 | resolution: ModResolution, 36 | }, 37 | } 38 | 39 | impl FetchProgress { 40 | pub fn resolution(&self) -> &ModResolution { 41 | match self { 42 | FetchProgress::Progress { resolution, .. } => resolution, 43 | FetchProgress::Complete { resolution, .. } => resolution, 44 | } 45 | } 46 | } 47 | 48 | #[async_trait::async_trait] 49 | pub trait ModProvider: Send + Sync { 50 | async fn resolve_mod( 51 | &self, 52 | spec: &ModSpecification, 53 | update: bool, 54 | cache: ProviderCache, 55 | ) -> Result; 56 | async fn fetch_mod( 57 | &self, 58 | url: &ModResolution, 59 | update: bool, 60 | cache: ProviderCache, 61 | blob_cache: &BlobCache, 62 | tx: Option>, 63 | ) -> Result; 64 | async fn update_cache(&self, cache: ProviderCache) -> Result<(), ProviderError>; 65 | /// Check if provider is configured correctly 66 | async fn check(&self) -> Result<(), ProviderError>; 67 | fn get_mod_info(&self, spec: &ModSpecification, cache: ProviderCache) -> Option; 68 | fn is_pinned(&self, spec: &ModSpecification, cache: ProviderCache) -> bool; 69 | fn get_version_name(&self, spec: &ModSpecification, cache: ProviderCache) -> Option; 70 | } 71 | 72 | #[derive(Debug, Snafu)] 73 | pub enum ProviderError { 74 | #[snafu(display("failed to initialize provider {id} with parameters {parameters:?}"))] 75 | InitProviderFailed { 76 | id: &'static str, 77 | parameters: HashMap, 78 | }, 79 | #[snafu(transparent)] 80 | CacheError { source: CacheError }, 81 | #[snafu(transparent)] 82 | DrgModioError { source: DrgModioError }, 83 | #[snafu(display("mod.io-related error encountered while working on mod {mod_id}: {source}"))] 84 | ModCtxtModioError { source: ::modio::Error, mod_id: u32 }, 85 | #[snafu(display("I/O error encountered while working on mod {mod_id}: {source}"))] 86 | ModCtxtIoError { source: std::io::Error, mod_id: u32 }, 87 | #[snafu(transparent)] 88 | BlobCacheError { source: BlobCacheError }, 89 | #[snafu(display("could not find mod provider for {url}"))] 90 | ProviderNotFound { url: String }, 91 | NoProvider { 92 | url: String, 93 | factory: &'static ProviderFactory, 94 | }, 95 | #[snafu(display("invalid url <{url}>"))] 96 | InvalidUrl { url: String }, 97 | #[snafu(display("request for <{url}> failed: {source}"))] 98 | RequestFailed { source: reqwest::Error, url: String }, 99 | #[snafu(display("response from <{url}> failed: {source}"))] 100 | ResponseError { source: reqwest::Error, url: String }, 101 | #[snafu(display("mime from <{url}> contains non-ascii characters"))] 102 | InvalidMime { 103 | source: reqwest::header::ToStrError, 104 | url: String, 105 | }, 106 | #[snafu(display("unexpected content type from <{url}>: {found_content_type}"))] 107 | UnexpectedContentType { 108 | found_content_type: String, 109 | url: String, 110 | }, 111 | #[snafu(display("error while fetching mod <{url}>"))] 112 | FetchError { source: reqwest::Error, url: String }, 113 | #[snafu(display("error processing <{url}> while writing to local buffer"))] 114 | BufferIoError { source: std::io::Error, url: String }, 115 | #[snafu(display("preview mod links cannot be added directly, please subscribe to the mod on mod.io and and then use the non-preview link"))] 116 | PreviewLink { url: String }, 117 | #[snafu(display("mod <{url}> does not have an associated modfile"))] 118 | NoAssociatedModfile { url: String }, 119 | #[snafu(display("multiple mods returned for name \"{name_id}\""))] 120 | AmbiguousModNameId { name_id: String }, 121 | #[snafu(display("no mods returned for name \"{name_id}\""))] 122 | NoModsForNameId { name_id: String }, 123 | } 124 | 125 | impl ProviderError { 126 | pub fn opt_mod_id(&self) -> Option { 127 | match self { 128 | ProviderError::DrgModioError { source } => source.opt_mod_id(), 129 | ProviderError::ModCtxtModioError { mod_id, .. } 130 | | ProviderError::ModCtxtIoError { mod_id, .. } => Some(*mod_id), 131 | _ => None, 132 | } 133 | } 134 | } 135 | 136 | #[derive(Clone)] 137 | pub struct ProviderFactory { 138 | pub id: &'static str, 139 | #[allow(clippy::type_complexity)] 140 | new: fn(&HashMap) -> Result, ProviderError>, 141 | can_provide: fn(&str) -> bool, 142 | pub parameters: &'static [ProviderParameter<'static>], 143 | } 144 | 145 | impl std::fmt::Debug for ProviderFactory { 146 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 147 | f.debug_struct("ProviderFactory") 148 | .field("id", &self.id) 149 | .field("parameters", &self.parameters) 150 | .finish() 151 | } 152 | } 153 | 154 | #[derive(Debug, Clone)] 155 | pub struct ProviderParameter<'a> { 156 | pub id: &'a str, 157 | pub name: &'a str, 158 | pub description: &'a str, 159 | pub link: Option<&'a str>, 160 | } 161 | 162 | inventory::collect!(ProviderFactory); 163 | -------------------------------------------------------------------------------- /src/providers/mod_store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::path::Path; 3 | 4 | use snafu::prelude::*; 5 | use tracing::*; 6 | 7 | use crate::providers::*; 8 | use crate::state::config::ConfigWrapper; 9 | 10 | pub struct ModStore { 11 | providers: Providers, 12 | cache: ProviderCache, 13 | blob_cache: BlobCache, 14 | } 15 | 16 | impl ModStore { 17 | pub fn new>( 18 | cache_path: P, 19 | parameters: &HashMap>, 20 | ) -> Result { 21 | let mut providers = HashMap::new(); 22 | for prov in Self::get_provider_factories() { 23 | let params = parameters.get(prov.id).cloned().unwrap_or_default(); 24 | if prov.parameters.iter().all(|p| params.contains_key(p.id)) { 25 | let Ok(provider) = (prov.new)(¶ms) else { 26 | return Err(ProviderError::InitProviderFailed { 27 | id: prov.id, 28 | parameters: params.to_owned(), 29 | }); 30 | }; 31 | providers.insert(prov.id, provider); 32 | } 33 | } 34 | 35 | let cache_metadata_path = cache_path.as_ref().join("cache.json"); 36 | 37 | let cache = read_cache_metadata_or_default(&cache_metadata_path)?; 38 | let cache = ConfigWrapper::new(&cache_metadata_path, cache); 39 | cache.save().unwrap(); 40 | 41 | Ok(Self { 42 | providers: RwLock::new(providers), 43 | cache: Arc::new(RwLock::new(cache)), 44 | blob_cache: BlobCache::new(cache_path.as_ref().join("blobs")), 45 | }) 46 | } 47 | 48 | pub fn get_provider_factories() -> impl Iterator { 49 | inventory::iter::() 50 | } 51 | 52 | pub fn add_provider( 53 | &self, 54 | provider_factory: &ProviderFactory, 55 | parameters: &HashMap, 56 | ) -> Result<(), ProviderError> { 57 | let provider = (provider_factory.new)(parameters)?; 58 | self.providers 59 | .write() 60 | .unwrap() 61 | .insert(provider_factory.id, provider); 62 | Ok(()) 63 | } 64 | 65 | pub async fn add_provider_checked( 66 | &self, 67 | provider_factory: &ProviderFactory, 68 | parameters: &HashMap, 69 | ) -> Result<(), ProviderError> { 70 | let provider = (provider_factory.new)(parameters)?; 71 | provider.check().await?; 72 | self.providers 73 | .write() 74 | .unwrap() 75 | .insert(provider_factory.id, provider); 76 | Ok(()) 77 | } 78 | 79 | pub fn get_provider(&self, url: &str) -> Result, ProviderError> { 80 | let factory = Self::get_provider_factories() 81 | .find(|f| (f.can_provide)(url)) 82 | .context(ProviderNotFoundSnafu { 83 | url: url.to_string(), 84 | })?; 85 | let lock = self.providers.read().unwrap(); 86 | Ok(match lock.get(factory.id) { 87 | Some(e) => e.clone(), 88 | None => NoProviderSnafu { 89 | url: url.to_string(), 90 | factory, 91 | } 92 | .fail()?, 93 | }) 94 | } 95 | 96 | pub async fn resolve_mods( 97 | &self, 98 | mods: &[ModSpecification], 99 | update: bool, 100 | ) -> Result, ProviderError> { 101 | use futures::stream::{self, StreamExt, TryStreamExt}; 102 | 103 | let mut to_resolve = mods.iter().cloned().collect::>(); 104 | let mut mods_map = HashMap::new(); 105 | 106 | // used to deduplicate dependencies from mods already present in the mod list 107 | let mut precise_mod_specs = HashSet::new(); 108 | 109 | while !to_resolve.is_empty() { 110 | for (u, m) in stream::iter( 111 | to_resolve 112 | .iter() 113 | .map(|u| self.resolve_mod(u.to_owned(), update)), 114 | ) 115 | .boxed() 116 | .buffer_unordered(5) 117 | .try_collect::>() 118 | .await? 119 | { 120 | precise_mod_specs.insert(m.spec.clone()); 121 | mods_map.insert(u, m); 122 | to_resolve.clear(); 123 | for m in mods_map.values() { 124 | for d in &m.suggested_dependencies { 125 | if !precise_mod_specs.contains(d) { 126 | to_resolve.insert(d.clone()); 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | Ok(mods_map) 134 | } 135 | 136 | pub async fn resolve_mod( 137 | &self, 138 | original_spec: ModSpecification, 139 | update: bool, 140 | ) -> Result<(ModSpecification, ModInfo), ProviderError> { 141 | let mut spec = original_spec.clone(); 142 | loop { 143 | match self 144 | .get_provider(&spec.url)? 145 | .resolve_mod(&spec, update, self.cache.clone()) 146 | .await? 147 | { 148 | ModResponse::Resolve(m) => { 149 | return Ok((original_spec, m)); 150 | } 151 | ModResponse::Redirect(redirected_spec) => spec = redirected_spec, 152 | }; 153 | } 154 | } 155 | 156 | pub async fn fetch_mods( 157 | &self, 158 | mods: &[&ModResolution], 159 | update: bool, 160 | tx: Option>, 161 | ) -> Result, ProviderError> { 162 | use futures::stream::{self, StreamExt, TryStreamExt}; 163 | 164 | stream::iter( 165 | mods.iter() 166 | .map(|res| self.fetch_mod(res, update, tx.clone())), 167 | ) 168 | .boxed() // without this the future becomes !Send https://github.com/rust-lang/rust/issues/104382 169 | .buffer_unordered(5) 170 | .try_collect::>() 171 | .await 172 | } 173 | 174 | pub async fn fetch_mods_ordered( 175 | &self, 176 | mods: &[&ModResolution], 177 | update: bool, 178 | tx: Option>, 179 | ) -> Result, ProviderError> { 180 | use futures::stream::{self, StreamExt, TryStreamExt}; 181 | 182 | stream::iter( 183 | mods.iter() 184 | .map(|res| self.fetch_mod(res, update, tx.clone())), 185 | ) 186 | .boxed() // without this the future becomes !Send https://github.com/rust-lang/rust/issues/104382 187 | .buffered(5) 188 | .try_collect::>() 189 | .await 190 | } 191 | 192 | pub async fn fetch_mod( 193 | &self, 194 | res: &ModResolution, 195 | update: bool, 196 | tx: Option>, 197 | ) -> Result { 198 | self.get_provider(&res.url.0)? 199 | .fetch_mod( 200 | res, 201 | update, 202 | self.cache.clone(), 203 | &self.blob_cache.clone(), 204 | tx, 205 | ) 206 | .await 207 | } 208 | 209 | pub async fn update_cache(&self) -> Result<(), ProviderError> { 210 | let providers = self.providers.read().unwrap().clone(); 211 | for (name, provider) in providers.iter() { 212 | info!("updating cache for {name} provider"); 213 | provider.update_cache(self.cache.clone()).await?; 214 | } 215 | Ok(()) 216 | } 217 | 218 | pub fn get_mod_info(&self, spec: &ModSpecification) -> Option { 219 | self.get_provider(&spec.url) 220 | .ok()? 221 | .get_mod_info(spec, self.cache.clone()) 222 | } 223 | 224 | pub fn is_pinned(&self, spec: &ModSpecification) -> bool { 225 | self.get_provider(&spec.url) 226 | .unwrap() 227 | .is_pinned(spec, self.cache.clone()) 228 | } 229 | 230 | pub fn get_version_name(&self, spec: &ModSpecification) -> Option { 231 | self.get_provider(&spec.url) 232 | .unwrap() 233 | .get_version_name(spec, self.cache.clone()) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/state/config.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::path::Path; 3 | 4 | use serde::de::DeserializeOwned; 5 | 6 | use super::*; 7 | 8 | pub trait ConfigTrait: std::fmt::Debug + Default + Serialize + DeserializeOwned {} 9 | impl ConfigTrait for T where T: std::fmt::Debug + Default + Serialize + DeserializeOwned {} 10 | 11 | /// Wrapper around an object that is written to a file when dropped 12 | #[derive(Debug)] 13 | pub struct ConfigWrapper { 14 | path: Option, 15 | config: C, 16 | } 17 | 18 | impl ConfigWrapper { 19 | /// Creates a new file backed config object that is automatically saved when dropped 20 | pub fn new>(path: P, config: C) -> Self { 21 | Self { 22 | config, 23 | path: Some(path.as_ref().to_path_buf()), 24 | } 25 | } 26 | 27 | /// Create wrapper that lives only in memory and has no file backing 28 | pub fn memory(config: C) -> Self { 29 | Self { config, path: None } 30 | } 31 | 32 | /// Try our best to ensure that the config written is complete to protect against partial 33 | /// or broken config writes if the tool crashes or is killed. 34 | /// 35 | /// This is achieved, best-effort, by writing to a temporary file then replacing the target file 36 | /// with the temporary file. 37 | /// 38 | /// See . 39 | pub fn save(&self) -> Result<(), StateError> { 40 | if let Some(final_path) = &self.path { 41 | let mut temp_file = tempfile::NamedTempFile::new_in(final_path.parent().unwrap())?; 42 | temp_file 43 | .write_all( 44 | &serde_json::to_vec_pretty(&self.config) 45 | .context(CfgSerializationFailedSnafu)?, 46 | ) 47 | .context(CfgSaveFailedSnafu)?; 48 | temp_file.persist(final_path)?; 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | impl std::ops::Deref for ConfigWrapper { 55 | type Target = C; 56 | fn deref(&self) -> &Self::Target { 57 | &self.config 58 | } 59 | } 60 | 61 | impl std::ops::DerefMut for ConfigWrapper { 62 | fn deref_mut(&mut self) -> &mut C { 63 | &mut self.config 64 | } 65 | } 66 | 67 | impl Drop for ConfigWrapper { 68 | fn drop(&mut self) { 69 | self.save().unwrap(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test_assets/lints/A.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/A.pak -------------------------------------------------------------------------------- /test_assets/lints/A/FSD/Content/A.uexp: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test_assets/lints/A/FSD/Content/AssetRegistry.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/A/FSD/Content/AssetRegistry.bin -------------------------------------------------------------------------------- /test_assets/lints/A/FSD/Content/B.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/A/FSD/Content/B.uexp -------------------------------------------------------------------------------- /test_assets/lints/A/FSD/Content/C.ushaderbytecode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/A/FSD/Content/C.ushaderbytecode -------------------------------------------------------------------------------- /test_assets/lints/B.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/B.pak -------------------------------------------------------------------------------- /test_assets/lints/B/FSD/Content/A.uexp: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test_assets/lints/B/FSD/Content/C.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/B/FSD/Content/C.uexp -------------------------------------------------------------------------------- /test_assets/lints/empty_archive.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /test_assets/lints/multiple_pak_files.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/multiple_pak_files.zip -------------------------------------------------------------------------------- /test_assets/lints/multiple_paks.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/multiple_paks.zip -------------------------------------------------------------------------------- /test_assets/lints/non_asset_files.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/non_asset_files.pak -------------------------------------------------------------------------------- /test_assets/lints/non_asset_files/never_gonna_give_you_up.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/non_asset_files/never_gonna_give_you_up.txt -------------------------------------------------------------------------------- /test_assets/lints/only_non_pak_files.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/only_non_pak_files.zip -------------------------------------------------------------------------------- /test_assets/lints/only_non_pak_files/never_gonna_give_you_up.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/only_non_pak_files/never_gonna_give_you_up.txt -------------------------------------------------------------------------------- /test_assets/lints/outdated_pak_version.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/outdated_pak_version.pak -------------------------------------------------------------------------------- /test_assets/lints/outdated_pak_version/foo.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/outdated_pak_version/foo.uexp -------------------------------------------------------------------------------- /test_assets/lints/reference.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/reference.pak -------------------------------------------------------------------------------- /test_assets/lints/reference/a.uasset: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test_assets/lints/reference/a.uexp: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test_assets/lints/split_asset_pairs.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/split_asset_pairs.pak -------------------------------------------------------------------------------- /test_assets/lints/split_asset_pairs/missing_uasset/a.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/split_asset_pairs/missing_uasset/a.uexp -------------------------------------------------------------------------------- /test_assets/lints/split_asset_pairs/missing_uexp/b.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/split_asset_pairs/missing_uexp/b.uasset -------------------------------------------------------------------------------- /test_assets/lints/split_asset_pairs/not_missing/c.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/split_asset_pairs/not_missing/c.uasset -------------------------------------------------------------------------------- /test_assets/lints/split_asset_pairs/not_missing/c.uexp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/split_asset_pairs/not_missing/c.uexp -------------------------------------------------------------------------------- /test_assets/lints/unmodified_game_assets.pak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trumank/mint/6335041f21b95976d29fe2cfbf282feb0c9f38ac/test_assets/lints/unmodified_game_assets.pak -------------------------------------------------------------------------------- /test_assets/lints/unmodified_game_assets/a.uasset: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test_assets/lints/unmodified_game_assets/a.uexp: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | mod lint; 2 | -------------------------------------------------------------------------------- /workspace_hack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workspace_hack" 3 | repository.workspace = true 4 | authors.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | edition.workspace = true 8 | 9 | [features] 10 | default = [] 11 | oodle_platform_dependent = ["dep:repak"] 12 | 13 | [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] 14 | repak = { workspace = true, optional = true, features = ["oodle_implicit_dynamic"] } 15 | 16 | [target.'cfg(not(any(target_os = "windows", target_os = "linux")))'.dependencies] 17 | repak = { workspace = true, optional = true } 18 | -------------------------------------------------------------------------------- /workspace_hack/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------