2 Commits
Author SHA1 Message Date
bbldrizzy 9b36349e1a Update README.md 2026-11-05 10:29:46 +01:00
bbldrizzy 2d6b2a3cd1 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.
2026-11-03 18:45:13 +00:00
37 changed files with 1285 additions and 452 deletions

No files matched your search

+92 -13
View File
@@ -6,18 +6,85 @@ on:
paths:
- "injector/**"
- "src/**"
- "scripts/**"
- "build.gradle"
- ".github/workflows/injector.yml"
jobs:
payload:
name: Payload jar
runs-on: ubuntu-latest
core-imgui:
name: ImGui (${{ matrix.platform }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: windows-x86_64
toolchain: windows-x86_64
- os: ubuntu-latest
platform: windows-aarch64
toolchain: windows-aarch64
- os: ubuntu-latest
platform: linux-x86_64
- os: ubuntu-latest
platform: linux-aarch64
toolchain: linux-aarch64
- os: macos-14
platform: macos-universal
toolchain: macos-universal
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Install native build tools
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y cmake g++ g++-aarch64-linux-gnu
- name: Install Windows cross compiler
if: startsWith(matrix.platform, 'windows-')
run: |
curl -fL --retry 3 https://github.com/mstorsjo/llvm-mingw/releases/download/20261020/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64.tar.xz -o "$RUNNER_TEMP/llvm-mingw.tar.xz"
tar -xf "$RUNNER_TEMP/llvm-mingw.tar.xz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64/bin" >> "$GITHUB_PATH"
- name: Build core-only ImGui
env:
TOOLCHAIN: ${{ matrix.toolchain }}
run: |
if [ -n "$TOOLCHAIN" ]; then
python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}" --toolchain "scripts/imgui-core/toolchains/$TOOLCHAIN.cmake"
else
python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}"
fi
- name: Exercise core JNI with the UI font
if: matrix.platform == 'linux-x86_64' || matrix.platform == 'macos-universal'
run: |
java -Dimgui.library.path="$PWD/build/core-natives/${{ matrix.platform }}" -cp build/imgui-core/deps/imgui-java-binding-1.92.7.1.jar scripts/imgui-core/SmokeTest.java src/main/resources/prestige_ui/fonts/DMSans-Regular.ttf
- uses: actions/upload-artifact@v4
with:
name: imgui-${{ matrix.platform }}
path: build/core-natives/*
if-no-files-found: error
payload:
name: Payload jar
needs: core-imgui
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check release packaging
run: python3 scripts/test_release.py
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- uses: actions/download-artifact@v4
with:
pattern: imgui-*
merge-multiple: true
path: build/core-natives
- name: Build remapped jar
run: |
chmod +x ./gradlew
@@ -33,42 +100,54 @@ jobs:
if-no-files-found: error
injector:
name: Injector (${{ matrix.target }})
name: OpenPrestige (${{ matrix.platform.target }})
needs: payload
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
include:
platform:
- os: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
files: |
injector/target/release/PrestigeInjector.exe
injector/target/release/OpenPrestigeInjector.exe
injector/target/release/prestige_native.dll
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
name: linux-x86_64
files: |
injector/target/release/PrestigeInjector
injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.so
- os: windows-11-arm
target: aarch64-pc-windows-msvc
name: windows-aarch64
files: |
injector/target/release/OpenPrestigeInjector.exe
injector/target/release/prestige_native.dll
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
name: linux-aarch64
files: |
injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.so
- os: macos-13
target: x86_64-apple-darwin
name: macos-x86_64
files: |
injector/target/release/PrestigeInjector
injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.dylib
- os: macos-14
target: aarch64-apple-darwin
name: macos-aarch64
files: |
injector/target/release/PrestigeInjector
injector/target/release/OpenPrestigeInjector
injector/target/release/libprestige_native.dylib
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
targets: ${{ matrix.platform.target }}
- name: Download payload jar
uses: actions/download-artifact@v4
with:
@@ -81,6 +160,6 @@ jobs:
- name: Upload injector
uses: actions/upload-artifact@v4
with:
name: injector-${{ matrix.name }}
path: ${{ matrix.files }}
name: openprestige-${{ matrix.platform.name }}
path: ${{ matrix.platform.files }}
if-no-files-found: error
+4
View File
@@ -20,3 +20,7 @@ openvape.zip
/injector/target/
/tools
/examples
# Python build helpers
__pycache__/
*.pyc
+3 -1
View File
@@ -1,6 +1,8 @@
# Open Prestige
A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11.
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 fully deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11.
---
- we will work on removing the param obf and also renaming vars in the coming days. stay tuned!
+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,
# and collects PrestigeInjector.exe + prestige_native.dll in dist\.
# native tools write progress/notes to stderr; failures are detected through $LASTEXITCODE instead
# Build OpenPrestige and collect the host binaries in dist/.
$ErrorActionPreference = 'Continue'
Set-Location $PSScriptRoot
if (Test-Path 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot') { $env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot' }
@@ -9,6 +7,6 @@ if ($LASTEXITCODE -ne 0) { throw 'gradle remapJar failed' }
cargo build --release --manifest-path injector\Cargo.toml
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
New-Item -ItemType Directory -Force dist | Out-Null
Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
Copy-Item injector\target\release\OpenPrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
Get-ChildItem build\libs\prestige-*.jar | Where-Object { $_.Name -notmatch '-(dev|sources)\.jar$' } | Copy-Item -Destination dist\ -Force
Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll + dist\prestige-*.jar (Fabric mod)'
Write-Host 'Done: dist\OpenPrestigeInjector.exe + native library + Fabric mod JAR'
+6 -16
View File
@@ -1,34 +1,24 @@
#!/bin/sh
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader
# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/.
# Optional: set JAVA_HOME to pick the JDK used by Gradle.
# Build OpenPrestige for the current host. Optional JAVA_HOME selects the Gradle JDK.
set -eu
cd "$(dirname "$0")"
case "$(uname -s)" in
Linux) LIB=libprestige_native.so ;;
Linux) LIB=libprestige_native.so ;;
Darwin) LIB=libprestige_native.dylib ;;
*) echo "unsupported OS: $(uname -s) (use build-injector.ps1 on Windows)" >&2; exit 1 ;;
*) echo "Use build-injector.ps1 on Windows" >&2; exit 1 ;;
esac
if [ -n "${JAVA_HOME:-}" ]; then
export JAVA_HOME
PATH="$JAVA_HOME/bin:$PATH"
export PATH
fi
./gradlew remapJar --console=plain
cargo build --release --manifest-path injector/Cargo.toml
mkdir -p dist
cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/
chmod +x dist/PrestigeInjector
# the same remapped jar is the Fabric mod (drop it into mods/)
cp -f "injector/target/release/OpenPrestigeInjector" "injector/target/release/$LIB" dist/
chmod +x dist/OpenPrestigeInjector
for jar in build/libs/prestige-*.jar; do
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
cp -f "$jar" dist/
done
echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)"
echo "Done: dist/OpenPrestigeInjector + dist/$LIB + Fabric mod JAR"
+25 -29
View File
@@ -1,12 +1,8 @@
@echo off
rem Builds release zips for every platform into dist\release\ (run on Windows):
rem prestige-<ver>-windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar
rem prestige-<ver>-linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro)
rem prestige-<ver>-macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon)
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.
rem Builds fully offline OpenPrestige bundles.
rem OpenPrestige CLI + native library with embedded payload
rem Requires Rust with MSVC x64/ARM64 tools, Python 3 and JDK 21.
rem Fetches pinned ImGui sources and cross-compiles the small JNI libraries.
setlocal EnableExtensions
cd /d "%~dp0"
set "SELF=%~f0"
@@ -23,55 +19,55 @@ where cargo >nul 2>&1 || (echo cargo not found: install Rust from https://rustup
where python >nul 2>&1 || (echo python not found: install Python 3 from https://python.org & goto :fail)
python -m ziglang version >nul 2>&1 || (
echo === Installing zig ^(pip ziglang^)
python -m pip install --user ziglang || goto :fail
python -m pip install --user ziglang==0.16.0 || goto :fail
)
where cargo-zigbuild >nul 2>&1 || (
echo === Installing cargo-zigbuild
cargo install --locked cargo-zigbuild || goto :fail
)
rustup target add x86_64-pc-windows-msvc x86_64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail
rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail
rem zigbuild's linker wrappers break on paths with spaces, so keep its cache on the 8.3 short path
if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild"
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI"
rem ---- core ImGui JNI libraries ------------------------------------------
echo === Fetching sources and building small ImGui libraries
python scripts\build_release_imgui.py || goto :fail
rem ---- payload jar -------------------------------------------------------
echo === Building payload jar
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
set "JAR="
for /f "delims=" %%j in ('dir /b /o-d "build\libs\prestige-*.jar" ^| findstr /v /i /c:"-dev.jar" /c:"-sources.jar"') do if not defined JAR set "JAR=%CD%\build\libs\%%j"
if not defined JAR (echo no remapped jar in build\libs & goto :fail)
rem every target embeds this exact jar
rem The native library uses this exact payload JAR.
set "PRESTIGE_PAYLOAD=%JAR%"
if exist "%OUT%" rmdir /s /q "%OUT%"
mkdir "%OUT%" || goto :fail
if not exist "%OUT%" mkdir "%OUT%" || goto :fail
rem ---- native builds -----------------------------------------------------
echo === Windows x86_64
cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail
call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || goto :fail
echo === Linux x86_64 (glibc 2.17+)
cargo zigbuild --release --manifest-path injector\Cargo.toml --target x86_64-unknown-linux-gnu.2.17 || goto :fail
call :package linux-x86_64 x86_64-unknown-linux-gnu PrestigeInjector libprestige_native.so || goto :fail
echo === macOS universal (x86_64 + arm64)
cargo zigbuild --release --manifest-path injector\Cargo.toml --target universal2-apple-darwin || goto :fail
call :package macos-universal universal2-apple-darwin PrestigeInjector libprestige_native.dylib || goto :fail
call :build windows-x86_64 x86_64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
call :build windows-aarch64 aarch64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
call :build linux-x86_64 x86_64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild x86_64-unknown-linux-gnu.2.17 || goto :fail
call :build linux-aarch64 aarch64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild aarch64-unknown-linux-gnu.2.17 || goto :fail
call :build macos-universal universal2-apple-darwin OpenPrestigeInjector libprestige_native.dylib zigbuild || goto :fail
copy /y "%JAR%" "%OUT%\" >nul || goto :fail
python scripts\package_release.py checksums "%OUT%" || goto :fail
echo.
echo === Done: %OUT%
dir /b "%OUT%"
call :maybe_pause
exit /b 0
rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file
:package
python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
rem platform, target directory, executable, library, cargo command, optional versioned target
:build
set "BUILD_TARGET=%~2"
if not "%~6"=="" set "BUILD_TARGET=%~6"
echo === OpenPrestige %~1 (offline)
cargo %~5 --release --manifest-path injector\Cargo.toml --target "%BUILD_TARGET%" || exit /b 1
python scripts\package_release.py offline "%OUT%" "%VERSION%" %~1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
exit /b %ERRORLEVEL%
:fail
+17
View File
@@ -87,12 +87,29 @@ java {
}
jar {
// Optional custom native builds take precedence over the upstream binaries.
from('build/core-natives') {
into 'prestige-natives'
}
from('build/arm-natives') {
into 'prestige-natives'
}
// Unpack the shaded libs (imgui classes + native dll, ClassTransform) into the mod jar.
from({
configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
}) {
exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
exclude 'module-info.class', 'META-INF/versions/**'
exclude 'imgui/extension/**'
if (file('build/core-natives/windows-x86_64/imgui-java64.dll').isFile()) {
exclude 'io/imgui/java/native-bin/imgui-java64.dll'
}
if (file('build/core-natives/linux-x86_64/libimgui-java64.so').isFile()) {
exclude 'io/imgui/java/native-bin/libimgui-java64.so'
}
if (file('build/core-natives/macos-universal/libimgui-java64.dylib').isFile()) {
exclude 'io/imgui/java/native-bin/libimgui-java64.dylib'
}
}
from({ zipTree(configurations.engineMappings.singleFile) }) {
include 'mappings/mappings.tiny'
+1 -1
View File
@@ -3,7 +3,7 @@
version = 4
[[package]]
name = "prestige-injector"
name = "openprestige-injector"
version = "0.1.0"
[[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`)
into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x, Fabric API installed). It uses the same overall
method as the openvape bundle, written from scratch in Rust with no external crates.
## Supported platforms
| OS | Architectures | Injector binary | Loader library |
|---------|--------------------|-----------------------|-----------------------------|
| Windows | x86_64 | `PrestigeInjector.exe`| `prestige_native.dll` |
| Linux | x86_64 | `PrestigeInjector` | `libprestige_native.so` |
| macOS | x86_64, arm64 | `PrestigeInjector` | `libprestige_native.dylib` |
The injector and library must be built for the same architecture as the target JVM.
## How it works
1. **Injector** lists the running Minecraft JVMs, then loads the library into the chosen process.
- **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
process through `CreateRemoteThread` and confirms the module is mapped.
- **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd <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.
This repository contains the original CLI injector, native library with embedded payload and game
sources. The graphical launcher and runtime downloader are maintained in the separate Prestige
repository. There is no GUI or HTTP dependency in this injector and no edition build flag.
## Build
The payload jar is embedded with `include_bytes!`, so each target needs its own build. Both scripts below build for the host OS only and run
`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`.
Run `build-injector.ps1` on Windows or `build-injector.sh` on Linux/macOS. Both build the Fabric JAR,
then the native library and CLI, collecting the results in `dist/`. The native library always embeds
the exact remapped JAR. The loose JAR is provided for optional installation as a Fabric mod.
```powershell
.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll
Run `build-release.bat` on Windows to build all platforms. `dist/release/` contains:
```text
openprestige-<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
./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib}
```
To build release zips for every platform from Windows, run `build-release.bat`. It cross-compiles Linux (x86_64,
glibc 2.17+, so one build covers every mainstream distro) and macOS (universal x86_64 + arm64) with `cargo-zigbuild`, installing
zig and cargo-zigbuild on first run, and writes `dist\release\prestige-<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.
Each ZIP contains `OpenPrestigeInjector` (or `.exe`), the native library, and a README.
The CI injector workflow builds only OpenPrestige. Build dependencies require internet access;
the resulting release bundle needs no runtime downloads.
## Use
Start a Fabric 1.21.11 instance, then run:
Extract the complete platform ZIP, start Minecraft, then run:
```sh
PrestigeInjector # interactive: pick the game from the list
PrestigeInjector <pid> [lib path] # inject into a specific pid, optionally with an explicit library path
OpenPrestigeInjector # Up/Down selects, Enter injects, Esc quits
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
profile needs Fabric Loader 0.19.x and Fabric API. Wait for the title screen before selecting the game
in the injector. The default Prestige menu key is Right Shift, which may overlap with a launcher keybind.
Windows and Linux support x86_64 and ARM64. macOS supports Intel and Apple Silicon.
The injector, library and JVM architectures must match. ARM64 execution needs hardware testing.
The game code currently targets Minecraft 1.21.11 Fabric, Fabric Loader 0.19+ and Fabric API.
For Lunar, the native log should identify `GenesisClassLoader`. Check the game log for `hooks installed`
and `client initialised`. Hook counts refer to classes with changed bytecode. A skipped or failed hook
can leave its feature unavailable even if the menu opens.
Build tools require Rust, Python 3 and JDK 21. On Windows, the full release build also requires
MSVC build tools for x64 and ARM64. `build-release.bat` fetches pinned ImGui sources and builds
the small JNI libraries before building the remapped JAR. It installs Zig and cargo-zigbuild if
needed, then cross-compiles Linux x64/ARM64 and universal macOS.
An offline JVM replay accepted all 42 changed classes from Lunar's installed 1.21.11 cache after
recalculating stack and local limits on the cached classes. Live injection and Dawn's in-game profile
still need smoke testing.
Both repositories currently start from the same game sources and use the compatible `.openprestige`
configuration and log locations. They are independent repositories, without submodules, shared
working files or edition build flags. Game code fixes must be ported between repositories explicitly.
## Install as a mod instead
## User data
The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the
`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS.
Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips.
| OS | Location |
| --- | --- |
| Windows | `%APPDATA%\.openprestige` |
| macOS | `~/Library/Application Support/.openprestige` |
| Linux | `$XDG_DATA_HOME/.openprestige` or `~/.local/share/.openprestige` |
## Requirements (Linux / macOS)
- Run the injector as the **same user** as the game.
- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`.
- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs
running under Rosetta.
- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported.
## Known limits
- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is
not `dlclose`d (unmapping code the thread is still returning into would crash the JVM). On Windows the library is freed.
- Linux and macOS support is compile-checked in CI; test on a real host before relying on it.
## Log
`prestige-native.log` is written beside the loader library, plus `[Prestige-Inject]` lines in the game log.
Configs live in `configs/`, logs in `logs/`, and local runtime files in `runtime/`.
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "prestige-injector"
name = "openprestige-injector"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "PrestigeInjector"
name = "OpenPrestigeInjector"
path = "src/main.rs"
+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
//! thread on Windows, the HotSpot attach protocol on Linux and macOS. The library does everything else once it is inside
//! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible.
//!
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector <pid> [lib path] non-interactive
//! OpenPrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! OpenPrestigeInjector <pid> [lib path] non-interactive
#[path = "../../paths.rs"]
mod paths;
mod platform;
use std::fs;
@@ -15,7 +17,6 @@ use std::time::{Duration, Instant};
/// The one credit line, shown once at the top of the screen.
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
/// How long to follow the DLL's progress after it has been loaded.
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
@@ -26,11 +27,15 @@ const GREEN: &str = "\x1b[92m";
const YELLOW: &str = "\x1b[93m";
const RED: &str = "\x1b[91m";
fn default_dll() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
fn prepare_runtime(_offline: bool, _ask_for_updates: bool) -> Result<PathBuf, String> {
let path = std::env::current_exe()
.map_err(|error| error.to_string())?
.with_file_name(platform::LIB_NAME);
if path.is_file() {
Ok(path)
} else {
Err(format!("OpenPrestige requires {} beside the CLI executable. Extract the complete offline bundle.", platform::LIB_NAME))
}
}
fn banner() -> String {
@@ -38,26 +43,53 @@ fn banner() -> String {
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
out.push_str(&format!(
" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"
));
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
out.push_str("OpenPrestige - offline CLI\n\n");
out
}
fn step(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn ok(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn fail(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn log_launcher(label: &str, message: &str) {
let path = paths::root()
.join("logs")
.join(format!("launcher-{}.log", std::process::id()));
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "[{}] {label} {message}", clock());
}
}
/// Seconds since the injector started, for the log prefix.
fn clock() -> String {
use std::sync::OnceLock;
static START: OnceLock<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 {
@@ -70,27 +102,47 @@ fn real_main() -> ExitCode {
platform::enable_ansi();
let _ = clock();
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() {
[] => {
open_invite_once();
(pick_process(), default_dll())
let (pid, dll): (Option<u32>, Result<PathBuf, String>) = match args.as_slice() {
_ if interactive => {
let library = prepare_runtime(args.iter().any(|arg| arg == "--offline"), true);
if let Err(error) = &library {
fail("runtime", error);
return ExitCode::from(1);
}
(pick_process(), library)
}
[pid] => (parse_pid(pid), default_dll()),
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
[pid] => (parse_pid(pid), prepare_runtime(true, false)),
[pid, dll] => (parse_pid(pid), Ok(PathBuf::from(dll))),
_ => {
eprintln!("usage: PrestigeInjector [<pid> [lib path]]");
eprintln!("usage: OpenPrestigeInjector [--console | --offline | <pid> [lib path]]");
return ExitCode::from(2);
}
};
let Some(pid) = pid else {
// interactive mode quit with Esc, or an unparsable pid was given
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
return if interactive {
ExitCode::SUCCESS
} else {
ExitCode::from(2)
};
};
if !interactive {
print!("{}", banner());
}
let dll = match dll {
Ok(path) => path,
Err(error) => {
fail("runtime", &error);
return ExitCode::from(1);
}
};
let code = run(pid, &dll);
if interactive {
@@ -121,35 +173,58 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
step("loader", &dll_display);
// The library appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
let log_path = dll.with_file_name("prestige-native.log");
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
let log_path = paths::native_log(pid);
let logs = [log_path.clone(), dll.with_file_name("prestige-native.log")];
let starts = logs
.each_ref()
.map(|path| fs::metadata(path).map(|m| m.len()).unwrap_or(0));
step("checking", "64-bit JVM, process access, library not already loaded");
step(
"checking",
"64-bit JVM, process access, library not already loaded",
);
step("injecting", "loading the native library into the JVM");
match platform::inject(pid, &dll) {
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
Ok(()) => ok(
"injected",
&format!("{} is loaded in the game process", platform::LIB_NAME),
),
Err(e) => {
fail("failed", &e);
return ExitCode::from(1);
}
}
step("transforming", "following the loader log (attach, class loader, hook transformer)...");
match follow_log(&log_path, log_start) {
step(
"transforming",
"following the loader log (attach, class loader, hook transformer)...",
);
match follow_log(&logs, starts) {
Outcome::Done => {
println!();
ok("success", "successfully injected - Prestige is running in the game");
ok(
"success",
"successfully injected - Prestige is running in the game",
);
ExitCode::SUCCESS
}
Outcome::Failed(line) => {
fail("failed", &format!("injection did not complete: {line}"));
println!("{DIM} details: {} and the game's latest.log{RESET}", log_path.display());
println!(
"{DIM} details: {} and the game's latest.log{RESET}",
log_path.display()
);
ExitCode::from(1)
}
Outcome::TimedOut => {
fail("timeout", "no completion message from the loader within 3 minutes");
println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display());
fail(
"timeout",
"no completion message from the loader within 3 minutes",
);
println!(
"{DIM} check {} and the game's latest.log{RESET}",
log_path.display()
);
ExitCode::from(1)
}
}
@@ -162,60 +237,61 @@ enum Outcome {
}
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
fn follow_log(path: &Path, start: u64) -> Outcome {
fn follow_log(paths: &[PathBuf; 2], mut offsets: [u64; 2]) -> Outcome {
let deadline = Instant::now() + FOLLOW_TIMEOUT;
let mut offset = start;
let mut pending = String::new();
let mut pending = [String::new(), String::new()];
while Instant::now() < deadline {
if let Ok(mut file) = fs::File::open(path) {
if file.seek(SeekFrom::Start(offset)).is_ok() {
let mut chunk = String::new();
if let Ok(n) = file.read_to_string(&mut chunk) {
offset += n as u64;
pending.push_str(&chunk);
for (index, path) in paths.iter().enumerate() {
let offset = &mut offsets[index];
let pending = &mut pending[index];
if let Ok(mut file) = fs::File::open(path) {
if file.seek(SeekFrom::Start(*offset)).is_ok() {
let mut chunk = String::new();
if let Ok(n) = file.read_to_string(&mut chunk) {
*offset += n as u64;
pending.push_str(&chunk);
}
}
}
}
while let Some(end) = pending.find('\n') {
let line: String = pending.drain(..=end).collect();
let line = line.trim_end();
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook");
if fatal {
fail("loader", message);
return Outcome::Failed(message.to_string());
while let Some(end) = pending.find('\n') {
let line: String = pending.drain(..=end).collect();
// Keep the output from older runtime bundles in the shared log too.
if index == 1 {
if let Ok(mut log) = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&paths[0])
{
if log.write_all(line.as_bytes()).is_ok() {
offsets[0] += line.len() as u64;
}
}
}
let line = line.trim_end();
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
let fatal = message.starts_with("FAILED:")
|| lower.contains("start failed")
|| lower.contains("client init failed")
|| lower.contains("could not hook");
if fatal {
fail("loader", message);
return Outcome::Failed(message.to_string());
}
if message.contains("client initialised") {
ok("loader", message);
return Outcome::Done;
}
step("loader", message);
}
if message.contains("client initialised") {
ok("loader", message);
return Outcome::Done;
}
step("loader", message);
}
std::thread::sleep(Duration::from_millis(200));
}
Outcome::TimedOut
}
fn config_path() -> Option<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.
fn pick_process() -> Option<u32> {
let mut selected = 0usize;
@@ -229,7 +305,9 @@ fn pick_process() -> Option<u32> {
let previous = selected;
match key {
platform::Key::Up => selected = selected.saturating_sub(1),
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
platform::Key::Down => {
selected = (selected + 1).min(processes.len().saturating_sub(1))
}
platform::Key::Enter => {
if let Some(p) = processes.get(selected) {
return Some(p.pid);
@@ -264,7 +342,10 @@ fn draw(processes: &[platform::JavaProcess], selected: usize) {
}
for (i, p) in processes.iter().enumerate() {
let marker = if i == selected { ">" } else { " " };
out.push_str(&format!("{marker} [{:>6}] {} ({})\n", p.pid, p.title, p.exe));
out.push_str(&format!(
"{marker} [{:>6}] {} ({})\n",
p.pid, p.title, p.exe
));
}
print!("{out}");
let _ = io::stdout().flush();
+2 -22
View File
@@ -1,6 +1,5 @@
//! Platform backends of the injector. Each backend exposes the same functions:
//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal.
use std::path::PathBuf;
//! enable_ansi, poll_key, java_processes, inject, restore_terminal.
/// A running Minecraft JVM shown in the picker.
#[derive(PartialEq)]
@@ -33,7 +32,7 @@ pub use unix_attach::inject;
#[cfg(unix)]
pub use unix_procs::java_processes;
#[cfg(unix)]
pub use unix_term::{enable_ansi, open_url, poll_key, restore_terminal};
pub use unix_term::{enable_ansi, poll_key, restore_terminal};
#[cfg(windows)]
pub const LIB_NAME: &str = "prestige_native.dll";
@@ -41,22 +40,3 @@ pub const LIB_NAME: &str = "prestige_native.dll";
pub const LIB_NAME: &str = "libprestige_native.dylib";
#[cfg(all(unix, not(target_os = "macos")))]
pub const LIB_NAME: &str = "libprestige_native.so";
/// Per-user configuration directory: APPDATA, $XDG_CONFIG_HOME or ~/.config, ~/Library/Application Support.
pub fn config_dir() -> Option<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`
//! 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> {
let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?;
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
let out = Command::new("stty")
.args(args)
.stdin(tty_stdin())
.stderr(Stdio::null())
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
/// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`.
@@ -113,7 +120,9 @@ pub fn poll_key() -> Option<Key> {
27 => {
let ms30 = Duration::from_millis(30);
match rx.recv_timeout(ms30) {
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => return Some(Key::Escape),
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => {
return Some(Key::Escape)
}
Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
Ok(b'A') => return Some(Key::Up),
Ok(b'B') => return Some(Key::Down),
@@ -134,19 +143,3 @@ pub fn restore_terminal() {
stty(&[&s]);
}
}
/// Opens a URL in the default browser (xdg-open / open). Returns whether the spawn succeeded.
pub fn open_url(url: &str) -> bool {
#[cfg(target_os = "macos")]
let opener = "open";
#[cfg(not(target_os = "macos"))]
let opener = "xdg-open";
Command::new(opener)
.arg(url)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|_| true)
.unwrap_or(false)
}
+106 -30
View File
@@ -39,16 +39,43 @@ struct ModuleEntry32W {
extern "system" {
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
fn CloseHandle(h: Handle) -> i32;
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
fn VirtualAllocEx(
process: Handle,
address: *mut c_void,
size: usize,
alloc_type: u32,
protect: u32,
) -> *mut c_void;
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle;
fn WriteProcessMemory(
process: Handle,
address: *mut c_void,
buf: *const c_void,
size: usize,
written: *mut usize,
) -> i32;
fn CreateRemoteThread(
process: Handle,
attrs: *mut c_void,
stack: usize,
start: *mut c_void,
param: *mut c_void,
flags: u32,
id: *mut u32,
) -> Handle;
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
fn GetModuleHandleW(name: *const u16) -> Handle;
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
fn IsWow64Process2(process: Handle, process_machine: *mut u16, native_machine: *mut u16)
-> i32;
fn GetCurrentProcess() -> Handle;
fn QueryFullProcessImageNameW(
process: Handle,
flags: u32,
buf: *mut u16,
size: *mut u32,
) -> i32;
fn GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
@@ -66,11 +93,6 @@ extern "system" {
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
}
#[link(name = "shell32")]
extern "system" {
fn ShellExecuteW(hwnd: Handle, verb: *const u16, file: *const u16, params: *const u16, dir: *const u16, show: i32) -> isize;
}
extern "C" {
fn _kbhit() -> i32;
fn _getch() -> i32;
@@ -109,7 +131,10 @@ pub fn poll_key() -> Option<Key> {
}
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> {
@@ -156,7 +181,9 @@ pub fn java_processes() -> Vec<JavaProcess> {
if out.iter().any(|w| w.pid == pid) {
continue;
}
let Some(path) = image_name(pid) else { continue };
let Some(path) = image_name(pid) else {
continue;
};
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
out.push(JavaProcess { pid, title, exe });
@@ -181,7 +208,11 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
let mut found = false;
let mut ok = Module32FirstW(snapshot, &mut entry);
while ok != 0 {
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
let len = entry
.sz_module
.iter()
.position(|&c| c == 0)
.unwrap_or(entry.sz_module.len());
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true;
break;
@@ -196,10 +227,16 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
unsafe {
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
let access = PROCESS_CREATE_THREAD
| PROCESS_QUERY_INFORMATION
| PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ;
let process = OpenProcess(access, 0, pid);
if process.is_null() {
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
return Err(last_error(
"OpenProcess (run as the same user, or as administrator)",
));
}
let result = inject_into(process, pid, dll);
CloseHandle(process);
@@ -208,18 +245,50 @@ pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
}
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
let mut wow64 = 0;
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
unsafe fn machine(process: Handle) -> Result<u16, String> {
let mut emulated = 0;
let mut native = 0;
if IsWow64Process2(process, &mut emulated, &mut native) == 0 {
return Err(last_error("IsWow64Process2"));
}
Ok(if emulated == 0 { native } else { emulated })
}
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let target = machine(process)?;
let injector = machine(GetCurrentProcess())?;
if target != injector {
return Err(format!("JVM architecture {target:#06x} differs from injector {injector:#06x}. Use matching x64 or ARM64 builds."));
}
let image = std::fs::read(dll).map_err(|error| format!("read loader: {error}"))?;
let pe = image
.get(0x3c..0x40)
.and_then(|bytes| bytes.try_into().ok())
.map(u32::from_le_bytes)
.ok_or("invalid loader PE header")? as usize;
let library = image
.get(pe + 4..pe + 6)
.and_then(|bytes| bytes.try_into().ok())
.map(u16::from_le_bytes)
.ok_or("invalid loader machine")?;
if library != target {
return Err("The loader DLL and JVM architectures differ.".into());
}
let file_name = dll
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
if module_loaded(pid, &file_name) {
return Err(format!("{file_name} is already loaded in process {pid}"));
}
let path = wide(dll);
let bytes = path.len() * 2;
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
let remote = VirtualAllocEx(
process,
std::ptr::null_mut(),
bytes,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
);
if remote.is_null() {
return Err(last_error("VirtualAllocEx"));
}
@@ -227,7 +296,14 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
Err(err)
};
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
if WriteProcessMemory(
process,
remote,
path.as_ptr() as *const c_void,
bytes,
std::ptr::null_mut(),
) == 0
{
return cleanup(last_error("WriteProcessMemory"));
}
@@ -237,7 +313,15 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
if load_library.is_null() {
return cleanup(last_error("resolve LoadLibraryW"));
}
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
let thread = CreateRemoteThread(
process,
std::ptr::null_mut(),
0,
load_library,
remote,
0,
std::ptr::null_mut(),
);
if thread.is_null() {
return cleanup(last_error("CreateRemoteThread"));
}
@@ -260,11 +344,3 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
}
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
}
/// Opens a URL in the default web browser.
pub fn open_url(url: &str) -> bool {
let verb: Vec<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(),
};
if !jar.is_file() {
panic!("payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", jar.display());
panic!(
"payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)",
jar.display()
);
}
println!("cargo:rerun-if-changed={}", jar.display());
println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", jar.display());
@@ -22,14 +25,22 @@ fn newest_remapped_jar() -> PathBuf {
if let Ok(entries) = fs::read_dir(&libs) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with("prestige-") || !name.ends_with(".jar") || name.ends_with("-dev.jar") || name.ends_with("-sources.jar") {
if !name.starts_with("prestige-")
|| !name.ends_with(".jar")
|| name.ends_with("-dev.jar")
|| name.ends_with("-sources.jar")
{
continue;
}
let time = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
let time = entry
.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().map_or(true, |(t, _)| time > *t) {
best = Some((time, entry.path()));
}
}
}
best.map(|(_, p)| p).unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
best.map(|(_, p)| p)
.unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
}
+2
View File
@@ -6,6 +6,8 @@
mod bridge;
mod jni;
mod log;
#[path = "../../paths.rs"]
mod paths;
mod worker;
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::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(windows)]
extern "system" {
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
}
#[cfg(unix)]
#[repr(C)]
struct DlInfo {
dli_fname: *const std::ffi::c_char,
dli_fbase: *mut c_void,
dli_sname: *const std::ffi::c_char,
dli_saddr: *mut c_void,
}
#[cfg(unix)]
extern "C" {
fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
}
#[cfg(unix)]
fn module_path() -> PathBuf {
fn anchor() {}
let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() };
let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) };
if ok != 0 && !info.dli_fname.is_null() {
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()))
} else {
std::env::temp_dir().join("libprestige_native")
}
}
#[cfg(windows)]
fn module_path() -> PathBuf {
let mut buf = [0u16; 1024];
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
PathBuf::from(String::from_utf16_lossy(&buf[..len]))
}
fn log_path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| {
module_path().with_file_name("prestige-native.log")
})
PATH.get_or_init(|| crate::paths::native_log(std::process::id()))
}
/// Appends a line to prestige-native.log beside the DLL.
pub fn log(message: &str) {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
if let Some(parent) = log_path().parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) {
let _ = writeln!(file, "[{secs}] {message}");
}
+4 -2
View File
@@ -29,6 +29,7 @@ pub fn run() {
}
unsafe fn install() -> Result<(), String> {
crate::paths::initialize().map_err(|error| format!("create OpenPrestige data directory: {error}"))?;
let vm = find_vm()?;
let env = attach_current_thread(vm)?;
struct Detach(VmPtr);
@@ -45,7 +46,7 @@ unsafe fn install() -> Result<(), String> {
log!("JVMTI ready");
let jar = write_payload()?;
log!("payload written to {}", jar.display());
log!("using payload at {}", jar.display());
let loader = add_to_knot(&jni, &jar)?;
log!("payload appended to the game class loader: {}", describe(&jni, loader));
@@ -103,7 +104,8 @@ unsafe fn find_vm() -> Result<VmPtr, 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()))?;
let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
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>
Zips are written with unix permissions so PrestigeInjector stays executable after unzip on Linux/macOS.
"""
import hashlib
import os
@@ -11,69 +9,7 @@ import sys
import time
import zipfile
README = {
"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
"""
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'
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):
family = platform.split("-")[0]
name = f"prestige-{version}-{platform}"
jar_name = os.path.basename(jar)
title, body = README[family].format(version=version, jar=jar_name).split("\n", 1)
readme = title + "\n" + TESTED + body + MOD_NOTE.format(jar=jar_name) + FOOTER
if family == "windows":
readme = readme.replace("\n", "\r\n")
if platform in ("windows-aarch64", "linux-aarch64"):
native = "imgui-java64.dll" if platform.startswith("windows") else "libimgui-java64.so"
with zipfile.ZipFile(jar) as payload:
entry = f"prestige-natives/{platform}/{native}"
if entry not in payload.namelist():
raise ValueError(f"ARM64 payload is missing {entry}. Build the core ImGui natives first.")
name = f"openprestige-{version}-{platform}"
out = os.path.join(out_dir, name + ".zip")
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for path, mode in ((injector, 0o755), (lib, 0o644), (jar, 0o644)):
with open(path, "rb") as f:
add(zf, f"{name}/{os.path.basename(path)}", f.read(), mode)
add(zf, f"{name}/README.txt", readme.encode(), 0o644)
os.makedirs(out_dir, exist_ok=True)
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as bundle:
executable = "OpenPrestigeInjector" + (".exe" if platform.startswith("windows-") else "")
for path, filename, permissions in ((injector, executable, 0o755), (lib, os.path.basename(lib), 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)")
@@ -114,7 +62,7 @@ def checksums(out_dir):
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:])
elif len(sys.argv) == 3 and sys.argv[1] == "checksums":
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;
import dev.zprestige.prestige.client.AppPaths;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,7 +32,7 @@ final class TransformCache {
try {
Path jar = Path.of(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI());
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);
prune(dir);
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.GsonBuilder;
import com.google.gson.JsonObject;
import dev.zprestige.prestige.client.AppPaths;
import dev.zprestige.prestige.client.Prestige;
import java.io.IOException;
@@ -17,7 +18,7 @@ import java.util.Locale;
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
* {@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.
@@ -45,6 +46,10 @@ public final class LocalConfigs {
private static String lastQuery = null;
static Path dir() {
return AppPaths.configs().resolve("presets");
}
private static Path legacyDir() {
Path base;
try { base = net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir(); } catch (Throwable t) { base = Path.of("."); }
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) {
try {
Entry e = GSON.fromJson(Files.readString(p, StandardCharsets.UTF_8), Entry.class);
if (e == null || e.name == null || e.name.isBlank()) continue;
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);
if (normalize(e)) ALL.add(e);
} 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); }
ALL.sort(Comparator.comparingInt((Entry e) -> e.id).reversed());
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 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.Theme;
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). */
public final class Overlay {
@@ -40,14 +41,19 @@ public final class Overlay {
public static volatile long lastCloseNanos;
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() {
if (posLoaded) return;
posLoaded = true;
try {
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);
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);
@@ -59,7 +65,8 @@ public final class Overlay {
static void savePos() {
try {
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); }
}
@@ -192,6 +199,7 @@ public final class Overlay {
static void init() throws Exception {
if (inited) return;
ImGuiNative.prepare();
ImGui.createContext();
ImGuiIO io = ImGui.getIO();
io.setIniFilename(null);