diff --git a/.cargo-husky/hooks/pre-commit b/.cargo-husky/hooks/pre-commit new file mode 100644 index 0000000..b0ec64f --- /dev/null +++ b/.cargo-husky/hooks/pre-commit @@ -0,0 +1,22 @@ +#!/bin/sh +# Pre-commit hook to check code formatting and lints + +# Check formatting +cargo fmt --check +if [ $? -ne 0 ]; then + echo "" + echo "❌ Code is not formatted. Run 'cargo fmt' to fix." + echo "" + exit 1 +fi + +# Run clippy +cargo clippy --all-targets --all-features -- -D warnings +if [ $? -ne 0 ]; then + echo "" + echo "❌ Clippy found issues. Fix the warnings above." + echo "" + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/.cargo/config.toml b/.cargo/config.toml index 9380186..f378481 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ -[build] -target = "i686-pc-windows-msvc" +[build] +target = "i686-pc-windows-msvc" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ff098e..2f7d77e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,39 +1,39 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@nightly - with: - targets: i686-pc-windows-msvc - components: rustfmt, clippy - - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Build - run: cargo build --release --target i686-pc-windows-msvc - - - name: Check formatting - run: cargo fmt --check - - - name: Clippy - run: cargo clippy --target i686-pc-windows-msvc -- -D warnings - +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@nightly + with: + targets: i686-pc-windows-msvc + components: rustfmt, clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --release --target i686-pc-windows-msvc + + - name: Check formatting + run: cargo fmt --check + + - name: Clippy + run: cargo clippy --target i686-pc-windows-msvc -- -D warnings + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f6f46a..5bdc631 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,65 +1,65 @@ -name: Release - -on: - workflow_dispatch: - -permissions: - contents: write - -jobs: - release: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Generate tag - id: tag - run: | - $date = Get-Date -Format "yyyy.MM.dd" - $baseTag = "v$date" - $counter = 0 - $tag = $baseTag - - while (git tag -l $tag) { - $counter++ - $tag = "$baseTag.$counter" - } - - echo "tag=$tag" >> $env:GITHUB_OUTPUT - echo "Generated tag: $tag" - - - name: Create and push tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag ${{ steps.tag.outputs.tag }} - git push origin ${{ steps.tag.outputs.tag }} - - - name: Install Rust - uses: dtolnay/rust-toolchain@nightly - with: - targets: i686-pc-windows-msvc - - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Build - run: cargo build --release --target i686-pc-windows-msvc - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.tag.outputs.tag }} - files: target/i686-pc-windows-msvc/release/systemdetection.dll - generate_release_notes: true - +name: Release + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate tag + id: tag + run: | + $date = Get-Date -Format "yyyy.MM.dd" + $baseTag = "v$date" + $counter = 0 + $tag = $baseTag + + while (git tag -l $tag) { + $counter++ + $tag = "$baseTag.$counter" + } + + echo "tag=$tag" >> $env:GITHUB_OUTPUT + echo "Generated tag: $tag" + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ steps.tag.outputs.tag }} + git push origin ${{ steps.tag.outputs.tag }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@nightly + with: + targets: i686-pc-windows-msvc + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --release --target i686-pc-windows-msvc + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + files: target/i686-pc-windows-msvc/release/systemdetection.dll + generate_release_notes: true + diff --git a/.gitignore b/.gitignore index 287a758..5afaebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,23 @@ -# Build outputs -**/target/ - -# Built DLLs (generated artifacts) -*.dll -*.pdb -*.exp -*.lib - -# IDA Pro files -*.i64 -*.id0 -*.id1 -*.id2 -*.nam -*.til - -# VS Code -.vscode/ - -# OS files -Thumbs.db -.DS_Store +# Build outputs +**/target/ + +# Built DLLs (generated artifacts) +*.dll +*.pdb +*.exp +*.lib + +# IDA Pro files +*.i64 +*.id0 +*.id1 +*.id2 +*.nam +*.til + +# VS Code +.vscode/ + +# OS files +Thumbs.db +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index eb3e881..57ac69c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,81 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "cargo-husky" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cppvtable" +version = "0.1.0" +source = "git+https://github.com/coconutbird/cppvtable.git#44f80c05fa7a2a7dc39da4bbc3a43c2a08828911" +dependencies = [ + "cppvtable-macro", + "paste", +] + +[[package]] +name = "cppvtable-macro" +version = "0.1.0" +source = "git+https://github.com/coconutbird/cppvtable.git#44f80c05fa7a2a7dc39da4bbc3a43c2a08828911" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "minhook" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455e088cb1600ffa5f1524c9702c9eeb0ba3604e0f4932efa165b99973272a8d" +dependencies = [ + "cc", + "tracing", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -20,11 +95,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -35,139 +116,156 @@ dependencies = [ name = "systemdetection" version = "0.1.0" dependencies = [ + "cargo-husky", + "cppvtable", + "minhook", "windows", ] [[package]] -name = "unicode-ident" -version = "1.0.23" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] [[package]] -name = "windows" -version = "0.58.0" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "windows-core", - "windows-targets", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", - "windows-targets", + "once_cell", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "windows-interface" -version = "0.58.0" +name = "windows" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", ] [[package]] -name = "windows-result" -version = "0.2.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-targets", + "windows-core", ] [[package]] -name = "windows-strings" -version = "0.1.0" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", "windows-result", - "windows-targets", + "windows-strings", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-core", + "windows-link", + "windows-threading", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 1955ed6..b2bca5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "systemdetection" version = "0.1.0" -edition = "2021" +edition = "2024" description = "Far Cry 2 systemdetection.dll drop-in replacement" [lib] @@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"] name = "systemdetection" [dependencies] -windows = { version = "0.58", features = [ +windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_System_Console", "Win32_System_SystemServices", @@ -17,10 +17,16 @@ windows = { version = "0.58", features = [ "Win32_System_Threading", "Win32_System_SystemInformation", "Win32_System_Registry", + "Win32_System_Memory", "Win32_UI_WindowsAndMessaging", ]} +cppvtable = { git = "https://github.com/coconutbird/cppvtable.git" } +minhook = "0.9" [profile.release] opt-level = 3 lto = true strip = true + +[dev-dependencies] +cargo-husky = { version = "1.5.0", features = ["precommit-hook", "user-hooks"] } diff --git a/LICENSE b/LICENSE index 6d072aa..2d89a72 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2026 coconutbird - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2026 coconutbird + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b597806..e63cc7b 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,53 @@ -# fc2-systemdetection - -[![CI](https://github.com/coconutbird/fc2-systemdetection/actions/workflows/ci.yml/badge.svg)](https://github.com/coconutbird/fc2-systemdetection/actions/workflows/ci.yml) -[![Release](https://img.shields.io/github/v/release/coconutbird/fc2-systemdetection)](https://github.com/coconutbird/fc2-systemdetection/releases/latest) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -Drop-in replacement for Far Cry 2's `systemdetection.dll`. - -## Why? - -The original DLL crashes on systems with 32+ logical CPU cores due to a bug in the CPU topology detection code. This replacement fixes that issue. - -## Installation - -1. Download `systemdetection.dll` from [Releases](https://github.com/coconutbird/fc2-systemdetection/releases) -2. Navigate to your Far Cry 2 installation folder -3. Backup the original `bin/systemdetection.dll` -4. Copy the downloaded DLL to the `bin` folder -5. Launch the game - -### Common Install Locations - -- **Steam**: `C:\Program Files (x86)\Steam\steamapps\common\Far Cry 2\bin` -- **GOG**: `C:\GOG Games\Far Cry 2\bin` -- **Ubisoft Connect**: `C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games\Far Cry 2\bin` - -## Building - -Requires Rust nightly and the 32-bit MSVC toolchain: - -``` -rustup target add i686-pc-windows-msvc -cargo build --release -``` - -The DLL will be at `target/i686-pc-windows-msvc/release/systemdetection.dll` - -## License - -[MIT](LICENSE) +# fc2-systemdetection + +[![CI](https://github.com/coconutbird/fc2-systemdetection/actions/workflows/ci.yml/badge.svg)](https://github.com/coconutbird/fc2-systemdetection/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/coconutbird/fc2-systemdetection)](https://github.com/coconutbird/fc2-systemdetection/releases/latest) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +Drop-in replacement for Far Cry 2's `systemdetection.dll` with bug fixes and quality-of-life improvements. + +## Features + +### CPU Crash Fix + +The original DLL crashes on systems with 32+ logical CPU cores due to a bug in the CPU topology detection code. This replacement fixes that issue. + +### Dunia Engine Patches + +Runtime patches applied to `Dunia.dll` (similar to [Far Cry 2 Multi Fixer](https://github.com/FoxAhead/Far-Cry-2-Multi-Fixer)): + +| Patch | Description | +| --------------------- | ------------------------------------------------------------- | +| **Jackal Tapes Fix** | Fixes incorrect tape recordings in the Southern map | +| **DevMode Unlock** | Enables developer console commands | +| **Predecessor Tapes** | Unlocks 7 bonus missions (originally tied to Ubisoft account) | +| **Machetes Unlock** | Unlocks 2 bonus machete skins | + +## Installation + +1. Download `systemdetection.dll` from [Releases](https://github.com/coconutbird/fc2-systemdetection/releases) +2. Navigate to your Far Cry 2 installation folder +3. Backup the original `bin/systemdetection.dll` +4. Copy the downloaded DLL to the `bin` folder +5. Launch the game + +### Common Install Locations + +- **Steam**: `C:\Program Files (x86)\Steam\steamapps\common\Far Cry 2\bin` +- **GOG**: `C:\GOG Games\Far Cry 2\bin` +- **Ubisoft Connect**: `C:\Program Files (x86)\Ubisoft\Ubisoft Game Launcher\games\Far Cry 2\bin` + +## Building + +Requires Rust nightly and the 32-bit MSVC toolchain: + +``` +rustup target add i686-pc-windows-msvc +cargo build --release +``` + +The DLL will be at `target/i686-pc-windows-msvc/release/systemdetection.dll` + +## License + +[MIT](LICENSE) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index cd52334..4ba39f0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ -[toolchain] -channel = "nightly" -targets = ["i686-pc-windows-msvc"] +[toolchain] +channel = "nightly" +targets = ["i686-pc-windows-msvc"] diff --git a/src/gear/cpu.rs b/src/gear/cpu.rs new file mode 100644 index 0000000..1cb3d80 --- /dev/null +++ b/src/gear/cpu.rs @@ -0,0 +1,182 @@ +//! GearCPU - CPU detection with fixed topology enumeration +//! +//! Structure layout (32 bytes + string object): +//! 0x00: vtable +//! 0x04: base class field +//! 0x08: cpu_freq_low (MHz * 1000000) +//! 0x0C: cpu_freq_high +//! 0x10: num_logical +//! 0x14: num_physical +//! 0x18: num_packages +//! 0x1C: vendor_id (4=Intel, 1=AMD) +//! 0x20: simd_level +//! 0x24: reserved +//! 0x28: cpu_info_string +//! +//! VTable: [destructor, GetCpuInfoAccess] + +use cppvtable::proc::{cppvtable, cppvtable_impl}; +use std::ffi::c_void; +use windows::Win32::System::Registry::{ + HKEY_LOCAL_MACHINE, KEY_READ, RegCloseKey, RegOpenKeyExA, RegQueryValueExA, +}; +use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO}; +use windows::Win32::System::Threading::{ + GetCurrentProcess, GetCurrentThread, GetProcessAffinityMask, SetThreadAffinityMask, +}; +use windows::core::PCSTR; + +/// GearBasicString - simplified string class at offset 0x28 of GearCPU +#[repr(C)] +pub struct GearBasicString { + pub vtable: *const c_void, // String vtable + pub length: u32, + pub data: [u8; 64], // Inline buffer for short strings +} + +impl Default for GearBasicString { + fn default() -> Self { + GearBasicString { + vtable: std::ptr::null(), + length: 0, + data: [0; 64], + } + } +} + +/// IGearCPU interface definition +#[cppvtable] +pub trait IGearCPU { + fn destructor(&mut self, flags: u8) -> *mut c_void; + fn get_cpu_info_access(&mut self) -> *mut GearBasicString; +} + +/// GearCPU class with fixed topology detection +#[repr(C)] +pub struct GearCPU { + pub vtable_i_gear_cpu: *const IGearCPUVTable, // offset 0x00 + _base_field: u32, // offset 0x04 (base class) + cpu_freq_low: u32, // offset 0x08 (MHz * 1000000) + cpu_freq_high: u32, // offset 0x0C + pub num_logical: u32, // offset 0x10 + pub num_physical: u32, // offset 0x14 + pub num_packages: u32, // offset 0x18 + pub vendor_id: u32, // offset 0x1C (4=Intel, 1=AMD) + pub simd_level: u32, // offset 0x20 + _reserved: u32, // offset 0x24 + cpu_info_string: GearBasicString, // offset 0x28 (this + 10 DWORDs = 40 bytes) +} + +#[cppvtable_impl(IGearCPU)] +impl GearCPU { + fn destructor(&mut self, flags: u8) -> *mut c_void { + if flags & 1 != 0 { + unsafe { + let _ = Box::from_raw(self as *mut Self); + } + } + self as *mut Self as *mut c_void + } + + /// GetCpuInfoAccess - returns pointer to cpu_info_string (this + 40) + fn get_cpu_info_access(&mut self) -> *mut GearBasicString { + &mut self.cpu_info_string as *mut GearBasicString + } +} + +impl GearCPU { + pub fn new() -> Self { + println!("systemdetection: Detecting CPU info"); + + let (logical, physical, packages) = unsafe { Self::get_cpu_topology_fixed() }; + let cpu_mhz = unsafe { Self::get_cpu_mhz() }; + + println!( + "systemdetection: CPU: {} logical, {} physical, {} packages, {} MHz", + logical, physical, packages, cpu_mhz + ); + + GearCPU { + vtable_i_gear_cpu: Self::VTABLE_I_GEAR_CPU, + _base_field: 0, + cpu_freq_low: cpu_mhz.saturating_mul(1_000_000), + cpu_freq_high: 0, + num_logical: logical, + num_physical: physical, + num_packages: packages, + vendor_id: 4, // Intel + simd_level: 8, // SSE4.2 + _reserved: 0, + cpu_info_string: GearBasicString::default(), + } + } + + /// Fixed CPU topology detection + /// Key fix: `while (mask != 0 && mask <= system_affinity)` instead of `while (1 << i)` + unsafe fn get_cpu_topology_fixed() -> (u32, u32, u32) { + unsafe { + let mut process_affinity: usize = 0; + let mut system_affinity: usize = 0; + + let process = GetCurrentProcess(); + let _ = GetProcessAffinityMask(process, &mut process_affinity, &mut system_affinity); + + let mut logical_count = 0u32; + let mut mask: usize = 1; + let thread = GetCurrentThread(); + + // Fixed: proper bounds checking + while mask != 0 && mask <= system_affinity { + if SetThreadAffinityMask(thread, mask) != 0 { + logical_count += 1; + } + mask = mask.wrapping_shl(1); + } + + let _ = SetThreadAffinityMask(thread, process_affinity); + + if logical_count == 0 { + let mut sys_info: SYSTEM_INFO = std::mem::zeroed(); + GetSystemInfo(&mut sys_info); + logical_count = sys_info.dwNumberOfProcessors; + } + + let physical_count = logical_count.div_ceil(2).max(1); + (logical_count.max(1), physical_count, 1) + } + } + + unsafe fn get_cpu_mhz() -> u32 { + unsafe { + let key_path = b"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\0"; + let value_name = b"~MHz\0"; + let mut hkey = std::mem::zeroed(); + + if RegOpenKeyExA( + HKEY_LOCAL_MACHINE, + PCSTR::from_raw(key_path.as_ptr()), + Some(0), + KEY_READ, + &mut hkey, + ) + .is_ok() + { + let mut mhz: u32 = 0; + let mut size = 4u32; + let _ = RegQueryValueExA( + hkey, + PCSTR::from_raw(value_name.as_ptr()), + None, + None, + Some(&mut mhz as *mut u32 as *mut u8), + Some(&mut size), + ); + let _ = RegCloseKey(hkey); + if mhz > 0 { + return mhz; + } + } + 3000 + } + } +} diff --git a/src/gear/graphics.rs b/src/gear/graphics.rs new file mode 100644 index 0000000..70a4705 --- /dev/null +++ b/src/gear/graphics.rs @@ -0,0 +1,78 @@ +//! GearGraphics - Graphics detection with safe defaults +//! +//! VTable: [destructor, GetAdapterInfo, GetMonitorCount, GetDesktopResolution] +//! +//! Returns safe default values instead of calling D3D which can crash on some systems. + +use cppvtable::proc::cppvtable; +use cppvtable::proc::cppvtable_impl; +use std::ffi::c_void; +use windows::Win32::Foundation::RECT; +use windows::Win32::UI::WindowsAndMessaging::{ + GetDesktopWindow, GetSystemMetrics, GetWindowRect, SM_CMONITORS, +}; + +/// IGearGraphics interface definition +#[cppvtable] +pub trait IGearGraphics { + fn destructor(&mut self, flags: u8) -> *mut c_void; + fn get_adapter_info(&mut self, index: u32) -> *const c_void; + fn get_monitor_count(&mut self) -> i32; + fn get_desktop_resolution(&mut self, width: *mut u32, height: *mut u32) -> i32; +} + +/// GearGraphics class - simplified structure +#[repr(C)] +pub struct GearGraphics { + pub vtable_i_gear_graphics: *const IGearGraphicsVTable, +} + +#[cppvtable_impl(IGearGraphics)] +impl GearGraphics { + fn destructor(&mut self, flags: u8) -> *mut c_void { + if flags & 1 != 0 { + unsafe { + let _ = Box::from_raw(self as *mut Self); + } + } + self as *mut Self as *mut c_void + } + + /// GetAdapterInfo - returns null (game will use defaults) + fn get_adapter_info(&mut self, _index: u32) -> *const c_void { + std::ptr::null() + } + + /// GetMonitorCount - uses GetSystemMetrics(SM_CMONITORS) + fn get_monitor_count(&mut self) -> i32 { + unsafe { GetSystemMetrics(SM_CMONITORS) } + } + + /// GetDesktopResolution - returns desktop window dimensions + fn get_desktop_resolution(&mut self, width: *mut u32, height: *mut u32) -> i32 { + unsafe { + let desktop = GetDesktopWindow(); + let mut rect: RECT = std::mem::zeroed(); + let _ = GetWindowRect(desktop, &mut rect); + + if !width.is_null() { + *width = (rect.right - rect.left) as u32; + } + if !height.is_null() { + *height = (rect.bottom - rect.top) as u32; + } + + rect.bottom - rect.top + } + } +} + +impl GearGraphics { + pub fn new() -> Self { + #[cfg(debug_assertions)] + println!("systemdetection: Initializing GearGraphics"); + GearGraphics { + vtable_i_gear_graphics: Self::VTABLE_I_GEAR_GRAPHICS, + } + } +} diff --git a/src/gear/hardware.rs b/src/gear/hardware.rs new file mode 100644 index 0000000..41b8f58 --- /dev/null +++ b/src/gear/hardware.rs @@ -0,0 +1,144 @@ +//! GearHardware - Main hardware detection class +//! +//! Structure layout (32 bytes): +//! 0x00: vtable +//! 0x04: m_cpu (GearCPU*) +//! 0x08: m_logical_disks (GearLogicalDisks*) +//! 0x0C: m_memory (GearMemory*) +//! 0x10: m_network (GearNetwork*) +//! 0x14: m_os (GearOS*) +//! 0x18: m_graphics (GearGraphics*) +//! 0x1C: m_audio (GearAudio*) +//! +//! VTable: [destructor, GetCpu, GetLogicalDisks, GetMemory, GetNetwork, GetOS, GetGraphics, GetAudio] + +use super::cpu::GearCPU; +use super::graphics::GearGraphics; +use cppvtable::proc::{cppvtable, cppvtable_impl}; +use std::ffi::c_void; +use std::ptr::null_mut; + +/// Stub classes for subsystems we don't need to fully implement +#[repr(C)] +pub struct GearLogicalDisks { + _vtable: *const c_void, +} +#[repr(C)] +pub struct GearMemory { + _vtable: *const c_void, +} +#[repr(C)] +pub struct GearNetwork { + _vtable: *const c_void, +} +#[repr(C)] +pub struct GearOS { + _vtable: *const c_void, +} +#[repr(C)] +pub struct GearAudio { + _vtable: *const c_void, +} + +/// IGearHardware interface definition +#[cppvtable] +pub trait IGearHardware { + fn destructor(&mut self, flags: u8) -> *mut c_void; + fn get_cpu(&mut self) -> *mut GearCPU; + fn get_logical_disks(&mut self) -> *mut GearLogicalDisks; + fn get_memory(&mut self) -> *mut GearMemory; + fn get_network(&mut self) -> *mut GearNetwork; + fn get_os(&mut self) -> *mut GearOS; + fn get_graphics(&mut self) -> *mut GearGraphics; + fn get_audio(&mut self) -> *mut GearAudio; +} + +/// GearHardware class (32 bytes) +#[repr(C)] +pub struct GearHardware { + pub vtable_i_gear_hardware: *const IGearHardwareVTable, // offset 0x00 + m_cpu: *mut GearCPU, // offset 0x04 + m_logical_disks: *mut GearLogicalDisks, // offset 0x08 + m_memory: *mut GearMemory, // offset 0x0C + m_network: *mut GearNetwork, // offset 0x10 + m_os: *mut GearOS, // offset 0x14 + m_graphics: *mut GearGraphics, // offset 0x18 + m_audio: *mut GearAudio, // offset 0x1C +} + +unsafe impl Send for GearHardware {} +unsafe impl Sync for GearHardware {} + +#[cppvtable_impl(IGearHardware)] +impl GearHardware { + fn destructor(&mut self, flags: u8) -> *mut c_void { + unsafe { + if !self.m_cpu.is_null() { + let _ = Box::from_raw(self.m_cpu); + } + if !self.m_graphics.is_null() { + let _ = Box::from_raw(self.m_graphics); + } + if flags & 1 != 0 { + let _ = Box::from_raw(self as *mut Self); + } + } + self as *mut Self as *mut c_void + } + + fn get_cpu(&mut self) -> *mut GearCPU { + if self.m_cpu.is_null() { + self.m_cpu = Box::into_raw(Box::new(GearCPU::new())); + } + self.m_cpu + } + + fn get_logical_disks(&mut self) -> *mut GearLogicalDisks { + self.m_logical_disks + } + + fn get_memory(&mut self) -> *mut GearMemory { + self.m_memory + } + + fn get_network(&mut self) -> *mut GearNetwork { + self.m_network + } + + fn get_os(&mut self) -> *mut GearOS { + self.m_os + } + + fn get_graphics(&mut self) -> *mut GearGraphics { + if self.m_graphics.is_null() { + self.m_graphics = Box::into_raw(Box::new(GearGraphics::new())); + } + self.m_graphics + } + + fn get_audio(&mut self) -> *mut GearAudio { + self.m_audio + } +} + +impl Default for GearHardware { + fn default() -> Self { + Self::new() + } +} + +impl GearHardware { + pub fn new() -> Self { + println!("systemdetection: Initializing GearHardware"); + GearHardware { + vtable_i_gear_hardware: Self::VTABLE_I_GEAR_HARDWARE, + m_cpu: null_mut(), + m_logical_disks: null_mut(), + m_memory: null_mut(), + m_network: null_mut(), + m_os: null_mut(), + m_graphics: null_mut(), + m_audio: null_mut(), + } + } +} diff --git a/src/gear/mod.rs b/src/gear/mod.rs new file mode 100644 index 0000000..30bcecc --- /dev/null +++ b/src/gear/mod.rs @@ -0,0 +1,12 @@ +//! Gear hardware detection subsystem +//! +//! This module contains the hardware detection classes that Far Cry 2 uses +//! to determine system capabilities. + +mod cpu; +mod graphics; +mod hardware; +mod score; + +pub use hardware::GearHardware; +pub use score::GearScore; diff --git a/src/gear/score.rs b/src/gear/score.rs new file mode 100644 index 0000000..1044910 --- /dev/null +++ b/src/gear/score.rs @@ -0,0 +1,101 @@ +//! GearScore - System scoring class +//! +//! Structure layout (20 bytes): +//! 0x00: vtable +//! 0x04: scores[0] (f32) - CPU score +//! 0x08: scores[1] (f32) - GPU score +//! 0x0C: confidence[0] (f32) - CPU confidence +//! 0x10: confidence[1] (f32) - GPU confidence +//! +//! VTable: [destructor, GetScore, ComputeScore] + +use cppvtable::proc::{cppvtable, cppvtable_impl}; +use std::ffi::c_void; + +/// Sentinel value for uncomputed scores (-FLT_MAX) +const SCORE_UNCOMPUTED: f32 = -3.4028235e38; + +/// IGearScore interface definition +#[cppvtable] +pub trait IGearScore { + fn destructor(&mut self, flags: u8) -> *mut c_void; + fn get_score(&mut self, score_type: i32, param: i32, confidence_out: *mut f32) -> f32; + fn compute_score(&mut self, score_type: i32, param: i32) -> i32; +} + +/// GearScore class (20 bytes) +#[repr(C)] +pub struct GearScore { + pub vtable_i_gear_score: *const IGearScoreVTable, // offset 0x00 + scores: [f32; 2], // offset 0x04 (CPU, GPU) + confidence: [f32; 2], // offset 0x0C (CPU, GPU confidence) +} + +unsafe impl Send for GearScore {} +unsafe impl Sync for GearScore {} + +#[cppvtable_impl(IGearScore)] +impl GearScore { + fn destructor(&mut self, flags: u8) -> *mut c_void { + if flags & 1 != 0 { + unsafe { + let _ = Box::from_raw(self as *mut Self); + } + } + self as *mut Self as *mut c_void + } + + /// GetScore - returns score for given type, computing if needed + fn get_score(&mut self, score_type: i32, param: i32, confidence_out: *mut f32) -> f32 { + if !(0..2).contains(&score_type) { + return 0.0; + } + + let idx = score_type as usize; + + // Compute score if not yet computed (check for -FLT_MAX sentinel) + if self.scores[idx] == SCORE_UNCOMPUTED { + self.compute_score(score_type, param); + } + + if !confidence_out.is_null() { + unsafe { + *confidence_out = self.confidence[idx]; + } + } + + self.scores[idx] + } + + /// ComputeScore - computes and stores score for given type + fn compute_score(&mut self, score_type: i32, _param: i32) -> i32 { + if !(0..2).contains(&score_type) { + return score_type; + } + + let idx = score_type as usize; + + // Return good default scores (0.8 = 80%) + self.scores[idx] = 0.8; + self.confidence[idx] = 1.0; + + score_type + } +} + +impl Default for GearScore { + fn default() -> Self { + Self::new() + } +} + +impl GearScore { + pub fn new() -> Self { + println!("systemdetection: Initializing GearScore"); + GearScore { + vtable_i_gear_score: Self::VTABLE_I_GEAR_SCORE, + scores: [SCORE_UNCOMPUTED; 2], + confidence: [1.0; 2], + } + } +} diff --git a/src/gear_cpu.rs b/src/gear_cpu.rs deleted file mode 100644 index bd72a0f..0000000 --- a/src/gear_cpu.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! GearCPU - CPU detection with fixed topology enumeration -//! -//! Structure layout (32 bytes + string object): -//! 0x00: vtable -//! 0x04: base class field -//! 0x08: cpu_freq_low (MHz * 1000000) -//! 0x0C: cpu_freq_high -//! 0x10: num_logical -//! 0x14: num_physical -//! 0x18: num_packages -//! 0x1C: vendor_id (4=Intel, 1=AMD) -//! 0x20: simd_level -//! 0x24: reserved -//! 0x28: cpu_info_string -//! -//! VTable: [destructor, GetCpuInfoAccess] - -use std::ffi::c_void; -use windows::core::PCSTR; -use windows::Win32::System::Registry::{ - RegCloseKey, RegOpenKeyExA, RegQueryValueExA, HKEY_LOCAL_MACHINE, KEY_READ, -}; -use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO}; -use windows::Win32::System::Threading::{ - GetCurrentProcess, GetCurrentThread, GetProcessAffinityMask, SetThreadAffinityMask, -}; - -/// GearBasicString - simplified string class at offset 0x28 of GearCPU -#[repr(C)] -pub struct GearBasicString { - pub vtable: *const c_void, // String vtable - pub length: u32, - pub data: [u8; 64], // Inline buffer for short strings -} - -impl Default for GearBasicString { - fn default() -> Self { - GearBasicString { - vtable: std::ptr::null(), - length: 0, - data: [0; 64], - } - } -} - -/// GearCPU class with fixed topology detection -#[repr(C)] -pub struct GearCPU { - vtable: *const GearCPUVTable, // offset 0x00 - _base_field: u32, // offset 0x04 (base class) - cpu_freq_low: u32, // offset 0x08 (MHz * 1000000) - cpu_freq_high: u32, // offset 0x0C - pub num_logical: u32, // offset 0x10 - pub num_physical: u32, // offset 0x14 - pub num_packages: u32, // offset 0x18 - pub vendor_id: u32, // offset 0x1C (4=Intel, 1=AMD) - pub simd_level: u32, // offset 0x20 - _reserved: u32, // offset 0x24 - cpu_info_string: GearBasicString, // offset 0x28 (this + 10 DWORDs = 40 bytes) -} - -#[repr(C)] -struct GearCPUVTable { - destructor: unsafe extern "thiscall" fn(*mut GearCPU, u8) -> *mut c_void, - get_cpu_info_access: unsafe extern "thiscall" fn(*mut GearCPU) -> *mut GearBasicString, -} - -static GEAR_CPU_VTABLE: GearCPUVTable = GearCPUVTable { - destructor: gear_cpu_destructor, - get_cpu_info_access: gear_cpu_get_cpu_info_access, -}; - -unsafe extern "thiscall" fn gear_cpu_destructor(this: *mut GearCPU, flags: u8) -> *mut c_void { - if flags & 1 != 0 { - let _ = Box::from_raw(this); - } - this as *mut c_void -} - -/// GetCpuInfoAccess - returns pointer to cpu_info_string (this + 40) -unsafe extern "thiscall" fn gear_cpu_get_cpu_info_access( - this: *mut GearCPU, -) -> *mut GearBasicString { - &mut (*this).cpu_info_string as *mut GearBasicString -} - -impl GearCPU { - pub fn new() -> Self { - println!("systemdetection: Detecting CPU info"); - - let (logical, physical, packages) = unsafe { Self::get_cpu_topology_fixed() }; - let cpu_mhz = unsafe { Self::get_cpu_mhz() }; - - println!( - "systemdetection: CPU: {} logical, {} physical, {} packages, {} MHz", - logical, physical, packages, cpu_mhz - ); - - GearCPU { - vtable: &GEAR_CPU_VTABLE, - _base_field: 0, - cpu_freq_low: cpu_mhz.saturating_mul(1_000_000), - cpu_freq_high: 0, - num_logical: logical, - num_physical: physical, - num_packages: packages, - vendor_id: 4, // Intel - simd_level: 8, // SSE4.2 - _reserved: 0, - cpu_info_string: GearBasicString::default(), - } - } - - /// Fixed CPU topology detection - /// Key fix: `while (mask != 0 && mask <= system_affinity)` instead of `while (1 << i)` - unsafe fn get_cpu_topology_fixed() -> (u32, u32, u32) { - let mut process_affinity: usize = 0; - let mut system_affinity: usize = 0; - - let process = GetCurrentProcess(); - let _ = GetProcessAffinityMask(process, &mut process_affinity, &mut system_affinity); - - let mut logical_count = 0u32; - let mut mask: usize = 1; - let thread = GetCurrentThread(); - - // Fixed: proper bounds checking - while mask != 0 && mask <= system_affinity { - if SetThreadAffinityMask(thread, mask) != 0 { - logical_count += 1; - } - mask = mask.wrapping_shl(1); - } - - let _ = SetThreadAffinityMask(thread, process_affinity); - - if logical_count == 0 { - let mut sys_info: SYSTEM_INFO = std::mem::zeroed(); - GetSystemInfo(&mut sys_info); - logical_count = sys_info.dwNumberOfProcessors; - } - - let physical_count = logical_count.div_ceil(2).max(1); - (logical_count.max(1), physical_count, 1) - } - - unsafe fn get_cpu_mhz() -> u32 { - let key_path = b"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\0"; - let value_name = b"~MHz\0"; - let mut hkey = std::mem::zeroed(); - - if RegOpenKeyExA( - HKEY_LOCAL_MACHINE, - PCSTR::from_raw(key_path.as_ptr()), - 0, - KEY_READ, - &mut hkey, - ) - .is_ok() - { - let mut mhz: u32 = 0; - let mut size = 4u32; - let _ = RegQueryValueExA( - hkey, - PCSTR::from_raw(value_name.as_ptr()), - None, - None, - Some(&mut mhz as *mut u32 as *mut u8), - Some(&mut size), - ); - let _ = RegCloseKey(hkey); - if mhz > 0 { - return mhz; - } - } - 3000 - } -} diff --git a/src/gear_graphics.rs b/src/gear_graphics.rs deleted file mode 100644 index b471c5e..0000000 --- a/src/gear_graphics.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! GearGraphics - Graphics detection with safe defaults -//! -//! VTable: [destructor, GetAdapterInfo, GetMonitorCount, GetDesktopResolution] -//! -//! Returns safe default values instead of calling D3D which can crash on some systems. - -use std::ffi::c_void; -use windows::Win32::Foundation::RECT; -use windows::Win32::UI::WindowsAndMessaging::{ - GetDesktopWindow, GetSystemMetrics, GetWindowRect, SM_CMONITORS, -}; - -/// GearGraphics class - simplified structure -#[repr(C)] -pub struct GearGraphics { - vtable: *const GearGraphicsVTable, -} - -#[repr(C)] -struct GearGraphicsVTable { - destructor: unsafe extern "thiscall" fn(*mut GearGraphics, u8) -> *mut c_void, - get_adapter_info: unsafe extern "thiscall" fn(*mut GearGraphics, u32) -> *const c_void, - get_monitor_count: unsafe extern "thiscall" fn(*mut GearGraphics) -> i32, - get_desktop_resolution: - unsafe extern "thiscall" fn(*mut GearGraphics, *mut u32, *mut u32) -> i32, -} - -static GEAR_GRAPHICS_VTABLE: GearGraphicsVTable = GearGraphicsVTable { - destructor: gear_graphics_destructor, - get_adapter_info: gear_graphics_get_adapter_info, - get_monitor_count: gear_graphics_get_monitor_count, - get_desktop_resolution: gear_graphics_get_desktop_resolution, -}; - -unsafe extern "thiscall" fn gear_graphics_destructor( - this: *mut GearGraphics, - flags: u8, -) -> *mut c_void { - if flags & 1 != 0 { - let _ = Box::from_raw(this); - } - this as *mut c_void -} - -/// GetAdapterInfo - returns null (game will use defaults) -unsafe extern "thiscall" fn gear_graphics_get_adapter_info( - _this: *mut GearGraphics, - _index: u32, -) -> *const c_void { - std::ptr::null() -} - -/// GetMonitorCount - uses GetSystemMetrics(SM_CMONITORS) -unsafe extern "thiscall" fn gear_graphics_get_monitor_count(_this: *mut GearGraphics) -> i32 { - GetSystemMetrics(SM_CMONITORS) -} - -/// GetDesktopResolution - returns desktop window dimensions -unsafe extern "thiscall" fn gear_graphics_get_desktop_resolution( - _this: *mut GearGraphics, - width: *mut u32, - height: *mut u32, -) -> i32 { - let desktop = GetDesktopWindow(); - let mut rect: RECT = std::mem::zeroed(); - let _ = GetWindowRect(desktop, &mut rect); - - if !width.is_null() { - *width = (rect.right - rect.left) as u32; - } - if !height.is_null() { - *height = (rect.bottom - rect.top) as u32; - } - - rect.bottom - rect.top -} - -impl GearGraphics { - pub fn new() -> Self { - #[cfg(debug_assertions)] - println!("systemdetection: Initializing GearGraphics"); - GearGraphics { - vtable: &GEAR_GRAPHICS_VTABLE, - } - } -} diff --git a/src/gear_hardware.rs b/src/gear_hardware.rs deleted file mode 100644 index 9a36907..0000000 --- a/src/gear_hardware.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! GearHardware - Main hardware detection class -//! -//! Structure layout (32 bytes): -//! 0x00: vtable -//! 0x04: m_cpu (GearCPU*) -//! 0x08: m_logical_disks (GearLogicalDisks*) -//! 0x0C: m_memory (GearMemory*) -//! 0x10: m_network (GearNetwork*) -//! 0x14: m_os (GearOS*) -//! 0x18: m_graphics (GearGraphics*) -//! 0x1C: m_audio (GearAudio*) -//! -//! VTable: [destructor, GetCpu, GetLogicalDisks, GetMemory, GetNetwork, GetOS, GetGraphics, GetAudio] - -use super::gear_cpu::GearCPU; -use super::gear_graphics::GearGraphics; -use std::ffi::c_void; -use std::ptr::null_mut; - -/// Stub classes for subsystems we don't need to fully implement -#[repr(C)] -pub struct GearLogicalDisks { - _vtable: *const c_void, -} -#[repr(C)] -pub struct GearMemory { - _vtable: *const c_void, -} -#[repr(C)] -pub struct GearNetwork { - _vtable: *const c_void, -} -#[repr(C)] -pub struct GearOS { - _vtable: *const c_void, -} -#[repr(C)] -pub struct GearAudio { - _vtable: *const c_void, -} - -/// GearHardware class (32 bytes) -#[repr(C)] -pub struct GearHardware { - vtable: *const GearHardwareVTable, // offset 0x00 - m_cpu: *mut GearCPU, // offset 0x04 - m_logical_disks: *mut GearLogicalDisks, // offset 0x08 - m_memory: *mut GearMemory, // offset 0x0C - m_network: *mut GearNetwork, // offset 0x10 - m_os: *mut GearOS, // offset 0x14 - m_graphics: *mut GearGraphics, // offset 0x18 - m_audio: *mut GearAudio, // offset 0x1C -} - -unsafe impl Send for GearHardware {} -unsafe impl Sync for GearHardware {} - -#[repr(C)] -struct GearHardwareVTable { - destructor: unsafe extern "thiscall" fn(*mut GearHardware, u8) -> *mut c_void, - get_cpu: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearCPU, - get_logical_disks: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearLogicalDisks, - get_memory: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearMemory, - get_network: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearNetwork, - get_os: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearOS, - get_graphics: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearGraphics, - get_audio: unsafe extern "thiscall" fn(*mut GearHardware) -> *mut GearAudio, -} - -static GEAR_HARDWARE_VTABLE: GearHardwareVTable = GearHardwareVTable { - destructor: gear_hardware_destructor, - get_cpu: gear_hardware_get_cpu, - get_logical_disks: gear_hardware_get_logical_disks, - get_memory: gear_hardware_get_memory, - get_network: gear_hardware_get_network, - get_os: gear_hardware_get_os, - get_graphics: gear_hardware_get_graphics, - get_audio: gear_hardware_get_audio, -}; - -unsafe extern "thiscall" fn gear_hardware_destructor( - this: *mut GearHardware, - flags: u8, -) -> *mut c_void { - if !(*this).m_cpu.is_null() { - let _ = Box::from_raw((*this).m_cpu); - } - if !(*this).m_graphics.is_null() { - let _ = Box::from_raw((*this).m_graphics); - } - if flags & 1 != 0 { - let _ = Box::from_raw(this); - } - this as *mut c_void -} - -unsafe extern "thiscall" fn gear_hardware_get_cpu(this: *mut GearHardware) -> *mut GearCPU { - if (*this).m_cpu.is_null() { - (*this).m_cpu = Box::into_raw(Box::new(GearCPU::new())); - } - (*this).m_cpu -} - -unsafe extern "thiscall" fn gear_hardware_get_logical_disks( - this: *mut GearHardware, -) -> *mut GearLogicalDisks { - (*this).m_logical_disks -} - -unsafe extern "thiscall" fn gear_hardware_get_memory(this: *mut GearHardware) -> *mut GearMemory { - (*this).m_memory -} - -unsafe extern "thiscall" fn gear_hardware_get_network(this: *mut GearHardware) -> *mut GearNetwork { - (*this).m_network -} - -unsafe extern "thiscall" fn gear_hardware_get_os(this: *mut GearHardware) -> *mut GearOS { - (*this).m_os -} - -unsafe extern "thiscall" fn gear_hardware_get_graphics( - this: *mut GearHardware, -) -> *mut GearGraphics { - if (*this).m_graphics.is_null() { - (*this).m_graphics = Box::into_raw(Box::new(GearGraphics::new())); - } - (*this).m_graphics -} - -unsafe extern "thiscall" fn gear_hardware_get_audio(this: *mut GearHardware) -> *mut GearAudio { - (*this).m_audio -} - -impl Default for GearHardware { - fn default() -> Self { - Self::new() - } -} - -impl GearHardware { - pub fn new() -> Self { - println!("systemdetection: Initializing GearHardware"); - GearHardware { - vtable: &GEAR_HARDWARE_VTABLE, - m_cpu: null_mut(), - m_logical_disks: null_mut(), - m_memory: null_mut(), - m_network: null_mut(), - m_os: null_mut(), - m_graphics: null_mut(), - m_audio: null_mut(), - } - } -} diff --git a/src/gear_score.rs b/src/gear_score.rs deleted file mode 100644 index 0cac48b..0000000 --- a/src/gear_score.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! GearScore - System scoring class -//! -//! Structure layout (20 bytes): -//! 0x00: vtable -//! 0x04: scores[0] (f32) - CPU score -//! 0x08: scores[1] (f32) - GPU score -//! 0x0C: confidence[0] (f32) - CPU confidence -//! 0x10: confidence[1] (f32) - GPU confidence -//! -//! VTable: [destructor, GetScore, ComputeScore] - -use std::ffi::c_void; - -/// Sentinel value for uncomputed scores (-FLT_MAX) -const SCORE_UNCOMPUTED: f32 = -3.4028235e38; - -/// GearScore class (20 bytes) -#[repr(C)] -pub struct GearScore { - vtable: *const GearScoreVTable, // offset 0x00 - scores: [f32; 2], // offset 0x04 (CPU, GPU) - confidence: [f32; 2], // offset 0x0C (CPU, GPU confidence) -} - -unsafe impl Send for GearScore {} -unsafe impl Sync for GearScore {} - -#[repr(C)] -struct GearScoreVTable { - destructor: unsafe extern "thiscall" fn(*mut GearScore, u8) -> *mut c_void, - get_score: unsafe extern "thiscall" fn(*mut GearScore, i32, i32, *mut f32) -> f32, - compute_score: unsafe extern "thiscall" fn(*mut GearScore, i32, i32) -> i32, -} - -static GEAR_SCORE_VTABLE: GearScoreVTable = GearScoreVTable { - destructor: gear_score_destructor, - get_score: gear_score_get_score, - compute_score: gear_score_compute_score, -}; - -unsafe extern "thiscall" fn gear_score_destructor(this: *mut GearScore, flags: u8) -> *mut c_void { - if flags & 1 != 0 { - let _ = Box::from_raw(this); - } - this as *mut c_void -} - -/// GetScore - returns score for given type, computing if needed -unsafe extern "thiscall" fn gear_score_get_score( - this: *mut GearScore, - score_type: i32, - param: i32, - confidence_out: *mut f32, -) -> f32 { - if !(0..2).contains(&score_type) { - return 0.0; - } - - let idx = score_type as usize; - - // Compute score if not yet computed (check for -FLT_MAX sentinel) - if (*this).scores[idx] == SCORE_UNCOMPUTED { - // Call vtable[2] (ComputeScore) like FC3 does - gear_score_compute_score(this, score_type, param); - } - - if !confidence_out.is_null() { - *confidence_out = (*this).confidence[idx]; - } - - (*this).scores[idx] -} - -/// ComputeScore - computes and stores score for given type -unsafe extern "thiscall" fn gear_score_compute_score( - this: *mut GearScore, - score_type: i32, - _param: i32, -) -> i32 { - if !(0..2).contains(&score_type) { - return score_type; - } - - let idx = score_type as usize; - - // Return good default scores (0.8 = 80%) - (*this).scores[idx] = 0.8; - (*this).confidence[idx] = 1.0; - - score_type -} - -impl Default for GearScore { - fn default() -> Self { - Self::new() - } -} - -impl GearScore { - pub fn new() -> Self { - println!("systemdetection: Initializing GearScore"); - GearScore { - vtable: &GEAR_SCORE_VTABLE, - scores: [SCORE_UNCOMPUTED; 2], - confidence: [1.0; 2], - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 8b9cf66..f09f79d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,57 +1,63 @@ //! Far Cry 2 systemdetection.dll drop-in replacement -mod gear_cpu; -mod gear_graphics; -mod gear_hardware; -mod gear_score; +mod gear; +mod patches; -pub use gear_hardware::GearHardware; -pub use gear_score::GearScore; +pub use gear::GearHardware; +pub use gear::GearScore; use std::ffi::c_void; use std::sync::OnceLock; -use windows::Win32::Foundation::{BOOL, HMODULE, TRUE}; +use windows::Win32::Foundation::HMODULE; use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; /// DLL entry point /// /// # Safety /// Called by Windows when the DLL is loaded/unloaded. -#[no_mangle] +#[unsafe(no_mangle)] #[allow(non_snake_case)] pub unsafe extern "system" fn DllMain( _hinst_dll: HMODULE, fdw_reason: u32, _lpv_reserved: *mut c_void, -) -> BOOL { +) -> i32 { if fdw_reason == DLL_PROCESS_ATTACH { #[cfg(debug_assertions)] - { + unsafe { init_console(); + } + #[cfg(debug_assertions)] + { println!("==========================================="); println!(" Far Cry 2 - systemdetection.dll replacement"); println!("==========================================="); } + + // Apply Dunia.dll patches + patches::apply_patches(); } - TRUE + 1 // TRUE } /// Initialize console for debug output #[cfg(debug_assertions)] unsafe fn init_console() { - use windows::core::PCSTR; use windows::Win32::System::Console::{AllocConsole, SetConsoleTitleA}; + use windows::core::PCSTR; - let _ = AllocConsole(); - let _ = SetConsoleTitleA(PCSTR::from_raw(c"FC2 SystemDetection".as_ptr() as *const u8)); + unsafe { + let _ = AllocConsole(); + let _ = SetConsoleTitleA(PCSTR::from_raw(c"FC2 SystemDetection".as_ptr() as *const u8)); - extern "C" { - fn freopen(filename: *const i8, mode: *const i8, stream: *mut c_void) -> *mut c_void; - fn __acrt_iob_func(index: u32) -> *mut c_void; - } + unsafe extern "C" { + fn freopen(filename: *const i8, mode: *const i8, stream: *mut c_void) -> *mut c_void; + fn __acrt_iob_func(index: u32) -> *mut c_void; + } - freopen(c"CONOUT$".as_ptr(), c"w".as_ptr(), __acrt_iob_func(1)); // stdout - freopen(c"CONOUT$".as_ptr(), c"w".as_ptr(), __acrt_iob_func(2)); // stderr + freopen(c"CONOUT$".as_ptr(), c"w".as_ptr(), __acrt_iob_func(1)); // stdout + freopen(c"CONOUT$".as_ptr(), c"w".as_ptr(), __acrt_iob_func(2)); // stderr + } } /// Global singleton for GearHardware @@ -64,12 +70,13 @@ static GEAR_SCORE: OnceLock> = OnceLock::new(); /// /// # Safety /// This function is called from C code and returns a raw pointer -#[no_mangle] +#[unsafe(no_mangle)] pub unsafe extern "C" fn GetHardwareInstance() -> *mut GearHardware { let hardware = GEAR_HARDWARE.get_or_init(|| { println!("systemdetection: Creating GearHardware instance"); Box::new(GearHardware::new()) }); + hardware.as_ref() as *const GearHardware as *mut GearHardware } @@ -77,11 +84,12 @@ pub unsafe extern "C" fn GetHardwareInstance() -> *mut GearHardware { /// /// # Safety /// This function is called from C code and returns a raw pointer -#[no_mangle] +#[unsafe(no_mangle)] pub unsafe extern "C" fn GetScoreInstance() -> *mut GearScore { let score = GEAR_SCORE.get_or_init(|| { println!("systemdetection: Creating GearScore instance"); Box::new(GearScore::new()) }); + score.as_ref() as *const GearScore as *mut GearScore } diff --git a/src/patches/hooks.rs b/src/patches/hooks.rs new file mode 100644 index 0000000..09b4584 --- /dev/null +++ b/src/patches/hooks.rs @@ -0,0 +1,62 @@ +//! Function hooks for Dunia.dll using MinHook +//! +//! This module provides detouring/hooking functionality to intercept +//! game functions and inject custom behavior. +//! +//! Currently no hooks are active - this is infrastructure for future use. + +#[allow(unused_imports)] +use crate::patches::sigscan::{Pattern, scan_module}; +#[allow(unused_imports)] +use minhook::{MH_STATUS, MinHook}; +#[allow(unused_imports)] +use std::ffi::c_void; +#[allow(unused_imports)] +use std::sync::OnceLock; + +/// Signature definitions for hookable functions +#[allow(dead_code)] +mod signatures { + // CFCXOptionGamePage::InitOptions - Game options page initialization + // sub esp, 160h | push ebx | push ebp | push esi | push edi | xor ebx, ebx + pub const INIT_OPTIONS: &str = "81 EC 60 01 00 00 53 55 56 57 33 DB"; + + // CreateSliderOption - Creates a slider widget + // mov eax, [esp+1Ch] | push ebx | push esi | mov esi, [esp+0Ch] + pub const CREATE_SLIDER: &str = "8B 44 24 1C 53 56 8B 74 24 0C"; +} + +/// Cached function addresses found via signature scanning +#[allow(dead_code)] +pub struct HookAddresses { + // Reserved for future hooks +} + +impl HookAddresses { + /// Scan for all hook target signatures + pub fn scan(_base: usize) -> Self { + // No hooks currently active + Self {} + } +} + +/// Install all function hooks +pub fn install_hooks(_addrs: &HookAddresses) -> Result<(), MH_STATUS> { + // No hooks currently active - infrastructure ready for future use + + #[cfg(debug_assertions)] + println!("hooks: No hooks to install (infrastructure ready)"); + + Ok(()) +} + +/// Cleanup hooks on unload +#[allow(dead_code)] +pub fn remove_hooks() { + #[cfg(debug_assertions)] + println!("hooks: Removing all hooks"); + + unsafe { + let _ = MinHook::disable_all_hooks(); + } +} diff --git a/src/patches/memory.rs b/src/patches/memory.rs new file mode 100644 index 0000000..d1ea380 --- /dev/null +++ b/src/patches/memory.rs @@ -0,0 +1,71 @@ +//! Memory patching utilities + +use std::ffi::c_void; +use windows::Win32::System::Memory::{ + PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS, VirtualProtect, +}; + +/// Write bytes to a memory address, handling page protection +pub fn write_bytes(address: usize, bytes: &[u8]) -> bool { + if bytes.is_empty() { + return true; + } + + let ptr = address as *mut u8; + let len = bytes.len(); + let mut old_protect = PAGE_PROTECTION_FLAGS(0); + + unsafe { + // Make memory writable + let protect_result = VirtualProtect( + ptr as *const c_void, + len, + PAGE_EXECUTE_READWRITE, + &mut old_protect, + ); + + if protect_result.is_err() { + #[cfg(debug_assertions)] + println!("patches: VirtualProtect failed for 0x{:08X}", address); + return false; + } + + // Write the bytes + std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len); + + // Restore original protection + let _ = VirtualProtect(ptr as *const c_void, len, old_protect, &mut old_protect); + } + + #[cfg(debug_assertions)] + println!("patches: Wrote {} bytes to 0x{:08X}", len, address); + + true +} + +/// Write a relative call instruction (E8 xx xx xx xx) +#[allow(dead_code)] +pub fn write_call(from: usize, to: usize) -> bool { + let relative = (to as isize) - (from as isize) - 5; + let mut bytes = [0u8; 5]; + bytes[0] = 0xE8; // CALL opcode + bytes[1..5].copy_from_slice(&(relative as i32).to_le_bytes()); + write_bytes(from, &bytes) +} + +/// Write a relative jump instruction (E9 xx xx xx xx) +#[allow(dead_code)] +pub fn write_jump(from: usize, to: usize) -> bool { + let relative = (to as isize) - (from as isize) - 5; + let mut bytes = [0u8; 5]; + bytes[0] = 0xE9; // JMP opcode + bytes[1..5].copy_from_slice(&(relative as i32).to_le_bytes()); + write_bytes(from, &bytes) +} + +/// Write NOP instructions +#[allow(dead_code)] +pub fn write_nops(address: usize, count: usize) -> bool { + let nops = vec![0x90u8; count]; + write_bytes(address, &nops) +} diff --git a/src/patches/mod.rs b/src/patches/mod.rs new file mode 100644 index 0000000..714fc83 --- /dev/null +++ b/src/patches/mod.rs @@ -0,0 +1,273 @@ +//! Dunia.dll runtime patches +//! +//! This module applies fixes to Dunia.dll at runtime, similar to Far Cry 2 Multi Fixer. +//! Since systemdetection.dll is loaded by Dunia.dll before initialization completes, +//! we can safely patch Dunia.dll from here. +//! +//! All signature scans are performed upfront before any patches are applied, +//! ensuring that patches don't corrupt signatures we haven't scanned yet. + +mod hooks; +mod memory; +mod sigscan; + +use memory::write_bytes; +use sigscan::{Pattern, scan_module}; +use windows::Win32::System::LibraryLoader::GetModuleHandleA; +use windows::core::PCSTR; + +/// Signature definitions +mod signatures { + // Jackal Tapes: cmp byte ptr [esi+74h], 0 | jnz short | cmp ecx, edx | jnz + pub const JACKAL_TAPES: &str = "80 7E 74 00 75 ?? 3B CA 75"; + + // DevMode: cmp byte ptr [ecx+offset], 0 | mov edx, [esp+arg] | jnz + pub const DEVMODE: &str = "80 79 ?? 00 8B 54 24 ?? 75"; + + // Predecessor Tapes: mov ecx, [ecx+0Ch] | test ecx, ecx | jz + // Function checks online service pointer, patch makes it always skip the null check + pub const PREDECESSOR_TAPES: &str = "8B 49 0C 85 C9 74 ?? 8B 44 24"; + + // Machetes: sub esp, ?? | push ebx | lea eax, [esp+??] | push eax | push + // This is the prologue of IsMachetesUnlocked function + pub const MACHETES: &str = "83 EC ?? 53 8D 44 24 ?? 50 68"; + + // No Blinking Items: String literals to corrupt + pub const MESH_HIGHLIGHT: &str = "4D 65 73 68 5F 48 69 67 68 6C 69 67 68 74"; // "Mesh_Highlight" + pub const ARCH_BLINK: &str = "61 72 63 68 42 6C 69 6E 6B"; // "archBlink" + pub const SAVE_DISK: &str = "67 61 64 67 65 74 73 2E 4F 62 6A 65 63 74 69 76 65 49 63 6F 6E 73 2E 53 61 76 65 44 69 73 6B"; // "gadgets.ObjectiveIcons.SaveDisk" +} + +/// Cached addresses from signature scans (found before patching) +#[allow(dead_code)] +struct PatchAddresses { + jackal_tapes: Option, + devmode: Option, + predecessor_tapes: Option, + machetes: Option, + mesh_highlight: Option, + arch_blink: Option, + save_disk: Option, +} + +impl PatchAddresses { + /// Scan for all signatures upfront, before any patches are applied + fn scan(base: usize) -> Self { + #[cfg(debug_assertions)] + println!("patches: Scanning for all signatures..."); + + let jackal_tapes = + Pattern::parse(signatures::JACKAL_TAPES).and_then(|p| scan_module(base, &p)); + let devmode = Pattern::parse(signatures::DEVMODE).and_then(|p| scan_module(base, &p)); + let predecessor_tapes = + Pattern::parse(signatures::PREDECESSOR_TAPES).and_then(|p| scan_module(base, &p)); + let machetes = Pattern::parse(signatures::MACHETES).and_then(|p| scan_module(base, &p)); + let mesh_highlight = + Pattern::parse(signatures::MESH_HIGHLIGHT).and_then(|p| scan_module(base, &p)); + let arch_blink = Pattern::parse(signatures::ARCH_BLINK).and_then(|p| scan_module(base, &p)); + let save_disk = Pattern::parse(signatures::SAVE_DISK).and_then(|p| scan_module(base, &p)); + + #[cfg(debug_assertions)] + { + println!( + "patches: Jackal Tapes: {:?}", + jackal_tapes.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: DevMode: {:?}", + devmode.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: Predecessor Tapes: {:?}", + predecessor_tapes.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: Machetes: {:?}", + machetes.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: Mesh_Highlight: {:?}", + mesh_highlight.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: archBlink: {:?}", + arch_blink.map(|a| format!("0x{:08X}", a)) + ); + println!( + "patches: SaveDisk: {:?}", + save_disk.map(|a| format!("0x{:08X}", a)) + ); + } + + Self { + jackal_tapes, + devmode, + predecessor_tapes, + machetes, + mesh_highlight, + arch_blink, + save_disk, + } + } +} + +/// Apply all enabled patches to Dunia.dll +pub fn apply_patches() { + // Get Dunia.dll base address + let dunia_base = + unsafe { GetModuleHandleA(PCSTR::from_raw(c"Dunia.dll".as_ptr() as *const u8)) }; + + let Ok(dunia) = dunia_base else { + #[cfg(debug_assertions)] + println!("patches: Dunia.dll not loaded, skipping patches"); + return; + }; + + if dunia.is_invalid() { + #[cfg(debug_assertions)] + println!("patches: Dunia.dll handle invalid, skipping patches"); + return; + } + + let base = dunia.0 as usize; + + #[cfg(debug_assertions)] + println!("patches: Dunia.dll base = 0x{:08X}", base); + + // IMPORTANT: Scan for ALL signatures BEFORE applying any patches + // This prevents patches from corrupting signatures we haven't found yet + let addrs = PatchAddresses::scan(base); + let hook_addrs = hooks::HookAddresses::scan(base); + + // Now apply patches using the cached addresses + apply_jackal_tapes_fix(&addrs); + // apply_no_blinking_items(&addrs); + apply_devmode_unlock(&addrs); + apply_predecessor_tapes_unlock(&addrs); + apply_machetes_unlock(&addrs); + + // Install function hooks (for FOV slider, etc.) + if let Err(_e) = hooks::install_hooks(&hook_addrs) { + #[cfg(debug_assertions)] + println!("patches: Failed to install hooks: {:?}", _e); + } +} + +/// Fix: Jackal Tapes - All tapes in Southern map play correct recordings +/// +/// The bug: In the Southern map, some Jackal tape pickups play incorrect recordings. +/// This is caused by an incorrect jump offset in the tape lookup logic. +fn apply_jackal_tapes_fix(addrs: &PatchAddresses) { + let Some(addr) = addrs.jackal_tapes else { + #[cfg(debug_assertions)] + println!("patches: Jackal Tapes signature not found, skipping"); + return; + }; + + // The jump offset byte is at offset 5 in the pattern (after "75") + let patch_addr = addr + 5; + + #[cfg(debug_assertions)] + println!("patches: Applying Jackal Tapes fix at 0x{:08X}", patch_addr); + + // Change jump offset (add 0x10 to fix tape index calculation) + let current = unsafe { *(patch_addr as *const u8) }; + write_bytes(patch_addr, &[current.wrapping_add(0x10)]); +} + +/// Visual: No Blinking Items - Remove highlight blinking on interactables +/// +/// Patches string literals to break the shader lookup, disabling the blinking effect. +#[allow(dead_code)] +fn apply_no_blinking_items(addrs: &PatchAddresses) { + // Patch "Mesh_Highlight" - change '_' to '.' + if let Some(addr) = addrs.mesh_highlight { + #[cfg(debug_assertions)] + println!("patches: Patching Mesh_Highlight at 0x{:08X}", addr); + write_bytes(addr + 4, &[0x2E]); // offset 4 = '_' + } + + // Patch "archBlink" - change 'k' to '.' + if let Some(addr) = addrs.arch_blink { + #[cfg(debug_assertions)] + println!("patches: Patching archBlink at 0x{:08X}", addr); + write_bytes(addr + 8, &[0x2E]); // offset 8 = 'k' + } + + // Patch "gadgets.ObjectiveIcons.SaveDisk" - change 'k' to '.' + if let Some(addr) = addrs.save_disk { + #[cfg(debug_assertions)] + println!("patches: Patching SaveDisk at 0x{:08X}", addr); + write_bytes(addr + 30, &[0x2E]); // offset 30 = 'k' + } +} + +/// Fix: DevMode Unlock - Enable developer console commands +/// +/// Patches CConsoleService_IsCommandVisible to always skip the devmode check, +/// making all "ConsoleDeveloperOnly" commands visible and usable. +fn apply_devmode_unlock(addrs: &PatchAddresses) { + let Some(addr) = addrs.devmode else { + #[cfg(debug_assertions)] + println!("patches: DevMode signature not found, skipping"); + return; + }; + + // The jnz opcode is at offset 8 in the pattern + let jnz_addr = addr + 8; + + #[cfg(debug_assertions)] + println!("patches: Applying DevMode unlock at 0x{:08X}", jnz_addr); + + // Change jnz (0x75) to jmp (0xEB) - always skip the devmode check + write_bytes(jnz_addr, &[0xEB]); +} + +/// Unlock: Predecessor Tapes - Unlock 7 bonus missions +/// +/// The predecessor tapes were originally tied to an online Ubisoft account. +/// This patches IsPredecessorTapesUnlocked to always return true. +fn apply_predecessor_tapes_unlock(addrs: &PatchAddresses) { + let Some(addr) = addrs.predecessor_tapes else { + #[cfg(debug_assertions)] + println!("patches: Predecessor Tapes signature not found, skipping"); + return; + }; + + // Pattern: 8B 49 0C 85 C9 74 ?? 8B 44 24 + // The jz opcode is at offset 5, jump offset at offset 6 + // We change "74 ??" (jz) to "EB 0E" (jmp +14) to skip the null check + let jz_addr = addr + 5; + + #[cfg(debug_assertions)] + println!( + "patches: Applying Predecessor Tapes unlock at 0x{:08X}", + jz_addr + ); + + // Change jz (0x74) to jmp (0xEB), and set offset to 0x0E + write_bytes(jz_addr, &[0xEB, 0x0E]); +} + +/// Unlock: Machetes - Unlock 2 bonus machete skins +/// +/// The bonus machetes were originally unlocked via a registry key. +/// This patches IsMachetesUnlocked to always return true. +fn apply_machetes_unlock(addrs: &PatchAddresses) { + let Some(addr) = addrs.machetes else { + #[cfg(debug_assertions)] + println!("patches: Machetes signature not found, skipping"); + return; + }; + + // Signature is at function prologue (sub esp | push ebx | lea eax | push eax | push) + // Offset from function start to "mov al, bl" (8A C3) is 0x69 (105 bytes) + // We change "mov al, bl" to "mov al, 1" (B0 01) to always return true + let patch_addr = addr + 0x69; + + #[cfg(debug_assertions)] + println!("patches: Applying Machetes unlock at 0x{:08X}", patch_addr); + + // Change "mov al, bl" (8A C3) to "mov al, 1" (B0 01) + write_bytes(patch_addr, &[0xB0, 0x01]); +} diff --git a/src/patches/sigscan.rs b/src/patches/sigscan.rs new file mode 100644 index 0000000..6dfd5bd --- /dev/null +++ b/src/patches/sigscan.rs @@ -0,0 +1,136 @@ +//! Signature scanning for pattern matching in memory +//! +//! Supports wildcard bytes using `??` in patterns. + +use windows::Win32::System::Memory::{ + MEMORY_BASIC_INFORMATION, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_READONLY, + PAGE_READWRITE, VirtualQuery, +}; +use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO}; + +/// A parsed signature pattern with optional wildcard bytes +pub struct Pattern { + bytes: Vec, + mask: Vec, // true = must match, false = wildcard +} + +impl Pattern { + /// Parse a pattern string like "80 79 ?? ?? 8B 54" + pub fn parse(pattern: &str) -> Option { + let mut bytes = Vec::new(); + let mut mask = Vec::new(); + + for part in pattern.split_whitespace() { + if part == "??" || part == "?" { + bytes.push(0); + mask.push(false); + } else { + let byte = u8::from_str_radix(part, 16).ok()?; + bytes.push(byte); + mask.push(true); + } + } + + if bytes.is_empty() { + return None; + } + + Some(Self { bytes, mask }) + } + + /// Check if pattern matches at the given memory location + /// + /// # Safety + /// Caller must ensure `ptr` points to readable memory of at least `self.len()` bytes. + #[inline] + pub unsafe fn matches_at(&self, ptr: *const u8) -> bool { + for (i, (&pattern_byte, &must_match)) in self.bytes.iter().zip(self.mask.iter()).enumerate() + { + if must_match && unsafe { *ptr.add(i) } != pattern_byte { + return false; + } + } + true + } + + /// Get the length of the pattern + pub fn len(&self) -> usize { + self.bytes.len() + } +} + +/// Scan a memory region for a pattern +/// +/// # Safety +/// Caller must ensure `start` to `start + size` is valid readable memory. +pub unsafe fn scan_region(start: usize, size: usize, pattern: &Pattern) -> Option { + if size < pattern.len() { + return None; + } + + let end = start + size - pattern.len(); + let mut addr = start; + + while addr <= end { + if unsafe { pattern.matches_at(addr as *const u8) } { + return Some(addr); + } + addr += 1; + } + + None +} + +/// Scan a module's executable sections for a pattern +/// +/// Returns the address of the first match, or None if not found. +pub fn scan_module(module_base: usize, pattern: &Pattern) -> Option { + // Initialize system info (not currently used but may be useful later) + let _page_size = unsafe { + let mut si = SYSTEM_INFO::default(); + GetSystemInfo(&mut si); + si.dwPageSize as usize + }; + + let mut addr = module_base; + let max_scan = 0x10000000; // 256MB max scan range + + while addr < module_base + max_scan { + let mut mbi = MEMORY_BASIC_INFORMATION::default(); + let result = unsafe { + VirtualQuery( + Some(addr as *const _), + &mut mbi, + size_of::(), + ) + }; + + if result == 0 { + break; + } + + // Check if this is a readable code section + let protect = mbi.Protect; + let is_readable = protect == PAGE_EXECUTE_READ + || protect == PAGE_EXECUTE_READWRITE + || protect == PAGE_READONLY + || protect == PAGE_READWRITE; + + if is_readable && mbi.RegionSize > 0 { + let region_start = mbi.BaseAddress as usize; + let region_size = mbi.RegionSize; + + if let Some(found) = unsafe { scan_region(region_start, region_size, pattern) } { + return Some(found); + } + } + + // Move to next region + addr = mbi.BaseAddress as usize + mbi.RegionSize; + if addr < mbi.BaseAddress as usize { + break; // Overflow protection + } + } + + None +}