feat(dll): add hot-injection DLL path with embedded GUI loader
Ship a self-contained OpenZenLoader.exe (OpenZen.dll embedded as
RCDATA) that injects into a running Minecraft 1.20.1 Forge
javaw.exe and brings up the Zen client without -javaagent or a
mods/ entry. The existing Forge mod path stays intact.
Native (native/):
- DLL: JNI_GetCreatedJavaVMs + JVMTI late-attach via the JDK's
instrument.dll Agent_OnAttach to obtain a real Instrumentation;
URLClassLoader on the game loader loads a single bridge class.
- Loader EXE: Win32 listview of javaw.exe/java.exe processes
with window titles, CreateRemoteThread+LoadLibraryW injection,
embedded DLL extracted to %TEMP%\OpenZenLoader at runtime.
Java:
- shit.zen.dll.GameLoaderBridge re-defines every jar class onto
the Forge GameClassLoader (fixed-point retry for super-class
deps) and extracts non-class resources to
%TEMP%\openzen-resources-<pid>, exposed via the
openzen.resources system property.
- shit.zen.dll.DllBootstrap only runs Bootstrap.init +
registerPatches + installPatchesAndRetransform; ZenClient
construction stays on MinecraftPatch.onTick lazy-init.
- shit.zen.asm.Bootstrap parses mapping.srg, detects SRG vs
mojmap runtime via Minecraft.tick reflection, exposes
remapMethod/remapField used at 5 PatchTransformer match
points and from ReflectionUtil.
- ReflectionUtil.resolveField walks the superclass chain trying
SRG then mojmap on each level so e.g. activeEffects on
LocalPlayer resolves on LivingEntity.
- shit.zen.utils.misc.Assets unifies resource lookup with a
fallback to openzen.resources; fonts, cloud assets and WebUI
static files route through it.
- PatchAgent.installPatchesAndRetransform is now idempotent so
DllBootstrap and ZenClient.init can both call it safely.
Build:
- ./gradlew jar forge mod jar (unchanged)
- ./gradlew dll single-file Loader EXE in build/dist
- CMake auto-located via vswhere when VS 2022 ships it.
- README documents both paths and required toolchain.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
f9068db44e
commit
c31c6a873a
35 files changed
+1782
-24
No files matched your search
@@ -23,3 +23,7 @@ run
|
||||
|
||||
# Files from Forge MDK
|
||||
forge*changelog.txt
|
||||
|
||||
# Native DLL build artifacts (CMake out-of-source build + staged jar)
|
||||
native/build/
|
||||
native/zen.jar
|
||||
@@ -14,10 +14,6 @@
|
||||
## 截图
|
||||

