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:
Shirona1337andClaude Opus 4.7 committed 2026-05-23 09:06:24 +08:00
1 parent f9068db44e
commit c31c6a873a
35 files changed
+1782 -24

No files matched your search

+4
View File
@@ -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
+64 -4
View File
@@ -14,10 +14,6 @@
## 截图
![Open Zen ClickGUI](img/screenshot.png)
## 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
View File
@@ -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'
}
+31
View File
@@ -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)
+45
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
#include "resource.h"
IDR_ZEN_JAR RCDATA "zen.jar"
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDR_ZEN_JAR 101
+138
View File
@@ -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
+69
View File
@@ -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
+57
View File
@@ -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
+102
View File
@@ -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
+107
View File
@@ -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;
}
+48
View File
@@ -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
+51
View File
@@ -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()
+3
View File
@@ -0,0 +1,3 @@
#include "resource.h"
IDR_OPENZEN_DLL RCDATA "OpenZen.dll"
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#define IDR_OPENZEN_DLL 201
+39
View File
@@ -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
+88
View File
@@ -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
+33
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
#include "loader.h"
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
return loader::run_ui(hInstance);
}
+69
View File
@@ -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
+259
View File
@@ -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
+43
View File
@@ -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.
+33 -2
View File
@@ -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) {
+2 -1
View File
@@ -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);
}
+2 -1
View File
@@ -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) {