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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user