|
||||
|
||||
## TODO
|
||||
- 直接构建为热注入DLL。
|
||||
- 你来提出Issue,我来解决。
|
||||
|
||||
## 原始 Jar + Mapping
|
||||
[原始Jar](./mapping/zen-orignial.jar)
|
||||
[Mapping](./mapping/zen.mapping)
|
||||
@@ -50,6 +46,70 @@
|
||||
## 状态与注意事项
|
||||
- 这是**尽力而为的反混淆结果**,部分符号是根据上下文重建的,可能与原作者的命名意图不一致。
|
||||
|
||||
## 构建
|
||||
|
||||
OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单文件 EXE,内嵌 DLL)**。Mod 路径只要 JDK,注入器路径还需要 MSVC 工具链。
|
||||
|
||||
### 共同前置
|
||||
|
||||
- **JDK 17**(推荐 Microsoft Build of OpenJDK / Temurin / Azul Zulu 任一)。
|
||||
- 必须设置 `JAVA_HOME` 环境变量指向该 JDK 安装目录(PowerShell 验证:`echo $env:JAVA_HOME`)。
|
||||
- 仓库根目录用 `gradlew.bat` 即可,**不需要**单独安装 Gradle。
|
||||
|
||||
首次执行会从 ForgeMaven 下载 1.20.1 + Forge 47.4.20 的 mappings 和依赖,耗时几分钟到十几分钟,取决于网络。
|
||||
|
||||
### 1. 构建为 Forge Mod (jar)
|
||||
|
||||
零额外依赖。
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat jar
|
||||
```
|
||||
|
||||
产物:`build/libs/hey-1.0-SNAPSHOT.jar`。把它丢进 `.minecraft/mods/`,按正常 Forge mod 启动即可。Mod ID 是 `hey`,Mod 列表里不会显示(`MinecraftPatch.onTick` 启动时会把自己从 `ModList` 摘掉)。
|
||||
|
||||
### 2. 构建为热注入器 (单文件 EXE)
|
||||
|
||||
产出一个独立的 `OpenZenLoader.exe`,DLL 已经作为资源段嵌入 EXE 内部。用户分发只需要这一个文件,运行后 GUI 列出当前所有 `javaw.exe` 进程(含 Minecraft 窗口标题),选中后点 Inject 即可。
|
||||
|
||||
#### 额外前置 — 必须项
|
||||
|
||||
1. **Visual Studio 2022**(Community 版即可,免费)。安装时勾选:
|
||||
- **"使用 C++ 的桌面开发"** 工作负载
|
||||
- 该工作负载的可选组件里勾上 **"适用于 Windows 的 C++ CMake 工具"**("C++ CMake tools for Windows")
|
||||
2. **`JAVA_HOME` 必须指向 JDK 17**(不只是 JRE)。CMake 需要它定位 `<JAVA_HOME>/include/jni.h` 和 `<JAVA_HOME>/include/win32/jvmti.h`。
|
||||
3. **CMake**:VS 2022 自带,Gradle 会自动检测——也可以独立安装 [CMake](https://cmake.org/download/) 并加入 PATH。Gradle 的检测顺序:
|
||||
1. `PATH` 上的 `cmake.exe`
|
||||
2. 通过 `vswhere.exe` 找 VS 2022 自带的 CMake
|
||||
3. 常见独立安装位置 (`%ProgramFiles%\CMake\bin\cmake.exe` 等)
|
||||
|
||||
#### 构建命令
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat dll
|
||||
```
|
||||
|
||||
产物:`build/dist/OpenZenLoader.exe`(OpenZen.dll 已内嵌为 RCDATA 资源段)。
|
||||
|
||||
构建流程:
|
||||
1. `jar` → 编译 Java 源码,执行 `reobfJar` 把字节码 mojmap→SRG
|
||||
2. `stageNativeJar` → 拷 jar 到 `native/zen.jar`,等待 DLL 把它作为资源段嵌入
|
||||
3. `configureNative` → CMake configure (`-A x64`)
|
||||
4. `buildNative` → 先 build `OpenZen.dll`,CMake 自定义命令把 DLL 拷到 loader 的 binary dir,rc.exe 再嵌入到 `OpenZenLoader.exe`
|
||||
5. `packageDist` → 把 EXE 拷到 `build/dist/`
|
||||
|
||||
#### 使用注入器
|
||||
|
||||
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
||||
2. 双击 `OpenZenLoader.exe`。
|
||||
3. 列表里找到对应的 `javaw.exe`(看 Window Title 列,应是 `Minecraft 1.20.1` 一类)。
|
||||
4. 点 Inject。
|
||||
5. 看到 `Injection ok. Watch %TEMP%\openzen.log for Java side.` 即成功。Minecraft 内右 Shift 触发 ClickGUI 验证。
|
||||
|
||||
诊断日志:
|
||||
- Native 端:`%TEMP%\openzen.log`
|
||||
- Java 端:Minecraft 自己的 `logs/latest.log`(搜索 `OpenZen-Bootstrap`、`GameLoaderBridge`、`PatchAgent`、`ZenBootstrap`)
|
||||
|
||||
## 致谢
|
||||
|
||||
- 原始混淆客户端:**Zen**。
|
||||
|
||||
+122
@@ -240,3 +240,125 @@ publishing {
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
|
||||
}
|
||||
|
||||
// ===== Native build (DLL injection path) =====
|
||||
//
|
||||
// Produces:
|
||||
// build/dist/OpenZen.dll - injectable DLL with embedded zen.jar
|
||||
// build/dist/OpenZenLoader.exe - GUI injector
|
||||
//
|
||||
// Requires:
|
||||
// - JAVA_HOME pointing at a JDK 17 install (for jni.h / jvmti.h)
|
||||
// - CMake either on PATH, bundled with Visual Studio 2019/2022
|
||||
// ("C++ CMake tools for Windows" workload), or installed standalone.
|
||||
// - MSVC build tools (Visual Studio 2019+).
|
||||
//
|
||||
// Usage:
|
||||
// ./gradlew dll
|
||||
//
|
||||
def nativeDir = file('native')
|
||||
def nativeBuildDir = file("$nativeDir/build")
|
||||
|
||||
ext.findCmake = {
|
||||
// 1. cmake already on PATH?
|
||||
try {
|
||||
def p = ['cmd', '/c', 'where', 'cmake'].execute()
|
||||
def out = new StringBuilder()
|
||||
p.consumeProcessOutput(out, new StringBuilder())
|
||||
p.waitForOrKill(2000)
|
||||
if (p.exitValue() == 0) {
|
||||
def first = out.toString().readLines().find { it?.trim() }
|
||||
if (first) return first.trim()
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 2. Visual Studio bundled cmake via vswhere.
|
||||
def pf86 = System.getenv('ProgramFiles(x86)') ?: 'C:/Program Files (x86)'
|
||||
def vswhere = "${pf86}/Microsoft Visual Studio/Installer/vswhere.exe"
|
||||
if (file(vswhere).exists()) {
|
||||
try {
|
||||
def out = new StringBuilder()
|
||||
def p = [vswhere, '-latest', '-products', '*',
|
||||
'-requires', 'Microsoft.VisualStudio.Component.VC.CMake.Project',
|
||||
'-property', 'installationPath'].execute()
|
||||
p.consumeProcessOutput(out, new StringBuilder())
|
||||
p.waitForOrKill(5000)
|
||||
def vsPath = out.toString().trim()
|
||||
if (vsPath) {
|
||||
def cmakeExe = "${vsPath}/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe"
|
||||
if (file(cmakeExe).exists()) return cmakeExe
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// 3. Standalone CMake installations.
|
||||
def pf = System.getenv('ProgramFiles') ?: 'C:/Program Files'
|
||||
def lad = System.getenv('LOCALAPPDATA') ?: ''
|
||||
def candidates = [
|
||||
"${pf}/CMake/bin/cmake.exe",
|
||||
"${pf86}/CMake/bin/cmake.exe",
|
||||
"${lad}/Programs/CMake/bin/cmake.exe",
|
||||
]
|
||||
for (path in candidates) {
|
||||
if (path && file(path).exists()) return path
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def cmakeMissingMessage =
|
||||
'CMake not found. Install one of:\n' +
|
||||
' - Standalone CMake: https://cmake.org/download/ (add to PATH)\n' +
|
||||
' - Visual Studio 2022 with "C++ CMake tools for Windows" workload\n' +
|
||||
'After installing, re-run ./gradlew dll'
|
||||
|
||||
tasks.register('stageNativeJar', Copy) {
|
||||
group = 'openzen'
|
||||
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
||||
'resource compiler can embed it.'
|
||||
dependsOn 'jar'
|
||||
from { tasks.jar.archiveFile }
|
||||
into nativeDir
|
||||
rename { 'zen.jar' }
|
||||
}
|
||||
|
||||
tasks.register('configureNative', Exec) {
|
||||
group = 'openzen'
|
||||
description = 'Run CMake configure on the native sub-projects (DLL + Loader).'
|
||||
dependsOn 'stageNativeJar'
|
||||
workingDir nativeDir
|
||||
commandLine 'cmake', '-S', '.', '-B', 'build', '-A', 'x64'
|
||||
doFirst {
|
||||
def cmake = findCmake()
|
||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||
logger.lifecycle("Using CMake: ${cmake}")
|
||||
commandLine cmake, '-S', '.', '-B', 'build', '-A', 'x64'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildNative', Exec) {
|
||||
group = 'openzen'
|
||||
description = 'Compile OpenZen.dll and OpenZenLoader.exe with CMake.'
|
||||
dependsOn 'configureNative'
|
||||
workingDir nativeDir
|
||||
commandLine 'cmake', '--build', 'build', '--config', 'Release'
|
||||
doFirst {
|
||||
def cmake = findCmake()
|
||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||
commandLine cmake, '--build', 'build', '--config', 'Release'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('packageDist', Copy) {
|
||||
group = 'openzen'
|
||||
description = 'Collect the Loader EXE into build/dist for distribution. ' +
|
||||
'OpenZen.dll is embedded inside the EXE, so we only ship one file.'
|
||||
dependsOn 'buildNative'
|
||||
from "$nativeBuildDir/loader/Release/OpenZenLoader.exe"
|
||||
into "$buildDir/dist"
|
||||
}
|
||||
|
||||
tasks.register('dll') {
|
||||
group = 'openzen'
|
||||
description = 'Build the injectable DLL and GUI Loader (final dist artifacts).'
|
||||
dependsOn 'packageDist'
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(OpenZenNative LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
if(NOT DEFINED ENV{JAVA_HOME})
|
||||
message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h")
|
||||
endif()
|
||||
|
||||
set(JDK_INCLUDE "$ENV{JAVA_HOME}/include")
|
||||
include_directories(${JDK_INCLUDE} ${JDK_INCLUDE}/win32)
|
||||
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "OpenZen native build only supports Windows")
|
||||
endif()
|
||||
|
||||
add_compile_definitions(
|
||||
UNICODE
|
||||
_UNICODE
|
||||
WIN32_LEAN_AND_MEAN
|
||||
NOMINMAX
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(/W3 /permissive- /utf-8 /EHsc)
|
||||
endif()
|
||||
|
||||
add_subdirectory(dll)
|
||||
add_subdirectory(loader)
|
||||
@@ -0,0 +1,45 @@
|
||||
set(JAR_SOURCE "${CMAKE_SOURCE_DIR}/zen.jar")
|
||||
set(JAR_STAGED "${CMAKE_CURRENT_BINARY_DIR}/zen.jar")
|
||||
|
||||
if(NOT EXISTS ${JAR_SOURCE})
|
||||
message(WARNING
|
||||
"zen.jar not found at ${JAR_SOURCE}; "
|
||||
"did you run the Gradle 'stageNativeJar' task first? "
|
||||
"The DLL will be built but the embedded jar resource will be empty.")
|
||||
file(WRITE ${JAR_STAGED} "")
|
||||
else()
|
||||
configure_file(${JAR_SOURCE} ${JAR_STAGED} COPYONLY)
|
||||
endif()
|
||||
|
||||
add_library(OpenZen SHARED
|
||||
src/main.cpp
|
||||
src/jvm_attach.cpp
|
||||
src/jar_extract.cpp
|
||||
src/class_loader.cpp
|
||||
src/diagnostics.cpp
|
||||
res/openzen.rc
|
||||
)
|
||||
|
||||
target_include_directories(OpenZen PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/res
|
||||
)
|
||||
|
||||
# rc.exe needs to find zen.jar (staged into CMAKE_CURRENT_BINARY_DIR by the
|
||||
# configure_file above) when assembling RCDATA from openzen.rc.
|
||||
set_source_files_properties(res/openzen.rc PROPERTIES
|
||||
COMPILE_FLAGS "/I\"${CMAKE_CURRENT_BINARY_DIR}\""
|
||||
)
|
||||
|
||||
target_compile_definitions(OpenZen PRIVATE
|
||||
OPENZEN_DLL_EXPORTS
|
||||
)
|
||||
|
||||
set_target_properties(OpenZen PROPERTIES
|
||||
OUTPUT_NAME "OpenZen"
|
||||
PREFIX ""
|
||||
)
|
||||
|
||||
# We do not link jvm.dll: jvm.dll is already loaded in the host process when the
|
||||
# DLL is injected, and JNI_GetCreatedJavaVMs is provided by it. We only need
|
||||
# the JNI headers at compile time.
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "resource.h"
|
||||
|
||||
IDR_ZEN_JAR RCDATA "zen.jar"
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IDR_ZEN_JAR 101
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "openzen.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace openzen::classes {
|
||||
|
||||
namespace {
|
||||
bool check_and_clear(JNIEnv* env, const char* where) {
|
||||
if (env->ExceptionCheck()) {
|
||||
log::error("JNI exception at %s", where);
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jvmtiEnv* get_jvmti(JavaVM* vm) {
|
||||
jvmtiEnv* jvmti = nullptr;
|
||||
if (vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_2) != JNI_OK || !jvmti) {
|
||||
log::error("GetEnv(JVMTI_VERSION_1_2) failed");
|
||||
return nullptr;
|
||||
}
|
||||
return jvmti;
|
||||
}
|
||||
|
||||
jobject class_loader_of(JNIEnv* env, jclass cls) {
|
||||
jclass classCls = env->FindClass("java/lang/Class");
|
||||
if (!classCls) return nullptr;
|
||||
jmethodID mid = env->GetMethodID(classCls, "getClassLoader",
|
||||
"()Ljava/lang/ClassLoader;");
|
||||
if (!mid) return nullptr;
|
||||
jobject loader = env->CallObjectMethod(cls, mid);
|
||||
env->DeleteLocalRef(classCls);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
return nullptr;
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
||||
jobject find_game_class_loader(JavaVM* vm, JNIEnv* env) {
|
||||
jvmtiEnv* jvmti = get_jvmti(vm);
|
||||
if (!jvmti) return nullptr;
|
||||
|
||||
jint count = 0;
|
||||
jclass* classes = nullptr;
|
||||
jvmtiError rc = jvmti->GetLoadedClasses(&count, &classes);
|
||||
if (rc != JVMTI_ERROR_NONE || !classes) {
|
||||
log::error("GetLoadedClasses failed: %d", (int)rc);
|
||||
return nullptr;
|
||||
}
|
||||
log::info("GetLoadedClasses returned %d classes", (int)count);
|
||||
|
||||
jobject game_loader = nullptr;
|
||||
const char* needle = "Lnet/minecraft/client/Minecraft;";
|
||||
|
||||
for (jint i = 0; i < count && !game_loader; ++i) {
|
||||
char* sig = nullptr;
|
||||
if (jvmti->GetClassSignature(classes[i], &sig, nullptr) != JVMTI_ERROR_NONE) continue;
|
||||
if (sig && std::strcmp(sig, needle) == 0) {
|
||||
log::info("Matched class signature %s", sig);
|
||||
game_loader = class_loader_of(env, classes[i]);
|
||||
}
|
||||
if (sig) jvmti->Deallocate((unsigned char*)sig);
|
||||
}
|
||||
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
|
||||
if (!game_loader) {
|
||||
log::error("Minecraft class not found among loaded classes");
|
||||
return nullptr;
|
||||
}
|
||||
return game_loader;
|
||||
}
|
||||
|
||||
jclass load_dll_bootstrap(JNIEnv* env, jobject game_loader,
|
||||
const std::wstring& jar_path) {
|
||||
// Build java.io.File(jar_path)
|
||||
jclass fileCls = env->FindClass("java/io/File");
|
||||
if (!fileCls) { check_and_clear(env, "FindClass File"); return nullptr; }
|
||||
jmethodID fileCtor = env->GetMethodID(fileCls, "<init>", "(Ljava/lang/String;)V");
|
||||
if (!fileCtor) { check_and_clear(env, "GetMethodID File.<init>"); return nullptr; }
|
||||
jstring jarStr = env->NewString(
|
||||
reinterpret_cast<const jchar*>(jar_path.c_str()),
|
||||
static_cast<jsize>(jar_path.size()));
|
||||
jobject file = env->NewObject(fileCls, fileCtor, jarStr);
|
||||
if (check_and_clear(env, "new File")) return nullptr;
|
||||
|
||||
// file.toURI()
|
||||
jmethodID toURI = env->GetMethodID(fileCls, "toURI", "()Ljava/net/URI;");
|
||||
jobject uri = env->CallObjectMethod(file, toURI);
|
||||
if (check_and_clear(env, "File.toURI")) return nullptr;
|
||||
|
||||
// uri.toURL()
|
||||
jclass uriCls = env->FindClass("java/net/URI");
|
||||
jmethodID toURL = env->GetMethodID(uriCls, "toURL", "()Ljava/net/URL;");
|
||||
jobject url = env->CallObjectMethod(uri, toURL);
|
||||
if (check_and_clear(env, "URI.toURL")) return nullptr;
|
||||
|
||||
// URL[] urls = { url };
|
||||
jclass urlCls = env->FindClass("java/net/URL");
|
||||
jobjectArray urls = env->NewObjectArray(1, urlCls, url);
|
||||
if (check_and_clear(env, "NewObjectArray URL[]")) return nullptr;
|
||||
|
||||
// new URLClassLoader(urls, gameLoader)
|
||||
jclass urlclCls = env->FindClass("java/net/URLClassLoader");
|
||||
if (!urlclCls) { check_and_clear(env, "FindClass URLClassLoader"); return nullptr; }
|
||||
jmethodID urlclCtor = env->GetMethodID(urlclCls, "<init>",
|
||||
"([Ljava/net/URL;Ljava/lang/ClassLoader;)V");
|
||||
if (!urlclCtor) { check_and_clear(env, "GetMethodID URLClassLoader.<init>"); return nullptr; }
|
||||
jobject urlcl = env->NewObject(urlclCls, urlclCtor, urls, game_loader);
|
||||
if (check_and_clear(env, "new URLClassLoader")) return nullptr;
|
||||
log::info("URLClassLoader constructed with parent=gameLoader");
|
||||
|
||||
// urlcl.loadClass("shit.zen.dll.GameLoaderBridge")
|
||||
//
|
||||
// We deliberately load GameLoaderBridge - not DllBootstrap - because the
|
||||
// bridge's job is to re-define every class in zen.jar onto the game
|
||||
// class loader so retransformed Minecraft classes can resolve our patch
|
||||
// handlers (defining-loader equality). DllBootstrap is then loaded by
|
||||
// the game loader from the bridge.
|
||||
jclass classLoaderCls = env->FindClass("java/lang/ClassLoader");
|
||||
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
|
||||
"(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
jstring name = env->NewStringUTF("shit.zen.dll.GameLoaderBridge");
|
||||
jobject loaded = env->CallObjectMethod(urlcl, loadClass, name);
|
||||
if (check_and_clear(env, "URLClassLoader.loadClass GameLoaderBridge")) return nullptr;
|
||||
if (!loaded) {
|
||||
log::error("loadClass returned null for GameLoaderBridge");
|
||||
return nullptr;
|
||||
}
|
||||
log::info("GameLoaderBridge class loaded via URLClassLoader");
|
||||
return static_cast<jclass>(loaded);
|
||||
}
|
||||
|
||||
} // namespace openzen::classes
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "openzen.h"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
|
||||
namespace openzen::log {
|
||||
|
||||
namespace {
|
||||
std::mutex g_mutex;
|
||||
HANDLE g_file = INVALID_HANDLE_VALUE;
|
||||
|
||||
void write_line(const char* level, const char* fmt, va_list ap) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
|
||||
char buf[2048];
|
||||
int prefix = std::snprintf(buf, sizeof buf,
|
||||
"[%02d:%02d:%02d.%03d %s] ",
|
||||
st.wHour, st.wMinute, st.wSecond,
|
||||
st.wMilliseconds, level);
|
||||
if (prefix < 0) prefix = 0;
|
||||
int body = std::vsnprintf(buf + prefix, sizeof buf - prefix - 2, fmt, ap);
|
||||
if (body < 0) body = 0;
|
||||
int total = prefix + body;
|
||||
if (total > (int)sizeof buf - 2) total = (int)sizeof buf - 2;
|
||||
buf[total++] = '\r';
|
||||
buf[total++] = '\n';
|
||||
|
||||
OutputDebugStringA(buf);
|
||||
|
||||
if (g_file != INVALID_HANDLE_VALUE) {
|
||||
DWORD written = 0;
|
||||
WriteFile(g_file, buf, (DWORD)total, &written, nullptr);
|
||||
FlushFileBuffers(g_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (g_file != INVALID_HANDLE_VALUE) return;
|
||||
|
||||
wchar_t tmp[MAX_PATH];
|
||||
DWORD n = GetTempPathW(MAX_PATH, tmp);
|
||||
if (n == 0 || n > MAX_PATH) return;
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
std::swprintf(path, MAX_PATH, L"%sopenzen.log", tmp);
|
||||
|
||||
g_file = CreateFileW(path, GENERIC_WRITE, FILE_SHARE_READ, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
}
|
||||
|
||||
void info(const char* fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_line("INFO", fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void error(const char* fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_line("ERROR", fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
} // namespace openzen::log
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "openzen.h"
|
||||
#include "resource.h"
|
||||
|
||||
namespace openzen::jar {
|
||||
|
||||
bool extract_embedded(std::wstring& out_path) {
|
||||
HRSRC info = FindResourceW(g_self_module, MAKEINTRESOURCEW(IDR_ZEN_JAR), RT_RCDATA);
|
||||
if (!info) {
|
||||
log::error("FindResource(IDR_ZEN_JAR) failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
DWORD size = SizeofResource(g_self_module, info);
|
||||
if (size == 0) {
|
||||
log::error("Embedded zen.jar resource is empty");
|
||||
return false;
|
||||
}
|
||||
HGLOBAL loaded = LoadResource(g_self_module, info);
|
||||
if (!loaded) {
|
||||
log::error("LoadResource failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
void* data = LockResource(loaded);
|
||||
if (!data) {
|
||||
log::error("LockResource failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t tmp[MAX_PATH];
|
||||
if (GetTempPathW(MAX_PATH, tmp) == 0) {
|
||||
log::error("GetTempPath failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
std::swprintf(path, MAX_PATH, L"%sopenzen-%lu.jar", tmp, GetCurrentProcessId());
|
||||
|
||||
HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
log::error("CreateFile %ls failed: %lu", path, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD written = 0;
|
||||
BOOL ok = WriteFile(file, data, size, &written, nullptr);
|
||||
CloseHandle(file);
|
||||
if (!ok || written != size) {
|
||||
log::error("WriteFile %ls failed: %lu", path, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
out_path.assign(path);
|
||||
log::info("Extracted zen.jar (%lu bytes) to %ls", (unsigned long)size, path);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace openzen::jar
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "openzen.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace openzen::jvm {
|
||||
|
||||
namespace {
|
||||
// Signature of Agent_OnAttach exported by the JDK's instrument.dll.
|
||||
using Agent_OnAttach_t = jint (JNICALL*)(JavaVM* vm, char* options, void* reserved);
|
||||
|
||||
bool find_instrument_dll(std::wstring& out_path) {
|
||||
// The JDK puts instrument.dll next to jvm.dll / java.dll. We locate
|
||||
// java.dll (loaded by the running JVM) and rewrite the filename.
|
||||
HMODULE javaDll = GetModuleHandleW(L"java.dll");
|
||||
if (!javaDll) {
|
||||
log::error("java.dll not loaded in current process");
|
||||
return false;
|
||||
}
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD n = GetModuleFileNameW(javaDll, path, MAX_PATH);
|
||||
if (n == 0 || n >= MAX_PATH) {
|
||||
log::error("GetModuleFileName(java.dll) failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
// Walk back to last backslash and append instrument.dll.
|
||||
wchar_t* slash = wcsrchr(path, L'\\');
|
||||
if (!slash) {
|
||||
log::error("Unexpected java.dll path: %ls", path);
|
||||
return false;
|
||||
}
|
||||
slash[1] = L'\0';
|
||||
out_path.assign(path);
|
||||
out_path.append(L"instrument.dll");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
JavaVM* find_vm() {
|
||||
// We are injected into a process that already loaded jvm.dll. We resolve
|
||||
// JNI_GetCreatedJavaVMs at runtime so the DLL does not have to link
|
||||
// against a specific JDK's jvm.lib at build time.
|
||||
HMODULE jvm_dll = GetModuleHandleW(L"jvm.dll");
|
||||
if (!jvm_dll) {
|
||||
log::error("jvm.dll is not loaded in the current process");
|
||||
return nullptr;
|
||||
}
|
||||
using JNI_GetCreatedJavaVMs_t = jint (JNICALL*)(JavaVM**, jsize, jsize*);
|
||||
auto fn = reinterpret_cast<JNI_GetCreatedJavaVMs_t>(
|
||||
GetProcAddress(jvm_dll, "JNI_GetCreatedJavaVMs"));
|
||||
if (!fn) {
|
||||
log::error("GetProcAddress(JNI_GetCreatedJavaVMs) failed: %lu", GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
JavaVM* vm = nullptr;
|
||||
jsize count = 0;
|
||||
jint rc = fn(&vm, 1, &count);
|
||||
if (rc != JNI_OK || count < 1 || !vm) {
|
||||
log::error("JNI_GetCreatedJavaVMs rc=%d count=%d", (int)rc, (int)count);
|
||||
return nullptr;
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
|
||||
jint attach_instrument(JavaVM* vm, const std::wstring& jar_path) {
|
||||
std::wstring instrument_path;
|
||||
if (!find_instrument_dll(instrument_path)) {
|
||||
return -1;
|
||||
}
|
||||
log::info("Loading %ls", instrument_path.c_str());
|
||||
|
||||
HMODULE inst = LoadLibraryW(instrument_path.c_str());
|
||||
if (!inst) {
|
||||
log::error("LoadLibrary instrument.dll failed: %lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto fn = reinterpret_cast<Agent_OnAttach_t>(GetProcAddress(inst, "Agent_OnAttach"));
|
||||
if (!fn) {
|
||||
log::error("GetProcAddress(Agent_OnAttach) failed: %lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// OpenJDK's instrument.dll parses the options string using parseArgumentTail:
|
||||
// the tail starts with the jar path (system encoding), optionally followed
|
||||
// by '=' and additional agent args. We pass only the jar path.
|
||||
int needed = WideCharToMultiByte(CP_ACP, 0, jar_path.c_str(), -1,
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
if (needed <= 0) {
|
||||
log::error("WideCharToMultiByte sizing failed: %lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
std::vector<char> options(needed);
|
||||
WideCharToMultiByte(CP_ACP, 0, jar_path.c_str(), -1,
|
||||
options.data(), needed, nullptr, nullptr);
|
||||
|
||||
log::info("Calling Agent_OnAttach with options=%s", options.data());
|
||||
jint rc = fn(vm, options.data(), nullptr);
|
||||
log::info("Agent_OnAttach returned %d", (int)rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace openzen::jvm
|
||||
@@ -0,0 +1,107 @@
|
||||
#include "openzen.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace openzen {
|
||||
HMODULE g_self_module = nullptr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool> g_already_attached{false};
|
||||
|
||||
DWORD WINAPI inject_thread(LPVOID) {
|
||||
using namespace openzen;
|
||||
|
||||
log::init();
|
||||
log::info("OpenZen.dll bootstrap thread started, pid=%lu", GetCurrentProcessId());
|
||||
|
||||
JavaVM* vm = jvm::find_vm();
|
||||
if (!vm) return 1;
|
||||
|
||||
JNIEnv* env = nullptr;
|
||||
JavaVMAttachArgs args{};
|
||||
args.version = JNI_VERSION_1_8;
|
||||
args.name = const_cast<char*>("OpenZen-Bootstrap");
|
||||
args.group = nullptr;
|
||||
if (vm->AttachCurrentThreadAsDaemon((void**)&env, &args) != JNI_OK || !env) {
|
||||
log::error("AttachCurrentThreadAsDaemon failed");
|
||||
return 2;
|
||||
}
|
||||
log::info("Attached bootstrap thread to JavaVM");
|
||||
|
||||
std::wstring jar_path;
|
||||
if (!jar::extract_embedded(jar_path)) {
|
||||
vm->DetachCurrentThread();
|
||||
return 3;
|
||||
}
|
||||
|
||||
jint rc = jvm::attach_instrument(vm, jar_path);
|
||||
if (rc != 0) {
|
||||
log::error("Agent_OnAttach reported error %d", (int)rc);
|
||||
// Continue anyway - some JDK builds report non-zero even on success
|
||||
// because of secondary cleanup; PatchAgent.agentmain may still have run.
|
||||
}
|
||||
|
||||
jobject game_loader = classes::find_game_class_loader(vm, env);
|
||||
if (!game_loader) {
|
||||
vm->DetachCurrentThread();
|
||||
return 4;
|
||||
}
|
||||
|
||||
jclass bridge_cls = classes::load_dll_bootstrap(env, game_loader, jar_path);
|
||||
if (!bridge_cls) {
|
||||
env->DeleteLocalRef(game_loader);
|
||||
vm->DetachCurrentThread();
|
||||
return 5;
|
||||
}
|
||||
|
||||
jmethodID load_mid = env->GetStaticMethodID(bridge_cls, "load",
|
||||
"(Ljava/lang/String;Ljava/lang/ClassLoader;)V");
|
||||
if (!load_mid) {
|
||||
log::error("GameLoaderBridge.load(String, ClassLoader) method not found");
|
||||
env->ExceptionClear();
|
||||
vm->DetachCurrentThread();
|
||||
return 6;
|
||||
}
|
||||
|
||||
jstring jar_jstr = env->NewString(
|
||||
reinterpret_cast<const jchar*>(jar_path.c_str()),
|
||||
static_cast<jsize>(jar_path.size()));
|
||||
|
||||
env->CallStaticVoidMethod(bridge_cls, load_mid, jar_jstr, game_loader);
|
||||
if (env->ExceptionCheck()) {
|
||||
log::error("GameLoaderBridge.load threw an exception");
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
} else {
|
||||
log::info("GameLoaderBridge.load returned without exception");
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(jar_jstr);
|
||||
env->DeleteLocalRef(bridge_cls);
|
||||
env->DeleteLocalRef(game_loader);
|
||||
vm->DetachCurrentThread();
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
// Idempotence: if the loader injects twice (or the host calls
|
||||
// LoadLibrary twice from different threads) we still only kick off the
|
||||
// bootstrap once.
|
||||
bool expected = false;
|
||||
if (!g_already_attached.compare_exchange_strong(expected, true)) {
|
||||
return TRUE;
|
||||
}
|
||||
openzen::g_self_module = module;
|
||||
DisableThreadLibraryCalls(module);
|
||||
// Never call JNI from inside DllMain - the loader lock is held. Kick
|
||||
// a separate worker thread that will do all the heavy lifting.
|
||||
HANDLE t = CreateThread(nullptr, 0, inject_thread, nullptr, 0, nullptr);
|
||||
if (t) CloseHandle(t);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <jni.h>
|
||||
#include <jvmti.h>
|
||||
#include <string>
|
||||
|
||||
namespace openzen {
|
||||
|
||||
extern HMODULE g_self_module;
|
||||
|
||||
namespace log {
|
||||
void init();
|
||||
void info(const char* fmt, ...);
|
||||
void error(const char* fmt, ...);
|
||||
}
|
||||
|
||||
namespace jar {
|
||||
// Extract the IDR_ZEN_JAR resource embedded in OpenZen.dll into a temporary
|
||||
// file under %TEMP%. Returns the absolute path on success.
|
||||
bool extract_embedded(std::wstring& out_path);
|
||||
}
|
||||
|
||||
namespace jvm {
|
||||
// Locate the running JavaVM in the current process. Returns nullptr if no
|
||||
// JVM is available (the DLL was injected into a non-Java process).
|
||||
JavaVM* find_vm();
|
||||
|
||||
// Call Agent_OnAttach in the JDK's instrument.dll, pointing it at the given
|
||||
// agent jar. After this returns 0 the jar's Agent-Class entry point
|
||||
// (PatchAgent.agentmain) will have been invoked and the JDK's
|
||||
// InstrumentationImpl will be live.
|
||||
jint attach_instrument(JavaVM* vm, const std::wstring& jar_path);
|
||||
}
|
||||
|
||||
namespace classes {
|
||||
// Walk loaded classes via JVMTI to find the class loader that defined
|
||||
// net.minecraft.client.Minecraft - the Forge GameClassLoader. Returns a
|
||||
// local JNI reference (caller manages lifetime).
|
||||
jobject find_game_class_loader(JavaVM* vm, JNIEnv* env);
|
||||
|
||||
// Build URLClassLoader(jar, parent=gameLoader) and load DllBootstrap.
|
||||
// Returns a local JNI reference to the class.
|
||||
jclass load_dll_bootstrap(JNIEnv* env, jobject game_loader,
|
||||
const std::wstring& jar_path);
|
||||
}
|
||||
|
||||
} // namespace openzen
|
||||
@@ -0,0 +1,51 @@
|
||||
# Stage the freshly-built OpenZen.dll into the loader's binary dir so rc.exe
|
||||
# can pick it up as an RCDATA resource (embedded in the .exe).
|
||||
set(EMBED_DIR "${CMAKE_CURRENT_BINARY_DIR}/embedded_dll")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBED_DIR}/OpenZen.dll"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${EMBED_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:OpenZen> "${EMBED_DIR}/OpenZen.dll"
|
||||
DEPENDS OpenZen
|
||||
COMMENT "Staging OpenZen.dll into loader resources"
|
||||
)
|
||||
add_custom_target(stage_dll_for_loader DEPENDS "${EMBED_DIR}/OpenZen.dll")
|
||||
|
||||
add_executable(OpenZenLoader WIN32
|
||||
src/main.cpp
|
||||
src/process_list.cpp
|
||||
src/injector.cpp
|
||||
src/embedded_dll.cpp
|
||||
src/window_title.cpp
|
||||
src/ui.cpp
|
||||
res/loader.rc
|
||||
)
|
||||
|
||||
add_dependencies(OpenZenLoader stage_dll_for_loader)
|
||||
|
||||
target_include_directories(OpenZenLoader PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/res
|
||||
)
|
||||
|
||||
# Tell rc.exe to look in EMBED_DIR (where OpenZen.dll was staged) when
|
||||
# resolving the RCDATA "OpenZen.dll" reference in loader.rc.
|
||||
set_source_files_properties(res/loader.rc PROPERTIES
|
||||
COMPILE_FLAGS "/I\"${EMBED_DIR}\""
|
||||
)
|
||||
|
||||
target_link_libraries(OpenZenLoader PRIVATE
|
||||
comctl32
|
||||
psapi
|
||||
shlwapi
|
||||
user32
|
||||
)
|
||||
|
||||
set_target_properties(OpenZenLoader PROPERTIES
|
||||
OUTPUT_NAME "OpenZenLoader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(OpenZenLoader PROPERTIES
|
||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /MANIFESTUAC:\"level='asInvoker' uiAccess='false'\""
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "resource.h"
|
||||
|
||||
IDR_OPENZEN_DLL RCDATA "OpenZen.dll"
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#define IDR_OPENZEN_DLL 201
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "loader.h"
|
||||
#include "resource.h"
|
||||
|
||||
namespace loader {
|
||||
|
||||
std::wstring extract_embedded_dll() {
|
||||
HMODULE self = GetModuleHandleW(nullptr);
|
||||
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
|
||||
if (!info) return L"";
|
||||
DWORD size = SizeofResource(self, info);
|
||||
if (size == 0) return L"";
|
||||
HGLOBAL loaded = LoadResource(self, info);
|
||||
if (!loaded) return L"";
|
||||
void* data = LockResource(loaded);
|
||||
if (!data) return L"";
|
||||
|
||||
wchar_t tmp[MAX_PATH];
|
||||
if (GetTempPathW(MAX_PATH, tmp) == 0) return L"";
|
||||
|
||||
wchar_t dir[MAX_PATH];
|
||||
std::swprintf(dir, MAX_PATH, L"%sOpenZenLoader", tmp);
|
||||
CreateDirectoryW(dir, nullptr);
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
std::swprintf(path, MAX_PATH, L"%s\\OpenZen.dll", dir);
|
||||
|
||||
HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return L"";
|
||||
|
||||
DWORD written = 0;
|
||||
BOOL ok = WriteFile(file, data, size, &written, nullptr);
|
||||
CloseHandle(file);
|
||||
if (!ok || written != size) return L"";
|
||||
|
||||
return std::wstring(path);
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "loader.h"
|
||||
|
||||
#include <shlwapi.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
std::wstring format_error(const wchar_t* where, DWORD err) {
|
||||
wchar_t msg[512] = {0};
|
||||
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, msg, 512, nullptr);
|
||||
std::wstringstream ss;
|
||||
ss << where << L" failed (code " << err << L"): " << msg;
|
||||
return ss.str();
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring inject(DWORD pid, const std::wstring& dll_path) {
|
||||
if (!PathFileExistsW(dll_path.c_str())) {
|
||||
return L"DLL not found: " + dll_path;
|
||||
}
|
||||
|
||||
HANDLE process = OpenProcess(
|
||||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
|
||||
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||||
FALSE, pid);
|
||||
if (!process) return format_error(L"OpenProcess", GetLastError());
|
||||
|
||||
SIZE_T payload_bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||
LPVOID remote = VirtualAllocEx(process, nullptr, payload_bytes,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remote) {
|
||||
DWORD err = GetLastError();
|
||||
CloseHandle(process);
|
||||
return format_error(L"VirtualAllocEx", err);
|
||||
}
|
||||
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory(process, remote, dll_path.c_str(), payload_bytes, &written)
|
||||
|| written != payload_bytes) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return format_error(L"WriteProcessMemory", err);
|
||||
}
|
||||
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
if (!kernel32) {
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return L"kernel32.dll handle missing in loader process";
|
||||
}
|
||||
LPTHREAD_START_ROUTINE loadLib =
|
||||
(LPTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryW");
|
||||
if (!loadLib) {
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return L"LoadLibraryW not exported by kernel32";
|
||||
}
|
||||
|
||||
HANDLE remote_thread = CreateRemoteThread(process, nullptr, 0, loadLib,
|
||||
remote, 0, nullptr);
|
||||
if (!remote_thread) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return format_error(L"CreateRemoteThread", err);
|
||||
}
|
||||
|
||||
WaitForSingleObject(remote_thread, 10000);
|
||||
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeThread(remote_thread, &exit_code);
|
||||
|
||||
CloseHandle(remote_thread);
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
|
||||
if (exit_code == 0) {
|
||||
return L"LoadLibraryW remote returned NULL (DLL failed to load - "
|
||||
L"check %TEMP%\\openzen.log)";
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace loader {
|
||||
|
||||
struct JavaProcess {
|
||||
DWORD pid;
|
||||
std::wstring image_name;
|
||||
std::wstring command_line;
|
||||
std::wstring window_title;
|
||||
};
|
||||
|
||||
// Enumerate processes whose image is javaw.exe / java.exe.
|
||||
std::vector<JavaProcess> list_java_processes();
|
||||
|
||||
// Inject the given DLL into the target process via CreateRemoteThread +
|
||||
// LoadLibraryW. Returns an empty string on success or a human-readable error.
|
||||
std::wstring inject(DWORD pid, const std::wstring& dll_path);
|
||||
|
||||
// Extract the IDR_OPENZEN_DLL resource baked into the loader EXE to a
|
||||
// temp file and return its absolute path. Empty string on failure.
|
||||
std::wstring extract_embedded_dll();
|
||||
|
||||
// Walk top-level windows and return the most informative title belonging to
|
||||
// the given pid ("" if none found).
|
||||
std::wstring window_title_for(DWORD pid);
|
||||
|
||||
int run_ui(HINSTANCE hInstance);
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "loader.h"
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
|
||||
return loader::run_ui(hInstance);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "loader.h"
|
||||
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
bool ci_equals(const std::wstring& a, const wchar_t* b) {
|
||||
std::wstring lower(a);
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](wchar_t c) { return (wchar_t)std::towlower(c); });
|
||||
return lower == b;
|
||||
}
|
||||
|
||||
std::wstring read_command_line(HANDLE process) {
|
||||
// Reading the full command line via NtQueryInformationProcess / PEB is
|
||||
// architecture-sensitive and not worth the complexity here. We surface
|
||||
// the executable's full path as a stand-in - users can identify the
|
||||
// Minecraft instance from PID + working directory if needed.
|
||||
wchar_t buf[MAX_PATH * 2];
|
||||
DWORD size = (DWORD)(sizeof buf / sizeof buf[0]);
|
||||
if (QueryFullProcessImageNameW(process, 0, buf, &size)) {
|
||||
return std::wstring(buf, size);
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<JavaProcess> list_java_processes() {
|
||||
std::vector<JavaProcess> result;
|
||||
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return result;
|
||||
|
||||
PROCESSENTRY32W pe{};
|
||||
pe.dwSize = sizeof pe;
|
||||
if (!Process32FirstW(snap, &pe)) {
|
||||
CloseHandle(snap);
|
||||
return result;
|
||||
}
|
||||
|
||||
do {
|
||||
std::wstring name = pe.szExeFile;
|
||||
if (!ci_equals(name, L"javaw.exe") && !ci_equals(name, L"java.exe")) continue;
|
||||
|
||||
JavaProcess jp;
|
||||
jp.pid = pe.th32ProcessID;
|
||||
jp.image_name = name;
|
||||
|
||||
HANDLE process = OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, jp.pid);
|
||||
if (process) {
|
||||
jp.command_line = read_command_line(process);
|
||||
CloseHandle(process);
|
||||
}
|
||||
jp.window_title = window_title_for(jp.pid);
|
||||
result.push_back(std::move(jp));
|
||||
} while (Process32NextW(snap, &pe));
|
||||
|
||||
CloseHandle(snap);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,259 @@
|
||||
#include "loader.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "comctl32.lib")
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t WND_CLASS[] = L"OpenZenLoaderWnd";
|
||||
|
||||
enum {
|
||||
ID_LIST = 1001,
|
||||
ID_REFRESH,
|
||||
ID_INJECT,
|
||||
ID_BROWSE,
|
||||
ID_DLLPATH,
|
||||
ID_LOG,
|
||||
};
|
||||
|
||||
HWND g_list = nullptr;
|
||||
HWND g_refresh = nullptr;
|
||||
HWND g_inject = nullptr;
|
||||
HWND g_browse = nullptr;
|
||||
HWND g_dll_edit = nullptr;
|
||||
HWND g_log = nullptr;
|
||||
std::vector<JavaProcess> g_processes;
|
||||
|
||||
void append_log(const std::wstring& line) {
|
||||
int len = GetWindowTextLengthW(g_log);
|
||||
SendMessageW(g_log, EM_SETSEL, len, len);
|
||||
std::wstring text = line + L"\r\n";
|
||||
SendMessageW(g_log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
|
||||
}
|
||||
|
||||
std::wstring get_dll_path() {
|
||||
int len = GetWindowTextLengthW(g_dll_edit);
|
||||
std::wstring s(len, L'\0');
|
||||
if (len > 0) GetWindowTextW(g_dll_edit, s.data(), len + 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
void refresh_list() {
|
||||
ListView_DeleteAllItems(g_list);
|
||||
g_processes = list_java_processes();
|
||||
for (size_t i = 0; i < g_processes.size(); ++i) {
|
||||
const auto& jp = g_processes[i];
|
||||
|
||||
LVITEMW item{};
|
||||
item.mask = LVIF_TEXT | LVIF_PARAM;
|
||||
item.iItem = (int)i;
|
||||
item.iSubItem = 0;
|
||||
wchar_t pid_text[32];
|
||||
std::swprintf(pid_text, 32, L"%lu", jp.pid);
|
||||
item.pszText = pid_text;
|
||||
item.lParam = (LPARAM)i;
|
||||
int row = ListView_InsertItem(g_list, &item);
|
||||
|
||||
ListView_SetItemText(g_list, row, 1,
|
||||
const_cast<LPWSTR>(jp.image_name.c_str()));
|
||||
ListView_SetItemText(g_list, row, 2,
|
||||
const_cast<LPWSTR>(jp.window_title.c_str()));
|
||||
ListView_SetItemText(g_list, row, 3,
|
||||
const_cast<LPWSTR>(jp.command_line.c_str()));
|
||||
}
|
||||
wchar_t msg[64];
|
||||
std::swprintf(msg, 64, L"Refreshed: %zu Java process(es)", g_processes.size());
|
||||
append_log(msg);
|
||||
}
|
||||
|
||||
void do_inject() {
|
||||
int sel = ListView_GetNextItem(g_list, -1, LVNI_SELECTED);
|
||||
if (sel < 0) {
|
||||
append_log(L"No process selected. Pick a row and try again.");
|
||||
return;
|
||||
}
|
||||
LVITEMW item{};
|
||||
item.mask = LVIF_PARAM;
|
||||
item.iItem = sel;
|
||||
ListView_GetItem(g_list, &item);
|
||||
size_t idx = (size_t)item.lParam;
|
||||
if (idx >= g_processes.size()) {
|
||||
append_log(L"Stale selection. Refresh and retry.");
|
||||
return;
|
||||
}
|
||||
const auto& jp = g_processes[idx];
|
||||
|
||||
std::wstring dll = get_dll_path();
|
||||
wchar_t msg[512];
|
||||
std::swprintf(msg, 512, L"Injecting %ls into pid %lu (%ls)",
|
||||
dll.c_str(), jp.pid, jp.image_name.c_str());
|
||||
append_log(msg);
|
||||
|
||||
std::wstring err = inject(jp.pid, dll);
|
||||
if (err.empty()) {
|
||||
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
|
||||
} else {
|
||||
append_log(L"Injection failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
void do_browse() {
|
||||
wchar_t buf[MAX_PATH] = {0};
|
||||
std::wstring current = get_dll_path();
|
||||
if (!current.empty() && current.size() < MAX_PATH) {
|
||||
wcscpy_s(buf, MAX_PATH, current.c_str());
|
||||
}
|
||||
|
||||
OPENFILENAMEW ofn{};
|
||||
ofn.lStructSize = sizeof ofn;
|
||||
ofn.hwndOwner = GetParent(g_browse);
|
||||
ofn.lpstrFilter = L"DLL Files\0*.dll\0All Files\0*.*\0";
|
||||
ofn.lpstrFile = buf;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
||||
|
||||
if (GetOpenFileNameW(&ofn)) {
|
||||
SetWindowTextW(g_dll_edit, buf);
|
||||
}
|
||||
}
|
||||
|
||||
void layout(HWND hwnd) {
|
||||
RECT rc; GetClientRect(hwnd, &rc);
|
||||
int W = rc.right - rc.left;
|
||||
int H = rc.bottom - rc.top;
|
||||
|
||||
int pad = 8;
|
||||
int btn_h = 24;
|
||||
int row_h = 26;
|
||||
|
||||
// Top row: DLL path edit + Browse
|
||||
MoveWindow(g_dll_edit, pad, pad, W - pad*3 - 80, btn_h, TRUE);
|
||||
MoveWindow(g_browse, W - pad - 80, pad, 80, btn_h, TRUE);
|
||||
|
||||
// Button row
|
||||
int by = pad + row_h;
|
||||
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
|
||||
MoveWindow(g_inject, pad + 110, by, 100, btn_h, TRUE);
|
||||
|
||||
// List view
|
||||
int list_y = by + row_h;
|
||||
int list_h = H * 60 / 100 - list_y;
|
||||
if (list_h < 100) list_h = 100;
|
||||
MoveWindow(g_list, pad, list_y, W - pad*2, list_h, TRUE);
|
||||
|
||||
// Log area
|
||||
int log_y = list_y + list_h + pad;
|
||||
int log_h = H - log_y - pad;
|
||||
if (log_h < 60) log_h = 60;
|
||||
MoveWindow(g_log, pad, log_y, W - pad*2, log_h, TRUE);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
switch (msg) {
|
||||
case WM_CREATE: {
|
||||
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance;
|
||||
|
||||
g_dll_edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_DLLPATH, hi, nullptr);
|
||||
// Extract the embedded OpenZen.dll to %TEMP%\OpenZenLoader and pre-fill
|
||||
// it as the default DLL path. Users can still override via Browse.
|
||||
std::wstring embedded = extract_embedded_dll();
|
||||
if (!embedded.empty()) {
|
||||
SetWindowTextW(g_dll_edit, embedded.c_str());
|
||||
}
|
||||
|
||||
g_browse = CreateWindowW(L"BUTTON", L"Browse...",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_BROWSE, hi, nullptr);
|
||||
|
||||
g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
|
||||
g_inject = CreateWindowW(L"BUTTON", L"Inject",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr);
|
||||
|
||||
g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
|
||||
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_LIST, hi, nullptr);
|
||||
ListView_SetExtendedListViewStyle(g_list,
|
||||
LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
|
||||
|
||||
LVCOLUMNW col{};
|
||||
col.mask = LVCF_TEXT | LVCF_WIDTH;
|
||||
col.cx = 70; col.pszText = const_cast<LPWSTR>(L"PID");
|
||||
ListView_InsertColumn(g_list, 0, &col);
|
||||
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
|
||||
ListView_InsertColumn(g_list, 1, &col);
|
||||
col.cx = 320; col.pszText = const_cast<LPWSTR>(L"Window Title");
|
||||
ListView_InsertColumn(g_list, 2, &col);
|
||||
col.cx = 300; col.pszText = const_cast<LPWSTR>(L"Path");
|
||||
ListView_InsertColumn(g_list, 3, &col);
|
||||
|
||||
g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE
|
||||
| ES_AUTOVSCROLL | ES_READONLY,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_LOG, hi, nullptr);
|
||||
|
||||
layout(hwnd);
|
||||
refresh_list();
|
||||
append_log(L"OpenZen Loader ready. Select a javaw.exe and press Inject.");
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: layout(hwnd); return 0;
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wp)) {
|
||||
case ID_REFRESH: refresh_list(); return 0;
|
||||
case ID_INJECT: do_inject(); return 0;
|
||||
case ID_BROWSE: do_browse(); return 0;
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY: PostQuitMessage(0); return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int run_ui(HINSTANCE hInstance) {
|
||||
INITCOMMONCONTROLSEX icc{};
|
||||
icc.dwSize = sizeof icc;
|
||||
icc.dwICC = ICC_LISTVIEW_CLASSES | ICC_STANDARD_CLASSES;
|
||||
InitCommonControlsEx(&icc);
|
||||
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof wc;
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wc.lpfnWndProc = wnd_proc;
|
||||
wc.hInstance = hInstance;
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
|
||||
wc.lpszClassName = WND_CLASS;
|
||||
RegisterClassExW(&wc);
|
||||
|
||||
HWND hwnd = CreateWindowExW(0, WND_CLASS, L"OpenZen Loader",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 820, 560,
|
||||
nullptr, nullptr, hInstance, nullptr);
|
||||
if (!hwnd) return 1;
|
||||
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "loader.h"
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
struct Search {
|
||||
DWORD pid;
|
||||
std::wstring best;
|
||||
};
|
||||
|
||||
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) {
|
||||
Search* s = reinterpret_cast<Search*>(lp);
|
||||
|
||||
if (!IsWindowVisible(hwnd)) return TRUE;
|
||||
// Skip child windows / tool windows: we want main app windows.
|
||||
if (GetWindow(hwnd, GW_OWNER) != nullptr) return TRUE;
|
||||
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
if (pid != s->pid) return TRUE;
|
||||
|
||||
int len = GetWindowTextLengthW(hwnd);
|
||||
if (len <= 0) return TRUE;
|
||||
std::wstring title(len, L'\0');
|
||||
GetWindowTextW(hwnd, title.data(), len + 1);
|
||||
title.resize(len);
|
||||
|
||||
// Prefer the longest title - usually the main Minecraft window which
|
||||
// includes version/world name vs a tiny "Java" tooltip window.
|
||||
if (title.size() > s->best.size()) {
|
||||
s->best = std::move(title);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring window_title_for(DWORD pid) {
|
||||
Search s{pid, {}};
|
||||
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
|
||||
return s.best;
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -21,6 +21,8 @@ public final class PatchAgent {
|
||||
public static final String INSTRUMENTATION_KEY = "asm.patchify.instrumentation";
|
||||
private static final Logger LOGGER = LogManager.getLogger("PatchAgent");
|
||||
|
||||
private static volatile boolean transformerInstalled = false;
|
||||
|
||||
private PatchAgent() {
|
||||
}
|
||||
|
||||
@@ -53,7 +55,11 @@ public final class PatchAgent {
|
||||
* Install a transformer for the currently registered patches and retransform any patch target
|
||||
* that is already loaded. Called from mod code once {@link PatchRegistry} is populated.
|
||||
*/
|
||||
public static void installPatchesAndRetransform() {
|
||||
public static synchronized void installPatchesAndRetransform() {
|
||||
if (transformerInstalled) {
|
||||
LOGGER.info("Patches already installed; skipping duplicate retransform request");
|
||||
return;
|
||||
}
|
||||
Instrumentation inst = getInstrumentation();
|
||||
if (inst == null) {
|
||||
LOGGER.warn("PatchAgent not attached; cannot install patches");
|
||||
@@ -61,6 +67,7 @@ public final class PatchAgent {
|
||||
}
|
||||
PatchClassFileTransformer transformer = new PatchClassFileTransformer();
|
||||
inst.addTransformer(transformer, true);
|
||||
transformerInstalled = true;
|
||||
List<Class<?>> retransform = new ArrayList<>();
|
||||
for (Class<?> patch : PatchRegistry.getPatches()) {
|
||||
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
import shit.zen.asm.Bootstrap;
|
||||
import shit.zen.asm.ILocals;
|
||||
import shit.zen.asm.Invocation;
|
||||
import shit.zen.asm.InvocationImpl;
|
||||
@@ -71,10 +72,11 @@ public final class PatchTransformer {
|
||||
if (patchAnnotation == null) {
|
||||
throw new IllegalArgumentException(patchClass.getName() + " is not @Patch");
|
||||
}
|
||||
String patchTargetOwner = Type.getInternalName(patchAnnotation.value());
|
||||
|
||||
Map<MethodKey, List<Method>> handlersByTarget = new HashMap<>();
|
||||
for (Method handler : patchClass.getDeclaredMethods()) {
|
||||
collectHandler(patchClass, handler, handlersByTarget);
|
||||
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget);
|
||||
}
|
||||
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
||||
patchClass.getName(), target.name,
|
||||
@@ -121,7 +123,8 @@ public final class PatchTransformer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectHandler(Class<?> patchClass, Method handler,
|
||||
private static void collectHandler(Class<?> patchClass, String patchTargetOwner,
|
||||
Method handler,
|
||||
Map<MethodKey, List<Method>> handlersByTarget) {
|
||||
if (!(handler.isAnnotationPresent(Inject.class)
|
||||
|| handler.isAnnotationPresent(Overwrite.class)
|
||||
@@ -168,6 +171,11 @@ public final class PatchTransformer {
|
||||
throw new IllegalArgumentException("@ModifyLocals " + handler + " must take a single ILocals");
|
||||
}
|
||||
}
|
||||
// Patch annotations carry the mojmap method name (the jar was compiled
|
||||
// against mojmap, and reobfJar does not rewrite string literals). In a
|
||||
// production Forge environment the live class only has SRG names, so
|
||||
// remap before matching against ClassNode.methods.
|
||||
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
|
||||
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
|
||||
}
|
||||
|
||||
@@ -331,7 +339,7 @@ public final class PatchTransformer {
|
||||
Type returnType = Type.getReturnType(method.desc);
|
||||
String[] split = ASMHelpers.splitOwnerName(invokeName);
|
||||
String invokeOwner = split[0];
|
||||
String invokeMethod = split[1];
|
||||
String invokeMethod = Bootstrap.remapMethod(split[0], split[1], invokeDesc);
|
||||
|
||||
Slice slice = handler.getAnnotation(Inject.class).slice();
|
||||
// PatchApplier.applyInvokeInject uses strict owner+name+desc matching.
|
||||
@@ -462,7 +470,7 @@ public final class PatchTransformer {
|
||||
String targetDesc = wrap.targetDesc();
|
||||
String[] split = ASMHelpers.splitOwnerName(target);
|
||||
String targetOwner = split[0];
|
||||
String targetMethod = split[1];
|
||||
String targetMethod = Bootstrap.remapMethod(split[0], split[1], targetDesc);
|
||||
|
||||
// Match what shit.zen.asm.PatchApplier did: accept either owner or name match,
|
||||
// so targets pointing at the declaring class (e.g. Entity/getYRot) still hit
|
||||
@@ -607,10 +615,11 @@ public final class PatchTransformer {
|
||||
}
|
||||
} else {
|
||||
String[] split = ASMHelpers.splitOwnerName(at.method());
|
||||
String invokeMethod = Bootstrap.remapMethod(split[0], split[1], at.desc());
|
||||
for (AbstractInsnNode insn : method.instructions) {
|
||||
if (insn instanceof MethodInsnNode m
|
||||
&& m.owner.equals(split[0])
|
||||
&& m.name.equals(split[1])
|
||||
&& m.name.equals(invokeMethod)
|
||||
&& m.desc.equals(at.desc())) {
|
||||
insertBefore = insn;
|
||||
foundAnchor = true;
|
||||
@@ -714,16 +723,20 @@ public final class PatchTransformer {
|
||||
String[] startSplit = slice.start().method().isEmpty()
|
||||
? null : ASMHelpers.splitOwnerName(slice.start().method());
|
||||
String startDesc = slice.start().desc();
|
||||
String startName = startSplit == null ? null
|
||||
: Bootstrap.remapMethod(startSplit[0], startSplit[1], startDesc);
|
||||
String[] endSplit = slice.end().method().isEmpty()
|
||||
? null : ASMHelpers.splitOwnerName(slice.end().method());
|
||||
String endDesc = slice.end().desc();
|
||||
String endName = endSplit == null ? null
|
||||
: Bootstrap.remapMethod(endSplit[0], endSplit[1], endDesc);
|
||||
boolean foundStart = defaultStart;
|
||||
for (AbstractInsnNode insn : insns) {
|
||||
if (!foundStart && startSplit != null && insn instanceof MethodInsnNode m
|
||||
&& m.owner.equals(startSplit[0]) && m.name.equals(startSplit[1]) && m.desc.equals(startDesc)) {
|
||||
&& m.owner.equals(startSplit[0]) && m.name.equals(startName) && m.desc.equals(startDesc)) {
|
||||
foundStart = true;
|
||||
} else if (!defaultEnd && endSplit != null && insn instanceof MethodInsnNode m
|
||||
&& m.owner.equals(endSplit[0]) && m.name.equals(endSplit[1]) && m.desc.equals(endDesc)) {
|
||||
&& m.owner.equals(endSplit[0]) && m.name.equals(endName) && m.desc.equals(endDesc)) {
|
||||
break;
|
||||
}
|
||||
// PatchApplier appends every insn in the slice, not only filter matches.
|
||||
|
||||
@@ -43,6 +43,7 @@ import shit.zen.patch.MinecraftPatch;
|
||||
import shit.zen.patch.PacketUtilsPatch;
|
||||
import shit.zen.patch.PlayerPatch;
|
||||
import shit.zen.patch.PlayerTabOverlayPatch;
|
||||
import shit.zen.asm.Bootstrap;
|
||||
import shit.zen.utils.rotation.RotationHandler;
|
||||
|
||||
@Mod(value = "hey")
|
||||
@@ -78,6 +79,18 @@ public class ZenClient extends ClientBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit entry point used by the DLL injection path
|
||||
* ({@code shit.zen.dll.DllBootstrap}). Constructs the singleton if not
|
||||
* already present and returns it.
|
||||
*/
|
||||
public static synchronized ZenClient bootstrapForDll() {
|
||||
if (instance == null) {
|
||||
new ZenClient();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
try {
|
||||
username = System.getProperty("user.name", "Player");
|
||||
@@ -102,6 +115,7 @@ public class ZenClient extends ClientBase {
|
||||
this.eventBus.register(this);
|
||||
this.commandManager.initCommands();
|
||||
this.eventBus.register(new IntroAnimation());
|
||||
Bootstrap.init();
|
||||
registerPatches();
|
||||
if (PatchAgent.getInstrumentation() != null) {
|
||||
PatchAgent.installPatchesAndRetransform();
|
||||
@@ -152,7 +166,7 @@ public class ZenClient extends ClientBase {
|
||||
for (String name : CLOUD_ASSET_NAMES) {
|
||||
File outFile = new File(targetDir, name);
|
||||
if (outFile.exists()) continue;
|
||||
try (InputStream in = ZenClient.class.getResourceAsStream("/assets/zen/cloud_assets/" + name)) {
|
||||
try (InputStream in = openCloudAsset(name)) {
|
||||
if (in == null) {
|
||||
logger.warn("Cloud asset missing on classpath: {}", name);
|
||||
continue;
|
||||
@@ -166,7 +180,24 @@ public class ZenClient extends ClientBase {
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerPatches() {
|
||||
private static InputStream openCloudAsset(String name) {
|
||||
String classpath = "/assets/zen/cloud_assets/" + name;
|
||||
InputStream is = ZenClient.class.getResourceAsStream(classpath);
|
||||
if (is != null) return is;
|
||||
String dir = System.getProperty("openzen.resources");
|
||||
if (dir != null) {
|
||||
File f = new File(dir, "assets/zen/cloud_assets/" + name);
|
||||
if (f.isFile()) {
|
||||
try {
|
||||
return new java.io.FileInputStream(f);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void registerPatches() {
|
||||
PatchRegistry.register(MinecraftPatch.class);
|
||||
PatchRegistry.register(LocalPlayerPatch.class);
|
||||
PatchRegistry.register(LivingEntityPatch.class);
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
package shit.zen.dll;
|
||||
|
||||
import asm.patchify.loader.PatchAgent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import shit.zen.ZenClient;
|
||||
import shit.zen.asm.Bootstrap;
|
||||
|
||||
/**
|
||||
* Final hand-off step on the DLL injection path. By the time {@link #start} is
|
||||
* invoked, {@code GameLoaderBridge} has already re-defined every class in the
|
||||
* jar onto the Forge GameClassLoader, so {@link ZenClient} and all 21 patch
|
||||
* handler classes share Minecraft's class loader.
|
||||
*
|
||||
* <p>This entry point intentionally does <b>not</b> construct {@link ZenClient}.
|
||||
* Once {@link PatchAgent#installPatchesAndRetransform()} returns, the
|
||||
* retransformed {@code Minecraft.tick()} contains the injected
|
||||
* {@code MinecraftPatch.onTick} prologue that lazily constructs {@link
|
||||
* ZenClient} on the next tick. Letting the existing tick-driven lazy-init run
|
||||
* keeps the DLL path and the mod path identical from {@code ZenClient.init}
|
||||
* onwards.</p>
|
||||
*/
|
||||
public final class DllBootstrap {
|
||||
private static final Logger LOGGER = LogManager.getLogger("DllBootstrap");
|
||||
private static volatile boolean started = false;
|
||||
|
||||
private DllBootstrap() {
|
||||
}
|
||||
|
||||
public static synchronized void start(String extractedJarPath) {
|
||||
if (started) {
|
||||
LOGGER.info("DllBootstrap.start ignored (already started)");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
try {
|
||||
LOGGER.info("DllBootstrap.start jar={}", extractedJarPath);
|
||||
LOGGER.info("DllBootstrap loader = {}", DllBootstrap.class.getClassLoader());
|
||||
LOGGER.info("ZenClient loader = {}", ZenClient.class.getClassLoader());
|
||||
LOGGER.info("Minecraft loader = {}", Minecraft.class.getClassLoader());
|
||||
LOGGER.info("PatchAgent inst = {}", PatchAgent.getInstrumentation());
|
||||
|
||||
// Load mojmap → SRG mappings before we attempt any retransform.
|
||||
Bootstrap.init();
|
||||
|
||||
// Register the 21 patch classes and trigger retransform of the
|
||||
// already-loaded Minecraft targets. After this returns,
|
||||
// Minecraft.tick / LocalPlayer.tick / etc. carry the injected
|
||||
// prologues; the next tick will invoke MinecraftPatch.onTick which
|
||||
// performs the real ZenClient construction via its existing
|
||||
// lazy-init path.
|
||||
ZenClient.registerPatches();
|
||||
PatchAgent.installPatchesAndRetransform();
|
||||
|
||||
LOGGER.info("DllBootstrap done. ZenClient will be constructed on the next Minecraft.tick.");
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("DllBootstrap.start failed", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package shit.zen.dll;
|
||||
|
||||
import asm.patchify.loader.PatchAgent;
|
||||
import java.io.InputStream;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* One-shot bootstrap that is loaded by a throwaway {@code URLClassLoader}
|
||||
* (parent = Forge GameClassLoader) and re-defines every class in zen.jar onto
|
||||
* the GameClassLoader itself.
|
||||
*
|
||||
* <p>This is necessary because retransformed Minecraft classes contain
|
||||
* {@code INVOKESTATIC} references to our patch handlers (e.g.
|
||||
* {@code INVOKESTATIC shit/zen/patch/ConnectionPatch.transformReceive}).
|
||||
* When the JVM resolves those references it uses the Minecraft class's
|
||||
* defining loader (cpw.mods.cl.ModuleClassLoader). If our classes only live
|
||||
* in a sibling URLClassLoader, resolution fails with
|
||||
* {@code NoClassDefFoundError} / {@code VerifyError}.</p>
|
||||
*
|
||||
* <p>Steps:</p>
|
||||
* <ol>
|
||||
* <li>Use the live {@link Instrumentation} (already populated by
|
||||
* {@code instrument.dll}'s {@code Agent_OnAttach}) to {@code redefineModule}
|
||||
* java.base/java.lang open to this module, so reflective access to
|
||||
* {@code ClassLoader.defineClass} works without JVM args.</li>
|
||||
* <li>Walk the jar. {@code .class} entries get re-defined on the game
|
||||
* loader; everything else is extracted to a temp directory and exposed
|
||||
* via the {@code openzen.resources} system property so
|
||||
* {@code Bootstrap} and {@code ZenClient} can still find their
|
||||
* resources (mapping.srg, cloud assets, webui static files).</li>
|
||||
* <li>Hand off to {@link DllBootstrap#start(String)} loaded through the
|
||||
* game loader.</li>
|
||||
* </ol>
|
||||
*/
|
||||
public final class GameLoaderBridge {
|
||||
private static final Logger LOGGER = LogManager.getLogger("GameLoaderBridge");
|
||||
public static final String RESOURCES_PROP = "openzen.resources";
|
||||
|
||||
private GameLoaderBridge() {
|
||||
}
|
||||
|
||||
public static void load(String jarPath, ClassLoader gameLoader) throws Throwable {
|
||||
LOGGER.info("GameLoaderBridge.load jar={} gameLoader={}", jarPath, gameLoader);
|
||||
long t0 = System.nanoTime();
|
||||
|
||||
Instrumentation inst = PatchAgent.getInstrumentation();
|
||||
if (inst == null) {
|
||||
throw new IllegalStateException("PatchAgent.getInstrumentation() returned null - "
|
||||
+ "did instrument.dll Agent_OnAttach run?");
|
||||
}
|
||||
|
||||
openJavaBaseToSelf(inst);
|
||||
|
||||
Method defineClass = ClassLoader.class.getDeclaredMethod(
|
||||
"defineClass", String.class, byte[].class, int.class, int.class);
|
||||
defineClass.setAccessible(true);
|
||||
|
||||
Path resourceDir = Paths.get(System.getProperty("java.io.tmpdir"),
|
||||
"openzen-resources-" + ProcessHandle.current().pid());
|
||||
Files.createDirectories(resourceDir);
|
||||
|
||||
// Pass 1: read every entry. Defer .class defines until pass 2 so we can
|
||||
// retry in dependency order (a patch class extending a zen base class
|
||||
// can only be defined after its super has been defined).
|
||||
LinkedHashMap<String, byte[]> pendingClasses = new LinkedHashMap<>();
|
||||
int resourceCount = 0;
|
||||
|
||||
try (ZipFile zip = new ZipFile(jarPath)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (entry.isDirectory()) continue;
|
||||
String name = entry.getName();
|
||||
if (name.startsWith("META-INF/")) continue;
|
||||
|
||||
byte[] bytes;
|
||||
try (InputStream is = zip.getInputStream(entry)) {
|
||||
bytes = is.readAllBytes();
|
||||
}
|
||||
|
||||
if (name.endsWith(".class")) {
|
||||
String className = name.substring(0, name.length() - 6).replace('/', '.');
|
||||
// GameLoaderBridge itself is already live in this URLClassLoader.
|
||||
if (className.equals(GameLoaderBridge.class.getName())) continue;
|
||||
pendingClasses.put(className, bytes);
|
||||
} else {
|
||||
Path target = resourceDir.resolve(name);
|
||||
Path parent = target.getParent();
|
||||
if (parent != null) Files.createDirectories(parent);
|
||||
Files.write(target, bytes);
|
||||
resourceCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int classCount = definePassUntilFixedPoint(defineClass, gameLoader, pendingClasses);
|
||||
|
||||
if (!pendingClasses.isEmpty()) {
|
||||
LOGGER.warn("{} class(es) could not be defined on gameLoader after fixed-point retry:",
|
||||
pendingClasses.size());
|
||||
for (Map.Entry<String, byte[]> e : pendingClasses.entrySet()) {
|
||||
try {
|
||||
defineClass.invoke(gameLoader, e.getKey(), e.getValue(), 0, e.getValue().length);
|
||||
} catch (Throwable t) {
|
||||
Throwable cause = t.getCause() != null ? t.getCause() : t;
|
||||
LOGGER.warn(" {} -> {}", e.getKey(), cause.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.setProperty(RESOURCES_PROP, resourceDir.toAbsolutePath().toString());
|
||||
|
||||
long ms = (System.nanoTime() - t0) / 1_000_000L;
|
||||
LOGGER.info("Defined {} classes, extracted {} resources to {} ({} ms)",
|
||||
classCount, resourceCount, resourceDir, ms);
|
||||
|
||||
Class<?> bootstrapCls = Class.forName("shit.zen.dll.DllBootstrap", true, gameLoader);
|
||||
LOGGER.info("DllBootstrap loader (should be gameLoader): {}", bootstrapCls.getClassLoader());
|
||||
Method start = bootstrapCls.getMethod("start", String.class);
|
||||
start.invoke(null, jarPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines classes onto {@code gameLoader} in repeated passes. Each pass
|
||||
* retries everything that previously failed; this terminates either when
|
||||
* all entries are defined or when a full pass made no progress (genuinely
|
||||
* missing dependency). The remaining failures stay in {@code pending} for
|
||||
* the caller to log.
|
||||
*/
|
||||
private static int definePassUntilFixedPoint(Method defineClass,
|
||||
ClassLoader gameLoader,
|
||||
LinkedHashMap<String, byte[]> pending) {
|
||||
int total = 0;
|
||||
boolean progressed = true;
|
||||
while (progressed && !pending.isEmpty()) {
|
||||
progressed = false;
|
||||
Iterator<Map.Entry<String, byte[]>> it = pending.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, byte[]> entry = it.next();
|
||||
try {
|
||||
defineClass.invoke(gameLoader, entry.getKey(),
|
||||
entry.getValue(), 0, entry.getValue().length);
|
||||
it.remove();
|
||||
progressed = true;
|
||||
total++;
|
||||
} catch (Throwable t) {
|
||||
// Leave it in the pending map; another pass might succeed
|
||||
// once dependencies are defined. The non-recoverable
|
||||
// failures are logged by the caller.
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private static void openJavaBaseToSelf(Instrumentation inst) {
|
||||
Module javaBase = ClassLoader.class.getModule();
|
||||
Module here = GameLoaderBridge.class.getModule();
|
||||
try {
|
||||
inst.redefineModule(
|
||||
javaBase,
|
||||
Set.of(),
|
||||
Map.of(),
|
||||
Map.of("java.lang", Set.of(here)),
|
||||
Set.of(),
|
||||
Map.of());
|
||||
} catch (Throwable t) {
|
||||
LOGGER.warn("redefineModule for java.base/java.lang failed (may be already open): {}",
|
||||
t.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import shit.zen.utils.misc.Assets;
|
||||
|
||||
public class StaticFileHandler extends AbstractHttpHandler {
|
||||
private final String resourcePath;
|
||||
@@ -29,7 +30,7 @@ public class StaticFileHandler extends AbstractHttpHandler {
|
||||
relative += "index.html";
|
||||
}
|
||||
String classpath = this.resourcePath + "/" + relative;
|
||||
InputStream resource = StaticFileHandler.class.getResourceAsStream(classpath);
|
||||
InputStream resource = Assets.open(classpath);
|
||||
if (resource == null) {
|
||||
return 404;
|
||||
}
|
||||
@@ -42,7 +43,7 @@ public class StaticFileHandler extends AbstractHttpHandler {
|
||||
|
||||
@Override
|
||||
public void sendResponse(int status, OutputStream out, HttpExchange exchange) throws IOException {
|
||||
InputStream resource = StaticFileHandler.class.getResourceAsStream(this.resourcePath + "/" + status + ".html");
|
||||
InputStream resource = Assets.open(this.resourcePath + "/" + status + ".html");
|
||||
if (resource != null) {
|
||||
try (resource) {
|
||||
exchange.getResponseHeaders().add("Content-Type", "text/html; charset=utf-8");
|
||||
|
||||
@@ -34,6 +34,10 @@ public class MinecraftPatch {
|
||||
public static volatile boolean initialized = false;
|
||||
private static HitResult savedHitResult;
|
||||
|
||||
public static void markInitialized() {
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@Inject(method = "tick", desc = "()V")
|
||||
public static void onTick(Minecraft minecraft, CallbackInfo callbackInfo) throws Throwable {
|
||||
if (!initialized) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package shit.zen.render;
|
||||
import java.awt.Font;
|
||||
import java.io.InputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import shit.zen.utils.misc.Assets;
|
||||
|
||||
public class FontStore {
|
||||
public static CustomFont OPENSANS_16 = loadFont(16.0f, "opensans.ttf");
|
||||
@@ -25,7 +26,7 @@ public class FontStore {
|
||||
|
||||
@NotNull
|
||||
public static CustomFont loadFont(float size, String name) {
|
||||
try (InputStream stream = FontStore.class.getResourceAsStream("/assets/zen/fonts/" + name)) {
|
||||
try (InputStream stream = Assets.open("/assets/zen/fonts/" + name)) {
|
||||
if (stream == null) {
|
||||
throw new java.io.IOException("Font not found: " + name);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.awt.Font;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import shit.zen.utils.misc.Assets;
|
||||
|
||||
public final class Fonts {
|
||||
private static final Map<String, FontRenderer> fontRendererCache = new HashMap<>();
|
||||
@@ -39,7 +40,7 @@ public final class Fonts {
|
||||
}
|
||||
|
||||
private static Font loadAwtFont(String name) {
|
||||
try (InputStream stream = Fonts.class.getResourceAsStream("/assets/zen/fonts/" + name)) {
|
||||
try (InputStream stream = Assets.open("/assets/zen/fonts/" + name)) {
|
||||
if (stream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package shit.zen.utils.misc;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Unified resource opener for the OpenZen jar.
|
||||
*
|
||||
* <p>Resolution order:
|
||||
* <ol>
|
||||
* <li>{@code Assets.class.getResourceAsStream(classpath)} — works whenever
|
||||
* the jar is on a class loader's search path (Forge mod path, dev
|
||||
* {@code -javaagent}, etc.).</li>
|
||||
* <li>The directory pointed to by the {@code openzen.resources} system
|
||||
* property — populated by {@code GameLoaderBridge} on the DLL injection
|
||||
* path because the jar is not on any class loader URL search there
|
||||
* (classes are defined directly onto the game loader).</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Always returns {@code null} (not throwing) when the resource is missing,
|
||||
* matching the contract of {@link Class#getResourceAsStream(String)}.</p>
|
||||
*/
|
||||
public final class Assets {
|
||||
private Assets() {
|
||||
}
|
||||
|
||||
public static InputStream open(String classpath) {
|
||||
InputStream cp = Assets.class.getResourceAsStream(classpath);
|
||||
if (cp != null) return cp;
|
||||
String dir = System.getProperty("openzen.resources");
|
||||
if (dir != null) {
|
||||
String relative = classpath.startsWith("/") ? classpath.substring(1) : classpath;
|
||||
File f = new File(dir, relative);
|
||||
if (f.isFile()) {
|
||||
try {
|
||||
return new FileInputStream(f);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -195,11 +195,38 @@ public final class ReflectionUtil {
|
||||
}
|
||||
|
||||
public static String getMappedFieldName(Class<?> clazz, String fieldName) {
|
||||
return fieldName;
|
||||
return shit.zen.asm.Bootstrap.remapField(clazz.getName().replace('.', '/'), fieldName);
|
||||
}
|
||||
|
||||
public static String getMappedMethodName(Class<?> clazz, String methodName, String descriptor) {
|
||||
return methodName;
|
||||
return shit.zen.asm.Bootstrap.remapMethod(clazz.getName().replace('.', '/'), methodName, descriptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks the field up by trying the supplied (mojmap) name first, then the
|
||||
* SRG-remapped name. Walks the superclass chain because the field may be
|
||||
* declared on an ancestor (e.g. {@code activeEffects} lives on
|
||||
* {@code LivingEntity}, not on {@code LocalPlayer}).
|
||||
*/
|
||||
private static Field resolveField(Class<?> clazz, String name) throws NoSuchFieldException {
|
||||
NoSuchFieldException last = null;
|
||||
for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
|
||||
String srg = shit.zen.asm.Bootstrap.remapField(
|
||||
c.getName().replace('.', '/'), name);
|
||||
try {
|
||||
return c.getDeclaredField(srg);
|
||||
} catch (NoSuchFieldException e) {
|
||||
last = e;
|
||||
}
|
||||
if (!srg.equals(name)) {
|
||||
try {
|
||||
return c.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw last != null ? last : new NoSuchFieldException(name);
|
||||
}
|
||||
|
||||
private static long getFieldOffset(Class<?> clazz, String name) {
|
||||
@@ -207,7 +234,7 @@ public final class ReflectionUtil {
|
||||
long offset = offsetCache.getLong(key);
|
||||
if (offset != 0L) return offset;
|
||||
try {
|
||||
offset = unsafe.objectFieldOffset(clazz.getDeclaredField(name));
|
||||
offset = unsafe.objectFieldOffset(resolveField(clazz, name));
|
||||
offsetCache.put(key, offset);
|
||||
return offset;
|
||||
} catch (NoSuchFieldException e) {
|
||||
@@ -220,7 +247,7 @@ public final class ReflectionUtil {
|
||||
ObjectLongPair<Object> pair = staticFieldCache.get(key);
|
||||
if (pair != null) return pair;
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(name);
|
||||
Field field = resolveField(clazz, name);
|
||||
ObjectLongPair<Object> p = new ObjectLongImmutablePair<>(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field));
|
||||
staticFieldCache.put(key, p);
|
||||
return p;
|
||||
@@ -235,9 +262,9 @@ public final class ReflectionUtil {
|
||||
ObjectLongPair<Object> pair = getStaticFieldPair(clazz, name);
|
||||
return unsafe.getObject(pair.first(), pair.secondLong());
|
||||
}
|
||||
// Fall back to a VarHandle
|
||||
// Instance-field path: also walk up the superclass chain.
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(name);
|
||||
Field field = resolveField(object.getClass(), name);
|
||||
field.setAccessible(true);
|
||||
return field.get(object);
|
||||
} catch (Exception e) {
|
||||
|
||||
Reference in new issue
Block a user