Build offline OpenPrestige release bundles

Rename the injector and package it with an embedded payload for Windows, Linux, and macOS x64/ARM64. Add a core-only ImGui native build, multi-platform CI and release packaging, and shared per-user storage with legacy config migration.
This commit is contained in:
bbldrizzy committed 2026-11-03 18:45:13 +00:00
1 parent 5152e6e0a6
commit 2d6b2a3cd1
37 files changed
+1284 -451

No files matched your search

+92 -13
View File
@@ -6,18 +6,85 @@ on:
paths: paths:
- "injector/**" - "injector/**"
- "src/**" - "src/**"
- "scripts/**"
- "build.gradle"
- ".github/workflows/injector.yml" - ".github/workflows/injector.yml"
jobs: jobs:
payload: core-imgui:
name: Payload jar name: ImGui (${{ matrix.platform }})
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: "21" 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 - name: Build remapped jar
run: | run: |
chmod +x ./gradlew chmod +x ./gradlew
@@ -33,42 +100,54 @@ jobs:
if-no-files-found: error if-no-files-found: error
injector: injector:
name: Injector (${{ matrix.target }}) name: OpenPrestige (${{ matrix.platform.target }})
needs: payload needs: payload
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.platform.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: platform:
- os: windows-latest - os: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
name: windows-x86_64 name: windows-x86_64
files: | files: |
injector/target/release/PrestigeInjector.exe injector/target/release/OpenPrestigeInjector.exe
injector/target/release/prestige_native.dll injector/target/release/prestige_native.dll
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
name: linux-x86_64 name: linux-x86_64
files: | 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 injector/target/release/libprestige_native.so
- os: macos-13 - os: macos-13
target: x86_64-apple-darwin target: x86_64-apple-darwin
name: macos-x86_64 name: macos-x86_64
files: | files: |
injector/target/release/PrestigeInjector injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.dylib injector/target/release/libprestige_native.dylib
- os: macos-14 - os: macos-14
target: aarch64-apple-darwin target: aarch64-apple-darwin
name: macos-aarch64 name: macos-aarch64
files: | files: |
injector/target/release/PrestigeInjector injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.dylib injector/target/release/libprestige_native.dylib
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.platform.target }}
- name: Download payload jar - name: Download payload jar
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -81,6 +160,6 @@ jobs:
- name: Upload injector - name: Upload injector
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: injector-${{ matrix.name }} name: openprestige-${{ matrix.platform.name }}
path: ${{ matrix.files }} path: ${{ matrix.platform.files }}
if-no-files-found: error if-no-files-found: error
+4
View File
@@ -20,3 +20,7 @@ openvape.zip
/injector/target/ /injector/target/
/tools /tools
/examples /examples
# Python build helpers
__pycache__/
*.pyc
+2
View File
@@ -1,5 +1,7 @@
# Open Prestige # 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. A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11.
--- ---
+3 -5
View File
@@ -1,6 +1,4 @@
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader DLL, # Build OpenPrestige and collect the host binaries in dist/.
# and collects PrestigeInjector.exe + prestige_native.dll in dist\.
# native tools write progress/notes to stderr; failures are detected through $LASTEXITCODE instead
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
Set-Location $PSScriptRoot 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' } 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 cargo build --release --manifest-path injector\Cargo.toml
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' } if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
New-Item -ItemType Directory -Force dist | Out-Null 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 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'
+6 -16
View File
@@ -1,34 +1,24 @@
#!/bin/sh #!/bin/sh
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader # Build OpenPrestige for the current host. Optional JAVA_HOME selects the Gradle JDK.
# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/.
# Optional: set JAVA_HOME to pick the JDK used by Gradle.
set -eu set -eu
cd "$(dirname "$0")" cd "$(dirname "$0")"
case "$(uname -s)" in case "$(uname -s)" in
Linux) LIB=libprestige_native.so ;; Linux) LIB=libprestige_native.so ;;
Darwin) LIB=libprestige_native.dylib ;; 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 esac
if [ -n "${JAVA_HOME:-}" ]; then if [ -n "${JAVA_HOME:-}" ]; then
export JAVA_HOME export JAVA_HOME
PATH="$JAVA_HOME/bin:$PATH" PATH="$JAVA_HOME/bin:$PATH"
export PATH export PATH
fi fi
./gradlew remapJar --console=plain ./gradlew remapJar --console=plain
cargo build --release --manifest-path injector/Cargo.toml cargo build --release --manifest-path injector/Cargo.toml
mkdir -p dist mkdir -p dist
cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/ cp -f "injector/target/release/OpenPrestigeInjector" "injector/target/release/$LIB" dist/
chmod +x dist/PrestigeInjector chmod +x dist/OpenPrestigeInjector
# the same remapped jar is the Fabric mod (drop it into mods/)
for jar in build/libs/prestige-*.jar; do for jar in build/libs/prestige-*.jar; do
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
cp -f "$jar" dist/ cp -f "$jar" dist/
done done
echo "Done: dist/OpenPrestigeInjector + dist/$LIB + Fabric mod JAR"
echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)"
+25 -29
View File
@@ -1,12 +1,8 @@
@echo off @echo off
rem Builds release zips for every platform into dist\release\ (run on Windows): rem Builds fully offline OpenPrestige bundles.
rem prestige-<ver>-windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar rem OpenPrestige CLI + native library with embedded payload
rem prestige-<ver>-linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro) rem Requires Rust with MSVC x64/ARM64 tools, Python 3 and JDK 21.
rem prestige-<ver>-macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon) rem Fetches pinned ImGui sources and cross-compiles the small JNI libraries.
rem prestige-<ver>.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.
setlocal EnableExtensions setlocal EnableExtensions
cd /d "%~dp0" cd /d "%~dp0"
set "SELF=%~f0" 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) 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 || ( python -m ziglang version >nul 2>&1 || (
echo === Installing zig ^(pip ziglang^) 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 || ( where cargo-zigbuild >nul 2>&1 || (
echo === Installing cargo-zigbuild echo === Installing cargo-zigbuild
cargo install --locked cargo-zigbuild || goto :fail 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 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" if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild"
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI" 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 ------------------------------------------------------- rem ---- payload jar -------------------------------------------------------
echo === Building payload jar echo === Building payload jar
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
set "JAR=" 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" 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) 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%" 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 ----------------------------------------------------- rem ---- native builds -----------------------------------------------------
echo === Windows x86_64 call :build windows-x86_64 x86_64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail call :build windows-aarch64 aarch64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || 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
echo === Linux x86_64 (glibc 2.17+) call :build macos-universal universal2-apple-darwin OpenPrestigeInjector libprestige_native.dylib zigbuild || goto :fail
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
copy /y "%JAR%" "%OUT%\" >nul || goto :fail copy /y "%JAR%" "%OUT%\" >nul || goto :fail
python scripts\package_release.py checksums "%OUT%" || goto :fail python scripts\package_release.py checksums "%OUT%" || goto :fail
echo. echo.
echo === Done: %OUT% echo === Done: %OUT%
dir /b "%OUT%"
call :maybe_pause call :maybe_pause
exit /b 0 exit /b 0
rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file rem platform, target directory, executable, library, cargo command, optional versioned target
:package :build
python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%" 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% exit /b %ERRORLEVEL%
:fail :fail
+17
View File
@@ -87,12 +87,29 @@ java {
} }
jar { 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. // Unpack the shaded libs (imgui classes + native dll, ClassTransform) into the mod jar.
from({ from({
configurations.shade.collect { it.isDirectory() ? it : zipTree(it) } configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
}) { }) {
exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA' exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
exclude 'module-info.class', 'META-INF/versions/**' 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) }) { from({ zipTree(configurations.engineMappings.singleFile) }) {
include 'mappings/mappings.tiny' include 'mappings/mappings.tiny'
+1 -1
View File
@@ -3,7 +3,7 @@
version = 4 version = 4
[[package]] [[package]]
name = "prestige-injector" name = "openprestige-injector"
version = "0.1.0" version = "0.1.0"
[[package]] [[package]]
+45 -90
View File
@@ -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`) This repository contains the original CLI injector, native library with embedded payload and game
into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x, Fabric API installed). It uses the same overall sources. The graphical launcher and runtime downloader are maintained in the separate Prestige
method as the openvape bundle, written from scratch in Rust with no external crates. repository. There is no GUI or HTTP dependency in this injector and no edition build flag.
## 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 <pid> JVMTI.agent_load`). The injector
creates `.attach_pid<pid>` and sends `SIGQUIT` to the JVM, connects to its attach socket (`/tmp/.java_pid<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.
## Build ## 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 Run `build-injector.ps1` on Windows or `build-injector.sh` on Linux/macOS. Both build the Fabric JAR,
`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`. 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 Run `build-release.bat` on Windows to build all platforms. `dist/release/` contains:
.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll
```text
openprestige-<version>-windows-x86_64.zip
openprestige-<version>-windows-aarch64.zip
openprestige-<version>-linux-x86_64.zip
openprestige-<version>-linux-aarch64.zip
openprestige-<version>-macos-universal.zip
prestige-<version>.jar
SHA256SUMS.txt
``` ```
```sh Each ZIP contains `OpenPrestigeInjector` (or `.exe`), the native library, and a README.
./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib} The CI injector workflow builds only OpenPrestige. Build dependencies require internet access;
``` the resulting release bundle needs no runtime downloads.
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-<ver>-{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.
## Use ## Use
Start a Fabric 1.21.11 instance, then run: Extract the complete platform ZIP, start Minecraft, then run:
```sh ```sh
PrestigeInjector # interactive: pick the game from the list OpenPrestigeInjector # Up/Down selects, Enter injects, Esc quits
PrestigeInjector <pid> [lib path] # inject into a specific pid, optionally with an explicit library path OpenPrestigeInjector <pid> [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 Windows and Linux support x86_64 and ARM64. macOS supports Intel and Apple Silicon.
profile needs Fabric Loader 0.19.x and Fabric API. Wait for the title screen before selecting the game The injector, library and JVM architectures must match. ARM64 execution needs hardware testing.
in the injector. The default Prestige menu key is Right Shift, which may overlap with a launcher keybind. 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` Build tools require Rust, Python 3 and JDK 21. On Windows, the full release build also requires
and `client initialised`. Hook counts refer to classes with changed bytecode. A skipped or failed hook MSVC build tools for x64 and ARM64. `build-release.bat` fetches pinned ImGui sources and builds
can leave its feature unavailable even if the menu opens. 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 Both repositories currently start from the same game sources and use the compatible `.openprestige`
recalculating stack and local limits on the cached classes. Live injection and Dawn's in-game profile configuration and log locations. They are independent repositories, without submodules, shared
still need smoke testing. 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 | OS | Location |
`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. | Windows | `%APPDATA%\.openprestige` |
| macOS | `~/Library/Application Support/.openprestige` |
| Linux | `$XDG_DATA_HOME/.openprestige` or `~/.local/share/.openprestige` |
## Requirements (Linux / macOS) Configs live in `configs/`, logs in `logs/`, and local runtime files in `runtime/`.
- 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.
+2 -2
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "prestige-injector" name = "openprestige-injector"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
name = "PrestigeInjector" name = "OpenPrestigeInjector"
path = "src/main.rs" path = "src/main.rs"
+11
View File
@@ -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}");
}
+160 -79
View File
@@ -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 //! (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 //! 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. //! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible.
//! //!
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc) //! OpenPrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector <pid> [lib path] non-interactive //! OpenPrestigeInjector <pid> [lib path] non-interactive
#[path = "../../paths.rs"]
mod paths;
mod platform; mod platform;
use std::fs; use std::fs;
@@ -15,7 +17,6 @@ use std::time::{Duration, Instant};
/// The one credit line, shown once at the top of the screen. /// The one credit line, shown once at the top of the screen.
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg"; 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. /// How long to follow the DLL's progress after it has been loaded.
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180); const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
@@ -26,11 +27,15 @@ const GREEN: &str = "\x1b[92m";
const YELLOW: &str = "\x1b[93m"; const YELLOW: &str = "\x1b[93m";
const RED: &str = "\x1b[91m"; const RED: &str = "\x1b[91m";
fn default_dll() -> PathBuf { fn prepare_runtime(_offline: bool, _ask_for_updates: bool) -> Result<PathBuf, String> {
std::env::current_exe() let path = std::env::current_exe()
.ok() .map_err(|error| error.to_string())?
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME))) .with_file_name(platform::LIB_NAME);
.unwrap_or_else(|| PathBuf::from(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 { fn banner() -> String {
@@ -38,26 +43,53 @@ fn banner() -> String {
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n")); out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n"); out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\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(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
out.push_str("OpenPrestige - offline CLI\n\n");
out out
} }
fn step(label: &str, message: &str) { 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) { 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) { 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. /// Seconds since the injector started, for the log prefix.
fn clock() -> String { fn clock() -> String {
use std::sync::OnceLock; use std::sync::OnceLock;
static START: OnceLock<Instant> = OnceLock::new(); static START: OnceLock<Instant> = 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 { fn main() -> ExitCode {
@@ -70,27 +102,47 @@ fn real_main() -> ExitCode {
platform::enable_ansi(); platform::enable_ansi();
let _ = clock(); let _ = clock();
let args: Vec<String> = std::env::args().skip(1).collect(); let args: Vec<String> = 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<u32>, PathBuf) = match args.as_slice() { let (pid, dll): (Option<u32>, Result<PathBuf, String>) = match args.as_slice() {
[] => { _ if interactive => {
open_invite_once(); let library = prepare_runtime(args.iter().any(|arg| arg == "--offline"), true);
(pick_process(), default_dll()) if let Err(error) = &library {
fail("runtime", error);
return ExitCode::from(1);
}
(pick_process(), library)
} }
[pid] => (parse_pid(pid), default_dll()), [pid] => (parse_pid(pid), prepare_runtime(true, false)),
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)), [pid, dll] => (parse_pid(pid), Ok(PathBuf::from(dll))),
_ => { _ => {
eprintln!("usage: PrestigeInjector [<pid> [lib path]]"); eprintln!("usage: OpenPrestigeInjector [--console | --offline | <pid> [lib path]]");
return ExitCode::from(2); return ExitCode::from(2);
} }
}; };
let Some(pid) = pid else { let Some(pid) = pid else {
// interactive mode quit with Esc, or an unparsable pid was given // 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 { if !interactive {
print!("{}", banner()); print!("{}", banner());
} }
let dll = match dll {
Ok(path) => path,
Err(error) => {
fail("runtime", &error);
return ExitCode::from(1);
}
};
let code = run(pid, &dll); let code = run(pid, &dll);
if interactive { if interactive {
@@ -121,35 +173,58 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string(); let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
step("loader", &dll_display); 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 = paths::native_log(pid);
let log_path = dll.with_file_name("prestige-native.log"); let logs = [log_path.clone(), dll.with_file_name("prestige-native.log")];
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0); 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"); step("injecting", "loading the native library into the JVM");
match platform::inject(pid, &dll) { 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) => { Err(e) => {
fail("failed", &e); fail("failed", &e);
return ExitCode::from(1); return ExitCode::from(1);
} }
} }
step("transforming", "following the loader log (attach, class loader, hook transformer)..."); step(
match follow_log(&log_path, log_start) { "transforming",
"following the loader log (attach, class loader, hook transformer)...",
);
match follow_log(&logs, starts) {
Outcome::Done => { Outcome::Done => {
println!(); println!();
ok("success", "successfully injected - Prestige is running in the game"); ok(
"success",
"successfully injected - Prestige is running in the game",
);
ExitCode::SUCCESS ExitCode::SUCCESS
} }
Outcome::Failed(line) => { Outcome::Failed(line) => {
fail("failed", &format!("injection did not complete: {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) ExitCode::from(1)
} }
Outcome::TimedOut => { Outcome::TimedOut => {
fail("timeout", "no completion message from the loader within 3 minutes"); fail(
println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display()); "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) 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. /// 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 deadline = Instant::now() + FOLLOW_TIMEOUT;
let mut offset = start; let mut pending = [String::new(), String::new()];
let mut pending = String::new();
while Instant::now() < deadline { while Instant::now() < deadline {
if let Ok(mut file) = fs::File::open(path) { for (index, path) in paths.iter().enumerate() {
if file.seek(SeekFrom::Start(offset)).is_ok() { let offset = &mut offsets[index];
let mut chunk = String::new(); let pending = &mut pending[index];
if let Ok(n) = file.read_to_string(&mut chunk) { if let Ok(mut file) = fs::File::open(path) {
offset += n as u64; if file.seek(SeekFrom::Start(*offset)).is_ok() {
pending.push_str(&chunk); 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') {
while let Some(end) = pending.find('\n') { let line: String = pending.drain(..=end).collect();
let line: String = pending.drain(..=end).collect(); // Keep the output from older runtime bundles in the shared log too.
let line = line.trim_end(); if index == 1 {
// lines look like "[unix seconds] message" if let Ok(mut log) = fs::OpenOptions::new()
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line); .create(true)
let lower = message.to_ascii_lowercase(); .append(true)
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied. .open(&paths[0])
let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook"); {
if fatal { if log.write_all(line.as_bytes()).is_ok() {
fail("loader", message); offsets[0] += line.len() as u64;
return Outcome::Failed(message.to_string()); }
}
}
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)); std::thread::sleep(Duration::from_millis(200));
} }
Outcome::TimedOut Outcome::TimedOut
} }
fn config_path() -> Option<PathBuf> {
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. /// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
fn pick_process() -> Option<u32> { fn pick_process() -> Option<u32> {
let mut selected = 0usize; let mut selected = 0usize;
@@ -229,7 +305,9 @@ fn pick_process() -> Option<u32> {
let previous = selected; let previous = selected;
match key { match key {
platform::Key::Up => selected = selected.saturating_sub(1), 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 => { platform::Key::Enter => {
if let Some(p) = processes.get(selected) { if let Some(p) = processes.get(selected) {
return Some(p.pid); return Some(p.pid);
@@ -264,7 +342,10 @@ fn draw(processes: &[platform::JavaProcess], selected: usize) {
} }
for (i, p) in processes.iter().enumerate() { for (i, p) in processes.iter().enumerate() {
let marker = if i == selected { ">" } else { " " }; 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}"); print!("{out}");
let _ = io::stdout().flush(); let _ = io::stdout().flush();
+2 -22
View File
@@ -1,6 +1,5 @@
//! Platform backends of the injector. Each backend exposes the same functions: //! Platform backends of the injector. Each backend exposes the same functions:
//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal. //! enable_ansi, poll_key, java_processes, inject, restore_terminal.
use std::path::PathBuf;
/// A running Minecraft JVM shown in the picker. /// A running Minecraft JVM shown in the picker.
#[derive(PartialEq)] #[derive(PartialEq)]
@@ -33,7 +32,7 @@ pub use unix_attach::inject;
#[cfg(unix)] #[cfg(unix)]
pub use unix_procs::java_processes; pub use unix_procs::java_processes;
#[cfg(unix)] #[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)] #[cfg(windows)]
pub const LIB_NAME: &str = "prestige_native.dll"; 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"; pub const LIB_NAME: &str = "libprestige_native.dylib";
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
pub const LIB_NAME: &str = "libprestige_native.so"; 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<PathBuf> {
#[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")),
}
}
}
@@ -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` //! 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 //! 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<String> { fn stty(args: &[&str]) -> Option<String> {
let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?; let out = Command::new("stty")
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string()) .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`. /// 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<Key> {
27 => { 27 => {
let ms30 = Duration::from_millis(30); let ms30 = Duration::from_millis(30);
match rx.recv_timeout(ms30) { 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'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
Ok(b'A') => return Some(Key::Up), Ok(b'A') => return Some(Key::Up),
Ok(b'B') => return Some(Key::Down), Ok(b'B') => return Some(Key::Down),
@@ -134,19 +143,3 @@ pub fn restore_terminal() {
stty(&[&s]); 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)
}
+106 -30
View File
@@ -39,16 +39,43 @@ struct ModuleEntry32W {
extern "system" { extern "system" {
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle; fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
fn CloseHandle(h: Handle) -> i32; 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 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 WriteProcessMemory(
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle; 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 WaitForSingleObject(h: Handle, ms: u32) -> u32;
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32; fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
fn GetModuleHandleW(name: *const u16) -> Handle; fn GetModuleHandleW(name: *const u16) -> Handle;
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void; fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32; fn IsWow64Process2(process: Handle, process_machine: *mut u16, native_machine: *mut u16)
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32; -> i32;
fn GetCurrentProcess() -> Handle;
fn QueryFullProcessImageNameW(
process: Handle,
flags: u32,
buf: *mut u16,
size: *mut u32,
) -> i32;
fn GetLastError() -> u32; fn GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle; fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32; fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
@@ -66,11 +93,6 @@ extern "system" {
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32; 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" { extern "C" {
fn _kbhit() -> i32; fn _kbhit() -> i32;
fn _getch() -> i32; fn _getch() -> i32;
@@ -109,7 +131,10 @@ pub fn poll_key() -> Option<Key> {
} }
fn wide(path: &Path) -> Vec<u16> { fn wide(path: &Path) -> Vec<u16> {
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<String> { fn image_name(pid: u32) -> Option<String> {
@@ -156,7 +181,9 @@ pub fn java_processes() -> Vec<JavaProcess> {
if out.iter().any(|w| w.pid == pid) { if out.iter().any(|w| w.pid == pid) {
continue; 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(); let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") { if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
out.push(JavaProcess { pid, title, 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 found = false;
let mut ok = Module32FirstW(snapshot, &mut entry); let mut ok = Module32FirstW(snapshot, &mut entry);
while ok != 0 { 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) { if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true; found = true;
break; break;
@@ -196,10 +227,16 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
/// LoadLibraryW(dll) in the target process through CreateRemoteThread. /// LoadLibraryW(dll) in the target process through CreateRemoteThread.
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> { pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
unsafe { 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); let process = OpenProcess(access, 0, pid);
if process.is_null() { 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); let result = inject_into(process, pid, dll);
CloseHandle(process); 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> { unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
let mut wow64 = 0; unsafe fn machine(process: Handle) -> Result<u16, String> {
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 { let mut emulated = 0;
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into()); 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) { if module_loaded(pid, &file_name) {
return Err(format!("{file_name} is already loaded in process {pid}")); return Err(format!("{file_name} is already loaded in process {pid}"));
} }
let path = wide(dll); let path = wide(dll);
let bytes = path.len() * 2; 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() { if remote.is_null() {
return Err(last_error("VirtualAllocEx")); 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); VirtualFreeEx(process, remote, 0, MEM_RELEASE);
Err(err) 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")); 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() { if load_library.is_null() {
return cleanup(last_error("resolve LoadLibraryW")); 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() { if thread.is_null() {
return cleanup(last_error("CreateRemoteThread")); 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?)")) 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<u16> = "open".encode_utf16().collect();
let target: Vec<u16> = 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 }
}
+15 -4
View File
@@ -10,7 +10,10 @@ fn main() {
Err(_) => newest_remapped_jar(), Err(_) => newest_remapped_jar(),
}; };
if !jar.is_file() { 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:rerun-if-changed={}", jar.display());
println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", 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) { if let Ok(entries) = fs::read_dir(&libs) {
for entry in entries.flatten() { for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string(); 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; 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) { if best.as_ref().map_or(true, |(t, _)| time > *t) {
best = Some((time, entry.path())); 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"))
} }
+2
View File
@@ -6,6 +6,8 @@
mod bridge; mod bridge;
mod jni; mod jni;
mod log; mod log;
#[path = "../../paths.rs"]
mod paths;
mod worker; mod worker;
use std::ffi::c_void; use std::ffi::c_void;
+4 -46
View File
@@ -1,61 +1,19 @@
use std::ffi::c_void;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH}; 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 { fn log_path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new(); static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| { PATH.get_or_init(|| crate::paths::native_log(std::process::id()))
module_path().with_file_name("prestige-native.log")
})
} }
/// Appends a line to prestige-native.log beside the DLL.
pub fn log(message: &str) { pub fn log(message: &str) {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); 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()) { if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) {
let _ = writeln!(file, "[{secs}] {message}"); let _ = writeln!(file, "[{secs}] {message}");
} }
+4 -2
View File
@@ -29,6 +29,7 @@ pub fn run() {
} }
unsafe fn install() -> Result<(), String> { unsafe fn install() -> Result<(), String> {
crate::paths::initialize().map_err(|error| format!("create OpenPrestige data directory: {error}"))?;
let vm = find_vm()?; let vm = find_vm()?;
let env = attach_current_thread(vm)?; let env = attach_current_thread(vm)?;
struct Detach(VmPtr); struct Detach(VmPtr);
@@ -45,7 +46,7 @@ unsafe fn install() -> Result<(), String> {
log!("JVMTI ready"); log!("JVMTI ready");
let jar = write_payload()?; let jar = write_payload()?;
log!("payload written to {}", jar.display()); log!("using payload at {}", jar.display());
let loader = add_to_knot(&jni, &jar)?; let loader = add_to_knot(&jni, &jar)?;
log!("payload appended to the game class loader: {}", describe(&jni, loader)); log!("payload appended to the game class loader: {}", describe(&jni, loader));
@@ -103,7 +104,8 @@ unsafe fn find_vm() -> Result<VmPtr, String> {
} }
fn write_payload() -> Result<PathBuf, String> { fn write_payload() -> Result<PathBuf, String> {
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()))?; 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())); let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?; std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
+36
View File
@@ -0,0 +1,36 @@
use std::io;
use std::path::PathBuf;
fn env_path(name: &str) -> Option<PathBuf> {
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<PathBuf> {
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"))
}
+182
View File
@@ -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 <vector>\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()
+127
View File
@@ -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("<III", image)
if magic != 0xFEEDFACF or cpu not in (0x01000007, 0x0100000C):
raise ValueError("The macOS build produced an unexpected Mach-O architecture.")
records.append((cpu, subtype, offset, len(image), 14))
offset = (offset + len(image) + 16383) & ~16383
if len({record[0] for record in records}) != 2:
raise ValueError("The universal library needs one Intel slice and one ARM64 slice.")
temporary = destination.with_suffix(".dylib.tmp")
with temporary.open("wb") as output:
output.write(struct.pack(">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("<I", header, 60)[0])
signature, machine = struct.unpack("<4sH", library.read(6))
expected = 0xAA64 if platform.endswith("aarch64") else 0x8664
if signature == b"PE\0\0" and machine == expected:
return
elif platform.startswith("linux-") and header[:6] == b"\x7fELF\x02\x01":
expected = 183 if platform.endswith("aarch64") else 62
if struct.unpack_from("<H", header, 18)[0] == expected:
return
elif platform.startswith("macos-") and header[:4] == b"\xcf\xfa\xed\xfe":
expected = 0x0100000C if platform.endswith("aarch64") else 0x01000007
if struct.unpack_from("<I", header, 4)[0] == expected:
return
raise ValueError(f"The {platform} build produced a library with an unexpected architecture.")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--platform", choices=[item[0] for item in TARGETS],
help="Build one slice for troubleshooting. Default builds the full release set.")
args = parser.parse_args()
ensure_tools()
cache = ROOT / "build/imgui-core-cache"
for platform, target, filename in TARGETS:
if args.platform and args.platform != platform:
continue
print(f"Building small ImGui JNI for {platform}", flush=True)
output = ROOT / "build/core-natives" / platform
if platform.startswith("macos-"):
output = ROOT / "build/imgui-core-slices" / platform
work = ROOT / "build/imgui-core-release" / platform
work.mkdir(parents=True, exist_ok=True)
log_path = work / "build.log"
command = [
sys.executable, str(ROOT / "scripts/build_imgui_core.py"),
"--build-dir", str(work),
"--cache-dir", str(cache), "--output", str(output), "--zig-target", target,
]
with log_path.open("w", encoding="utf-8") as log:
result = subprocess.run(command, stdout=log, stderr=subprocess.STDOUT)
if result.returncode:
print("\n".join(log_path.read_text(encoding="utf-8", errors="replace").splitlines()[-40:]), flush=True)
raise RuntimeError(f"The {platform} build failed. Full log: {log_path}")
if not (output / filename).is_file():
raise RuntimeError(f"The {platform} build did not produce {filename}.")
validate_native(output / filename, platform)
print(f"Built {output / filename} ({(output / filename).stat().st_size:,} bytes)", flush=True)
if not args.platform:
destination = ROOT / "build/core-natives/macos-universal"
destination.mkdir(parents=True, exist_ok=True)
slices = ROOT / "build/imgui-core-slices"
universal([
slices / "macos-x86_64/libimgui-java64.dylib",
slices / "macos-aarch64/libimgui-java64.dylib",
], destination / "libimgui-java64.dylib")
for license_file in (slices / "macos-x86_64").glob("LICENSE-*.txt"):
shutil.copyfile(license_file, destination / license_file.name)
print("All five release ImGui native libraries are ready.", flush=True)
if __name__ == "__main__":
main()
+58
View File
@@ -0,0 +1,58 @@
cmake_minimum_required(VERSION 3.20)
project(openprestige_imgui LANGUAGES CXX)
file(GLOB GENERATED CONFIGURE_DEPENDS "${CORE_JNI_DIR}/imgui_*.cpp")
add_library(imgui-java64 SHARED
${GENERATED}
"${CORE_JNI_DIR}/imgui.cpp"
"${CORE_JNI_DIR}/imgui_draw.cpp"
"${CORE_JNI_DIR}/imgui_tables.cpp"
"${CORE_JNI_DIR}/imgui_widgets.cpp"
"${CORE_JNI_DIR}/imgui_demo.cpp"
"${CORE_JNI_DIR}/jni_jvm.cpp"
"${CORE_JNI_DIR}/jni_common.cpp"
"${CORE_JNI_DIR}/jni_binding_struct.cpp"
"${CORE_JNI_DIR}/jni_callbacks.cpp"
"${CORE_JNI_DIR}/jni_assertion.cpp"
"${CORE_JNI_DIR}/jni_internal.cpp"
)
target_compile_features(imgui-java64 PRIVATE cxx_std_17)
if(WIN32)
set(JNI_OS win32)
elseif(APPLE)
set(JNI_OS mac)
else()
set(JNI_OS linux)
endif()
target_include_directories(imgui-java64 PRIVATE
"${CORE_JNI_DIR}"
"${CORE_JNI_DIR}/jni-headers"
"${CORE_JNI_DIR}/jni-headers/${JNI_OS}"
)
target_compile_definitions(imgui-java64 PRIVATE IMGUI_DISABLE_DEMO_WINDOWS)
set_target_properties(imgui-java64 PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
LIBRARY_OUTPUT_DIRECTORY "${CORE_OUTPUT_DIR}"
RUNTIME_OUTPUT_DIRECTORY "${CORE_OUTPUT_DIR}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CORE_OUTPUT_DIR}"
)
if(MSVC)
target_compile_options(imgui-java64 PRIVATE /O1 /Gy /GL /utf-8)
target_link_options(imgui-java64 PRIVATE /LTCG /OPT:REF /OPT:ICF)
else()
target_compile_options(imgui-java64 PRIVATE -Os -ffunction-sections -fdata-sections)
if(APPLE)
target_link_options(imgui-java64 PRIVATE -Wl,-dead_strip)
else()
target_link_options(imgui-java64 PRIVATE -Wl,--gc-sections -s)
endif()
if(MINGW)
target_link_options(imgui-java64 PRIVATE -static-libgcc -static-libstdc++)
endif()
endif()
if(WIN32)
set_target_properties(imgui-java64 PROPERTIES PREFIX "")
target_link_libraries(imgui-java64 PRIVATE imm32 user32)
endif()
+8
View File
@@ -0,0 +1,8 @@
import com.badlogic.gdx.jnigen.NativeCodeGenerator;
public final class GenerateCore {
public static void main(String[] args) throws Exception {
new NativeCodeGenerator().generate(args[0], args[1], args[2], null,
new String[] {"**/extension/**", "**/lwjgl3/**"});
}
}
+49
View File
@@ -0,0 +1,49 @@
# Core ImGui JNI build
OpenPrestige uses core ImGui with its Java OpenGL backend. This build keeps all core and internal
JNI methods from imgui-java 1.92.7.1, and removes the native extension implementations for ImPlot,
ImNodes, ImGuizmo, imgui-node-editor, text editor, file dialog, knobs, and memory editor. Demo
windows are disabled. Core callbacks, font rendering, tables, docking, and draw lists remain available.
The source revisions and generator dependencies are pinned in `../build_imgui_core.py`.
The build consumes upstream generated Java without modifying it. A portability step replaces
GNU variable-length arrays in the generated C++ with standard C++ vectors for MSVC.
## Windows x64
Set `JAVA_HOME` to JDK 21 and install Visual Studio C++ tools and CMake. From the repository root run
```powershell
python scripts/build_imgui_core.py --output build/core-natives/windows-x86_64 --architecture x64
```
The output contains `imgui-java64.dll` and the upstream licenses. Gradle picks up
`build/core-natives`, embeds it under `prestige-natives`, and omits the corresponding upstream
native. The Java extension wrappers are omitted from the shaded payload.
Use a distinct `--build-dir` for each target. ARM64 cross builds use the toolchain files in
`toolchains`, which require LLVM-MinGW on Windows targets and an aarch64 Linux cross compiler on
Linux targets. The macOS toolchain produces a universal library. CI builds all five variants.
## Full release build on Windows
`build-release.bat` calls `python scripts/build_release_imgui.py` before Gradle builds the JAR.
The helper fetches the pinned sources and generator dependencies, installs CMake, Ninja and Zig
when needed, and builds Windows and Linux x64/ARM64 plus both macOS slices. It combines the macOS
slices into a universal library. Downloads are cached in `build/imgui-core-cache`, and compilation
is incremental under `build/imgui-core-release`.
The complete release still requires JDK 21, Rust, and the MSVC x64/ARM64 build tools used by the
Rust Windows targets. Python's package installer needs network access on the first run.
To diagnose one cross build, use `python scripts/build_release_imgui.py --platform windows-aarch64`.
Running the helper without arguments builds the entire ImGui release set.
## JNI verification
```powershell
java -Dimgui.library.path=build/core-natives/windows-x86_64 -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
```
This initializes JNI, builds the real UI font atlas, and generates geometry from widgets and draw
lists. A Minecraft smoke test should additionally exercise the ClickGUI and OpenGL backend.
+59
View File
@@ -0,0 +1,59 @@
import imgui.ImDrawList;
import imgui.ImFontConfig;
import imgui.ImGui;
import imgui.ImGuiIO;
import imgui.type.ImInt;
import imgui.type.ImString;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
/** Exercises core JNI entry points, real UI fonts, input widgets and draw data without a game. */
public final class SmokeTest {
public static void main(String[] args) throws Exception {
ImGui.createContext();
try {
ImGuiIO io = ImGui.getIO();
io.setIniFilename(null);
io.setDisplaySize(1000, 600);
io.setDeltaTime(1.0f / 60);
ImFontConfig config = new ImFontConfig();
try {
config.setPixelSnapH(true);
io.getFonts().addFontFromMemoryTTF(Files.readAllBytes(Path.of(args[0])), 18, config);
} finally {
config.destroy();
}
if (!io.getFonts().build()) throw new AssertionError("Font atlas did not build");
ImInt width = new ImInt();
ImInt height = new ImInt();
ByteBuffer pixels = io.getFonts().getTexDataAsRGBA32(width, height);
if (pixels == null || width.get() <= 0 || height.get() <= 0) {
throw new AssertionError("Font atlas is empty");
}
ImString input = new ImString("OpenPrestige", 64);
float[] value = {0.5f};
int vertices = 0;
for (int frame = 0; frame < 3; frame++) {
ImGui.newFrame();
ImGui.setNextWindowSize(500, 350);
ImGui.begin("Core JNI smoke test");
ImGui.text("OpenPrestige core-only ImGui");
ImGui.button("Inject");
ImGui.sliderFloat("Scale", value, 0, 1);
ImGui.inputText("Config name", input);
ImDrawList draw = ImGui.getWindowDrawList();
draw.addCircleFilled(100, 100, 20, 0xff80a0ff);
draw.addText(120, 100, 0xffffffff, "Draw list");
ImGui.end();
ImGui.render();
vertices = ImGui.getDrawData().getTotalVtxCount();
}
if (vertices <= 0) throw new AssertionError("No UI geometry was generated");
System.out.println("Core JNI passed. Font atlas " + width.get() + "x" + height.get()
+ ", vertices " + vertices);
} finally {
ImGui.destroyContext();
}
}
}
@@ -0,0 +1,3 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
@@ -0,0 +1,2 @@
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "" FORCE)
@@ -0,0 +1,4 @@
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CXX_COMPILER aarch64-w64-mingw32-clang++)
set(CMAKE_RC_COMPILER aarch64-w64-mingw32-windres)
@@ -0,0 +1,4 @@
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-clang++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
+28 -80
View File
@@ -1,9 +1,7 @@
"""Packs one platform's release zip, or writes SHA256SUMS.txt for dist/release. Called by release.bat. """Package openprestige releases.
package_release.py zip <out_dir> <version> <platform> <injector> <native_lib> <payload_jar> package_release.py offline <out_dir> <version> <platform> <injector> <native_lib> <payload_jar>
package_release.py checksums <out_dir> package_release.py checksums <out_dir>
Zips are written with unix permissions so PrestigeInjector stays executable after unzip on Linux/macOS.
""" """
import hashlib import hashlib
import os import os
@@ -11,69 +9,7 @@ import sys
import time import time
import zipfile import zipfile
README = { FOOTER = '\nCredits\n Cracked by Skid Solutions\n technix sourcing / Frida scripts\n stutheguyy fixing\n bynbang / nitrogenoliquido ZKM transformers\n dario AI models\n\nDiscord: https://discord.gg/x2FJyGrDNg\n'
"windows": """Prestige {version} - Windows (x86_64)
Contents
PrestigeInjector.exe injector
prestige_native.dll loader library (keep it next to the injector)
{jar} the same client as a Fabric mod (optional, see below)
Inject
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
2. Run PrestigeInjector.exe and pick the game from the list.
If SmartScreen blocks the exe: More info -> Run anyway.
""",
"linux": """Prestige {version} - Linux (x86_64)
Works on any glibc distro from 2012 on (glibc 2.17+): Ubuntu, Debian, Fedora, Arch, Mint, openSUSE, ...
Not for musl distros (Alpine) or ARM machines.
Contents
PrestigeInjector injector
libprestige_native.so loader library (keep it next to the injector)
{jar} the same client as a Fabric mod (optional, see below)
Inject
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
2. In a terminal in this folder: ./PrestigeInjector
Run it as the same user as the game. The game must not use -XX:+DisableAttachMechanism.
""",
"macos": """Prestige {version} - macOS (universal: Intel + Apple Silicon)
Contents
PrestigeInjector injector
libprestige_native.dylib loader library (keep it next to the injector)
{jar} the same client as a Fabric mod (optional, see below)
Inject
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
2. In Terminal, in this folder, clear the download quarantine once:
xattr -dr com.apple.quarantine .
3. Run: ./PrestigeInjector
Run it as the same user as the game. The game must not use -XX:+DisableAttachMechanism.
""",
}
MOD_NOTE = """
Install as a mod instead
Put {jar} into the mods/ folder of a Fabric 1.21.11 instance; no injector needed.
Do not use both at once.
"""
TESTED = "Tested on Prism Launcher with Minecraft 1.21.11 + Fabric.\n"
FOOTER = """
Credits
Cracked by Skid Solutions
technix sourcing / Frida scripts
stutheguyy fixing
bynbang / nitrogenoliquido ZKM transformers
dario AI models
Discord: https://discord.gg/x2FJyGrDNg
"""
def add(zf, arcname, data, mode): def add(zf, arcname, data, mode):
@@ -85,19 +21,31 @@ def add(zf, arcname, data, mode):
def make_zip(out_dir, version, platform, injector, lib, jar): def make_zip(out_dir, version, platform, injector, lib, jar):
family = platform.split("-")[0] if platform in ("windows-aarch64", "linux-aarch64"):
name = f"prestige-{version}-{platform}" native = "imgui-java64.dll" if platform.startswith("windows") else "libimgui-java64.so"
jar_name = os.path.basename(jar) with zipfile.ZipFile(jar) as payload:
title, body = README[family].format(version=version, jar=jar_name).split("\n", 1) entry = f"prestige-natives/{platform}/{native}"
readme = title + "\n" + TESTED + body + MOD_NOTE.format(jar=jar_name) + FOOTER if entry not in payload.namelist():
if family == "windows": raise ValueError(f"ARM64 payload is missing {entry}. Build the core ImGui natives first.")
readme = readme.replace("\n", "\r\n") name = f"openprestige-{version}-{platform}"
out = os.path.join(out_dir, name + ".zip") out = os.path.join(out_dir, name + ".zip")
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf: os.makedirs(out_dir, exist_ok=True)
for path, mode in ((injector, 0o755), (lib, 0o644), (jar, 0o644)): with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as bundle:
with open(path, "rb") as f: executable = "OpenPrestigeInjector" + (".exe" if platform.startswith("windows-") else "")
add(zf, f"{name}/{os.path.basename(path)}", f.read(), mode) for path, filename, permissions in ((injector, executable, 0o755), (lib, os.path.basename(lib), 0o644)):
add(zf, f"{name}/README.txt", readme.encode(), 0o644) with open(path, "rb") as file:
add(bundle, f"{name}/{filename}", file.read(), permissions)
readme = (f"OpenPrestige {version} - {platform}\n\n"
f"Extract this complete folder and run {executable}.\n"
"Original CLI: Up/Down selects a Minecraft JVM, Enter injects, Esc quits.\n"
f"Keep {os.path.basename(lib)} beside the executable.\n"
"The native library embeds its payload. No external JAR or runtime download is required.\n"
"The launcher has no downloader or update checks.\n\n"
"Requires a matching 64-bit Minecraft 1.21.11 Fabric JVM.\n"
"Linux/macOS: run from a terminal as the same user as the game; attach must be enabled.\n"
"The Fabric mod JAR is available separately. Use either injection or the mod.\n"
+ FOOTER)
add(bundle, f"{name}/README.txt", readme.encode(), 0o644)
print(f" wrote {out} ({os.path.getsize(out) // 1024} KiB)") print(f" wrote {out} ({os.path.getsize(out) // 1024} KiB)")
@@ -114,7 +62,7 @@ def checksums(out_dir):
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) == 8 and sys.argv[1] == "zip": if len(sys.argv) == 8 and sys.argv[1] == "offline":
make_zip(*sys.argv[2:]) make_zip(*sys.argv[2:])
elif len(sys.argv) == 3 and sys.argv[1] == "checksums": elif len(sys.argv) == 3 and sys.argv[1] == "checksums":
checksums(sys.argv[2]) checksums(sys.argv[2])
+48
View File
@@ -0,0 +1,48 @@
"""Check this repository's release contents and architecture requirements."""
import contextlib
import hashlib
import io
from pathlib import Path
import tempfile
import unittest
import zipfile
import package_release
class ReleasePackaging(unittest.TestCase):
def test_contents_and_checksums(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
injector = root / "injector.exe"
library = root / "prestige_native.dll"
jar = root / "prestige-1.2.3.jar"
injector.write_bytes(b"launcher")
library.write_bytes(b"library")
with zipfile.ZipFile(jar, "w") as payload:
payload.writestr("example.class", b"payload")
out = root / "release"
with contextlib.redirect_stdout(io.StringIO()):
package_release.make_zip(str(out), "1.2.3", "windows-x86_64", str(injector), str(library), str(jar))
archive = next(out.glob("*.zip"))
with zipfile.ZipFile(archive) as bundle:
names = {Path(name).name for name in bundle.namelist()}
self.assertEqual(names, {"OpenPrestigeInjector.exe", library.name, "README.txt"})
readme = bundle.read("openprestige-1.2.3-windows-x86_64/README.txt").decode()
self.assertIn("embeds its payload", readme)
self.assertNotIn("Keep the payload JAR beside", readme)
package_release.checksums(str(out))
expected = f"{hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.name}\n"
self.assertEqual((out / "SHA256SUMS.txt").read_text(), expected)
def test_arm_native_required(self):
with tempfile.TemporaryDirectory() as directory:
jar = Path(directory) / "prestige-1.2.3.jar"
with zipfile.ZipFile(jar, "w"):
pass
with self.assertRaisesRegex(ValueError, "ARM64 payload is missing"):
package_release.make_zip(directory, "1.2.3", "windows-aarch64", "unused.exe", "unused.dll", str(jar))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,42 @@
package dev.zprestige.prestige.client;
import java.nio.file.Path;
public final class AppPaths {
private static final Path ROOT = root();
private AppPaths() {}
public static Path configs() {
return ROOT.resolve("configs");
}
public static Path runtime() {
return ROOT.resolve("runtime");
}
private static Path root() {
String os = System.getProperty("os.name", "");
Path base;
if (os.startsWith("Windows")) {
base = env("APPDATA");
Path home = env("USERPROFILE");
if (base == null && home != null) base = home.resolve("AppData").resolve("Roaming");
} else {
Path home = env("HOME");
if (os.startsWith("Mac")) {
base = home == null ? null : home.resolve("Library").resolve("Application Support");
} else {
base = env("XDG_DATA_HOME");
if (base == null || !base.isAbsolute()) base = home == null ? null : home.resolve(".local").resolve("share");
}
}
if (base == null) base = Path.of(System.getProperty("java.io.tmpdir"));
return base.resolve(".openprestige");
}
private static Path env(String name) {
String value = System.getenv(name);
return value == null || value.isEmpty() ? null : Path.of(value);
}
}
@@ -1,5 +1,6 @@
package dev.zprestige.prestige.inject; package dev.zprestige.prestige.inject;
import dev.zprestige.prestige.client.AppPaths;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -31,7 +32,7 @@ final class TransformCache {
try { try {
Path jar = Path.of(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI()); Path jar = Path.of(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI());
if (!Files.isRegularFile(jar)) return null; if (!Files.isRegularFile(jar)) return null;
Path dir = Path.of(System.getProperty("java.io.tmpdir"), "PrestigeLoader", "cache"); Path dir = AppPaths.runtime().resolve("cache");
Files.createDirectories(dir); Files.createDirectories(dir);
prune(dir); prune(dir);
return new TransformCache(dir, MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(jar))); return new TransformCache(dir, MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(jar)));
@@ -3,6 +3,7 @@ package dev.zprestige.prestige.ui;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import dev.zprestige.prestige.client.AppPaths;
import dev.zprestige.prestige.client.Prestige; import dev.zprestige.prestige.client.Prestige;
import java.io.IOException; import java.io.IOException;
@@ -17,7 +18,7 @@ import java.util.Locale;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* Local (offline) backend of the Configs page. Every config is one JSON file in {@code <gameDir>/prestige/configs/}: * Local (offline) backend of the Configs page. Every config is one JSON file in the shared OpenPrestige data directory:
* the card metadata plus the client's own snapshot ({@code ConfigManager.saveToJson()}), which is applied again with * the card metadata plus the client's own snapshot ({@code ConfigManager.saveToJson()}), which is applied again with
* {@code ConfigManager.loadFromJson(String)}. "Public configs" lists the configs saved with Public visibility, "Personal configs" * {@code ConfigManager.loadFromJson(String)}. "Public configs" lists the configs saved with Public visibility, "Personal configs"
* lists all of them; Create saves the current module/HUD state, Update overwrites it, Delete removes the file. * lists all of them; Create saves the current module/HUD state, Update overwrites it, Delete removes the file.
@@ -45,6 +46,10 @@ public final class LocalConfigs {
private static String lastQuery = null; private static String lastQuery = null;
static Path dir() { static Path dir() {
return AppPaths.configs().resolve("presets");
}
private static Path legacyDir() {
Path base; Path base;
try { base = net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir(); } catch (Throwable t) { base = Path.of("."); } try { base = net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir(); } catch (Throwable t) { base = Path.of("."); }
return base.resolve("prestige").resolve("configs"); return base.resolve("prestige").resolve("configs");
@@ -86,21 +91,61 @@ public final class LocalConfigs {
for (Path p : (Iterable<Path>) s.filter(f -> f.getFileName().toString().endsWith(".json"))::iterator) { for (Path p : (Iterable<Path>) s.filter(f -> f.getFileName().toString().endsWith(".json"))::iterator) {
try { try {
Entry e = GSON.fromJson(Files.readString(p, StandardCharsets.UTF_8), Entry.class); Entry e = GSON.fromJson(Files.readString(p, StandardCharsets.UTF_8), Entry.class);
if (e == null || e.name == null || e.name.isBlank()) continue; if (normalize(e)) ALL.add(e);
if (e.tags == null) e.tags = new ArrayList<>();
e.tags.removeIf(java.util.Objects::isNull);
if (e.desc == null) e.desc = "";
if (e.author == null) e.author = "";
if (e.date == null) e.date = "";
ALL.add(e);
} catch (Throwable t) { System.out.println("[prestige-ui] config " + p.getFileName() + ": " + t); } } catch (Throwable t) { System.out.println("[prestige-ui] config " + p.getFileName() + ": " + t); }
} }
} }
importLegacy(d);
} catch (IOException e) { System.out.println("[prestige-ui] configs dir: " + e); } } catch (IOException e) { System.out.println("[prestige-ui] configs dir: " + e); }
ALL.sort(Comparator.comparingInt((Entry e) -> e.id).reversed()); ALL.sort(Comparator.comparingInt((Entry e) -> e.id).reversed());
rebuild(); rebuild();
} }
private static boolean normalize(Entry entry) {
if (entry == null || entry.name == null || entry.name.isBlank()) return false;
if (entry.tags == null) entry.tags = new ArrayList<>();
entry.tags.removeIf(java.util.Objects::isNull);
if (entry.desc == null) entry.desc = "";
if (entry.author == null) entry.author = "";
if (entry.date == null) entry.date = "";
return true;
}
private static JsonObject configWithoutId(Entry entry) {
JsonObject config = GSON.toJsonTree(entry).getAsJsonObject();
config.remove("id");
return config;
}
private static void importLegacy(Path directory) throws IOException {
Path legacy = legacyDir().toAbsolutePath().normalize();
if (!Files.isDirectory(legacy)) return;
String key = java.util.UUID.nameUUIDFromBytes(legacy.toString().getBytes(StandardCharsets.UTF_8)).toString();
Path marker = directory.resolve(".imported-" + key);
if (Files.exists(marker)) return;
try (Stream<Path> files = Files.list(legacy)) {
for (Path path : files.filter(p -> p.getFileName().toString().endsWith(".json")).toList()) {
Entry entry;
try {
entry = GSON.fromJson(Files.readString(path, StandardCharsets.UTF_8), Entry.class);
} catch (RuntimeException error) {
System.out.println("[prestige-ui] could not import " + path.getFileName() + ": " + error);
continue;
}
if (!normalize(entry)) continue;
JsonObject content = configWithoutId(entry);
if (ALL.stream().anyMatch(existing -> configWithoutId(existing).equals(content))) continue;
int originalId = entry.id;
if (ALL.stream().anyMatch(existing -> existing.id == originalId)) {
entry.id = ALL.stream().mapToInt(existing -> existing.id).max().orElse(0) + 1;
}
Files.writeString(file(entry), GSON.toJson(entry), StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE_NEW);
ALL.add(entry);
}
}
Files.writeString(marker, "");
}
private static Path file(Entry e) { return dir().resolve(e.id + "_" + e.name.replaceAll("[^A-Za-z0-9_.-]", "_") + ".json"); } private static Path file(Entry e) { return dir().resolve(e.id + "_" + e.name.replaceAll("[^A-Za-z0-9_.-]", "_") + ".json"); }
private static void write(Entry e) { private static void write(Entry e) {
@@ -0,0 +1,54 @@
package dev.zprestige.prestige.ui.mc;
import dev.zprestige.prestige.client.AppPaths;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.FileAlreadyExistsException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Locale;
/** Selects the native binding by JVM architecture before ImGui initializes. */
final class ImGuiNative {
private ImGuiNative() {}
static void prepare() throws IOException {
if (System.getProperty("imgui.library.path") != null) return;
String arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT);
boolean arm = arch.equals("aarch64") || arch.equals("arm64");
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
String family = os.startsWith("windows") ? "windows" : os.contains("mac") ? "macos" : "linux";
String platform = family.equals("macos") ? "macos-universal" : family + (arm ? "-aarch64" : "-x86_64");
String filename = System.mapLibraryName("imgui-java64");
String resource = "/prestige-natives/" + platform + "/" + filename;
try (InputStream stream = ImGuiNative.class.getResourceAsStream(resource)) {
if (stream == null) {
if (arm && !family.equals("macos")) throw new IOException("Missing ARM64 ImGui binding " + resource);
return;
}
byte[] bytes = stream.readAllBytes();
String digest;
try {
digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (NoSuchAlgorithmException error) {
throw new IOException("SHA-256 is unavailable", error);
}
Path directory = AppPaths.runtime().resolve("imgui").resolve(platform).resolve(digest);
Files.createDirectories(directory);
Path library = directory.resolve(filename);
if (!Files.isRegularFile(library)) {
Path temporary = Files.createTempFile(directory, "binding-", ".tmp");
try {
Files.write(temporary, bytes);
try { Files.move(temporary, library); } catch (FileAlreadyExistsException ignored) {}
} finally {
Files.deleteIfExists(temporary);
}
}
System.setProperty("imgui.library.path", directory.toAbsolutePath().toString());
}
}
}
@@ -20,6 +20,7 @@ import dev.zprestige.prestige.ui.Input;
import dev.zprestige.prestige.ui.Lib; import dev.zprestige.prestige.ui.Lib;
import dev.zprestige.prestige.ui.Theme; import dev.zprestige.prestige.ui.Theme;
import dev.zprestige.prestige.ui.Ui; import dev.zprestige.prestige.ui.Ui;
import dev.zprestige.prestige.client.AppPaths;
/** Draws the ported ClickGUI over the finished frame (called from the RenderSystem.flipFrame mixin). */ /** Draws the ported ClickGUI over the finished frame (called from the RenderSystem.flipFrame mixin). */
public final class Overlay { public final class Overlay {
@@ -40,14 +41,19 @@ public final class Overlay {
public static volatile long lastCloseNanos; public static volatile long lastCloseNanos;
static Path posFile() { static Path posFile() {
try { return net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("prestige_ui.json"); } catch (Throwable t) { return null; } return AppPaths.configs().resolve("prestige_ui.json");
} }
static void loadPos() { static void loadPos() {
if (posLoaded) return; if (posLoaded) return;
posLoaded = true; posLoaded = true;
try { try {
Path f = posFile(); Path f = posFile();
if (f == null || !Files.exists(f)) return; if (!Files.exists(f)) {
Path legacy = net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("prestige_ui.json");
if (!Files.isRegularFile(legacy)) return;
Files.createDirectories(f.getParent());
try { Files.copy(legacy, f); } catch (java.nio.file.FileAlreadyExistsException ignored) {}
}
String t = Files.readString(f); String t = Files.readString(f);
java.util.regex.Matcher mx = java.util.regex.Pattern.compile("\"x\"\\s*:\\s*(-?[0-9.]+)").matcher(t); java.util.regex.Matcher mx = java.util.regex.Pattern.compile("\"x\"\\s*:\\s*(-?[0-9.]+)").matcher(t);
java.util.regex.Matcher my = java.util.regex.Pattern.compile("\"y\"\\s*:\\s*(-?[0-9.]+)").matcher(t); java.util.regex.Matcher my = java.util.regex.Pattern.compile("\"y\"\\s*:\\s*(-?[0-9.]+)").matcher(t);
@@ -59,7 +65,8 @@ public final class Overlay {
static void savePos() { static void savePos() {
try { try {
Path f = posFile(); Path f = posFile();
if (f != null) Files.writeString(f, "{\"x\": " + fracX + ", \"y\": " + fracY + ", \"allowUpscale\": " + allowUpscale + "}\n"); Files.createDirectories(f.getParent());
Files.writeString(f, "{\"x\": " + fracX + ", \"y\": " + fracY + ", \"allowUpscale\": " + allowUpscale + "}\n");
} catch (Throwable t) { System.out.println("[prestige-ui] savePos " + t); } } catch (Throwable t) { System.out.println("[prestige-ui] savePos " + t); }
} }
@@ -192,6 +199,7 @@ public final class Overlay {
static void init() throws Exception { static void init() throws Exception {
if (inited) return; if (inited) return;
ImGuiNative.prepare();
ImGui.createContext(); ImGui.createContext();
ImGuiIO io = ImGui.getIO(); ImGuiIO io = ImGui.getIO();
io.setIniFilename(null); io.setIniFilename(null);