6 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
bbldrizzy 5152e6e0a6 make fabric entry points wired through an adapter 2026-11-03 11:46:46 +00:00
bbldrizzy 0abefe7a04 Merge branch 'main' of https://codeberg.org/SkidSolutions/OpenPrestige 2026-11-03 11:42:12 +00:00
bbldrizzy 036b24f2ae Improve launcher injection and hook reliability
Find Fabric through the render thread's class loader and make bytecode transformations safer for launcher-provided class definitions. Improve hook reporting, process-picker refresh behavior, and self-destruct cleanup; document Dawn and Lunar 1.21.11 setup.
2026-11-03 11:40:43 +00:00
bbldrizzy 41d3e4a65e Update README.md 2026-10-28 11:27:50 +01:00
50 changed files with 1506 additions and 517 deletions

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
+5
View File
@@ -19,3 +19,8 @@ openvape.zip
/dist/ /dist/
/injector/target/ /injector/target/
/tools /tools
/examples
# Python build helpers
__pycache__/
*.pyc
+3 -3
View File
@@ -1,12 +1,12 @@
# Open Prestige # 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! - we will work on removing the param obf and also renaming vars in the coming days. stay tuned!
- if vikas wants to pay me to remove this, make ticket in discord and we can negotiate. - if vikas wants to pay me to remove this, make ticket in discord and we can negotiate.
@ 1500 discord members, we will release injector
<p align="center"> <p align="center">
<img src="docs/img/deobf-bird.png" width="260" alt="Deobf?"> <img src="docs/img/deobf-bird.png" width="260" alt="Deobf?">
+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 -75
View File
@@ -1,96 +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 Fabric client 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`;
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot 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.
## Install as a mod instead ## Platforms and requirements
The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the Windows and Linux support x86_64 and ARM64. macOS supports Intel and Apple Silicon.
`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS. The injector, library and JVM architectures must match. ARM64 execution needs hardware testing.
Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips. The game code currently targets Minecraft 1.21.11 Fabric, Fabric Loader 0.19+ and Fabric API.
## Requirements (Linux / macOS) 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.
- Run the injector as the **same user** as the game. Both repositories currently start from the same game sources and use the compatible `.openprestige`
- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`. configuration and log locations. They are independent repositories, without submodules, shared
- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs working files or edition build flags. Game code fixes must be ported between repositories explicitly.
running under Rosetta.
- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported.
## Known limits ## User data
- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is | OS | Location |
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. | Windows | `%APPDATA%\.openprestige` |
| macOS | `~/Library/Application Support/.openprestige` |
| Linux | `$XDG_DATA_HOME/.openprestige` or `~/.local/share/.openprestige` |
## Log Configs live in `configs/`, logs in `logs/`, and local runtime files in `runtime/`.
`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}");
}
+171 -85
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,76 +237,77 @@ 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;
let mut processes = platform::java_processes(); let mut processes = platform::java_processes();
let mut last_refresh = Instant::now(); let mut last_refresh = Instant::now();
draw(&processes, selected);
loop { loop {
if selected >= processes.len() {
selected = processes.len().saturating_sub(1);
}
draw(&processes, selected);
let deadline = Instant::now() + Duration::from_millis(50); let deadline = Instant::now() + Duration::from_millis(50);
while Instant::now() < deadline { while Instant::now() < deadline {
if let Some(key) = platform::poll_key() { if let Some(key) = platform::poll_key() {
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);
@@ -239,12 +315,19 @@ fn pick_process() -> Option<u32> {
} }
platform::Key::Escape => return None, platform::Key::Escape => return None,
} }
draw(&processes, selected); if selected != previous {
draw(&processes, selected);
}
} }
std::thread::sleep(Duration::from_millis(10)); std::thread::sleep(Duration::from_millis(10));
} }
if last_refresh.elapsed() >= Duration::from_millis(750) { if last_refresh.elapsed() >= Duration::from_millis(750) {
processes = platform::java_processes(); let refreshed = platform::java_processes();
if refreshed != processes {
processes = refreshed;
selected = selected.min(processes.len().saturating_sub(1));
draw(&processes, selected);
}
last_refresh = Instant::now(); last_refresh = Instant::now();
} }
} }
@@ -259,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();
+3 -22
View File
@@ -1,8 +1,8 @@
//! 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)]
pub struct JavaProcess { pub struct JavaProcess {
pub pid: u32, pub pid: u32,
pub title: String, pub title: String,
@@ -32,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";
@@ -40,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"))
} }
-3
View File
@@ -56,9 +56,6 @@ impl Jni {
pub unsafe fn exception_clear(&self) { pub unsafe fn exception_clear(&self) {
f!(self, 17, unsafe extern "system" fn(EnvPtr))(self.env) f!(self, 17, unsafe extern "system" fn(EnvPtr))(self.env)
} }
pub unsafe fn new_global_ref(&self, o: JObject) -> JObject {
f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o)
}
pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool { pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool {
f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0 f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0
} }
+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}");
} }
+29 -12
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,10 +46,10 @@ 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 Fabric Knot class loader"); log!("payload appended to the game class loader: {}", describe(&jni, loader));
register_natives(&jni, loader)?; register_natives(&jni, loader)?;
log!("natives registered"); log!("natives registered");
@@ -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()))?;
@@ -111,7 +113,7 @@ fn write_payload() -> Result<PathBuf, String> {
} }
/// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader. /// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader.
/// Returns a global reference to the Knot target class loader. /// Returns a local reference to the Fabric launcher's target class loader.
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> { unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
jni.ensure_local_capacity(4096); jni.ensure_local_capacity(4096);
let (base, fabric_loader) = locate_fabric(jni)?; let (base, fabric_loader) = locate_fabric(jni)?;
@@ -149,23 +151,30 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
if loader.is_null() { if loader.is_null() {
return Err("Knot target class loader is null".into()); return Err("Knot target class loader is null".into());
} }
Ok(jni.new_global_ref(loader)) Ok(loader)
} }
const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase"; const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase";
/// Finds the class loader that can see fabric-loader. The system class loader is tried first (official launcher, most /// Prefer the render thread's context loader when a launcher supplies its own loader hierarchy.
/// others); launchers that run the game from their own URLClassLoader (Prism/MultiMC, many modpack launchers) keep /// Fall back to the system loader and then the other thread context loaders.
/// fabric-loader out of it, so every thread's context class loader is probed too. Returns the FabricLauncherBase class
/// and the loader it was found through (null = FindClass / system loader).
unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> { unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
let (render, loaders) = thread_loaders(jni)?;
if let Some(loader) = render {
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
log!("fabric-loader found through the render thread: {}", describe(jni, loader));
return Ok((class, loader));
}
}
let base = jni.find_class(&FABRIC_BASE.replace('.', "/")); let base = jni.find_class(&FABRIC_BASE.replace('.', "/"));
if jni.take_exception().is_none() && !base.is_null() { if jni.take_exception().is_none() && !base.is_null() {
return Ok((base, std::ptr::null_mut())); return Ok((base, std::ptr::null_mut()));
} }
log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders"); log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders");
let loaders = thread_loaders(jni)?;
for &loader in &loaders { for &loader in &loaders {
if render.is_some_and(|r| jni.is_same_object(r, loader)) {
continue;
}
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) { if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
log!("fabric-loader found through {}", describe(jni, loader)); log!("fabric-loader found through {}", describe(jni, loader));
return Ok((class, loader)); return Ok((class, loader));
@@ -180,10 +189,11 @@ unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
} }
/// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader). /// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader).
unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> { unsafe fn thread_loaders(jni: &Jni) -> Result<(Option<JObject>, Vec<JObject>), String> {
let thread_class = jni.find_class("java/lang/Thread"); let thread_class = jni.find_class("java/lang/Thread");
let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;"); let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;");
let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;"); let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;");
let get_name = jni.get_method_id(thread_class, "getName", "()Ljava/lang/String;");
let map_class = jni.find_class("java/util/Map"); let map_class = jni.find_class("java/util/Map");
let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;"); let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;");
let set_class = jni.find_class("java/util/Set"); let set_class = jni.find_class("java/util/Set");
@@ -195,6 +205,7 @@ unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]); let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
jni.check("thread list")?; jni.check("thread list")?;
let mut loaders: Vec<JObject> = Vec::new(); let mut loaders: Vec<JObject> = Vec::new();
let mut render = None;
for i in 0..jni.get_array_length(jni_non_null(threads)?) { for i in 0..jni.get_array_length(jni_non_null(threads)?) {
let thread = jni.get_object_array_element(threads, i); let thread = jni.get_object_array_element(threads, i);
if thread.is_null() { if thread.is_null() {
@@ -204,11 +215,17 @@ unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
if jni.take_exception().is_some() || loader.is_null() { if jni.take_exception().is_some() || loader.is_null() {
continue; continue;
} }
let name = jni.call_object(thread, get_name, &[]);
if jni.take_exception().is_none() && !name.is_null() {
if matches!(jni.get_string_utf(name).as_str(), "Render thread" | "Client thread") {
render = Some(loader);
}
}
if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) { if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) {
loaders.push(loader); loaders.push(loader);
} }
} }
Ok(loaders) Ok((render, loaders))
} }
unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> { unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> {
+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()
@@ -69,7 +69,7 @@ public class Module extends SettingHolder implements MinecraftAccess {
this.setLastEnabled(this.isEnabled()); this.setLastEnabled(this.isEnabled());
if (this.isEnabled()) { if (this.isEnabled()) {
Prestige.eventBus.register(this); Prestige.eventBus.register(this);
if (mc.world != null && mc.player != null) { if (!requiresWorldOnEnable() || (mc.world != null && mc.player != null)) {
this.onEnable(); this.onEnable();
} else { } else {
Prestige.moduleManager.queueEnable(this); Prestige.moduleManager.queueEnable(this);
@@ -89,7 +89,7 @@ public class Module extends SettingHolder implements MinecraftAccess {
public void applyState() { public void applyState() {
if (this.isEnabled()) { if (this.isEnabled()) {
Prestige.eventBus.register(this); Prestige.eventBus.register(this);
if (mc.world != null && mc.player != null) { if (!requiresWorldOnEnable() || (mc.world != null && mc.player != null)) {
this.onEnable(); this.onEnable();
} else { } else {
Prestige.moduleManager.queueEnable(this); Prestige.moduleManager.queueEnable(this);
@@ -123,6 +123,10 @@ public class Module extends SettingHolder implements MinecraftAccess {
public void onEnable() { public void onEnable() {
} }
protected boolean requiresWorldOnEnable() {
return true;
}
public void onDisable() { public void onDisable() {
} }
@@ -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);
}
}
@@ -34,11 +34,10 @@ import dev.zprestige.prestige.util.SimulatedClickManager;
import dev.zprestige.prestige.util.FrameDeltaTimer; import dev.zprestige.prestige.util.FrameDeltaTimer;
import dev.zprestige.prestige.util.NotificationManager; import dev.zprestige.prestige.util.NotificationManager;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.api.ModInitializer;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ChatScreen; import net.minecraft.client.gui.screen.ChatScreen;
public class Prestige implements ModInitializer { public class Prestige {
private static final AtomicBoolean initialized = new AtomicBoolean(false); private static final AtomicBoolean initialized = new AtomicBoolean(false);
private static final java.util.concurrent.atomic.AtomicInteger renderErrorCount = new java.util.concurrent.atomic.AtomicInteger(); private static final java.util.concurrent.atomic.AtomicInteger renderErrorCount = new java.util.concurrent.atomic.AtomicInteger();
public static EventBus eventBus; public static EventBus eventBus;
@@ -160,8 +159,4 @@ public class Prestige implements ModInitializer {
} }
} }
@Override
public void onInitialize() {
this.init();
}
} }
@@ -0,0 +1,21 @@
package dev.zprestige.prestige.fabric;
import dev.zprestige.prestige.client.Prestige;
import dev.zprestige.prestige.ui.mc.Entry;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
public final class FabricEntrypoints implements ModInitializer, ClientModInitializer {
@Override
public void onInitialize() {
new Prestige().init();
}
@Override
public void onInitializeClient() {
Entry entry = new Entry();
entry.initialize();
ClientTickEvents.END_CLIENT_TICK.register(entry::tick);
}
}
@@ -1,19 +1,26 @@
package dev.zprestige.prestige.inject; package dev.zprestige.prestige.inject;
import dev.zprestige.prestige.client.Prestige;
import dev.zprestige.prestige.engine.EnginePlugin; import dev.zprestige.prestige.engine.EnginePlugin;
import dev.zprestige.prestige.engine.EngineState; import dev.zprestige.prestige.engine.EngineState;
import dev.zprestige.prestige.fabric.FabricEntrypoints;
import dev.zprestige.prestige.hooks.Definer; import dev.zprestige.prestige.hooks.Definer;
import dev.zprestige.prestige.ui.mc.Entry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.ToIntBiFunction; import java.util.function.ToIntBiFunction;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.lenni0451.classtransform.TransformerManager; import net.lenni0451.classtransform.TransformerManager;
import net.lenni0451.classtransform.utils.mappings.SuperMappingFiller;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
/** /**
* Entry point of the injected payload. prestige_native.dll appends this jar to the Fabric (Knot) class path, registers the * Entry point of the injected payload. prestige_native.dll appends this jar to the Fabric (Knot) class path, registers the
@@ -41,11 +48,12 @@ public final class Bootstrap {
log("waiting for the Minecraft client"); log("waiting for the Minecraft client");
waitForClient(); waitForClient();
Report report = install(NativeBridge::classBytes, NativeBridge::redefine, nativeBatch()); Report report = install(NativeBridge::classBytes, NativeBridge::redefine, nativeBatch());
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed)); log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved"
+ ", " + report.unchanged + " unchanged" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
if (report.redefined == 0) throw new IllegalStateException("no hooks installed");
// Some clients (Lunar) name a few classes and methods differently. Keep going with the hooks that did apply: a missing // Some clients (Lunar) name a few classes and methods differently. Keep going with the hooks that did apply: a missing
// hook only disables the features behind it, while stopping here would leave the client without Prestige at all. // hook only disables the features behind it, while stopping here would leave the client without Prestige at all.
if (!report.failed.isEmpty()) { if (!report.failed.isEmpty()) {
if (report.redefined == 0) throw new IllegalStateException("could not hook " + report.failed);
log("continuing with " + report.redefined + " hooked classes; skipped " + report.failed); log("continuing with " + report.redefined + " hooked classes; skipped " + report.failed);
} }
log("initialising Prestige on the client thread"); log("initialising Prestige on the client thread");
@@ -67,7 +75,7 @@ public final class Bootstrap {
} }
public static final class Report { public static final class Report {
public int redefined, moved; public int redefined, moved, unchanged;
public final List<String> failed = new ArrayList<>(); public final List<String> failed = new ArrayList<>();
} }
@@ -147,6 +155,26 @@ public final class Bootstrap {
} }
long captured = System.nanoTime(); long captured = System.nanoTime();
// Genesis can expose different class resources from the definitions installed in its JVM.
// Resolve inherited Yarn members using the actual superclass and interface declarations.
var mappings = manager.getMapper().getRemapper();
if (!mappings.isEmpty()) {
Map<Class<?>, ClassNode> declarations = new HashMap<>();
for (Class<?> type : types) {
if (type == null) continue;
try {
Set<ClassNode> parents = new LinkedHashSet<>();
collectParents(type.getSuperclass(), declarations, parents);
for (Class<?> iface : type.getInterfaces()) collectParents(iface, declarations, parents);
ClassNode target = new ClassNode();
target.name = Type.getInternalName(type);
SuperMappingFiller.fillSuperMembers(target, parents, mappings);
} catch (LinkageError e) {
log("could not inspect inherited mappings for " + type.getName() + ": " + e);
}
}
}
// 3. ClassTransform, one class at a time (its mapper is not thread-safe: running it on several threads corrupts its state). // 3. ClassTransform, one class at a time (its mapper is not thread-safe: running it on several threads corrupts its state).
// A result that is already in the disk cache skips the transformation, which is where nearly all of the time goes. // A result that is already in the disk cache skips the transformation, which is where nearly all of the time goes.
InPlaceTransformer.Result[] results = new InPlaceTransformer.Result[total]; InPlaceTransformer.Result[] results = new InPlaceTransformer.Result[total];
@@ -167,6 +195,12 @@ public final class Bootstrap {
continue; continue;
} }
results[i] = transformer.transform(target, originals[i]); results[i] = transformer.transform(target, originals[i]);
if (results[i].target() == originals[i]) {
log("no bytecode changes for " + target);
report.unchanged++;
results[i] = null;
continue;
}
if (cache != null && results[i].helpers().isEmpty()) cache.put(target, originals[i], results[i].target()); if (cache != null && results[i].helpers().isEmpty()) cache.put(target, originals[i], results[i].target());
} catch (Throwable t) { } catch (Throwable t) {
fail(report, target, t); fail(report, target, t);
@@ -211,6 +245,24 @@ public final class Bootstrap {
return report; return report;
} }
private static void collectParents(Class<?> type, Map<Class<?>, ClassNode> declarations, Set<ClassNode> parents) {
if (type == null || !type.getName().startsWith("net.minecraft.")) return;
ClassNode declaration = declarations.computeIfAbsent(type, parent -> {
ClassNode node = new ClassNode();
node.name = Type.getInternalName(parent);
for (var method : parent.getDeclaredMethods()) {
node.methods.add(new MethodNode(method.getModifiers(), method.getName(), Type.getMethodDescriptor(method), null, null));
}
for (var field : parent.getDeclaredFields()) {
node.fields.add(new FieldNode(field.getModifiers(), field.getName(), Type.getDescriptor(field.getType()), null, null));
}
return node;
});
if (!parents.add(declaration)) return;
collectParents(type.getSuperclass(), declarations, parents);
for (Class<?> iface : type.getInterfaces()) collectParents(iface, declarations, parents);
}
private static void fail(Report report, String target, Throwable t) { private static void fail(Report report, String target, Throwable t) {
report.failed.add(target); report.failed.add(target);
log("hook failed for " + target + ": " + t); log("hook failed for " + target + ": " + t);
@@ -244,8 +296,9 @@ public final class Bootstrap {
private static void initClient() { private static void initClient() {
try { try {
new Prestige().onInitialize(); FabricEntrypoints entrypoints = new FabricEntrypoints();
new Entry().onInitializeClient(); entrypoints.onInitialize();
entrypoints.onInitializeClient();
log("client initialised"); log("client initialised");
} catch (Throwable t) { } catch (Throwable t) {
log("client init failed: " + t); log("client init failed: " + t);
@@ -4,6 +4,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -11,8 +12,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import net.lenni0451.classtransform.TransformerManager; import net.lenni0451.classtransform.TransformerManager;
import net.lenni0451.classtransform.utils.ASMUtils;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type; import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AbstractInsnNode;
@@ -48,7 +51,7 @@ public final class InPlaceTransformer {
/** @param dottedName runtime name of the target class, @param current the bytes of the class as currently loaded */ /** @param dottedName runtime name of the target class, @param current the bytes of the class as currently loaded */
public Result transform(String dottedName, byte[] current) throws Exception { public Result transform(String dottedName, byte[] current) throws Exception {
byte[] out = manager.transform(dottedName, current, true); byte[] out = manager.transform(dottedName, current, false);
if (out == null) throw new IllegalStateException("ClassTransform failed for " + dottedName); if (out == null) throw new IllegalStateException("ClassTransform failed for " + dottedName);
ClassNode before = read(current); ClassNode before = read(current);
ClassNode after = read(out); ClassNode after = read(out);
@@ -63,9 +66,9 @@ public final class InPlaceTransformer {
before.methods.forEach(m -> existing.put(m.name + m.desc, m)); before.methods.forEach(m -> existing.put(m.name + m.desc, m));
Map<String, MethodNode> moved = new LinkedHashMap<>(); Map<String, MethodNode> moved = new LinkedHashMap<>();
for (MethodNode m : after.methods) if (!existing.containsKey(m.name + m.desc)) moved.put(m.name + m.desc, m); for (MethodNode m : after.methods) if (!existing.containsKey(m.name + m.desc)) moved.put(m.name + m.desc, m);
if (moved.isEmpty()) return new Result(out, Map.of(), 0); if (!after.interfaces.equals(before.interfaces))
if (after.interfaces.size() != before.interfaces.size())
throw new IllegalStateException(dottedName + ": transformer changed the interface list"); throw new IllegalStateException(dottedName + ": transformer changed the interface list");
if (moved.isEmpty()) return new Result(writeTarget(current, before, after, existing), Map.of(), 0);
String helperName = HOOK_PACKAGE + "H" + (sequence.getAndIncrement()) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_'); String helperName = HOOK_PACKAGE + "H" + (sequence.getAndIncrement()) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_');
ClassNode helper = new ClassNode(); ClassNode helper = new ClassNode();
@@ -100,13 +103,44 @@ public final class InPlaceTransformer {
accessors.finish(); accessors.finish();
after.methods.removeIf(m -> moved.containsKey(m.name + m.desc)); after.methods.removeIf(m -> moved.containsKey(m.name + m.desc));
ClassWriter targetWriter = new ClassWriter(0);
after.accept(targetWriter);
ClassWriter helperWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
helper.accept(helperWriter);
Map<String, byte[]> helpers = new LinkedHashMap<>(); Map<String, byte[]> helpers = new LinkedHashMap<>();
helpers.put(helperName.replace('/', '.'), helperWriter.toByteArray()); helpers.put(helperName.replace('/', '.'), ASMUtils.toBytes(helper, manager.getClassTree(), manager.getClassProvider()));
return new Result(targetWriter.toByteArray(), helpers, moved.size()); return new Result(writeTarget(current, before, after, existing), helpers, moved.size());
}
/**
* Recompute frames only in changed methods. Launchers can transform unrelated methods and
* supply valid frames for them even when their class resources are unavailable to ClassTransform.
*/
private byte[] writeTarget(byte[] current, ClassNode before, ClassNode after, Map<String, MethodNode> originals) {
List<MethodNode> changed = new ArrayList<>();
for (MethodNode method : after.methods) {
MethodNode original = originals.get(method.name + method.desc);
if (original == null) throw new IllegalStateException("Unexpected method " + method.name + method.desc);
if (!Arrays.equals(methodBytes(original), methodBytes(method))) changed.add(method);
}
if (changed.isEmpty()) return current;
after.methods = changed;
ClassNode computed = read(ASMUtils.toBytes(after, manager.getClassTree(), manager.getClassProvider()));
Map<String, MethodNode> replacements = new HashMap<>();
computed.methods.forEach(m -> replacements.put(m.name + m.desc, m));
before.methods.replaceAll(m -> replacements.getOrDefault(m.name + m.desc, m));
ClassWriter writer = new ClassWriter(new ClassReader(current), 0);
before.accept(writer);
return writer.toByteArray();
}
private static byte[] methodBytes(MethodNode method) {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, "MethodComparison", null, "java/lang/Object", null);
MethodVisitor visitor = writer.visitMethod(method.access, method.name, method.desc, method.signature,
method.exceptions.toArray(String[]::new));
method.accept(new MethodVisitor(Opcodes.ASM9, visitor) {
@Override public void visitFrame(int type, int locals, Object[] local, int stackSize, Object[] stack) {}
@Override public void visitMaxs(int maxStack, int maxLocals) { super.visitMaxs(0, 0); }
});
writer.visitEnd();
return writer.toByteArray();
} }
private static ClassNode read(byte[] bytes) { private static ClassNode read(byte[] bytes) {
@@ -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)));
@@ -7,20 +7,15 @@ import net.lenni0451.classtransform.annotations.CTarget;
import net.lenni0451.classtransform.annotations.CTransformer; import net.lenni0451.classtransform.annotations.CTransformer;
import net.lenni0451.classtransform.annotations.injection.CInject; import net.lenni0451.classtransform.annotations.injection.CInject;
import net.minecraft.client.gui.hud.InGameOverlayRenderer; import net.minecraft.client.gui.hud.InGameOverlayRenderer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.util.math.MatrixStack;
@CTransformer({InGameOverlayRenderer.class}) @CTransformer({InGameOverlayRenderer.class})
public class InGameOverlayRendererMixin { public class InGameOverlayRendererMixin {
@CInject( @CInject(
method = {"renderFireOverlay"}, method = {"renderFireOverlay"},
target = {@CTarget( target = {@CTarget("HEAD")},
value = "INVOKE",
target = "Lnet/minecraft/client/util/math/MatrixStack;translate(FFF)V"
)},
cancellable = true cancellable = true
) )
private static void hideFireOverlayBeforeTranslate(MatrixStack matrices, VertexConsumerProvider vertexConsumers, InjectionCallback ci) { private static void hideFireOverlayAtHead(InjectionCallback ci) {
FireOverlayEvent event = HookBridge.createFireOverlayEvent(); FireOverlayEvent event = HookBridge.createFireOverlayEvent();
if (HookBridge.postFireOverlayEvent(event)) { if (HookBridge.postFireOverlayEvent(event)) {
ci.cancel(); ci.cancel();
@@ -8,10 +8,12 @@ import net.lenni0451.classtransform.annotations.CTransformer;
import net.lenni0451.classtransform.annotations.injection.CInject; import net.lenni0451.classtransform.annotations.injection.CInject;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.damage.DamageSource;
import net.minecraft.particle.BlockParticleEffect;
import net.minecraft.particle.ParticleEffect; import net.minecraft.particle.ParticleEffect;
import net.minecraft.registry.entry.RegistryEntry; import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvent; import net.minecraft.sound.SoundEvent;
import net.minecraft.util.collection.Pool;
import net.minecraft.world.World.ExplosionSourceType; import net.minecraft.world.World.ExplosionSourceType;
import net.minecraft.world.explosion.ExplosionBehavior; import net.minecraft.world.explosion.ExplosionBehavior;
@@ -33,6 +35,7 @@ public class ServerWorldMixin {
ExplosionSourceType explosionSourceType, ExplosionSourceType explosionSourceType,
ParticleEffect smallParticle, ParticleEffect smallParticle,
ParticleEffect largeParticle, ParticleEffect largeParticle,
Pool<BlockParticleEffect> blockParticles,
RegistryEntry<SoundEvent> soundEvent, RegistryEntry<SoundEvent> soundEvent,
InjectionCallback ci InjectionCallback ci
) { ) {
@@ -2,6 +2,7 @@ package dev.zprestige.prestige.module.misc;
import dev.zprestige.prestige.Module; import dev.zprestige.prestige.Module;
import dev.zprestige.prestige.client.Prestige; import dev.zprestige.prestige.client.Prestige;
import dev.zprestige.prestige.engine.EngineState;
import dev.zprestige.prestige.inject.Bootstrap; import dev.zprestige.prestige.inject.Bootstrap;
import dev.zprestige.prestige.inject.NativeBridge; import dev.zprestige.prestige.inject.NativeBridge;
import dev.zprestige.prestige.ui.mc.Overlay; import dev.zprestige.prestige.ui.mc.Overlay;
@@ -22,9 +23,10 @@ public class SelfDestruct extends Module {
@Override @Override
public void onEnable() { public void onEnable() {
if (Prestige.selfDestructed) return;
Prestige.selfDestructed = true; Prestige.selfDestructed = true;
System.out.println("[Prestige] Self Destruct started");
Overlay.shutdown(); Overlay.shutdown();
mc.setScreen(null);
List<Module> modules = new ArrayList<>(Prestige.moduleManager.getModules()); List<Module> modules = new ArrayList<>(Prestige.moduleManager.getModules());
for (Module module : modules) { for (Module module : modules) {
@@ -41,10 +43,13 @@ public class SelfDestruct extends Module {
} }
// Disposing this module now would null its own "Enabled" value while Module.toggle still has to read it. // Disposing this module now would null its own "Enabled" value while Module.toggle still has to read it.
mc.execute(() -> dispose(modules)); EngineState.ready = false;
mc.send(() -> dispose(modules));
} }
private void dispose(List<Module> modules) { private void dispose(List<Module> modules) {
Prestige.eventBus.unregister(this);
Prestige.eventBus.unregister(Prestige.moduleManager);
for (Module module : modules) { for (Module module : modules) {
try { try {
module.dispose(); module.dispose();
@@ -56,15 +61,20 @@ public class SelfDestruct extends Module {
Prestige.moduleManager.setModules(new ArrayList<>()); Prestige.moduleManager.setModules(new ArrayList<>());
if (Bootstrap.isInjected()) { if (Bootstrap.isInjected()) {
// one more tick so no hook of the current tick is still being entered when the original classes come back unhook();
mc.execute(() -> mc.execute(SelfDestruct::unhook)); } else {
System.out.println("[Prestige] Self Destruct finished");
} }
} }
private static void unhook() { private static void unhook() {
try { try {
Bootstrap.uninstall(); if (Bootstrap.uninstall() == 0) {
NativeBridge.release(); NativeBridge.release();
System.out.println("[Prestige] Self Destruct finished");
} else {
System.out.println("[Prestige] Self Destruct left hooks installed. Restart the game to clear them.");
}
} catch (UnsatisfiedLinkError ignored) { } catch (UnsatisfiedLinkError ignored) {
// loader already gone // loader already gone
} catch (Throwable error) { } catch (Throwable error) {
@@ -73,6 +83,11 @@ public class SelfDestruct extends Module {
} }
} }
@Override
protected boolean requiresWorldOnEnable() {
return false;
}
@Override @Override
public boolean isPersistent() { public boolean isPersistent() {
return false; return false;
@@ -89,7 +89,7 @@ public class RenderCore {
Render2D.currentBuffers = BUFFER_SOURCE; Render2D.currentBuffers = BUFFER_SOURCE;
worldContext = context; worldContext = context;
try { try {
((RenderCallback)RenderEvents.WORLD_RENDER.invoker()).onRender(BUFFER_SOURCE, context); RenderEvents.WORLD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context); BUFFER_SOURCE.flush(context);
} catch (Throwable throwable) { } catch (Throwable throwable) {
LOGGER.error("World render failed", throwable); LOGGER.error("World render failed", throwable);
@@ -102,9 +102,9 @@ public class RenderCore {
Render2D.currentContext = context; Render2D.currentContext = context;
Render2D.currentBuffers = BUFFER_SOURCE; Render2D.currentBuffers = BUFFER_SOURCE;
try { try {
((RenderCallback)RenderEvents.HUD_RENDER.invoker()).onRender(BUFFER_SOURCE, context); RenderEvents.HUD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context); BUFFER_SOURCE.flush(context);
((RenderCallback)RenderEvents.AFTER_HUD_RENDER.invoker()).onRender(BUFFER_SOURCE, context); RenderEvents.AFTER_HUD_RENDER.invoker().onRender(BUFFER_SOURCE, context);
BUFFER_SOURCE.flush(context); BUFFER_SOURCE.flush(context);
} catch (Throwable throwable) { } catch (Throwable throwable) {
LOGGER.error("HUD render failed", throwable); LOGGER.error("HUD render failed", throwable);
@@ -1,27 +1,34 @@
package dev.zprestige.prestige.render; package dev.zprestige.prestige.render;
import net.fabricmc.fabric.api.event.Event; import java.util.Objects;
import net.fabricmc.fabric.api.event.EventFactory; import java.util.concurrent.CopyOnWriteArrayList;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public interface RenderEvents { public interface RenderEvents {
Logger LOGGER = LoggerFactory.getLogger(RenderEvents.class); Logger LOGGER = LoggerFactory.getLogger(RenderEvents.class);
Event HUD_RENDER = createEvent(); RenderEvent HUD_RENDER = new RenderEvent();
Event WORLD_RENDER = createEvent(); RenderEvent WORLD_RENDER = new RenderEvent();
Event AFTER_HUD_RENDER = createEvent(); RenderEvent AFTER_HUD_RENDER = new RenderEvent();
@NotNull final class RenderEvent {
private static Event createEvent() { private final CopyOnWriteArrayList<RenderCallback> listeners = new CopyOnWriteArrayList<>();
return EventFactory.createArrayBacked(RenderCallback.class, listeners -> (buffers, context) -> { private final RenderCallback invoker = (buffers, context) -> {
for (RenderCallback listener : (listeners)) { for (RenderCallback listener : listeners) {
try { try {
listener.onRender(buffers, context); listener.onRender(buffers, context);
} catch (Throwable throwable) { } catch (Throwable throwable) {
LOGGER.error("Render listener failed", throwable); LOGGER.error("Render listener failed", throwable);
} }
} }
}); };
public void register(RenderCallback listener) {
listeners.add(Objects.requireNonNull(listener));
}
public RenderCallback invoker() {
return invoker;
}
} }
} }
@@ -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) {
@@ -1,19 +1,16 @@
package dev.zprestige.prestige.ui.mc; package dev.zprestige.prestige.ui.mc;
import dev.zprestige.prestige.ui.Input; import dev.zprestige.prestige.ui.Input;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
/** Opens the ported ClickGUI with the configured menu key (Input.menuKey, default RIGHT SHIFT); closing is handled by Input via the key mixin. */ /** Opens the ported ClickGUI with the configured menu key (Input.menuKey, default RIGHT SHIFT); closing is handled by Input via the key mixin. */
public final class Entry implements ClientModInitializer { public final class Entry {
static boolean prev; static boolean prev;
private static int cachedVk = -1, cachedGlfw; private static int cachedVk = -1, cachedGlfw;
@Override public void onInitializeClient() { public void initialize() {
try { Skid.run(); } catch (Throwable t) { System.out.println("[prestige-ui] splash " + t); } try { Skid.run(); } catch (Throwable t) { System.out.println("[prestige-ui] splash " + t); }
ClientTickEvents.END_CLIENT_TICK.register(this::tick);
System.out.println("[prestige-ui] loaded (menu key opens the menu)"); System.out.println("[prestige-ui] loaded (menu key opens the menu)");
} }
@@ -30,7 +27,7 @@ public final class Entry implements ClientModInitializer {
static boolean bridgeTried; static boolean bridgeTried;
void tick(MinecraftClient mc) { public void tick(MinecraftClient mc) {
if (dev.zprestige.prestige.client.Prestige.selfDestructed) return; if (dev.zprestige.prestige.client.Prestige.selfDestructed) return;
if (!bridgeTried) { bridgeTried = true; dev.zprestige.prestige.ui.bridge.Bridge.bind(); } if (!bridgeTried) { bridgeTried = true; dev.zprestige.prestige.ui.bridge.Bridge.bind(); }
try { try {
@@ -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);
+2 -2
View File
@@ -6,8 +6,8 @@
"description": "Prestige client with the imgui-java ClickGUI.", "description": "Prestige client with the imgui-java ClickGUI.",
"environment": "client", "environment": "client",
"entrypoints": { "entrypoints": {
"main": ["dev.zprestige.prestige.client.Prestige"], "main": ["dev.zprestige.prestige.fabric.FabricEntrypoints"],
"client": ["dev.zprestige.prestige.ui.mc.Entry", "dev.zprestige.prestige.engine.EngineDev"] "client": ["dev.zprestige.prestige.fabric.FabricEntrypoints", "dev.zprestige.prestige.engine.EngineDev"]
}, },
"mixins": ["prestige-engine.mixins.json", "prestige.mixins.json"], "mixins": ["prestige-engine.mixins.json", "prestige.mixins.json"],
"accessWidener": "prestige-client.accesswidener", "accessWidener": "prestige-client.accesswidener",