diff --git a/.github/workflows/injector.yml b/.github/workflows/injector.yml index 863081a..0073066 100644 --- a/.github/workflows/injector.yml +++ b/.github/workflows/injector.yml @@ -6,18 +6,85 @@ on: paths: - "injector/**" - "src/**" + - "scripts/**" + - "build.gradle" - ".github/workflows/injector.yml" jobs: - payload: - name: Payload jar - runs-on: ubuntu-latest + core-imgui: + name: ImGui (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: windows-x86_64 + toolchain: windows-x86_64 + - os: ubuntu-latest + platform: windows-aarch64 + toolchain: windows-aarch64 + - os: ubuntu-latest + platform: linux-x86_64 + - os: ubuntu-latest + platform: linux-aarch64 + toolchain: linux-aarch64 + - os: macos-14 + platform: macos-universal + toolchain: macos-universal steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: "21" + - name: Install native build tools + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake g++ g++-aarch64-linux-gnu + - name: Install Windows cross compiler + if: startsWith(matrix.platform, 'windows-') + run: | + curl -fL --retry 3 https://github.com/mstorsjo/llvm-mingw/releases/download/20261020/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64.tar.xz -o "$RUNNER_TEMP/llvm-mingw.tar.xz" + tar -xf "$RUNNER_TEMP/llvm-mingw.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64/bin" >> "$GITHUB_PATH" + - name: Build core-only ImGui + env: + TOOLCHAIN: ${{ matrix.toolchain }} + run: | + if [ -n "$TOOLCHAIN" ]; then + python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}" --toolchain "scripts/imgui-core/toolchains/$TOOLCHAIN.cmake" + else + python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}" + fi + - name: Exercise core JNI with the UI font + if: matrix.platform == 'linux-x86_64' || matrix.platform == 'macos-universal' + run: | + java -Dimgui.library.path="$PWD/build/core-natives/${{ matrix.platform }}" -cp build/imgui-core/deps/imgui-java-binding-1.92.7.1.jar scripts/imgui-core/SmokeTest.java src/main/resources/prestige_ui/fonts/DMSans-Regular.ttf + - uses: actions/upload-artifact@v4 + with: + name: imgui-${{ matrix.platform }} + path: build/core-natives/* + if-no-files-found: error + + payload: + name: Payload jar + needs: core-imgui + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check release packaging + run: python3 scripts/test_release.py + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - uses: actions/download-artifact@v4 + with: + pattern: imgui-* + merge-multiple: true + path: build/core-natives - name: Build remapped jar run: | chmod +x ./gradlew @@ -33,42 +100,54 @@ jobs: if-no-files-found: error injector: - name: Injector (${{ matrix.target }}) + name: OpenPrestige (${{ matrix.platform.target }}) needs: payload - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.platform.os }} strategy: fail-fast: false matrix: - include: + platform: - os: windows-latest target: x86_64-pc-windows-msvc name: windows-x86_64 files: | - injector/target/release/PrestigeInjector.exe + injector/target/release/OpenPrestigeInjector.exe injector/target/release/prestige_native.dll - os: ubuntu-latest target: x86_64-unknown-linux-gnu name: linux-x86_64 files: | - injector/target/release/PrestigeInjector + injector/target/release/OpenPrestigeInjector + injector/target/release/libprestige_native.so + - os: windows-11-arm + target: aarch64-pc-windows-msvc + name: windows-aarch64 + files: | + injector/target/release/OpenPrestigeInjector.exe + injector/target/release/prestige_native.dll + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + name: linux-aarch64 + files: | + injector/target/release/OpenPrestigeInjector injector/target/release/libprestige_native.so - os: macos-13 target: x86_64-apple-darwin name: macos-x86_64 files: | - injector/target/release/PrestigeInjector + injector/target/release/OpenPrestigeInjector injector/target/release/libprestige_native.dylib - os: macos-14 target: aarch64-apple-darwin name: macos-aarch64 files: | - injector/target/release/PrestigeInjector + injector/target/release/OpenPrestigeInjector injector/target/release/libprestige_native.dylib steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - targets: ${{ matrix.target }} + targets: ${{ matrix.platform.target }} - name: Download payload jar uses: actions/download-artifact@v4 with: @@ -81,6 +160,6 @@ jobs: - name: Upload injector uses: actions/upload-artifact@v4 with: - name: injector-${{ matrix.name }} - path: ${{ matrix.files }} + name: openprestige-${{ matrix.platform.name }} + path: ${{ matrix.platform.files }} if-no-files-found: error diff --git a/.gitignore b/.gitignore index c3b5866..1dbf406 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ openvape.zip /injector/target/ /tools /examples + +# Python build helpers +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 20fc6b1..bd2bfaa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Open Prestige +This repository builds the fully offline OpenPrestige CLI with its payload embedded in the native library. The graphical launcher and runtime downloads are maintained in the separate Prestige repository. See [injector build instructions](injector/README.md). + A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11. --- diff --git a/build-injector.ps1 b/build-injector.ps1 index 3c251cd..945eae3 100644 --- a/build-injector.ps1 +++ b/build-injector.ps1 @@ -1,6 +1,4 @@ -# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader DLL, -# and collects PrestigeInjector.exe + prestige_native.dll in dist\. -# native tools write progress/notes to stderr; failures are detected through $LASTEXITCODE instead +# Build OpenPrestige and collect the host binaries in dist/. $ErrorActionPreference = 'Continue' Set-Location $PSScriptRoot if (Test-Path 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot') { $env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot' } @@ -9,6 +7,6 @@ if ($LASTEXITCODE -ne 0) { throw 'gradle remapJar failed' } cargo build --release --manifest-path injector\Cargo.toml if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' } New-Item -ItemType Directory -Force dist | Out-Null -Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force +Copy-Item injector\target\release\OpenPrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force Get-ChildItem build\libs\prestige-*.jar | Where-Object { $_.Name -notmatch '-(dev|sources)\.jar$' } | Copy-Item -Destination dist\ -Force -Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll + dist\prestige-*.jar (Fabric mod)' +Write-Host 'Done: dist\OpenPrestigeInjector.exe + native library + Fabric mod JAR' diff --git a/build-injector.sh b/build-injector.sh index cd99bdf..3896858 100644 --- a/build-injector.sh +++ b/build-injector.sh @@ -1,34 +1,24 @@ #!/bin/sh -# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader -# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/. -# Optional: set JAVA_HOME to pick the JDK used by Gradle. +# Build OpenPrestige for the current host. Optional JAVA_HOME selects the Gradle JDK. set -eu - cd "$(dirname "$0")" - case "$(uname -s)" in - Linux) LIB=libprestige_native.so ;; + Linux) LIB=libprestige_native.so ;; Darwin) LIB=libprestige_native.dylib ;; - *) echo "unsupported OS: $(uname -s) (use build-injector.ps1 on Windows)" >&2; exit 1 ;; + *) echo "Use build-injector.ps1 on Windows" >&2; exit 1 ;; esac - if [ -n "${JAVA_HOME:-}" ]; then export JAVA_HOME PATH="$JAVA_HOME/bin:$PATH" export PATH fi - ./gradlew remapJar --console=plain cargo build --release --manifest-path injector/Cargo.toml - mkdir -p dist -cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/ -chmod +x dist/PrestigeInjector - -# the same remapped jar is the Fabric mod (drop it into mods/) +cp -f "injector/target/release/OpenPrestigeInjector" "injector/target/release/$LIB" dist/ +chmod +x dist/OpenPrestigeInjector for jar in build/libs/prestige-*.jar; do case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac cp -f "$jar" dist/ done - -echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)" +echo "Done: dist/OpenPrestigeInjector + dist/$LIB + Fabric mod JAR" diff --git a/build-release.bat b/build-release.bat index 8372f19..4657f5c 100644 --- a/build-release.bat +++ b/build-release.bat @@ -1,12 +1,8 @@ @echo off -rem Builds release zips for every platform into dist\release\ (run on Windows): -rem prestige--windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar -rem prestige--linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro) -rem prestige--macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon) -rem prestige-.jar the Fabric mod on its own -rem SHA256SUMS.txt -rem Linux and macOS are cross-compiled with cargo-zigbuild + zig; both are installed on first run if missing. -rem Requires: Rust (rustup), Python 3, JDK 21. +rem Builds fully offline OpenPrestige bundles. +rem OpenPrestige CLI + native library with embedded payload +rem Requires Rust with MSVC x64/ARM64 tools, Python 3 and JDK 21. +rem Fetches pinned ImGui sources and cross-compiles the small JNI libraries. setlocal EnableExtensions cd /d "%~dp0" set "SELF=%~f0" @@ -23,55 +19,55 @@ where cargo >nul 2>&1 || (echo cargo not found: install Rust from https://rustup where python >nul 2>&1 || (echo python not found: install Python 3 from https://python.org & goto :fail) python -m ziglang version >nul 2>&1 || ( echo === Installing zig ^(pip ziglang^) - python -m pip install --user ziglang || goto :fail + python -m pip install --user ziglang==0.16.0 || goto :fail ) where cargo-zigbuild >nul 2>&1 || ( echo === Installing cargo-zigbuild cargo install --locked cargo-zigbuild || goto :fail ) -rustup target add x86_64-pc-windows-msvc x86_64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail +rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail rem zigbuild's linker wrappers break on paths with spaces, so keep its cache on the 8.3 short path if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild" for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI" +rem ---- core ImGui JNI libraries ------------------------------------------ +echo === Fetching sources and building small ImGui libraries +python scripts\build_release_imgui.py || goto :fail + rem ---- payload jar ------------------------------------------------------- echo === Building payload jar call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail set "JAR=" for /f "delims=" %%j in ('dir /b /o-d "build\libs\prestige-*.jar" ^| findstr /v /i /c:"-dev.jar" /c:"-sources.jar"') do if not defined JAR set "JAR=%CD%\build\libs\%%j" if not defined JAR (echo no remapped jar in build\libs & goto :fail) -rem every target embeds this exact jar +rem The native library uses this exact payload JAR. set "PRESTIGE_PAYLOAD=%JAR%" -if exist "%OUT%" rmdir /s /q "%OUT%" -mkdir "%OUT%" || goto :fail + +if not exist "%OUT%" mkdir "%OUT%" || goto :fail rem ---- native builds ----------------------------------------------------- -echo === Windows x86_64 -cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail -call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || goto :fail - -echo === Linux x86_64 (glibc 2.17+) -cargo zigbuild --release --manifest-path injector\Cargo.toml --target x86_64-unknown-linux-gnu.2.17 || goto :fail -call :package linux-x86_64 x86_64-unknown-linux-gnu PrestigeInjector libprestige_native.so || goto :fail - -echo === macOS universal (x86_64 + arm64) -cargo zigbuild --release --manifest-path injector\Cargo.toml --target universal2-apple-darwin || goto :fail -call :package macos-universal universal2-apple-darwin PrestigeInjector libprestige_native.dylib || goto :fail +call :build windows-x86_64 x86_64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail +call :build windows-aarch64 aarch64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail +call :build linux-x86_64 x86_64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild x86_64-unknown-linux-gnu.2.17 || goto :fail +call :build linux-aarch64 aarch64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild aarch64-unknown-linux-gnu.2.17 || goto :fail +call :build macos-universal universal2-apple-darwin OpenPrestigeInjector libprestige_native.dylib zigbuild || goto :fail copy /y "%JAR%" "%OUT%\" >nul || goto :fail python scripts\package_release.py checksums "%OUT%" || goto :fail - echo. echo === Done: %OUT% -dir /b "%OUT%" call :maybe_pause exit /b 0 -rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file -:package -python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%" +rem platform, target directory, executable, library, cargo command, optional versioned target +:build +set "BUILD_TARGET=%~2" +if not "%~6"=="" set "BUILD_TARGET=%~6" +echo === OpenPrestige %~1 (offline) +cargo %~5 --release --manifest-path injector\Cargo.toml --target "%BUILD_TARGET%" || exit /b 1 +python scripts\package_release.py offline "%OUT%" "%VERSION%" %~1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%" exit /b %ERRORLEVEL% :fail diff --git a/build.gradle b/build.gradle index ba7f31e..c2cb2c7 100644 --- a/build.gradle +++ b/build.gradle @@ -87,12 +87,29 @@ java { } jar { + // Optional custom native builds take precedence over the upstream binaries. + from('build/core-natives') { + into 'prestige-natives' + } + from('build/arm-natives') { + into 'prestige-natives' + } // Unpack the shaded libs (imgui classes + native dll, ClassTransform) into the mod jar. from({ configurations.shade.collect { it.isDirectory() ? it : zipTree(it) } }) { exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA' exclude 'module-info.class', 'META-INF/versions/**' + exclude 'imgui/extension/**' + if (file('build/core-natives/windows-x86_64/imgui-java64.dll').isFile()) { + exclude 'io/imgui/java/native-bin/imgui-java64.dll' + } + if (file('build/core-natives/linux-x86_64/libimgui-java64.so').isFile()) { + exclude 'io/imgui/java/native-bin/libimgui-java64.so' + } + if (file('build/core-natives/macos-universal/libimgui-java64.dylib').isFile()) { + exclude 'io/imgui/java/native-bin/libimgui-java64.dylib' + } } from({ zipTree(configurations.engineMappings.singleFile) }) { include 'mappings/mappings.tiny' diff --git a/injector/Cargo.lock b/injector/Cargo.lock index 8cb5a39..2e58461 100644 --- a/injector/Cargo.lock +++ b/injector/Cargo.lock @@ -3,7 +3,7 @@ version = 4 [[package]] -name = "prestige-injector" +name = "openprestige-injector" version = "0.1.0" [[package]] diff --git a/injector/README.md b/injector/README.md index f0d7213..a20063c 100644 --- a/injector/README.md +++ b/injector/README.md @@ -1,111 +1,66 @@ -# Prestige injector (Rust) +# OpenPrestige offline injector -`PrestigeInjector` loads the native loader library (`prestige_native.dll` / `libprestige_native.so` / `libprestige_native.dylib`) -into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x, Fabric API installed). It uses the same overall -method as the openvape bundle, written from scratch in Rust with no external crates. - -## Supported platforms - -| OS | Architectures | Injector binary | Loader library | -|---------|--------------------|-----------------------|-----------------------------| -| Windows | x86_64 | `PrestigeInjector.exe`| `prestige_native.dll` | -| Linux | x86_64 | `PrestigeInjector` | `libprestige_native.so` | -| macOS | x86_64, arm64 | `PrestigeInjector` | `libprestige_native.dylib` | - -The injector and library must be built for the same architecture as the target JVM. - -## How it works - -1. **Injector** lists the running Minecraft JVMs, then loads the library into the chosen process. - - **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen - process through `CreateRemoteThread` and confirms the module is mapped. - - **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd JVMTI.agent_load`). The injector - creates `.attach_pid` and sends `SIGQUIT` to the JVM, connects to its attach socket (`/tmp/.java_pid`, under - `$TMPDIR` on macOS) and asks it to load the library. The JVM `dlopen`s `libprestige_native` and calls its - `Agent_OnAttach` entry point. No ptrace, no root, no per-architecture shellcode. - Processes are found via `/proc` (Linux) or `ps` (macOS); there are no window titles, so the picker shows the pid and - instance name. -2. **The native library** only spawns a worker thread from its entry point (`DllMain` on Windows, `Agent_OnAttach` on - unix, which also hands over the `JavaVM*`). The worker: - - waits for the JVM (Windows: `jvm.dll`, `JNI_GetCreatedJavaVMs`), attaches, acquires the JVMTI redefine/retransform capabilities; - - writes the embedded payload jar (the remapped `prestige-*.jar`) to the temp directory under `PrestigeLoader`; - - looks for Fabric through the render thread's context class loader, then the system and other thread loaders; - - `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the launcher's game class loader; - - registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`. -3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes - (JVMTI retransform capture), runs the hooks through `InPlaceTransformer` and redefines the class. The bundled ClassTransform - inlines hook handlers into the target methods, so normally nothing is added to the class and a plain redefine works. If a - transformer ever does add methods (a running JVM cannot), they are moved into generated `dev.zprestige.prestige.hooks.H*` - helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles. - Finally the Prestige and ClickGUI entrypoints run on the client thread. +This repository contains the original CLI injector, native library with embedded payload and game +sources. The graphical launcher and runtime downloader are maintained in the separate Prestige +repository. There is no GUI or HTTP dependency in this injector and no edition build flag. ## Build -The payload jar is embedded with `include_bytes!`, so each target needs its own build. Both scripts below build for the host OS only and run -`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`. +Run `build-injector.ps1` on Windows or `build-injector.sh` on Linux/macOS. Both build the Fabric JAR, +then the native library and CLI, collecting the results in `dist/`. The native library always embeds +the exact remapped JAR. The loose JAR is provided for optional installation as a Fabric mod. -```powershell -.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll +Run `build-release.bat` on Windows to build all platforms. `dist/release/` contains: + +```text +openprestige--windows-x86_64.zip +openprestige--windows-aarch64.zip +openprestige--linux-x86_64.zip +openprestige--linux-aarch64.zip +openprestige--macos-universal.zip +prestige-.jar +SHA256SUMS.txt ``` -```sh -./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib} -``` - -To build release zips for every platform from Windows, run `build-release.bat`. It cross-compiles Linux (x86_64, -glibc 2.17+, so one build covers every mainstream distro) and macOS (universal x86_64 + arm64) with `cargo-zigbuild`, installing -zig and cargo-zigbuild on first run, and writes `dist\release\prestige--{windows-x86_64,linux-x86_64,macos-universal}.zip`, -the bare mod jar and `SHA256SUMS.txt`. - -Set `JAVA_HOME` beforehand to choose the JDK for Gradle (`build-injector.sh` honours it). The GitHub Actions workflow -`.github/workflows/injector.yml` builds and tests all four targets (Windows x86_64, Linux x86_64, macOS x86_64, macOS arm64) -and uploads them as artifacts. +Each ZIP contains `OpenPrestigeInjector` (or `.exe`), the native library, and a README. +The CI injector workflow builds only OpenPrestige. Build dependencies require internet access; +the resulting release bundle needs no runtime downloads. ## Use -Start a Fabric 1.21.11 instance, then run: +Extract the complete platform ZIP, start Minecraft, then run: ```sh -PrestigeInjector # interactive: pick the game from the list -PrestigeInjector [lib path] # inject into a specific pid, optionally with an explicit library path +OpenPrestigeInjector # Up/Down selects, Enter injects, Esc quits +OpenPrestigeInjector [lib path] # optional explicit library path ``` -On Windows this is `PrestigeInjector.exe`. By default the library is looked up beside the injector. +Keep the native library beside the executable. The CLI uses that library and never looks in +Prestige's downloaded runtime cache. It has no update checks or `--download` command. +The library writes its embedded JAR locally so the JVM can load it. No external JAR is needed for +injection. Linux/macOS require a terminal, the same user as the game, and JVM attach enabled. -### Dawn and Lunar Client 1.21.11 +## Platforms and requirements -Use a 1.21.11 Fabric profile in Dawn or enable Lunar Client's Fabric add-on for 1.21.11. The target -profile needs Fabric Loader 0.19.x and Fabric API. Wait for the title screen before selecting the game -in the injector. The default Prestige menu key is Right Shift, which may overlap with a launcher keybind. +Windows and Linux support x86_64 and ARM64. macOS supports Intel and Apple Silicon. +The injector, library and JVM architectures must match. ARM64 execution needs hardware testing. +The game code currently targets Minecraft 1.21.11 Fabric, Fabric Loader 0.19+ and Fabric API. -For Lunar, the native log should identify `GenesisClassLoader`. Check the game log for `hooks installed` -and `client initialised`. Hook counts refer to classes with changed bytecode. A skipped or failed hook -can leave its feature unavailable even if the menu opens. +Build tools require Rust, Python 3 and JDK 21. On Windows, the full release build also requires +MSVC build tools for x64 and ARM64. `build-release.bat` fetches pinned ImGui sources and builds +the small JNI libraries before building the remapped JAR. It installs Zig and cargo-zigbuild if +needed, then cross-compiles Linux x64/ARM64 and universal macOS. -An offline JVM replay accepted all 42 changed classes from Lunar's installed 1.21.11 cache after -recalculating stack and local limits on the cached classes. Live injection and Dawn's in-game profile -still need smoke testing. +Both repositories currently start from the same game sources and use the compatible `.openprestige` +configuration and log locations. They are independent repositories, without submodules, shared +working files or edition build flags. Game code fixes must be ported between repositories explicitly. -## Install as a mod instead +## User data -The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the -`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS. -Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips. +| OS | Location | +| --- | --- | +| Windows | `%APPDATA%\.openprestige` | +| macOS | `~/Library/Application Support/.openprestige` | +| Linux | `$XDG_DATA_HOME/.openprestige` or `~/.local/share/.openprestige` | -## Requirements (Linux / macOS) - -- Run the injector as the **same user** as the game. -- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`. -- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs - running under Rosetta. -- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported. - -## Known limits - -- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is - not `dlclose`d (unmapping code the thread is still returning into would crash the JVM). On Windows the library is freed. -- Linux and macOS support is compile-checked in CI; test on a real host before relying on it. - -## Log - -`prestige-native.log` is written beside the loader library, plus `[Prestige-Inject]` lines in the game log. +Configs live in `configs/`, logs in `logs/`, and local runtime files in `runtime/`. diff --git a/injector/crates/injector/Cargo.toml b/injector/crates/injector/Cargo.toml index 3987c70..ec5aa75 100644 --- a/injector/crates/injector/Cargo.toml +++ b/injector/crates/injector/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "prestige-injector" +name = "openprestige-injector" version = "0.1.0" edition = "2021" [[bin]] -name = "PrestigeInjector" +name = "OpenPrestigeInjector" path = "src/main.rs" diff --git a/injector/crates/injector/build.rs b/injector/crates/injector/build.rs new file mode 100644 index 0000000..9449f41 --- /dev/null +++ b/injector/crates/injector/build.rs @@ -0,0 +1,11 @@ +fn main() { + let properties = "../../../gradle.properties"; + println!("cargo:rerun-if-changed={properties}"); + let contents = std::fs::read_to_string(properties).expect("read gradle.properties"); + let version = contents + .lines() + .find_map(|line| line.strip_prefix("mod_version=")) + .expect("mod_version is required") + .trim(); + println!("cargo:rustc-env=PRESTIGE_VERSION={version}"); +} diff --git a/injector/crates/injector/src/main.rs b/injector/crates/injector/src/main.rs index 96949e2..e353f64 100644 --- a/injector/crates/injector/src/main.rs +++ b/injector/crates/injector/src/main.rs @@ -1,10 +1,12 @@ -//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library +//! OpenPrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library //! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW //! thread on Windows, the HotSpot attach protocol on Linux and macOS. The library does everything else once it is inside //! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible. //! -//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc) -//! PrestigeInjector [lib path] non-interactive +//! OpenPrestigeInjector interactive process picker (Up/Down, Enter, Esc) +//! OpenPrestigeInjector [lib path] non-interactive +#[path = "../../paths.rs"] +mod paths; mod platform; use std::fs; @@ -15,7 +17,6 @@ use std::time::{Duration, Instant}; /// The one credit line, shown once at the top of the screen. const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg"; -const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg"; /// How long to follow the DLL's progress after it has been loaded. const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180); @@ -26,11 +27,15 @@ const GREEN: &str = "\x1b[92m"; const YELLOW: &str = "\x1b[93m"; const RED: &str = "\x1b[91m"; -fn default_dll() -> PathBuf { - std::env::current_exe() - .ok() - .and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME))) - .unwrap_or_else(|| PathBuf::from(platform::LIB_NAME)) +fn prepare_runtime(_offline: bool, _ask_for_updates: bool) -> Result { + let path = std::env::current_exe() + .map_err(|error| error.to_string())? + .with_file_name(platform::LIB_NAME); + if path.is_file() { + Ok(path) + } else { + Err(format!("OpenPrestige requires {} beside the CLI executable. Extract the complete offline bundle.", platform::LIB_NAME)) + } } fn banner() -> String { @@ -38,26 +43,53 @@ fn banner() -> String { out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n")); out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n"); out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n"); - out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n")); + out.push_str(&format!( + " |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n" + )); out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n")); + out.push_str("OpenPrestige - offline CLI\n\n"); out } fn step(label: &str, message: &str) { - println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock()); + println!( + "{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", + clock() + ); + log_launcher(label, message); } fn ok(label: &str, message: &str) { - println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock()); + println!( + "{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", + clock() + ); + log_launcher(label, message); } fn fail(label: &str, message: &str) { - println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", clock()); + println!( + "{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", + clock() + ); + log_launcher(label, message); +} + +fn log_launcher(label: &str, message: &str) { + let path = paths::root() + .join("logs") + .join(format!("launcher-{}.log", std::process::id())); + if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "[{}] {label} {message}", clock()); + } } /// Seconds since the injector started, for the log prefix. fn clock() -> String { use std::sync::OnceLock; static START: OnceLock = OnceLock::new(); - format!("{:.1}s", START.get_or_init(Instant::now).elapsed().as_secs_f32()) + format!( + "{:.1}s", + START.get_or_init(Instant::now).elapsed().as_secs_f32() + ) } fn main() -> ExitCode { @@ -70,27 +102,47 @@ fn real_main() -> ExitCode { platform::enable_ansi(); let _ = clock(); let args: Vec = std::env::args().skip(1).collect(); - let interactive = args.is_empty(); + if let Err(error) = paths::initialize() { + fail("storage", &error.to_string()); + return ExitCode::from(1); + } + let interactive = args.is_empty() + || matches!(args.as_slice(), [flag] if flag == "--console" || flag == "--offline"); - let (pid, dll): (Option, PathBuf) = match args.as_slice() { - [] => { - open_invite_once(); - (pick_process(), default_dll()) + let (pid, dll): (Option, Result) = match args.as_slice() { + _ if interactive => { + let library = prepare_runtime(args.iter().any(|arg| arg == "--offline"), true); + if let Err(error) = &library { + fail("runtime", error); + return ExitCode::from(1); + } + (pick_process(), library) } - [pid] => (parse_pid(pid), default_dll()), - [pid, dll] => (parse_pid(pid), PathBuf::from(dll)), + [pid] => (parse_pid(pid), prepare_runtime(true, false)), + [pid, dll] => (parse_pid(pid), Ok(PathBuf::from(dll))), _ => { - eprintln!("usage: PrestigeInjector [ [lib path]]"); + eprintln!("usage: OpenPrestigeInjector [--console | --offline | [lib path]]"); return ExitCode::from(2); } }; let Some(pid) = pid else { // interactive mode quit with Esc, or an unparsable pid was given - return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) }; + return if interactive { + ExitCode::SUCCESS + } else { + ExitCode::from(2) + }; }; if !interactive { print!("{}", banner()); } + let dll = match dll { + Ok(path) => path, + Err(error) => { + fail("runtime", &error); + return ExitCode::from(1); + } + }; let code = run(pid, &dll); if interactive { @@ -121,35 +173,58 @@ fn run(pid: u32, dll: &Path) -> ExitCode { let dll_display = dll_display.trim_start_matches(r"\\?\").to_string(); step("loader", &dll_display); - // The library appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown. - let log_path = dll.with_file_name("prestige-native.log"); - let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0); + let log_path = paths::native_log(pid); + let logs = [log_path.clone(), dll.with_file_name("prestige-native.log")]; + let starts = logs + .each_ref() + .map(|path| fs::metadata(path).map(|m| m.len()).unwrap_or(0)); - step("checking", "64-bit JVM, process access, library not already loaded"); + step( + "checking", + "64-bit JVM, process access, library not already loaded", + ); step("injecting", "loading the native library into the JVM"); match platform::inject(pid, &dll) { - Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)), + Ok(()) => ok( + "injected", + &format!("{} is loaded in the game process", platform::LIB_NAME), + ), Err(e) => { fail("failed", &e); return ExitCode::from(1); } } - step("transforming", "following the loader log (attach, class loader, hook transformer)..."); - match follow_log(&log_path, log_start) { + step( + "transforming", + "following the loader log (attach, class loader, hook transformer)...", + ); + match follow_log(&logs, starts) { Outcome::Done => { println!(); - ok("success", "successfully injected - Prestige is running in the game"); + ok( + "success", + "successfully injected - Prestige is running in the game", + ); ExitCode::SUCCESS } Outcome::Failed(line) => { fail("failed", &format!("injection did not complete: {line}")); - println!("{DIM} details: {} and the game's latest.log{RESET}", log_path.display()); + println!( + "{DIM} details: {} and the game's latest.log{RESET}", + log_path.display() + ); ExitCode::from(1) } Outcome::TimedOut => { - fail("timeout", "no completion message from the loader within 3 minutes"); - println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display()); + fail( + "timeout", + "no completion message from the loader within 3 minutes", + ); + println!( + "{DIM} check {} and the game's latest.log{RESET}", + log_path.display() + ); ExitCode::from(1) } } @@ -162,60 +237,61 @@ enum Outcome { } /// Prints the lines the loader/bootstrap append to the log until they report success or failure. -fn follow_log(path: &Path, start: u64) -> Outcome { +fn follow_log(paths: &[PathBuf; 2], mut offsets: [u64; 2]) -> Outcome { let deadline = Instant::now() + FOLLOW_TIMEOUT; - let mut offset = start; - let mut pending = String::new(); + let mut pending = [String::new(), String::new()]; while Instant::now() < deadline { - if let Ok(mut file) = fs::File::open(path) { - if file.seek(SeekFrom::Start(offset)).is_ok() { - let mut chunk = String::new(); - if let Ok(n) = file.read_to_string(&mut chunk) { - offset += n as u64; - pending.push_str(&chunk); + for (index, path) in paths.iter().enumerate() { + let offset = &mut offsets[index]; + let pending = &mut pending[index]; + if let Ok(mut file) = fs::File::open(path) { + if file.seek(SeekFrom::Start(*offset)).is_ok() { + let mut chunk = String::new(); + if let Ok(n) = file.read_to_string(&mut chunk) { + *offset += n as u64; + pending.push_str(&chunk); + } } } - } - while let Some(end) = pending.find('\n') { - let line: String = pending.drain(..=end).collect(); - let line = line.trim_end(); - // lines look like "[unix seconds] message" - let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line); - let lower = message.to_ascii_lowercase(); - // Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied. - let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook"); - if fatal { - fail("loader", message); - return Outcome::Failed(message.to_string()); + while let Some(end) = pending.find('\n') { + let line: String = pending.drain(..=end).collect(); + // Keep the output from older runtime bundles in the shared log too. + if index == 1 { + if let Ok(mut log) = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&paths[0]) + { + if log.write_all(line.as_bytes()).is_ok() { + offsets[0] += line.len() as u64; + } + } + } + let line = line.trim_end(); + // lines look like "[unix seconds] message" + let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line); + let lower = message.to_ascii_lowercase(); + // Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied. + let fatal = message.starts_with("FAILED:") + || lower.contains("start failed") + || lower.contains("client init failed") + || lower.contains("could not hook"); + if fatal { + fail("loader", message); + return Outcome::Failed(message.to_string()); + } + if message.contains("client initialised") { + ok("loader", message); + return Outcome::Done; + } + step("loader", message); } - if message.contains("client initialised") { - ok("loader", message); - return Outcome::Done; - } - step("loader", message); } std::thread::sleep(Duration::from_millis(200)); } Outcome::TimedOut } -fn config_path() -> Option { - platform::config_dir().map(|dir| dir.join("PrestigeInjector").join("settings.txt")) -} - -/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it. -fn open_invite_once() { - let Some(path) = config_path() else { return }; - if path.exists() { - return; - } - let _ = platform::open_url(INVITE_URL); - if let Some(dir) = path.parent() { - let _ = fs::create_dir_all(dir); - } - let _ = fs::write(&path, "invite_opened=true\n"); -} - /// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid. fn pick_process() -> Option { let mut selected = 0usize; @@ -229,7 +305,9 @@ fn pick_process() -> Option { let previous = selected; match key { platform::Key::Up => selected = selected.saturating_sub(1), - platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)), + platform::Key::Down => { + selected = (selected + 1).min(processes.len().saturating_sub(1)) + } platform::Key::Enter => { if let Some(p) = processes.get(selected) { return Some(p.pid); @@ -264,7 +342,10 @@ fn draw(processes: &[platform::JavaProcess], selected: usize) { } for (i, p) in processes.iter().enumerate() { let marker = if i == selected { ">" } else { " " }; - out.push_str(&format!("{marker} [{:>6}] {} ({})\n", p.pid, p.title, p.exe)); + out.push_str(&format!( + "{marker} [{:>6}] {} ({})\n", + p.pid, p.title, p.exe + )); } print!("{out}"); let _ = io::stdout().flush(); diff --git a/injector/crates/injector/src/platform/mod.rs b/injector/crates/injector/src/platform/mod.rs index c64d4cb..c418dd0 100644 --- a/injector/crates/injector/src/platform/mod.rs +++ b/injector/crates/injector/src/platform/mod.rs @@ -1,6 +1,5 @@ //! Platform backends of the injector. Each backend exposes the same functions: -//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal. -use std::path::PathBuf; +//! enable_ansi, poll_key, java_processes, inject, restore_terminal. /// A running Minecraft JVM shown in the picker. #[derive(PartialEq)] @@ -33,7 +32,7 @@ pub use unix_attach::inject; #[cfg(unix)] pub use unix_procs::java_processes; #[cfg(unix)] -pub use unix_term::{enable_ansi, open_url, poll_key, restore_terminal}; +pub use unix_term::{enable_ansi, poll_key, restore_terminal}; #[cfg(windows)] pub const LIB_NAME: &str = "prestige_native.dll"; @@ -41,22 +40,3 @@ pub const LIB_NAME: &str = "prestige_native.dll"; pub const LIB_NAME: &str = "libprestige_native.dylib"; #[cfg(all(unix, not(target_os = "macos")))] pub const LIB_NAME: &str = "libprestige_native.so"; - -/// Per-user configuration directory: APPDATA, $XDG_CONFIG_HOME or ~/.config, ~/Library/Application Support. -pub fn config_dir() -> Option { - #[cfg(windows)] - { - std::env::var_os("APPDATA").map(PathBuf::from) - } - #[cfg(target_os = "macos")] - { - std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library").join("Application Support")) - } - #[cfg(all(unix, not(target_os = "macos")))] - { - match std::env::var_os("XDG_CONFIG_HOME") { - Some(dir) if !dir.is_empty() => Some(PathBuf::from(dir)), - _ => std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")), - } - } -} diff --git a/injector/crates/injector/src/platform/unix_term.rs b/injector/crates/injector/src/platform/unix_term.rs index e07154e..3a396c6 100644 --- a/injector/crates/injector/src/platform/unix_term.rs +++ b/injector/crates/injector/src/platform/unix_term.rs @@ -1,4 +1,4 @@ -//! Terminal input and URL opening on Linux and macOS. std only (no termios structs). +//! Terminal input on Linux and macOS. std only (no termios structs). //! //! Raw mode is entered lazily on the first `poll_key` call via `stty -icanon -echo min 1 time 0` //! on /dev/tty (ISIG stays on, so Ctrl-C still raises SIGINT). A reader thread blocks on the tty @@ -37,8 +37,15 @@ fn tty_stdin() -> Stdio { } fn stty(args: &[&str]) -> Option { - let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?; - out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string()) + let out = Command::new("stty") + .args(args) + .stdin(tty_stdin()) + .stderr(Stdio::null()) + .output() + .ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).trim().to_string()) } /// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`. @@ -113,7 +120,9 @@ pub fn poll_key() -> Option { 27 => { let ms30 = Duration::from_millis(30); match rx.recv_timeout(ms30) { - Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => return Some(Key::Escape), + Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => { + return Some(Key::Escape) + } Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) { Ok(b'A') => return Some(Key::Up), Ok(b'B') => return Some(Key::Down), @@ -134,19 +143,3 @@ pub fn restore_terminal() { stty(&[&s]); } } - -/// Opens a URL in the default browser (xdg-open / open). Returns whether the spawn succeeded. -pub fn open_url(url: &str) -> bool { - #[cfg(target_os = "macos")] - let opener = "open"; - #[cfg(not(target_os = "macos"))] - let opener = "xdg-open"; - Command::new(opener) - .arg(url) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .map(|_| true) - .unwrap_or(false) -} diff --git a/injector/crates/injector/src/platform/win.rs b/injector/crates/injector/src/platform/win.rs index e3c2bb7..eb5f549 100644 --- a/injector/crates/injector/src/platform/win.rs +++ b/injector/crates/injector/src/platform/win.rs @@ -39,16 +39,43 @@ struct ModuleEntry32W { extern "system" { fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle; fn CloseHandle(h: Handle) -> i32; - fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void; + fn VirtualAllocEx( + process: Handle, + address: *mut c_void, + size: usize, + alloc_type: u32, + protect: u32, + ) -> *mut c_void; fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32; - fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32; - fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle; + fn WriteProcessMemory( + process: Handle, + address: *mut c_void, + buf: *const c_void, + size: usize, + written: *mut usize, + ) -> i32; + fn CreateRemoteThread( + process: Handle, + attrs: *mut c_void, + stack: usize, + start: *mut c_void, + param: *mut c_void, + flags: u32, + id: *mut u32, + ) -> Handle; fn WaitForSingleObject(h: Handle, ms: u32) -> u32; fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32; fn GetModuleHandleW(name: *const u16) -> Handle; fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void; - fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32; - fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32; + fn IsWow64Process2(process: Handle, process_machine: *mut u16, native_machine: *mut u16) + -> i32; + fn GetCurrentProcess() -> Handle; + fn QueryFullProcessImageNameW( + process: Handle, + flags: u32, + buf: *mut u16, + size: *mut u32, + ) -> i32; fn GetLastError() -> u32; fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle; fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32; @@ -66,11 +93,6 @@ extern "system" { fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32; } -#[link(name = "shell32")] -extern "system" { - fn ShellExecuteW(hwnd: Handle, verb: *const u16, file: *const u16, params: *const u16, dir: *const u16, show: i32) -> isize; -} - extern "C" { fn _kbhit() -> i32; fn _getch() -> i32; @@ -109,7 +131,10 @@ pub fn poll_key() -> Option { } fn wide(path: &Path) -> Vec { - path.as_os_str().encode_wide().chain(std::iter::once(0)).collect() + path.as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect() } fn image_name(pid: u32) -> Option { @@ -156,7 +181,9 @@ pub fn java_processes() -> Vec { if out.iter().any(|w| w.pid == pid) { continue; } - let Some(path) = image_name(pid) else { continue }; + let Some(path) = image_name(pid) else { + continue; + }; let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string(); if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") { out.push(JavaProcess { pid, title, exe }); @@ -181,7 +208,11 @@ fn module_loaded(pid: u32, file_name: &str) -> bool { let mut found = false; let mut ok = Module32FirstW(snapshot, &mut entry); while ok != 0 { - let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len()); + let len = entry + .sz_module + .iter() + .position(|&c| c == 0) + .unwrap_or(entry.sz_module.len()); if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) { found = true; break; @@ -196,10 +227,16 @@ fn module_loaded(pid: u32, file_name: &str) -> bool { /// LoadLibraryW(dll) in the target process through CreateRemoteThread. pub fn inject(pid: u32, dll: &Path) -> Result<(), String> { unsafe { - let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ; + let access = PROCESS_CREATE_THREAD + | PROCESS_QUERY_INFORMATION + | PROCESS_VM_OPERATION + | PROCESS_VM_WRITE + | PROCESS_VM_READ; let process = OpenProcess(access, 0, pid); if process.is_null() { - return Err(last_error("OpenProcess (run as the same user, or as administrator)")); + return Err(last_error( + "OpenProcess (run as the same user, or as administrator)", + )); } let result = inject_into(process, pid, dll); CloseHandle(process); @@ -208,18 +245,50 @@ pub fn inject(pid: u32, dll: &Path) -> Result<(), String> { } unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> { - let mut wow64 = 0; - if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 { - return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into()); + unsafe fn machine(process: Handle) -> Result { + let mut emulated = 0; + let mut native = 0; + if IsWow64Process2(process, &mut emulated, &mut native) == 0 { + return Err(last_error("IsWow64Process2")); + } + Ok(if emulated == 0 { native } else { emulated }) } - let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let target = machine(process)?; + let injector = machine(GetCurrentProcess())?; + if target != injector { + return Err(format!("JVM architecture {target:#06x} differs from injector {injector:#06x}. Use matching x64 or ARM64 builds.")); + } + let image = std::fs::read(dll).map_err(|error| format!("read loader: {error}"))?; + let pe = image + .get(0x3c..0x40) + .and_then(|bytes| bytes.try_into().ok()) + .map(u32::from_le_bytes) + .ok_or("invalid loader PE header")? as usize; + let library = image + .get(pe + 4..pe + 6) + .and_then(|bytes| bytes.try_into().ok()) + .map(u16::from_le_bytes) + .ok_or("invalid loader machine")?; + if library != target { + return Err("The loader DLL and JVM architectures differ.".into()); + } + let file_name = dll + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); if module_loaded(pid, &file_name) { return Err(format!("{file_name} is already loaded in process {pid}")); } let path = wide(dll); let bytes = path.len() * 2; - let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + let remote = VirtualAllocEx( + process, + std::ptr::null_mut(), + bytes, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + ); if remote.is_null() { return Err(last_error("VirtualAllocEx")); } @@ -227,7 +296,14 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin VirtualFreeEx(process, remote, 0, MEM_RELEASE); Err(err) }; - if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 { + if WriteProcessMemory( + process, + remote, + path.as_ptr() as *const c_void, + bytes, + std::ptr::null_mut(), + ) == 0 + { return cleanup(last_error("WriteProcessMemory")); } @@ -237,7 +313,15 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin if load_library.is_null() { return cleanup(last_error("resolve LoadLibraryW")); } - let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut()); + let thread = CreateRemoteThread( + process, + std::ptr::null_mut(), + 0, + load_library, + remote, + 0, + std::ptr::null_mut(), + ); if thread.is_null() { return cleanup(last_error("CreateRemoteThread")); } @@ -260,11 +344,3 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin } Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)")) } - -/// Opens a URL in the default web browser. -pub fn open_url(url: &str) -> bool { - let verb: Vec = "open".encode_utf16().collect(); - let target: Vec = url.encode_utf16().chain(std::iter::once(0)).collect(); - // ShellExecute returns a value greater than 32 on success - unsafe { ShellExecuteW(std::ptr::null_mut(), verb.as_ptr(), target.as_ptr(), std::ptr::null(), std::ptr::null(), 1) > 32 } -} diff --git a/injector/crates/loader/build.rs b/injector/crates/loader/build.rs index 3cc4fd0..26cfb8e 100644 --- a/injector/crates/loader/build.rs +++ b/injector/crates/loader/build.rs @@ -10,7 +10,10 @@ fn main() { Err(_) => newest_remapped_jar(), }; if !jar.is_file() { - panic!("payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", jar.display()); + panic!( + "payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", + jar.display() + ); } println!("cargo:rerun-if-changed={}", jar.display()); println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", jar.display()); @@ -22,14 +25,22 @@ fn newest_remapped_jar() -> PathBuf { if let Ok(entries) = fs::read_dir(&libs) { for entry in entries.flatten() { let name = entry.file_name().to_string_lossy().to_string(); - if !name.starts_with("prestige-") || !name.ends_with(".jar") || name.ends_with("-dev.jar") || name.ends_with("-sources.jar") { + if !name.starts_with("prestige-") + || !name.ends_with(".jar") + || name.ends_with("-dev.jar") + || name.ends_with("-sources.jar") + { continue; } - let time = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH); + let time = entry + .metadata() + .and_then(|m| m.modified()) + .unwrap_or(std::time::UNIX_EPOCH); if best.as_ref().map_or(true, |(t, _)| time > *t) { best = Some((time, entry.path())); } } } - best.map(|(_, p)| p).unwrap_or_else(|| libs.join("prestige-0.1.0.jar")) + best.map(|(_, p)| p) + .unwrap_or_else(|| libs.join("prestige-0.1.0.jar")) } diff --git a/injector/crates/loader/src/lib.rs b/injector/crates/loader/src/lib.rs index 645447c..6e750ad 100644 --- a/injector/crates/loader/src/lib.rs +++ b/injector/crates/loader/src/lib.rs @@ -6,6 +6,8 @@ mod bridge; mod jni; mod log; +#[path = "../../paths.rs"] +mod paths; mod worker; use std::ffi::c_void; diff --git a/injector/crates/loader/src/log.rs b/injector/crates/loader/src/log.rs index 684f787..60b1f1d 100644 --- a/injector/crates/loader/src/log.rs +++ b/injector/crates/loader/src/log.rs @@ -1,61 +1,19 @@ -use std::ffi::c_void; -#[cfg(unix)] -use std::os::unix::ffi::OsStrExt; use std::fs::OpenOptions; use std::io::Write; use std::path::PathBuf; use std::sync::OnceLock; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(windows)] -extern "system" { - fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32; -} - -#[cfg(unix)] -#[repr(C)] -struct DlInfo { - dli_fname: *const std::ffi::c_char, - dli_fbase: *mut c_void, - dli_sname: *const std::ffi::c_char, - dli_saddr: *mut c_void, -} - -#[cfg(unix)] -extern "C" { - fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32; -} - -#[cfg(unix)] -fn module_path() -> PathBuf { - fn anchor() {} - let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() }; - let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) }; - if ok != 0 && !info.dli_fname.is_null() { - let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; - PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes())) - } else { - std::env::temp_dir().join("libprestige_native") - } -} - -#[cfg(windows)] -fn module_path() -> PathBuf { - let mut buf = [0u16; 1024]; - let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize; - PathBuf::from(String::from_utf16_lossy(&buf[..len])) -} - fn log_path() -> &'static PathBuf { static PATH: OnceLock = OnceLock::new(); - PATH.get_or_init(|| { - module_path().with_file_name("prestige-native.log") - }) + PATH.get_or_init(|| crate::paths::native_log(std::process::id())) } -/// Appends a line to prestige-native.log beside the DLL. pub fn log(message: &str) { let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + if let Some(parent) = log_path().parent() { + let _ = std::fs::create_dir_all(parent); + } if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) { let _ = writeln!(file, "[{secs}] {message}"); } diff --git a/injector/crates/loader/src/worker.rs b/injector/crates/loader/src/worker.rs index 551a645..ed6678b 100644 --- a/injector/crates/loader/src/worker.rs +++ b/injector/crates/loader/src/worker.rs @@ -29,6 +29,7 @@ pub fn run() { } unsafe fn install() -> Result<(), String> { + crate::paths::initialize().map_err(|error| format!("create OpenPrestige data directory: {error}"))?; let vm = find_vm()?; let env = attach_current_thread(vm)?; struct Detach(VmPtr); @@ -45,7 +46,7 @@ unsafe fn install() -> Result<(), String> { log!("JVMTI ready"); let jar = write_payload()?; - log!("payload written to {}", jar.display()); + log!("using payload at {}", jar.display()); let loader = add_to_knot(&jni, &jar)?; log!("payload appended to the game class loader: {}", describe(&jni, loader)); @@ -103,7 +104,8 @@ unsafe fn find_vm() -> Result { } fn write_payload() -> Result { - let dir = std::env::temp_dir().join("PrestigeLoader"); + let dir = crate::paths::initialize().map_err(|e| format!("create OpenPrestige data directory: {e}"))? + .join("runtime").join("payloads"); std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?; let path = dir.join(format!("prestige-payload-{}.jar", std::process::id())); std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?; diff --git a/injector/crates/paths.rs b/injector/crates/paths.rs new file mode 100644 index 0000000..6995ea8 --- /dev/null +++ b/injector/crates/paths.rs @@ -0,0 +1,36 @@ +use std::io; +use std::path::PathBuf; + +fn env_path(name: &str) -> Option { + std::env::var_os(name) + .filter(|path| !path.is_empty()) + .map(PathBuf::from) +} + +pub fn root() -> PathBuf { + #[cfg(windows)] + let base = env_path("APPDATA") + .or_else(|| env_path("USERPROFILE").map(|home| home.join("AppData/Roaming"))); + #[cfg(target_os = "macos")] + let base = env_path("HOME").map(|home| home.join("Library/Application Support")); + #[cfg(all(unix, not(target_os = "macos")))] + let base = env_path("XDG_DATA_HOME") + .filter(|path| path.is_absolute()) + .or_else(|| env_path("HOME").map(|home| home.join(".local/share"))); + base.unwrap_or_else(std::env::temp_dir) + .join(".openprestige") +} + +pub fn initialize() -> io::Result { + let root = root(); + for name in ["configs", "logs", "runtime"] { + std::fs::create_dir_all(root.join(name))?; + } + Ok(root) +} + +pub fn native_log(pid: u32) -> PathBuf { + root() + .join("logs") + .join(format!("prestige-native-{pid}.log")) +} diff --git a/scripts/build_imgui_core.py b/scripts/build_imgui_core.py new file mode 100644 index 0000000..ce48178 --- /dev/null +++ b/scripts/build_imgui_core.py @@ -0,0 +1,182 @@ +"""Build the core-only JNI library matching imgui-java 1.92.7.1. + +Requires Python 3, a JDK, CMake, and a C++17 compiler. Select a cross compiler +with --toolchain or a Visual Studio target with --architecture ARM64. +""" +import argparse +import os +import re +from pathlib import Path +import shutil +import subprocess +import tempfile +import urllib.request +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +BINDING_COMMIT = "b164a88602af3e28483efbb4d85aea372010c7c5" +IMGUI_COMMIT = "b1bcb12a624af7509894c8e77dd47416997777fa" +NATIVE_FILES = ( + "_common.h", "_internal.h", "imconfig.h", + "jni_jvm", "jni_common", "jni_binding_struct", "jni_callbacks", "jni_assertion", "jni_internal", +) + + +def write_if_changed(path, data): + if not path.is_file() or path.read_bytes() != data: + path.write_bytes(data) + +def download(url, destination): + if not destination.is_file(): + destination.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(dir=destination.parent, suffix=".download", delete=False) as output: + temporary = Path(output.name) + try: + with urllib.request.urlopen(url, timeout=60) as response: + shutil.copyfileobj(response, output) + except BaseException: + output.close() + temporary.unlink(missing_ok=True) + raise + try: + if not destination.is_file(): + try: + temporary.replace(destination) + except PermissionError: + if not destination.is_file(): + raise + finally: + temporary.unlink(missing_ok=True) + return destination + + +def archive(repository, commit, cache): + path = download(f"https://codeload.github.com/{repository}/zip/{commit}", + cache / "downloads" / f"{repository.split('/')[-1]}-{commit}.zip") + return zipfile.ZipFile(path) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--build-dir", type=Path, default=ROOT / "build/imgui-core") + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--toolchain", type=Path) + parser.add_argument("--architecture", choices=("x64", "ARM64")) + parser.add_argument("--cache-dir", type=Path) + parser.add_argument("--zig-target", choices=( + "x86_64-windows-gnu", "aarch64-windows-gnu", + "x86_64-linux-gnu.2.17", "aarch64-linux-gnu.2.17", + "x86_64-macos.11.0", "aarch64-macos.11.0", + )) + args = parser.parse_args() + # cmd.exe cannot use extended-length (\\?\) working directory paths. + build = Path(os.path.abspath(args.build_dir)) + cache = Path(os.path.abspath(args.cache_dir)) if args.cache_dir else build + output = Path(os.path.abspath(args.output)) + generated = build / "jni" + sources = build / "sources" + for directory in (build, output, generated, sources): + directory.mkdir(parents=True, exist_ok=True) + + dependencies = [] + for group, artifact, version in ( + ("com.badlogicgames.gdx", "gdx-jnigen", "2.5.2"), + ("com.badlogicgames.gdx", "gdx-jnigen-loader", "2.5.2"), + ("com.github.javaparser", "javaparser-core", "3.14.14"), + ("io.github.spair", "imgui-java-binding", "1.92.7.1"), + ): + name = f"{artifact}-{version}.jar" + dependencies.append(download( + f"https://repo.maven.apache.org/maven2/{group.replace('.', '/')}/{artifact}/{version}/{name}", + cache / "deps" / name, + )) + binding = dependencies[-1] + with zipfile.ZipFile(dependencies[0]) as generator: + header_prefix = "com/badlogic/gdx/jnigen/resources/headers/" + for entry in generator.infolist(): + if entry.filename.startswith(header_prefix) and not entry.is_dir(): + destination = generated / "jni-headers" / entry.filename[len(header_prefix):] + destination.parent.mkdir(parents=True, exist_ok=True) + write_if_changed(destination, generator.read(entry)) + with archive("SpaiR/imgui-java", BINDING_COMMIT, cache) as upstream: + prefix = upstream.namelist()[0] + java_prefix = prefix + "imgui-binding/src/generated/java/" + native_prefix = prefix + "imgui-binding/src/main/native/" + for entry in upstream.infolist(): + if entry.filename.startswith(java_prefix) and entry.filename.endswith(".java"): + relative = entry.filename[len(java_prefix):] + if relative.startswith(("imgui/extension/", "imgui/lwjgl3/")): + continue + target = sources / relative + target.parent.mkdir(parents=True, exist_ok=True) + write_if_changed(target, upstream.read(entry)) + if entry.filename.startswith(native_prefix): + name = entry.filename[len(native_prefix):] + if name in NATIVE_FILES or name.rsplit(".", 1)[0] in NATIVE_FILES: + write_if_changed(generated / name, upstream.read(entry)) + write_if_changed(output / "LICENSE-imgui-java.txt", upstream.read(prefix + "LICENSE")) + with archive("ocornut/imgui", IMGUI_COMMIT, cache) as upstream: + prefix = upstream.namelist()[0] + for entry in upstream.infolist(): + name = entry.filename[len(prefix):] + if "/" not in name and name.endswith((".h", ".cpp")) and name != "imconfig.h": + write_if_changed(generated / name, upstream.read(entry)) + write_if_changed(output / "LICENSE-imgui.txt", upstream.read(prefix + "LICENSE.txt")) + + java_home = os.environ.get("JAVA_HOME") + java = str(Path(java_home) / "bin/java") if java_home else shutil.which("java") + if not java: + raise RuntimeError("Set JAVA_HOME to a JDK.") + env = os.environ.copy() + if java_home: + env["PATH"] = str(Path(java_home) / "bin") + os.pathsep + env["PATH"] + # jnigen constructs its javac command as a string. Relative paths avoid its + # whitespace bug without depending on Windows short filename support. + write_if_changed(build / "binding.jar", binding.read_bytes()) + subprocess.run([ + java, "-cp", os.pathsep.join(map(str, dependencies)), + str(ROOT / "scripts/imgui-core/GenerateCore.java"), + "sources", "binding.jar", "jni", + ], check=True, env=env, cwd=build) + # Upstream's GNU builds accept variable-length arrays. Use standard C++ + # storage in generated JNI code so the identical methods also build in MSVC. + declaration = re.compile(r"\b(ImVec2|const char\*|jlong)\s+(\w+)\[(\w+)\];") + for source in generated.glob("imgui_*.cpp"): + text = source.read_text(encoding="utf-8") + text, count = declaration.subn( + lambda match: f"std::vector<{match[1]}> {match[2]}Storage({match[3]});\n" + f" auto* {match[2]} = {match[2]}Storage.data();", + text, + ) + if count: + source.write_text("#include \n" + text, encoding="utf-8") + configuration = [ + "cmake", "-S", str(ROOT / "scripts/imgui-core"), "-B", str(build / "cmake"), + f"-DCORE_JNI_DIR={generated.as_posix()}", f"-DCORE_OUTPUT_DIR={output.as_posix()}", "-DCMAKE_BUILD_TYPE=Release", + ] + if args.zig_target: + if args.toolchain or args.architecture: + parser.error("--zig-target cannot be combined with --toolchain or --architecture") + import ziglang + executable = Path(ziglang.__file__).parent / ("zig.exe" if os.name == "nt" else "zig") + family = "Windows" if "windows" in args.zig_target else "Darwin" if "macos" in args.zig_target else "Linux" + toolchain = build / "zig-toolchain.cmake" + toolchain.write_text( + f'set(CMAKE_SYSTEM_NAME {family})\n' + f'set(CMAKE_SYSTEM_PROCESSOR {args.zig_target.split("-")[0]})\n' + f'set(CMAKE_CXX_COMPILER "{executable.as_posix()}")\n' + f'set(CMAKE_CXX_COMPILER_ARG1 "c++ -target {args.zig_target} -Wno-nullability-completeness")\n', + encoding="utf-8", + ) + configuration.extend(("-G", "Ninja", f"-DCMAKE_TOOLCHAIN_FILE={toolchain.as_posix()}")) + if args.toolchain: + configuration.append(f"-DCMAKE_TOOLCHAIN_FILE={args.toolchain.resolve().as_posix()}") + if args.architecture: + configuration.extend(("-A", args.architecture)) + subprocess.run(configuration, check=True, env=env) + subprocess.run(["cmake", "--build", str(build / "cmake"), "--config", "Release", "--parallel", "4"], + check=True, env=env) + + +if __name__ == "__main__": + main() diff --git a/scripts/build_release_imgui.py b/scripts/build_release_imgui.py new file mode 100644 index 0000000..772a0b2 --- /dev/null +++ b/scripts/build_release_imgui.py @@ -0,0 +1,127 @@ +"""Fetch and build every core ImGui native required by build-release.bat.""" +import argparse +import importlib.util +import importlib.metadata +import os +from pathlib import Path +import shutil +import struct +import subprocess +import sys +import sysconfig + +ROOT = Path(__file__).resolve().parents[1] +TARGETS = ( + ("windows-x86_64", "x86_64-windows-gnu", "imgui-java64.dll"), + ("windows-aarch64", "aarch64-windows-gnu", "imgui-java64.dll"), + ("linux-x86_64", "x86_64-linux-gnu.2.17", "libimgui-java64.so"), + ("linux-aarch64", "aarch64-linux-gnu.2.17", "libimgui-java64.so"), + ("macos-x86_64", "x86_64-macos.11.0", "libimgui-java64.dylib"), + ("macos-aarch64", "aarch64-macos.11.0", "libimgui-java64.dylib"), +) + + +def ensure_tools(): + scripts = Path(sys.executable).parent / "Scripts" + user_scheme = "nt_user" if os.name == "nt" else "posix_user" + user_scripts = Path(sysconfig.get_path("scripts", scheme=user_scheme)) + os.environ["PATH"] = os.pathsep.join((str(scripts), str(user_scripts), os.environ["PATH"])) + missing = [tool for tool in ("cmake", "ninja") if not shutil.which(tool)] + if importlib.util.find_spec("ziglang") is None or importlib.metadata.version("ziglang") != "0.16.0": + missing.append("ziglang==0.16.0") + if missing: + subprocess.run([sys.executable, "-m", "pip", "install", "--user", *missing], check=True) + for tool in ("cmake", "ninja"): + if not shutil.which(tool): + raise RuntimeError(f"{tool} is unavailable after installation. Add the Python Scripts directory to PATH.") + + +def universal(slices, destination): + images = [path.read_bytes() for path in slices] + records = [] + offset = 16384 + for image in images: + magic, cpu, subtype = struct.unpack_from("II", 0xCAFEBABE, len(images))) + for record in records: + output.write(struct.pack(">IIIII", *record)) + for record, image in zip(records, images): + output.seek(record[2]) + output.write(image) + temporary.replace(destination) + + +def validate_native(path, platform): + with path.open("rb") as library: + header = library.read(64) + if platform.startswith("windows-") and header[:2] == b"MZ": + library.seek(struct.unpack_from("