Import OpenZen deobfuscated source
Build Loader / build (push) Canceled after 0s

这个提交包含在:
Administrator committed 2026-09-15 03:14:09 +08:00
当前提交 0da7d49fd6
465 file changed
+164690

No files matched your search

+49
查看文件
@@ -0,0 +1,49 @@
cmake_minimum_required(VERSION 3.20)
# CMP0091 NEW lets us drive the MSVC runtime via MSVC_RUNTIME_LIBRARY
# instead of patching CMAKE_CXX_FLAGS_*. CMake 3.15+ default, set
# explicitly so the build stays predictable across CMake versions.
cmake_policy(SET CMP0091 NEW)
project(OpenZenNative LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Statically link the MSVC C/C++ runtime into both the loader EXE and the
# injected DLL. This matters most for the DLL: our manual mapper resolves
# import addresses locally and assumes the imported DLL has the same base
# in both processes - true for kernel32/user32/ntdll (KnownDLLs) but NOT
# for vcruntime140/ucrtbase, so the mapped DLL would otherwise call CRT
# functions through wild pointers and crash the target Java process. /MT
# eliminates those imports entirely.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
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)
# /MP enables cl.exe-level multi-process compilation: every translation
# unit inside a single vcxproj compiles in parallel. Without it, MSBuild
# only parallelises *across* projects (the -j N forwarded by
# `cmake --build --parallel` controls that), so a single project with
# a dozen .cpp files would still compile serially.
add_compile_options(/W3 /permissive- /utf-8 /EHsc /MP)
endif()
add_subdirectory(dll)
add_subdirectory(loader)
+45
查看文件
@@ -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
查看文件
@@ -0,0 +1,3 @@
#include "resource.h"
IDR_ZEN_JAR RCDATA "zen.jar"
+3
查看文件
@@ -0,0 +1,3 @@
#pragma once
#define IDR_ZEN_JAR 101
+177
查看文件
@@ -0,0 +1,177 @@
#include "openzen.h"
#include "generated_names.h" // OZ_BRIDGE_FQCN — generated by build.gradle ext.obfuscateJar
#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);
// Anchor classes that are loaded early in every supported MC launch and
// whose JVM internal name is stable across runtimes:
// * net/minecraft/client/Minecraft - mojmap (ForgeGradle dev, modern
// Forge obf->official remap)
// * net/minecraft/client/class_310 - MCP/srg intermediate names
// * net/minecraft/client/ClientBrandRetriever and
// net/minecraft/client/main/Main - Mojang leaves these un-obfuscated
// even in the obfuscated client jar; Main
// is the spawn entry point.
// Any of them is loaded by the same class loader that owns the game classes
// (Forge GameClassLoader in production, the dev class loader in runClient).
static const char* const kNeedles[] = {
"Lnet/minecraft/client/Minecraft;",
"Lnet/minecraft/client/class_310;",
"Lnet/minecraft/client/ClientBrandRetriever;",
"Lnet/minecraft/client/main/Main;",
};
jobject game_loader = nullptr;
const char* matched = nullptr;
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) {
for (const char* needle : kNeedles) {
if (std::strcmp(sig, needle) == 0) {
matched = needle;
game_loader = class_loader_of(env, classes[i]);
break;
}
}
jvmti->Deallocate((unsigned char*)sig);
}
}
jvmti->Deallocate((unsigned char*)classes);
if (!game_loader) {
// Give the loader EXE something actionable to show the user.
std::string tried;
for (const char* needle : kNeedles) {
if (!tried.empty()) tried += ", ";
tried += needle;
}
log::error("No game class loader found; tried: %s "
"(the target process does not look like an MC/Forge JVM)", tried.c_str());
return nullptr;
}
log::info("Matched game class loader via %s", matched);
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(OZ_BRIDGE_FQCN) (the build-time-obfuscated 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.
//
// The build renames every OpenZen class to an opaque generated name (see
// build.gradle ext.obfuscateJar). It also emits generated_names.h with the
// bridge's new FQCN as OZ_BRIDGE_FQCN, so this stays in lockstep with the
// embedded jar without any hard-coded class name. The bridge's
// load(String, ClassLoader) method name is preserved by the rename, so the
// GetStaticMethodID(bridge_cls, "load", ...) lookup in main.cpp still works.
jclass classLoaderCls = env->FindClass("java/lang/ClassLoader");
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;");
jstring name = env->NewStringUTF(OZ_BRIDGE_FQCN);
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
+93
查看文件
@@ -0,0 +1,93 @@
#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;
// Per-bootstrap log file: %TEMP%\openzen-<pid>-<ticks>.log
//
// The old shared %TEMP%\openzen.log is unusable: the FIRST bootstrap keeps
// its CREATE_ALWAYS handle open for the life of the process, so every later
// injection (other processes, or a re-injection into the same JVM) hits
// ERROR_SHARING_VIOLATION and logs nothing — failures become invisible.
// Unique names make every injection independently diagnosable; stale files
// from earlier attempts are removed best-effort (unremovable ones simply
// stay until the owning process exits).
const DWORD pid = GetCurrentProcessId();
wchar_t base[MAX_PATH];
std::swprintf(base, MAX_PATH, L"%sopenzen-%lu-", tmp, pid);
WIN32_FIND_DATAW fd;
HANDLE find = FindFirstFileW((std::wstring(base) + L"*.log").c_str(), &fd);
if (find != INVALID_HANDLE_VALUE) {
do {
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
DeleteFileW((std::wstring(base) + fd.cFileName).c_str());
} while (FindNextFileW(find, &fd));
FindClose(find);
}
wchar_t path[MAX_PATH];
std::swprintf(path, MAX_PATH, L"%sopenzen-%lu-%llu.log", tmp, pid,
(unsigned long long)GetTickCount64());
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
+125
查看文件
@@ -0,0 +1,125 @@
#include "openzen.h"
#include "resource.h"
namespace openzen::jar {
namespace {
// Walk the PE resource directory tree of an in-memory image to find an
// RT_RCDATA entry by integer ID, without using FindResource / LoadResource.
// We avoid the Win32 resource APIs here because the DLL may have been
// manual-mapped: it is not in the loader's module list, so FindResource's
// internal LdrFindResource_U call against `gSelfModule` cannot find a
// matching LDR_DATA_TABLE_ENTRY and bails out.
const void* find_rcdata(HMODULE module_base, WORD id, DWORD& out_size) {
out_size = 0;
auto base = reinterpret_cast<BYTE*>(module_base);
auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr;
auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return nullptr;
const auto& res_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
if (res_dir.Size == 0) return nullptr;
BYTE* root_base = base + res_dir.VirtualAddress;
auto find_id_child = [&](PIMAGE_RESOURCE_DIRECTORY dir, WORD wanted_id)
-> PIMAGE_RESOURCE_DIRECTORY_ENTRY {
auto entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(dir + 1);
WORD total = dir->NumberOfNamedEntries + dir->NumberOfIdEntries;
// ID entries follow the named entries.
for (WORD i = dir->NumberOfNamedEntries; i < total; ++i) {
auto e = entry + i;
if (!e->NameIsString && e->Id == wanted_id) return e;
}
return nullptr;
};
// Level 1: resource type (RT_RCDATA = 10).
auto root = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(root_base);
auto type_entry = find_id_child(root, 10);
if (!type_entry || !type_entry->DataIsDirectory) return nullptr;
// Level 2: resource name (our integer id).
auto name_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
root_base + type_entry->OffsetToDirectory);
auto name_entry = find_id_child(name_dir, id);
if (!name_entry || !name_entry->DataIsDirectory) return nullptr;
// Level 3: language. Take the first available.
auto lang_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
root_base + name_entry->OffsetToDirectory);
WORD lang_count = lang_dir->NumberOfNamedEntries + lang_dir->NumberOfIdEntries;
if (lang_count == 0) return nullptr;
auto lang_entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(lang_dir + 1);
auto data_entry = reinterpret_cast<PIMAGE_RESOURCE_DATA_ENTRY>(
root_base + lang_entry->OffsetToData);
out_size = data_entry->Size;
return base + data_entry->OffsetToData;
}
} // namespace
bool extract_embedded(std::wstring& out_path) {
DWORD size = 0;
const void* data = find_rcdata(g_self_module, IDR_ZEN_JAR, size);
if (!data || size == 0) {
log::error("PE resource lookup for IDR_ZEN_JAR (RT_RCDATA) failed");
return false;
}
wchar_t tmp[MAX_PATH];
if (GetTempPathW(MAX_PATH, tmp) == 0) {
log::error("GetTempPath failed: %lu", GetLastError());
return false;
}
// Unique-per-attempt name: openzen-<pid>-<ticks>.jar
//
// A fixed openzen-<pid>.jar name breaks RE-injection into the same JVM:
// once the JVM has consumed the jar (Java-side agent + the URLClassLoader
// that loads GameLoaderBridge keep it open for the process lifetime), a
// later CreateFileW(..., CREATE_ALWAYS) on the same path fails with
// ERROR_SHARING_VIOLATION (32) and the whole bootstrap dies at
// extraction. Unique names make every injection independent. Stale
// artifacts from earlier attempts are removed best-effort.
const DWORD pid = GetCurrentProcessId();
wchar_t base[MAX_PATH];
std::swprintf(base, MAX_PATH, L"%sopenzen-%lu-", tmp, pid);
WIN32_FIND_DATAW fd;
HANDLE find = FindFirstFileW((std::wstring(base) + L"*.jar").c_str(), &fd);
if (find != INVALID_HANDLE_VALUE) {
do {
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue;
DeleteFileW((std::wstring(base) + fd.cFileName).c_str());
} while (FindNextFileW(find, &fd));
FindClose(find);
}
wchar_t path[MAX_PATH];
std::swprintf(path, MAX_PATH, L"%sopenzen-%lu-%llu.jar", tmp, pid,
(unsigned long long)GetTickCount64());
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
查看文件
@@ -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
+131
查看文件
@@ -0,0 +1,131 @@
#include "openzen.h"
#include <atomic>
extern "C" volatile LONG OpenZenBootstrapResult = -1;
namespace openzen {
HMODULE g_self_module = nullptr;
}
namespace {
std::atomic<bool> g_already_attached{false};
void finish(LONG code) {
OpenZenBootstrapResult = code;
}
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) {
finish(1);
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");
finish(2);
return 2;
}
log::info("Attached bootstrap thread to JavaVM");
std::wstring jar_path;
if (!jar::extract_embedded(jar_path)) {
vm->DetachCurrentThread();
finish(3);
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();
finish(4);
return 4;
}
jclass bridge_cls = classes::load_dll_bootstrap(env, game_loader, jar_path);
if (!bridge_cls) {
env->DeleteLocalRef(game_loader);
vm->DetachCurrentThread();
finish(5);
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();
finish(6);
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();
env->DeleteLocalRef(jar_jstr);
env->DeleteLocalRef(bridge_cls);
env->DeleteLocalRef(game_loader);
vm->DetachCurrentThread();
finish(7);
return 7;
}
log::info("GameLoaderBridge.load returned without exception");
env->DeleteLocalRef(jar_jstr);
env->DeleteLocalRef(bridge_cls);
env->DeleteLocalRef(game_loader);
vm->DetachCurrentThread();
finish(0);
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) {
finish(8);
} else {
CloseHandle(t);
}
}
return TRUE;
}
+58
查看文件
@@ -0,0 +1,58 @@
#pragma once
#include <windows.h>
#include <jni.h>
#include <jvmti.h>
#include <string>
// Bootstrap verdict of the injected DLL, exported so the manual mapper in the
// loader EXE can poll it through the export table and report what actually
// happened instead of pretending DllMain == success.
//
// -1 still running / never started
// 0 Java bootstrap finished (patches retransformed; client comes up on the
// next Minecraft tick)
// 1+ step-specific failure code (see loader/src/manual_map.cpp)
extern "C" __declspec(dllexport) volatile LONG OpenZenBootstrapResult;
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
+82
查看文件
@@ -0,0 +1,82 @@
# OpenZenLoader — the GUI injector.
#
# Pure Win32 + GDI+: no third-party UI toolkit, no vcpkg. Everything links
# against libraries that ship with the Windows SDK, so a fresh checkout
# configures and builds in seconds (the slow part is compiling the injected
# DLL, not this target).
# 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/cli.h
src/cli.cpp
src/wgfx.h
src/wgfx.cpp
src/splash_win.h
src/splash_win.cpp
src/main_win.h
src/main_win.cpp
src/overlay_win.h
src/overlay_win.cpp
src/process_list.cpp
src/injector.cpp
src/manual_map.cpp
src/embedded_dll.cpp
src/window_title.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
gdiplus # owner-drawn UI rendering
dwmapi # DwmSetWindowAttribute for Win11 rounded corners
psapi
shlwapi
shell32 # CommandLineToArgvW for the --nogui CLI mode
user32
gdi32
)
# Build-time git revision passed in by Gradle (gradle.properties /
# CI). Stamped into the window title so a built loader can be
# traced back to its source commit.
if(DEFINED OPENZEN_BUILD_REVISION AND NOT OPENZEN_BUILD_REVISION STREQUAL "")
target_compile_definitions(OpenZenLoader PRIVATE
OPENZEN_BUILD_REVISION="${OPENZEN_BUILD_REVISION}")
endif()
set_target_properties(OpenZenLoader PROPERTIES
OUTPUT_NAME "OpenZenLoader"
)
if(MSVC)
# requireAdministrator: lets the loader OpenProcess / VirtualAllocEx /
# CreateRemoteThread against javaw.exe even when the launcher was
# itself started elevated. UAC consent prompts once on launch.
set_target_properties(OpenZenLoader PROPERTIES
LINK_FLAGS "/SUBSYSTEM:WINDOWS /MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\""
)
endif()
+3
查看文件
@@ -0,0 +1,3 @@
#include "resource.h"
IDR_OPENZEN_DLL RCDATA "OpenZen.dll"
+3
查看文件
@@ -0,0 +1,3 @@
#pragma once
#define IDR_OPENZEN_DLL 201
+140
查看文件
@@ -0,0 +1,140 @@
#include "cli.h"
#include "loader.h"
#include <windows.h>
#include <shellapi.h>
#include <string>
#include <vector>
namespace cli {
namespace {
// A /SUBSYSTEM:WINDOWS binary has no console of its own. When launched
// from a terminal we attach to the parent's console so WriteConsoleW
// output lands back in the caller's window. Best-effort: when the parent
// has no console (double-click launch) output is simply dropped and the
// exit code carries the result.
void AttachParentConsole() {
if (!AttachConsole(ATTACH_PARENT_PROCESS)) return;
HANDLE conout = CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (conout == INVALID_HANDLE_VALUE) return;
SetStdHandle(STD_OUTPUT_HANDLE, conout);
SetStdHandle(STD_ERROR_HANDLE, conout);
}
void Write(const std::wstring& text) {
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
if (!out || out == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteConsoleW(out, text.c_str(), static_cast<DWORD>(text.size()),
&written, nullptr);
}
bool ParsePid(const std::wstring& text, unsigned long& out) {
if (text.empty() ||
text.find_first_not_of(L"0123456789") != std::wstring::npos) {
return false;
}
out = wcstoul(text.c_str(), nullptr, 10);
return true;
}
const wchar_t* kUsage =
L"OpenZenLoader - command line mode\n"
L"\n"
L"Usage:\n"
L" OpenZenLoader.exe <JavaPID> --nogui\n"
L" Inject into the running Java process without showing the GUI.\n"
L" OpenZenLoader.exe --help\n"
L" Show this help.\n"
L"\n"
L"Exit codes: 0 = injected, 1 = injection failed, 2 = bad arguments\n";
} // namespace
int RunHeadless() {
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!argv) return -1;
bool headless = false;
bool help = false;
bool bad = false;
bool havePid = false;
unsigned long pid = 0;
std::wstring badArg;
for (int i = 1; i < argc; ++i) {
const std::wstring arg = argv[i];
if (arg == L"--nogui" || arg == L"/nogui") {
headless = true;
} else if (arg == L"--help" || arg == L"-h" || arg == L"/?") {
help = true;
} else if (unsigned long value = 0; ParsePid(arg, value) && !havePid) {
pid = value;
havePid = true;
} else {
bad = true;
badArg = arg;
}
}
LocalFree(argv);
// No CLI request at all -> run the GUI as before.
if (!headless && !help && !bad) return -1;
if (help && !bad) {
AttachParentConsole();
Write(kUsage);
return 0;
}
AttachParentConsole();
if (bad) {
Write(L"Unknown argument: " + badArg + L"\n\n" + kUsage);
return 2;
}
if (help) {
Write(kUsage);
return 0;
}
if (!havePid) {
Write(std::wstring(L"--nogui requires the PID of a running Java "
L"process.\n\n") + kUsage);
return 2;
}
// Same target filter the GUI list uses: only javaw.exe / java.exe.
bool found = false;
std::wstring title;
for (const loader::JavaProcess& p : loader::list_java_processes()) {
if (p.pid == pid) {
found = true;
title = p.window_title.empty() ? p.image_name : p.window_title;
break;
}
}
if (!found) {
Write(L"Injection failed: PID " + std::to_wstring(pid) +
L" is not a running Java (javaw.exe/java.exe) process\n");
return 1;
}
Write(L"Injecting into PID " + std::to_wstring(pid) + L" (" + title +
L")...\n");
const std::wstring err = loader::inject(pid);
if (err.empty()) {
Write(L"Injection complete\n");
return 0;
}
Write(L"Injection failed: " + err + L"\n");
return 1;
}
} // namespace cli
+12
查看文件
@@ -0,0 +1,12 @@
#pragma once
namespace cli {
// Runs the loader headlessly when the command line asks for it
// ("OpenZenLoader.exe <JavaPID> --nogui" or "--help"). Attaches to the
// parent console for output and returns the process exit code to use.
// Returns -1 when no CLI mode was requested, in which case the normal
// GUI path should run.
int RunHeadless();
} // namespace cli
+21
查看文件
@@ -0,0 +1,21 @@
#include "loader.h"
#include "resource.h"
namespace loader {
bool get_embedded_dll(const void*& out_data, size_t& out_size) {
HMODULE self = GetModuleHandleW(nullptr);
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
if (!info) return false;
DWORD size = SizeofResource(self, info);
if (size == 0) return false;
HGLOBAL loaded = LoadResource(self, info);
if (!loaded) return false;
void* data = LockResource(loaded);
if (!data) return false;
out_data = data;
out_size = size;
return true;
}
} // namespace loader
+15
查看文件
@@ -0,0 +1,15 @@
#include "loader.h"
#include "manual_map.h"
namespace loader {
std::wstring inject(DWORD pid) {
const void* dll_data = nullptr;
size_t dll_size = 0;
if (!get_embedded_dll(dll_data, dll_size)) {
return L"Embedded OpenZen.dll resource not found in loader EXE";
}
return inject_in_memory(pid, dll_data, dll_size);
}
} // namespace loader
+40
查看文件
@@ -0,0 +1,40 @@
#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;
std::wstring window_class;
};
struct WindowInfo {
std::wstring title;
std::wstring class_name;
};
// Enumerate processes whose image is javaw.exe / java.exe.
std::vector<JavaProcess> list_java_processes();
// Map the embedded OpenZen.dll directly into the target process and run its
// DllMain via shellcode. The DLL bytes never touch disk. Returns an empty
// string on success or a human-readable error message.
std::wstring inject(DWORD pid);
// Return a pointer into the loader EXE's resource section that holds the
// embedded OpenZen.dll along with its byte size. The pointer remains valid
// for the lifetime of the loader process.
bool get_embedded_dll(const void*& out_data, size_t& out_size);
// Walk top-level windows and return the title + class name of the most
// informative window belonging to the given pid (longest title wins).
// Returns empty strings if none found.
WindowInfo window_info_for(DWORD pid);
} // namespace loader
+242
查看文件
@@ -0,0 +1,242 @@
//
// main.cpp — Win32 entry point and window orchestration.
//
// Two modes:
//
// GUI (no arguments): splash plays its ~1 s intro, the main window fades
// in, and clicking a row's Inject opens the progress overlay which runs
// the injection on a worker thread; on completion the loader fades out.
//
// Headless (command line): inject without any UI and exit. Progress is
// printed to the parent console when present (exit code 0 = bootstrap
// complete); failures pop a message box. Useful for launcher integrations
// and scripted starts:
// OpenZenLoader.exe 34028 --nogui inject into PID 34028, no UI
// OpenZenLoader.exe 34028 same (--nogui implied by the pid)
// OpenZenLoader.exe --nogui inject into every detected
// Minecraft instance
// OpenZenLoader.exe --help print usage, exit 0
//
// The GUI itself is pure Win32 + GDI+ (no Qt): the whole UI lives in wgfx /
// splash / main / overlay and links only system libraries.
//
#include "loader.h"
#include "main_win.h"
#include "overlay_win.h"
#include "splash_win.h"
#include "wgfx.h"
#include <cstdlib>
#include <gdiplus.h>
#include <vector>
namespace {
const wchar_t* kUsage =
L"OpenZenLoader - command line mode\n"
L"\n"
L"Usage:\n"
L" OpenZenLoader.exe <JavaPID> [--nogui]\n"
L" Inject OpenZen into the running Java process (must be a\n"
L" javaw.exe/java.exe process). Prints progress; exit code 0 = ok.\n"
L" OpenZenLoader.exe --nogui\n"
L" Inject into every detected Minecraft instance.\n"
L" OpenZenLoader.exe --help\n"
L" Show this help (exit code 0).\n"
L"\n"
L"Exit codes: 0 = success, 1 = injection failed, 2 = bad arguments\n";
// A /SUBSYSTEM:WINDOWS binary has no console of its own. When launched from a
// terminal we attach to the parent's console so WriteConsoleW output lands
// back in the caller's window. Best-effort: when the parent has no console
// (double-click launch) output is simply dropped and the exit code carries
// the result.
bool AttachParentConsole() {
if (!AttachConsole(ATTACH_PARENT_PROCESS)) return false;
HANDLE conout = CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (conout == INVALID_HANDLE_VALUE) return false;
SetStdHandle(STD_OUTPUT_HANDLE, conout);
SetStdHandle(STD_ERROR_HANDLE, conout);
return true;
}
void Write(const std::wstring& text) {
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
if (!out || out == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteConsoleW(out, text.c_str(), static_cast<DWORD>(text.size()),
&written, nullptr);
}
// Headless: inject one pid, report via exit code (+ message box on error).
int RunHeadless(unsigned long pid) {
Write(L"Injecting into PID " + std::to_wstring(pid) + L"...\n");
std::wstring err = loader::inject(pid);
if (err.empty()) {
Write(L"Injection complete: OpenZen bootstrap finished; "
L"the client should appear on the next tick.\n");
ui::LogLine(L"headless inject ok, pid=" + std::to_wstring(pid));
return 0;
}
Write(L"Injection failed: " + err + L"\n");
ui::LogLine(L"headless inject FAILED, pid=" + std::to_wstring(pid) +
L": " + err);
MessageBoxW(nullptr,
(L"Injection failed (PID " + std::to_wstring(pid) + L"):\n" +
err)
.c_str(),
L"OpenZen Loader", MB_OK | MB_ICONERROR);
return 1;
}
// Headless: inject into every Minecraft instance we can find.
int RunHeadlessAll() {
auto procs = loader::list_java_processes();
std::vector<unsigned long> targets;
std::vector<std::wstring> labels;
for (const auto& jp : procs) {
if (!ui::LooksLikeMinecraft(jp)) continue;
targets.push_back(jp.pid);
labels.push_back(jp.window_title.empty() ? jp.window_class
: jp.window_title);
}
if (targets.empty()) {
ui::LogLine(L"headless inject: no Minecraft instance detected");
Write(L"No Minecraft instances detected.\n");
MessageBoxW(nullptr, L"No Minecraft instances detected.",
L"OpenZen Loader", MB_OK | MB_ICONWARNING);
return 1;
}
int failed = 0;
for (size_t i = 0; i < targets.size(); ++i) {
std::wstring err = loader::inject(targets[i]);
if (err.empty()) {
Write(L"Injected " + labels[i] + L" (PID " +
std::to_wstring(targets[i]) + L"): bootstrap complete.\n");
ui::LogLine(L"headless inject ok, pid=" +
std::to_wstring(targets[i]));
} else {
++failed;
Write(L"Inject FAILED for " + labels[i] + L" (PID " +
std::to_wstring(targets[i]) + L"): " + err + L"\n");
ui::LogLine(L"headless inject FAILED, pid=" +
std::to_wstring(targets[i]) + L": " + err);
MessageBoxW(nullptr,
(L"Injection failed (" + labels[i] + L", PID " +
std::to_wstring(targets[i]) + L"):\n" + err)
.c_str(),
L"OpenZen Loader", MB_OK | MB_ICONERROR);
}
}
return failed == 0 ? 0 : 1;
}
} // namespace
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) {
unsigned long cliPid = 0;
bool nogui = false;
bool help = false;
bool bad = false;
std::wstring badArg;
for (int i = 1; i < __argc; ++i) {
std::wstring a = __wargv[i];
if (_wcsicmp(a.c_str(), L"--nogui") == 0 ||
_wcsicmp(a.c_str(), L"/nogui") == 0) {
nogui = true;
} else if (_wcsicmp(a.c_str(), L"--help") == 0 ||
_wcsicmp(a.c_str(), L"-h") == 0 ||
_wcsicmp(a.c_str(), L"/?") == 0) {
help = true;
} else {
wchar_t* end = nullptr;
unsigned long v = wcstoul(a.c_str(), &end, 10);
if (end && *end == L'\0' && v != 0) {
cliPid = v;
} else {
bad = true;
badArg = a;
}
}
}
bool haveCli = help || bad || cliPid != 0 || nogui;
if (haveCli) {
// Don't run the message loop below: cli.cpp holds the console-attach
// helpers too, but the GUI stays out of the headless path entirely.
AttachParentConsole();
if (help && !bad) {
Write(kUsage);
return 0;
}
if (bad) {
Write(L"Unknown argument: " + badArg + L"\n" + kUsage);
return 2;
}
if (cliPid != 0) {
// Only ever inject into a Java process: a typo'd PID must not end
// up mapping our DLL into notepad.exe.
bool isJava = false;
std::wstring image;
for (const auto& p : loader::list_java_processes()) {
if (p.pid == cliPid) {
isJava = true;
image = p.image_name;
break;
}
}
if (!isJava) {
Write(L"Injection failed: PID " + std::to_wstring(cliPid) +
L" is not a running Java (javaw.exe/java.exe) process\n");
return 1;
}
return RunHeadless(cliPid);
}
// --nogui without a PID: every Minecraft instance it can find.
return RunHeadlessAll();
}
ui::InitDpi();
ui::LogLine(L"--- loader start (gui), scale=" +
std::to_wstring(ui::g_scale));
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken = 0;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
int rc = 0;
{
ui::SplashWindow splash;
ui::MainWindow main;
// Splash done -> reveal the main window with its entrance animation.
splash.onFinished = [&main] { main.PlayEntrance(); };
// Row Inject click -> show the progress overlay and run the real
// injection on the overlay's worker thread.
std::unique_ptr<ui::OverlayWindow> overlay;
main.onInjectRequested = [&](unsigned long pid,
const std::wstring& title) {
if (main.IsInjecting()) return;
main.SetInjectionInFlight(true);
overlay = std::make_unique<ui::OverlayWindow>();
overlay->onCompleted = [&main](bool) { main.PlayExit(); };
overlay->Show(main.hwnd(), pid, title);
};
// Main window gone -> end the process.
main.onClosed = [] { PostQuitMessage(0); };
splash.Show();
MSG msg{};
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
rc = static_cast<int>(msg.wParam);
}
Gdiplus::GdiplusShutdown(gdiplusToken);
return rc;
}
+841
查看文件
@@ -0,0 +1,841 @@
#include "main_win.h"
#include "loader.h"
#include <algorithm>
#include <cmath>
#include <windowsx.h>
#include <dwmapi.h>
namespace ui {
bool LooksLikeMinecraft(const loader::JavaProcess& jp) {
const std::wstring& t = jp.window_title;
if (t.size() >= 9 && _wcsnicmp(t.c_str(), L"Minecraft", 9) == 0) return true;
return _wcsicmp(jp.window_class.c_str(), L"GLFW30") == 0;
}
namespace {
constexpr int kCornerRadius = 12;
constexpr int kBaseWidth = 760;
constexpr int kBaseHeight = 500;
constexpr int kSizeJitter = 10; // +/- px random size jitter per launch
constexpr float kTitleBarH = 38.0f;
constexpr float kRowH = 56.0f;
constexpr float kBodyMarginX = 18.0f;
constexpr float kBodyMarginY = 14.0f;
constexpr float kBodySpacing = 10.0f;
constexpr int kTimerAnim = 1; // 30 fps animation clock
constexpr int kTimerPoll = 2; // instance list refresh
constexpr double kEntranceFade = 0.52;
constexpr double kEntranceSlide = 0.56;
constexpr double kEntranceDy = 18.0;
constexpr double kExitFade = 0.28;
constexpr double kRowEntrance = 0.28;
constexpr double kRowHover = 0.14;
constexpr double kPulsePeriod = 1.8;
float Scl(float v) { return static_cast<float>(v * g_scale); }
// Fills `title`/`class` for the Minecraft filter; returns false to skip.
bool MinecraftFilter(const loader::JavaProcess& jp, std::wstring* outTitle) {
if (!LooksLikeMinecraft(jp)) return false;
if (!jp.window_title.empty()) {
*outTitle = jp.window_title;
} else {
*outTitle = L"(starting up — " + jp.window_class + L")";
}
return true;
}
} // namespace
// ---- creation / lifecycle ----
void MainWindow::Create() {
baseW_ = static_cast<int>(kBaseWidth * g_scale);
baseH_ = static_cast<int>(kBaseHeight * g_scale);
WNDCLASSW wc{};
wc.lpfnWndProc = &MainWindow::Thunk;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"OZLoaderMain";
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
RegisterClassW(&wc);
std::wstring osTitle = RandomIdent(8, 16);
hwnd_ = CreateWindowExW(WS_EX_LAYERED | WS_EX_APPWINDOW, wc.lpszClassName,
osTitle.c_str(),
WS_POPUP | WS_SYSMENU | WS_MINIMIZEBOX,
0, 0, baseW_, baseH_, nullptr, nullptr,
wc.hInstance, this);
w_ = baseW_;
h_ = baseH_;
RECT wa{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0);
baseX_ = (wa.left + wa.right - w_) / 2;
baseY_ = (wa.top + wa.bottom - h_) / 2;
SetWindowPos(hwnd_, nullptr, baseX_, baseY_, 0, 0,
SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE);
// Windows 11 rounded corners; harmless no-op on Windows 10.
constexpr int DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL = 33;
constexpr int DWMWCP_ROUND_LOCAL = 2;
int pref = DWMWCP_ROUND_LOCAL;
DwmSetWindowAttribute(hwnd_, DWMWA_WINDOW_CORNER_PREFERENCE_LOCAL, &pref,
sizeof(pref));
status_ = L"Watching for Minecraft processes…";
Refresh();
SetTimer(hwnd_, kTimerAnim, 33, nullptr);
SetTimer(hwnd_, kTimerPoll, 1000, nullptr);
}
void MainWindow::PlayEntrance() {
Create();
ShowWindow(hwnd_, SW_SHOW);
BringWindowToTop(hwnd_);
SetForegroundWindow(hwnd_);
tEntrance_ = Now();
Tick();
}
void MainWindow::PlayExit() {
if (closing_) return;
closing_ = true;
KillTimer(hwnd_, kTimerPoll);
if (injecting_) {
// Injection in flight: tear down at once, no graceful fade (the
// Qt version effectively aborted too when quit raced the worker).
DestroyWindow(hwnd_);
return;
}
tExit_ = Now();
}
void MainWindow::Tick() {
if (!hwnd_ || w_ < 1) return;
Render();
BYTE alpha = 255;
if (tExit_ >= 0.0) {
double k = Since(tExit_, kExitFade);
alpha = static_cast<BYTE>((1.0 - EaseInCubic(k)) * 255.0);
if (k >= 1.0) {
DestroyWindow(hwnd_);
return;
}
} else if (tEntrance_ >= 0.0 && !entranceDone_) {
double k = Since(tEntrance_, kEntranceFade);
alpha = static_cast<BYTE>(EaseOutCubic(k) * 255.0);
double ks = Since(tEntrance_, kEntranceSlide);
int dy = static_cast<int>((1.0 - EaseOutCubic(ks)) * kEntranceDy * g_scale);
SetWindowPos(hwnd_, nullptr, baseX_, baseY_ + dy, 0, 0,
SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE);
if (ks >= 1.0 && k >= 1.0) entranceDone_ = true;
}
canvas_.Present(hwnd_, alpha);
}
// ---- data ----
void MainWindow::Refresh() {
auto procs = loader::list_java_processes();
std::vector<Row> next;
next.reserve(procs.size());
for (const auto& jp : procs) {
Row r;
if (!MinecraftFilter(jp, &r.title)) continue;
r.pid = jp.pid;
next.push_back(std::move(r));
}
std::unordered_map<unsigned long, Row> old;
old.reserve(rows_.size());
for (auto& r : rows_) old.emplace(r.pid, std::move(r));
double now = Now();
rows_.clear();
rows_.reserve(next.size());
for (auto& n : next) {
auto it = old.find(n.pid);
if (it != old.end()) {
Row r = it->second;
r.title = n.title; // titles can change during startup
rows_.push_back(std::move(r));
} else {
n.born = now;
rows_.push_back(std::move(n));
}
}
status_ = L"Watching " + std::to_wstring(rows_.size()) +
L" Minecraft instance(s).";
float maxScroll = ContentHeight() - ComputeGeo(w_, h_).listBottom +
ComputeGeo(w_, h_).listTop;
if (scroll_ > maxScroll) scroll_ = std::max(0.0f, maxScroll);
}
// ---- layout ----
MainWindow::Geo MainWindow::ComputeGeo(int w, int h) const {
Geo g{};
g.titleBarH = Scl(kTitleBarH);
g.bodyL = Scl(kBodyMarginX);
g.bodyR = static_cast<float>(w) - Scl(kBodyMarginX);
g.bodyR = (g.bodyL > g.bodyR) ? g.bodyL : g.bodyR;
Gdiplus::Bitmap bmp1(1, 1, PixelFormat32bppPARGB);
Gdiplus::Graphics mg(&bmp1);
Gdiplus::Font* fTitle = Font(L"Segoe UI", Scl(18), true);
Gdiplus::Font* fHint = Font(L"Segoe UI", Scl(12));
Gdiplus::Font* fStatus = Font(L"Segoe UI", Scl(13));
g.titleY = g.titleBarH + Scl(kBodyMarginY);
g.titleH = LineHeight(mg, fTitle);
g.hintY = g.titleY + g.titleH + Scl(kBodySpacing);
Gdiplus::RectF hintBounds;
const std::wstring hint =
L"Click Inject on the instance you want to load OpenZen into. "
L"List refreshes every second.";
mg.MeasureString(hint.c_str(), static_cast<INT>(hint.size()), fHint,
Gdiplus::RectF(g.bodyL, 0, g.bodyR - g.bodyL, 10000),
&NearFormat(), &hintBounds);
g.hintH = hintBounds.Height;
g.listTop = g.hintY + g.hintH + Scl(kBodySpacing);
g.statusH = LineHeight(mg, fStatus) + Scl(2 * 7) + 2;
g.statusTop = static_cast<float>(h) - Scl(kBodyMarginY) - g.statusH;
g.listBottom = g.statusTop - Scl(kBodySpacing);
if (g.listBottom < g.listTop) g.listBottom = g.listTop;
g.btnW = Scl(46);
g.closeBtnX = static_cast<float>(w) - g.btnW;
g.minBtnX = g.closeBtnX - g.btnW;
g.rowX0 = g.bodyL + Scl(2);
g.rowX1 = g.bodyR - Scl(9);
g.scrollX = g.bodyR - Scl(9);
g.scrollW = Scl(8);
g.scrollY0 = g.listTop + Scl(4);
g.scrollY1 = g.listBottom - Scl(4);
return g;
}
float MainWindow::ContentHeight() const {
return static_cast<float>(rows_.size()) * Scl(kRowH) + Scl(4);
}
float MainWindow::RowHoverValue(size_t i, double now) const {
const Row& r = rows_[i];
double k = (now - r.hoverStart) / kRowHover;
if (k < 0) k = 0;
if (k > 1) k = 1;
return static_cast<float>(r.hoverFrom +
(static_cast<float>(r.hoverTarget) - r.hoverFrom) *
EaseOutCubic(k));
}
void MainWindow::LayoutRowRect(const Geo& g, size_t i, float* y0,
float* y1) const {
*y0 = g.listTop + Scl(2) + static_cast<float>(i) * Scl(kRowH) - scroll_;
*y1 = *y0 + Scl(kRowH);
}
// ---- rendering ----
void MainWindow::Render() {
if (!canvas_.Resize(w_, h_)) return;
Gdiplus::Graphics& g = canvas_.g();
canvas_.Clear();
double now = Now();
float wf = static_cast<float>(w_);
float hf = static_cast<float>(h_);
Geo geo = ComputeGeo(w_, h_);
Gdiplus::RectF panelRect(0.5f, 0.5f, wf - 1.0f, hf - 1.0f);
Gdiplus::GraphicsPath* panel =
RoundedRectPath(panelRect, Scl(kCornerRadius));
// Rounded gradient panel.
{
Gdiplus::LinearGradientBrush bg(panelRect, Hex(0x1f2127), Hex(0x15171c),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&bg, panel);
}
// Everything below is clipped to the rounded panel so the square title
// bar corners never poke out of the rounded window shape.
Gdiplus::GraphicsState st = g.Save();
g.SetClip(panel);
// --- title bar ---
{
Gdiplus::RectF tbRect(0, 0, wf, geo.titleBarH);
Gdiplus::LinearGradientBrush tb(tbRect, Hex(0x23252c), Hex(0x1b1d22),
Gdiplus::LinearGradientModeVertical);
g.FillRectangle(&tb, tbRect);
Gdiplus::Pen hairline(Rgba(255, 255, 255, 14), 1.0f);
g.DrawLine(&hairline, 0.0f, geo.titleBarH, wf, geo.titleBarH);
// Pulsing scan-status dot: solid core + breathing halo.
double pulse = 0.5 - 0.5 * cos(Now() * 2.0 * 3.14159265358979 /
kPulsePeriod);
float dcx = Scl(20.0f);
float dcy = geo.titleBarH / 2.0f + Scl(1.5f);
float halo = Scl(5.0f + 5.0f * static_cast<float>(pulse));
int haloA = static_cast<int>(40 + 80 * pulse);
Gdiplus::SolidBrush haloBrush(Rgba(110, 200, 140, haloA));
g.FillEllipse(&haloBrush, dcx - halo, dcy - halo, halo * 2, halo * 2);
float core = Scl(3.4f);
Gdiplus::SolidBrush coreBrush(Rgba(120, 230, 150));
g.FillEllipse(&coreBrush, dcx - core, dcy - core, core * 2, core * 2);
// Title (with build revision when provided by the build).
std::wstring title = L"OpenZen Loader";
#ifdef OPENZEN_BUILD_REVISION
title = L"OpenZen Loader · build " + std::wstring(
L"" OPENZEN_BUILD_REVISION).substr(0, 7);
#endif
Gdiplus::Font* fTb = Font(L"Segoe UI", Scl(12), true);
float textY = geo.titleBarH / 2.0f - LineHeight(g, fTb) / 2.0f;
DrawText(g, fTb, title, Scl(40.0f), textY, Hex(0xe7ecf5));
// Minimize / close buttons.
Gdiplus::Font* fBtn = Font(L"Segoe UI", Scl(14));
float btnH = geo.titleBarH;
auto fillBtn = [&](float x, bool hover, bool pressed, bool isClose) {
if (isClose) {
if (pressed) {
Gdiplus::SolidBrush b(Hex(0x8a2920));
g.FillRectangle(&b, x, 0.0f, geo.btnW, btnH);
} else if (hover) {
Gdiplus::SolidBrush b(Hex(0xc0392b));
g.FillRectangle(&b, x, 0.0f, geo.btnW, btnH);
}
} else if (pressed) {
Gdiplus::SolidBrush b(Rgba(255, 255, 255, 28));
g.FillRectangle(&b, x, 0.0f, geo.btnW, btnH);
} else if (hover) {
Gdiplus::SolidBrush b(Rgba(255, 255, 255, 18));
g.FillRectangle(&b, x, 0.0f, geo.btnW, btnH);
}
};
{
fillBtn(geo.minBtnX, hoverMin_, pressed_ == 1, false);
fillBtn(geo.closeBtnX, hoverClose_, pressed_ == 2, true);
float cy = btnH / 2.0f;
// en-dash glyph
Gdiplus::Pen dash(hoverMin_ ? Gdiplus::Color(255, 255, 255)
: Hex(0xaab1bf),
Scl(1.2f));
g.DrawLine(&dash, geo.minBtnX + Scl(16), cy, geo.minBtnX + Scl(30), cy);
// X glyph (two strokes)
Gdiplus::Pen xpen(hoverClose_ ? Gdiplus::Color(255, 255, 255)
: Hex(0xaab1bf),
Scl(1.2f));
float cxm = (geo.closeBtnX + wf) / 2.0f;
float r = Scl(6.0f);
g.DrawLine(&xpen, cxm - r, cy - r, cxm + r, cy + r);
g.DrawLine(&xpen, cxm - r, cy + r, cxm + r, cy - r);
}
}
// --- body ---
{
Gdiplus::Font* fTitle = Font(L"Segoe UI", Scl(18), true);
DrawText(g, fTitle, L"Minecraft Instances", geo.bodyL + Scl(2),
geo.titleY + Scl(2), Gdiplus::Color(255, 255, 255));
Gdiplus::Font* fHint = Font(L"Segoe UI", Scl(12));
const std::wstring hint =
L"Click Inject on the instance you want to load OpenZen into. "
L"List refreshes every second.";
DrawText(g, fHint, hint, geo.bodyL + Scl(2), geo.hintY, Hex(0x8a8e98),
geo.bodyR - geo.bodyL);
}
// --- instance list ---
{
g.SetClip(Gdiplus::RectF(geo.bodyL, geo.listTop,
geo.bodyR - geo.bodyL,
geo.listBottom - geo.listTop));
if (rows_.empty()) {
Gdiplus::Font* fEmpty = Font(L"Segoe UI", Scl(12), false, true);
float lh = LineHeight(g, fEmpty);
float cy = (geo.listTop + geo.listBottom) / 2.0f;
DrawTextCentered(g, fEmpty, L"No Minecraft instances detected.",
(geo.rowX0 + geo.rowX1) / 2.0f, cy - lh - Scl(6),
Hex(0x6a6f7a));
DrawTextCentered(g, fEmpty,
L"Start the game and it will show up here.",
(geo.rowX0 + geo.rowX1) / 2.0f, cy + Scl(6),
Hex(0x6a6f7a));
}
for (size_t i = 0; i < rows_.size(); ++i) {
float y0, y1;
LayoutRowRect(geo, i, &y0, &y1);
if (y1 < geo.listTop || y0 > geo.listBottom) continue;
double e = Since(rows_[i].born, kRowEntrance);
float hov = RowHoverValue(i, now);
float dy = (1.0f - static_cast<float>(e)) * Scl(8.0f);
float alpha = static_cast<float>(e);
Gdiplus::RectF rect(geo.rowX0 + Scl(4), y0 + Scl(4) + dy,
geo.rowX1 - geo.rowX0 - Scl(8),
Scl(kRowH) - Scl(8));
Gdiplus::GraphicsPath* rp = RoundedRectPath(rect, Scl(9));
// Base fill (dimmest), entrance alpha applied.
{
Gdiplus::LinearGradientBrush base(
rect,
Rgba(38, 41, 48, static_cast<int>(255 * alpha)),
Rgba(30, 32, 38, static_cast<int>(255 * alpha)),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&base, rp);
}
if (hov > 0.001f) {
Gdiplus::LinearGradientBrush hover(
rect,
Rgba(74, 131, 224,
static_cast<int>(38 * hov * alpha)),
Rgba(50, 96, 189,
static_cast<int>(22 * hov * alpha)),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&hover, rp);
}
// Left accent stripe, brightens with hover.
{
Gdiplus::RectF stripe(rect.X, rect.Y, Scl(3), rect.Height);
Gdiplus::GraphicsPath* sp = RoundedRectPath(stripe, Scl(2));
int sa = static_cast<int>((60 + 160 * hov) *
(alpha < 0.999f ? alpha : 1.0f));
Gdiplus::SolidBrush sb(Rgba(85, 135, 235, sa));
g.FillPath(&sb, sp);
delete sp;
}
// Outline brightens with hover.
{
Gdiplus::Pen border(Rgba(255, 255, 255,
static_cast<int>((22 + 36 * hov) * alpha)),
1.0f);
g.DrawPath(&border, rp);
}
delete rp;
// --- row content ---
Gdiplus::Font* fPid = Font(L"Consolas", Scl(12), true);
Gdiplus::Font* fRowTitle = Font(L"Segoe UI", Scl(13));
Gdiplus::Font* fInject = Font(L"Segoe UI", Scl(12), true);
float pidX = geo.rowX0 + Scl(14);
float cy = (y0 + y1) / 2.0f;
DrawText(g, fPid, std::to_wstring(rows_[i].pid), pidX,
cy - LineHeight(g, fPid) / 2.0f, Hex(0x9aa3b2));
float titleX = pidX + Scl(72 + 12);
DrawText(g, fRowTitle, rows_[i].title, titleX,
cy - LineHeight(g, fRowTitle) / 2.0f, Hex(0xe7eaf2),
geo.rowX1 - Scl(12) - Scl(80) - titleX);
// Inject button (gradient, rounded 6, h 30).
float btnH = Scl(30);
std::wstring label = L"Inject";
float btnW = TextWidth(g, fInject, label) + Scl(2 * 18);
float btnX = geo.rowX1 - Scl(12) - btnW;
float btnY = cy - btnH / 2.0f;
Gdiplus::RectF btnRect(btnX, btnY, btnW, btnH);
Gdiplus::GraphicsPath* bp = RoundedRectPath(btnRect, Scl(6));
bool enabled = !injecting_;
bool btnHover = (hoverRow_ == static_cast<int>(i));
bool btnPressed = (pressed_ == 3 &&
pressedPid_ == rows_[i].pid);
if (!enabled) {
Gdiplus::SolidBrush b(Hex(0x2c2f37));
g.FillPath(&b, bp);
Gdiplus::Pen p(Hex(0x2c2f37), 1.0f);
g.DrawPath(&p, bp);
} else if (btnPressed) {
Gdiplus::LinearGradientBrush b(btnRect, Hex(0x2c5db0),
Hex(0x1e468f),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&b, bp);
Gdiplus::Pen p(Rgba(255, 255, 255, 22), 1.0f);
g.DrawPath(&p, bp);
} else if (btnHover) {
Gdiplus::LinearGradientBrush b(btnRect, Hex(0x5a93f0),
Hex(0x4275d8),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&b, bp);
Gdiplus::Pen p(Rgba(255, 255, 255, 60), 1.0f);
g.DrawPath(&p, bp);
} else {
Gdiplus::LinearGradientBrush b(btnRect, Hex(0x4a83e0),
Hex(0x3260bd),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&b, bp);
Gdiplus::Pen p(Rgba(255, 255, 255, 22), 1.0f);
g.DrawPath(&p, bp);
}
Gdiplus::Color labelCol =
enabled ? Gdiplus::Color(255, 255, 255) : Hex(0x6a6f7a);
Gdiplus::SolidBrush lb(labelCol);
{
Gdiplus::StringFormat sf(&NearFormat());
sf.SetAlignment(Gdiplus::StringAlignmentCenter);
sf.SetLineAlignment(Gdiplus::StringAlignmentCenter);
g.DrawString(label.c_str(), static_cast<INT>(label.size()),
fInject, btnRect, &sf, &lb);
}
delete bp;
}
g.ResetClip();
}
// --- scrollbar ---
{
float contentH = ContentHeight();
float viewH = geo.listBottom - geo.listTop;
if (contentH > viewH + 0.5f) {
float trackH = geo.scrollY1 - geo.scrollY0;
float handleH = trackH * viewH / contentH;
if (handleH < Scl(28)) handleH = Scl(28);
float maxScroll = contentH - viewH;
float handleY = geo.scrollY0 +
(trackH - handleH) * (scroll_ / maxScroll);
Gdiplus::RectF handle(geo.scrollX + Scl(1), handleY, Scl(8),
handleH);
Gdiplus::GraphicsPath* hp = RoundedRectPath(handle, Scl(4));
Gdiplus::SolidBrush hb(hoverRow_ == -2 || scrollDrag_
? Hex(0x4a4e58)
: Hex(0x3a3d45));
g.FillPath(&hb, hp);
delete hp;
}
}
// --- status strip ---
{
Gdiplus::RectF stRect(geo.bodyL, geo.statusTop, geo.bodyR - geo.bodyL,
geo.statusH);
Gdiplus::GraphicsPath* sp = RoundedRectPath(stRect, Scl(7));
Gdiplus::SolidBrush bg(Rgba(35, 37, 43, 200));
g.FillPath(&bg, sp);
Gdiplus::Pen border(Hex(0x2c2e35), 1.0f);
g.DrawPath(&border, sp);
delete sp;
Gdiplus::Font* fStatus = Font(L"Segoe UI", Scl(13));
DrawText(g, fStatus, status_, geo.bodyL + Scl(11),
geo.statusTop + Scl(7), Hex(0xc2c6cf),
stRect.Width - Scl(2 * 11));
}
g.Restore(st);
// Crisp hairline border above everything else.
{
Gdiplus::Pen border(Rgba(255, 255, 255, 26), 1.0f);
g.DrawPath(&border, panel);
}
delete panel;
}
// ---- interaction ----
namespace {
bool InRect(float x, float y, float rx, float ry, float rw, float rh) {
return x >= rx && x < rx + rw && y >= ry && y < ry + rh;
}
} // namespace
LRESULT CALLBACK MainWindow::Thunk(HWND h, UINT m, WPARAM wp, LPARAM lp) {
if (m == WM_NCCREATE) {
auto* self = reinterpret_cast<MainWindow*>(
reinterpret_cast<CREATESTRUCTW*>(lp)->lpCreateParams);
SetWindowLongPtrW(h, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
// Must be set before the first Handle() call: WM_NCCREATE itself is
// routed to Handle() -> DefWindowProcW(hwnd_, ...), and a NULL hwnd_
// there makes WM_NCCREATE return FALSE, which aborts window creation
// outright (CreateWindowExW fails with ERROR_INVALID_WINDOW_HANDLE).
self->hwnd_ = h;
}
auto* self =
reinterpret_cast<MainWindow*>(GetWindowLongPtrW(h, GWLP_USERDATA));
return self ? self->Handle(m, wp, lp) : DefWindowProcW(h, m, wp, lp);
}
LRESULT MainWindow::Handle(UINT m, WPARAM wp, LPARAM lp) {
switch (m) {
case WM_TIMER: {
if (wp == kTimerAnim) Tick();
else if (wp == kTimerPoll && !injecting_) Refresh();
return 0;
}
case WM_PAINT: {
PAINTSTRUCT ps;
BeginPaint(hwnd_, &ps);
EndPaint(hwnd_, &ps);
if (tEntrance_ >= 0.0) Tick();
return 0;
}
case WM_ERASEBKGND:
return 1;
case WM_GETMINMAXINFO: {
auto* mmi = reinterpret_cast<MINMAXINFO*>(lp);
mmi->ptMinTrackSize.x = static_cast<LONG>((kBaseWidth - kSizeJitter) * g_scale);
mmi->ptMinTrackSize.y = static_cast<LONG>((kBaseHeight - kSizeJitter) * g_scale);
return 0;
}
case WM_SIZE: {
int cw = static_cast<int>(LOWORD(lp));
int ch = static_cast<int>(HIWORD(lp));
if (wp != SIZE_MINIMIZED && cw > 0 && ch > 0) {
w_ = cw;
h_ = ch;
float maxScroll =
ContentHeight() - ComputeGeo(w_, h_).listBottom +
ComputeGeo(w_, h_).listTop;
if (scroll_ > maxScroll) scroll_ = std::max(0.0f, maxScroll);
if (tEntrance_ < 0.0 || entranceDone_) Tick();
}
return 0;
}
case WM_MOUSEMOVE: {
float x = static_cast<float>(GET_X_LPARAM(lp));
float y = static_cast<float>(GET_Y_LPARAM(lp));
Geo geo = ComputeGeo(w_, h_);
// Ask for a WM_MOUSELEAVE so row/button hover state clears when
// the cursor exits the window.
{
TRACKMOUSEEVENT tme{sizeof(tme), TME_LEAVE, hwnd_, 0};
TrackMouseEvent(&tme);
}
if (scrollDrag_) {
float trackH = geo.scrollY1 - geo.scrollY0;
float contentH = ContentHeight();
float viewH = geo.listBottom - geo.listTop;
float handleH = trackH * viewH / contentH;
if (handleH < Scl(28)) handleH = Scl(28);
float rel = y - geo.scrollY0 - scrollGrab_;
float maxScroll = contentH - viewH;
float t = (trackH - handleH) > 0 ? rel / (trackH - handleH) : 0;
scroll_ = t * maxScroll;
if (scroll_ < 0) scroll_ = 0;
if (scroll_ > maxScroll) scroll_ = maxScroll;
Tick();
return 0;
}
bool newMin = InRect(x, y, geo.minBtnX, 0, geo.btnW, geo.titleBarH);
bool newClose =
InRect(x, y, geo.closeBtnX, 0, geo.btnW, geo.titleBarH);
int newHoverRow = -1;
if (!newMin && !newClose && y >= geo.listTop &&
y <= geo.listBottom && x >= geo.rowX0 && x <= geo.rowX1) {
float rel = y - geo.listTop - Scl(2) + scroll_;
int idx = static_cast<int>(rel / Scl(kRowH));
if (idx >= 0 && idx < static_cast<int>(rows_.size()))
newHoverRow = idx;
}
if (newHoverRow != hoverRow_) {
double now = Now();
if (hoverRow_ >= 0 &&
hoverRow_ < static_cast<int>(rows_.size())) {
Row& r = rows_[hoverRow_];
r.hoverFrom = RowHoverValue(hoverRow_, now);
r.hoverStart = now;
r.hoverTarget = false;
}
if (newHoverRow >= 0 &&
newHoverRow < static_cast<int>(rows_.size())) {
Row& r = rows_[newHoverRow];
r.hoverFrom = RowHoverValue(newHoverRow, now);
r.hoverStart = now;
r.hoverTarget = true;
}
hoverRow_ = newHoverRow;
}
hoverMin_ = newMin;
hoverClose_ = newClose;
// Hand cursor over interactive elements.
bool overBtn = hoverMin_ || hoverClose_;
bool overInject = false;
if (hoverRow_ >= 0) {
float y0, y1;
LayoutRowRect(geo, hoverRow_, &y0, &y1);
float btnH = Scl(30);
std::wstring label = L"Inject";
Gdiplus::Bitmap bmp1(1, 1, PixelFormat32bppPARGB);
Gdiplus::Graphics mg(&bmp1);
float btnW = TextWidth(mg, Font(L"Segoe UI", Scl(12), true),
label) + Scl(2 * 18);
float btnX = geo.rowX1 - Scl(12) - btnW;
overInject = InRect(x, y, btnX, (y0 + y1) / 2.0f - btnH / 2.0f,
btnW, btnH);
}
SetCursor(LoadCursorW(nullptr,
(overBtn || overInject) ? IDC_HAND
: IDC_ARROW));
Tick();
return 0;
}
case WM_MOUSELEAVE: {
hoverMin_ = hoverClose_ = false;
if (hoverRow_ >= 0 &&
hoverRow_ < static_cast<int>(rows_.size())) {
Row& r = rows_[hoverRow_];
r.hoverFrom = RowHoverValue(hoverRow_, Now());
r.hoverStart = Now();
r.hoverTarget = false;
}
hoverRow_ = -1;
Tick();
return 0;
}
case WM_LBUTTONDOWN: {
float x = static_cast<float>(GET_X_LPARAM(lp));
float y = static_cast<float>(GET_Y_LPARAM(lp));
Geo geo = ComputeGeo(w_, h_);
pressed_ = 0;
if (InRect(x, y, geo.minBtnX, 0, geo.btnW, geo.titleBarH)) {
pressed_ = 1;
} else if (InRect(x, y, geo.closeBtnX, 0, geo.btnW,
geo.titleBarH)) {
pressed_ = 2;
} else if (hoverRow_ >= 0 && hoverRow_ < (int)rows_.size()) {
float y0, y1;
LayoutRowRect(geo, hoverRow_, &y0, &y1);
float btnH = Scl(30);
Gdiplus::Bitmap bmp1(1, 1, PixelFormat32bppPARGB);
Gdiplus::Graphics mg(&bmp1);
float btnW = TextWidth(mg, Font(L"Segoe UI", Scl(12), true),
L"Inject") + Scl(2 * 18);
float btnX = geo.rowX1 - Scl(12) - btnW;
if (InRect(x, y, btnX, (y0 + y1) / 2.0f - btnH / 2.0f, btnW,
btnH)) {
pressed_ = 3;
pressedPid_ = rows_[hoverRow_].pid;
}
}
// Scrollbar drag start.
if (pressed_ == 0) {
float contentH = ContentHeight();
float viewH = geo.listBottom - geo.listTop;
if (contentH > viewH + 0.5f && InRect(x, y, geo.scrollX,
geo.scrollY0,
Scl(9),
geo.scrollY1 -
geo.scrollY0)) {
float trackH = geo.scrollY1 - geo.scrollY0;
float handleH = trackH * viewH / contentH;
if (handleH < Scl(28)) handleH = Scl(28);
float maxScroll = contentH - viewH;
float handleY =
geo.scrollY0 +
(trackH - handleH) * (scroll_ / maxScroll);
if (y >= handleY && y <= handleY + handleH) {
scrollDrag_ = true;
scrollGrab_ = y - handleY;
SetCapture(hwnd_);
Tick();
return 0;
}
}
}
SetCapture(hwnd_);
Tick();
return 0;
}
case WM_LBUTTONUP: {
if (scrollDrag_) {
scrollDrag_ = false;
if (GetCapture() == hwnd_) ReleaseCapture();
}
float x = static_cast<float>(GET_X_LPARAM(lp));
float y = static_cast<float>(GET_Y_LPARAM(lp));
Geo geo = ComputeGeo(w_, h_);
if (pressed_ == 1 &&
InRect(x, y, geo.minBtnX, 0, geo.btnW, geo.titleBarH)) {
ShowWindow(hwnd_, SW_MINIMIZE);
} else if (pressed_ == 2 &&
InRect(x, y, geo.closeBtnX, 0, geo.btnW,
geo.titleBarH)) {
PostMessageW(hwnd_, WM_CLOSE, 0, 0);
} else if (pressed_ == 3 && !injecting_) {
for (const auto& r : rows_) {
if (r.pid == pressedPid_ && onInjectRequested) {
onInjectRequested(r.pid, r.title);
break;
}
}
}
pressed_ = 0;
Tick();
return 0;
}
case WM_MOUSEWHEEL: {
float delta =
static_cast<float>(GET_WHEEL_DELTA_WPARAM(wp)) / 120.0f;
float maxScroll = ContentHeight() -
ComputeGeo(w_, h_).listBottom +
ComputeGeo(w_, h_).listTop;
scroll_ -= delta * Scl(48);
if (scroll_ < 0) scroll_ = 0;
if (scroll_ > maxScroll) scroll_ = maxScroll;
Tick();
return 0;
}
case WM_SETCURSOR:
return 0; // cursor set in WM_MOUSEMOVE
case WM_NCHITTEST: {
// Title-bar drag anywhere on the custom bar except the buttons.
POINT pt{GET_X_LPARAM(lp), GET_Y_LPARAM(lp)};
RECT wr;
GetWindowRect(hwnd_, &wr);
float y = static_cast<float>(pt.y - wr.top);
float x = static_cast<float>(pt.x - wr.left);
Geo geo = ComputeGeo(w_, h_);
if (y >= 0 && y < geo.titleBarH &&
!InRect(x, y, geo.minBtnX, 0, geo.btnW, geo.titleBarH) &&
!InRect(x, y, geo.closeBtnX, 0, geo.btnW, geo.titleBarH)) {
return HTCAPTION;
}
return HTCLIENT;
}
case WM_NCLBUTTONDBLCLK:
return 0; // no maximize on double-click
case WM_CLOSE:
PlayExit();
return 0;
case WM_DESTROY:
KillTimer(hwnd_, kTimerAnim);
KillTimer(hwnd_, kTimerPoll);
hwnd_ = nullptr;
if (onClosed) onClosed();
return 0;
default:
return DefWindowProcW(hwnd_, m, wp, lp);
}
}
} // namespace ui
+115
查看文件
@@ -0,0 +1,115 @@
#pragma once
//
// main_win.h — main loader window (GDI+ replacement for MainWindow +
// TitleBar + InstanceList + InstanceRow).
//
// A single frameless, per-pixel-alpha layered window, fully owner-drawn:
// rounded gradient panel, custom title bar (pulsing status dot, minimize /
// close), the scrolling Minecraft-instance list with hover-tinted rows and
// gradient Inject buttons, and the rounded status strip. A 1 s poll refreshes
// the instance list, a 30 fps timer drives the animations.
//
#include "loader.h"
#include "wgfx.h"
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace ui {
// True when a java process looks like a Minecraft window: its main window
// title starts with "Minecraft" (in-game state) or the LWJGL GLFW window
// class ("GLFW30") is in use (still true before the title gets set).
bool LooksLikeMinecraft(const loader::JavaProcess& jp);
class MainWindow {
public:
// Fired with (pid, title) when the user clicks a row's Inject button.
std::function<void(unsigned long, const std::wstring&)> onInjectRequested;
// Fired when the window is gone (after the exit fade). main.cpp quits.
std::function<void()> onClosed;
// Creates (hidden) and plays the entrance animation. Call after the
// splash finishes.
void PlayEntrance();
// Fade out, then destroy. Idempotent. If an injection is in flight the
// window is torn down immediately (no fade) so the process can exit.
void PlayExit();
// Called by main.cpp when the overlay takes over.
void SetInjectionInFlight(bool on) { injecting_ = on; }
bool IsInjecting() const { return injecting_; }
HWND hwnd() const { return hwnd_; }
private:
static LRESULT CALLBACK Thunk(HWND, UINT, WPARAM, LPARAM);
LRESULT Handle(UINT, WPARAM, LPARAM);
void Create();
void Render();
void Tick();
void Refresh();
// --- layout helpers (shared by Render and hit-testing) ---
struct Geo {
float titleBarH;
float bodyL, bodyR;
float titleY, titleH;
float hintY, hintH;
float listTop, listBottom;
float statusTop, statusH;
float minBtnX, closeBtnX, btnW;
float rowX0, rowX1;
float scrollX, scrollW, scrollY0, scrollY1;
};
Geo ComputeGeo(int w, int h) const;
float ContentHeight() const;
float RowHoverValue(size_t i, double now) const;
void LayoutRowRect(const Geo& g, size_t i, float* y0, float* y1) const;
HWND hwnd_ = nullptr;
LayeredCanvas canvas_;
struct Row {
unsigned long pid = 0;
std::wstring title;
double born = 0.0; // entrance animation start
double hoverStart = 0.0;
float hoverFrom = 0.0f;
bool hoverTarget = false;
};
std::vector<Row> rows_;
std::wstring status_;
// scroll state
float scroll_ = 0.0f; // current offset in px
bool scrollDrag_ = false;
float scrollGrab_ = 0.0f; // px inside the handle where the drag began
// mouse state
int hoverRow_ = -1;
bool hoverMin_ = false;
bool hoverClose_ = false;
int pressed_ = 0; // 1=min 2=close 3=inject row
unsigned long pressedPid_ = 0;
// animation state
double tEntrance_ = -1.0; // <0 = not started
double tExit_ = -1.0;
int baseX_ = 0, baseY_ = 0; // window position at rest
int w_ = 0, h_ = 0;
bool entranceDone_ = false;
bool injecting_ = false;
bool closing_ = false;
// jittered base size
int baseW_ = 0, baseH_ = 0;
};
} // namespace ui
+387
查看文件
@@ -0,0 +1,387 @@
#include "manual_map.h"
#include <psapi.h>
#include <tlhelp32.h>
#include <sstream>
#include <vector>
#pragma comment(lib, "psapi.lib")
namespace loader {
namespace {
std::wstring fmt_err(const wchar_t* where, DWORD err) {
wchar_t msg[256] = {0};
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, err, 0, msg, 256, nullptr);
std::wstringstream ss;
ss << where << L" failed (" << err << L"): " << msg;
return ss.str();
}
const IMAGE_NT_HEADERS* nt_of(const void* base) {
auto dos = static_cast<const IMAGE_DOS_HEADER*>(base);
return reinterpret_cast<const IMAGE_NT_HEADERS*>(
static_cast<const BYTE*>(base) + dos->e_lfanew);
}
// Run LoadLibraryW(name) inside the target process and return the resulting
// HMODULE seen by that process, or nullptr on failure.
HMODULE remote_load_library(HANDLE process, const wchar_t* dll_name) {
SIZE_T sz = (std::wcslen(dll_name) + 1) * sizeof(wchar_t);
LPVOID arg = VirtualAllocEx(process, nullptr, sz,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!arg) return nullptr;
SIZE_T written = 0;
if (!WriteProcessMemory(process, arg, dll_name, sz, &written) || written != sz) {
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
return nullptr;
}
auto load_lib = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(
GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_lib, arg, 0, nullptr);
if (!thread) {
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
return nullptr;
}
WaitForSingleObject(thread, 10000);
DWORD ret = 0;
GetExitCodeThread(thread, &ret);
CloseHandle(thread);
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
// On x64 GetExitCodeThread returns a DWORD which truncates HMODULE, but
// ASLR keeps HMODULEs within 32 bits for almost every module on Windows,
// so this works in practice. The remote_module enumerator below is the
// fallback if the truncation ever bites us.
return reinterpret_cast<HMODULE>(static_cast<ULONG_PTR>(ret));
}
HMODULE find_remote_module(HANDLE process, const wchar_t* name) {
HMODULE mods[1024];
DWORD cb = 0;
if (!EnumProcessModulesEx(process, mods, sizeof mods, &cb, LIST_MODULES_ALL)) {
return nullptr;
}
DWORD count = cb / sizeof(HMODULE);
for (DWORD i = 0; i < count; ++i) {
wchar_t buf[MAX_PATH];
if (GetModuleBaseNameW(process, mods[i], buf, MAX_PATH)) {
if (_wcsicmp(buf, name) == 0) return mods[i];
}
}
return nullptr;
}
// Resolve the RVA of a named export directly from the in-memory image we built
// locally. Relocations don't move the export RVA, so the remote symbol
// address is simply remote_image_base + rva.
DWORD find_export_rva(const BYTE* image, const IMAGE_NT_HEADERS* nt,
const char* name) {
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (dir.Size == 0) return 0;
auto exp = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(image + dir.VirtualAddress);
if (exp->NumberOfNames == 0) return 0;
auto names = reinterpret_cast<const DWORD*>(image + exp->AddressOfNames);
auto funcs = reinterpret_cast<const DWORD*>(image + exp->AddressOfFunctions);
auto ords = reinterpret_cast<const WORD*>(image + exp->AddressOfNameOrdinals);
for (DWORD i = 0; i < exp->NumberOfNames; ++i) {
const char* cand = reinterpret_cast<const char*>(image + names[i]);
if (std::strcmp(cand, name) == 0) {
return funcs[ords[i]];
}
}
return 0;
}
// Maps the injected DLL's bootstrap verdict code to a short human-readable
// description. See dll/src/main.cpp for where the codes are produced.
std::wstring bootstrap_verdict(DWORD code) {
switch (code) {
case 0: return L"fine (java bootstrap complete)";
case 1: return L"no JavaVM in target (not a Java process?)";
case 2: return L"AttachCurrentThreadAsDaemon failed";
case 3: return L"failed to extract embedded zen.jar";
case 4: return L"Minecraft/Forge classes not found (target is not a Forge MC instance)";
case 5: return L"GameLoaderBridge class load failed";
case 6: return L"GameLoaderBridge.load(String, ClassLoader) not found";
case 7: return L"GameLoaderBridge.load threw (bootstrap.start failed)";
case 8: return L"bootstrap thread creation failed";
default: return L"bootstrap reported code " + std::to_wstring(code);
}
}
void apply_relocations(BYTE* image, const IMAGE_NT_HEADERS* nt, ULONGLONG delta) {
if (delta == 0) return;
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (dir.Size == 0) return;
BYTE* block_ptr = image + dir.VirtualAddress;
BYTE* end = block_ptr + dir.Size;
while (block_ptr < end) {
auto block = reinterpret_cast<IMAGE_BASE_RELOCATION*>(block_ptr);
if (block->SizeOfBlock == 0) break;
DWORD count = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
auto entries = reinterpret_cast<WORD*>(block + 1);
BYTE* page = image + block->VirtualAddress;
for (DWORD i = 0; i < count; ++i) {
WORD type = entries[i] >> 12;
WORD off = entries[i] & 0x0FFF;
if (type == IMAGE_REL_BASED_DIR64) {
*reinterpret_cast<ULONGLONG*>(page + off) += delta;
} else if (type == IMAGE_REL_BASED_HIGHLOW) {
*reinterpret_cast<DWORD*>(page + off) += static_cast<DWORD>(delta);
}
// IMAGE_REL_BASED_ABSOLUTE (0) is a padding entry; ignore.
}
block_ptr += block->SizeOfBlock;
}
}
bool resolve_imports(HANDLE process, BYTE* local_image,
const IMAGE_NT_HEADERS* nt, std::wstring& err) {
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
if (dir.Size == 0) return true;
auto desc = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(local_image + dir.VirtualAddress);
while (desc->Name) {
const char* dll_name_ansi = reinterpret_cast<const char*>(local_image + desc->Name);
wchar_t dll_name_w[MAX_PATH] = {0};
MultiByteToWideChar(CP_ACP, 0, dll_name_ansi, -1, dll_name_w, MAX_PATH);
HMODULE remote_mod = find_remote_module(process, dll_name_w);
if (!remote_mod) {
remote_mod = remote_load_library(process, dll_name_w);
if (!remote_mod) remote_mod = find_remote_module(process, dll_name_w);
}
if (!remote_mod) {
std::wstringstream ss;
ss << L"Remote LoadLibrary failed for dependency " << dll_name_w;
err = ss.str();
return false;
}
// Use the loader's own copy of the dependency to walk its export table
// and compute remote function addresses. This works because Windows
// resolves each DLL's exports to a constant RVA, so
// remote_fn = remote_mod_base + (local_fn - local_mod_base)
HMODULE local_mod = GetModuleHandleA(dll_name_ansi);
if (!local_mod) local_mod = LoadLibraryA(dll_name_ansi);
if (!local_mod) {
std::wstringstream ss;
ss << L"Local LoadLibrary failed for dependency " << dll_name_w;
err = ss.str();
return false;
}
auto thunk = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image +
(desc->OriginalFirstThunk ? desc->OriginalFirstThunk : desc->FirstThunk));
auto iat = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image + desc->FirstThunk);
while (thunk->u1.AddressOfData) {
FARPROC local_fn = nullptr;
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
local_fn = GetProcAddress(local_mod,
reinterpret_cast<LPCSTR>(IMAGE_ORDINAL(thunk->u1.Ordinal)));
} else {
auto by_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(
local_image + thunk->u1.AddressOfData);
local_fn = GetProcAddress(local_mod, by_name->Name);
}
if (local_fn) {
ULONGLONG remote_fn = reinterpret_cast<ULONGLONG>(remote_mod) +
(reinterpret_cast<ULONGLONG>(local_fn) -
reinterpret_cast<ULONGLONG>(local_mod));
iat->u1.Function = remote_fn;
}
++thunk;
++iat;
}
++desc;
}
return true;
}
DWORD section_protection(DWORD characteristics) {
bool x = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
bool r = (characteristics & IMAGE_SCN_MEM_READ) != 0;
bool w = (characteristics & IMAGE_SCN_MEM_WRITE) != 0;
if (x && r && w) return PAGE_EXECUTE_READWRITE;
if (x && r) return PAGE_EXECUTE_READ;
if (x) return PAGE_EXECUTE;
if (r && w) return PAGE_READWRITE;
if (r) return PAGE_READONLY;
return PAGE_NOACCESS;
}
} // namespace
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size) {
if (!dll_bytes || dll_size < sizeof(IMAGE_DOS_HEADER)) {
return L"DLL payload too small";
}
auto dos = static_cast<const IMAGE_DOS_HEADER*>(dll_bytes);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return L"Bad DOS signature";
auto nt = nt_of(dll_bytes);
if (nt->Signature != IMAGE_NT_SIGNATURE) return L"Bad NT signature";
if (nt->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64) {
return L"DLL is not x64 (only AMD64 supported)";
}
HANDLE process = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, pid);
if (!process) return fmt_err(L"OpenProcess", GetLastError());
SIZE_T image_size = nt->OptionalHeader.SizeOfImage;
LPVOID remote_image = VirtualAllocEx(process, nullptr, image_size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remote_image) {
DWORD err = GetLastError();
CloseHandle(process);
return fmt_err(L"VirtualAllocEx (image)", err);
}
// Build the in-memory image locally before pushing it across, so we can
// apply relocations + import patches in cheap local memory rather than
// round-tripping ReadProcessMemory/WriteProcessMemory.
std::vector<BYTE> local_image(image_size, 0);
std::memcpy(local_image.data(), dll_bytes, nt->OptionalHeader.SizeOfHeaders);
auto sect = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
if (sect->SizeOfRawData == 0) continue;
if (sect->PointerToRawData + sect->SizeOfRawData > dll_size) continue;
std::memcpy(local_image.data() + sect->VirtualAddress,
static_cast<const BYTE*>(dll_bytes) + sect->PointerToRawData,
sect->SizeOfRawData);
}
ULONGLONG delta = reinterpret_cast<ULONGLONG>(remote_image) -
nt->OptionalHeader.ImageBase;
apply_relocations(local_image.data(), nt, delta);
std::wstring imp_err;
if (!resolve_imports(process, local_image.data(), nt, imp_err)) {
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
CloseHandle(process);
return imp_err;
}
SIZE_T written = 0;
if (!WriteProcessMemory(process, remote_image, local_image.data(),
image_size, &written) || written != image_size) {
DWORD err = GetLastError();
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
CloseHandle(process);
return fmt_err(L"WriteProcessMemory (image)", err);
}
// Tighten section permissions to match the original PE characteristics.
sect = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
if (sect->Misc.VirtualSize == 0) continue;
DWORD prot = section_protection(sect->Characteristics);
DWORD old = 0;
VirtualProtectEx(process,
static_cast<BYTE*>(remote_image) + sect->VirtualAddress,
sect->Misc.VirtualSize, prot, &old);
}
// CreateRemoteThread can only deliver one pointer-sized arg, so we drop a
// tiny x64 trampoline that calls
// DllMain(hModule = remote_image,
// fdwReason = DLL_PROCESS_ATTACH,
// lpvReserved = NULL)
// through the standard MSVC ABI before returning.
BYTE shellcode[] = {
0x48, 0xB9, 0,0,0,0,0,0,0,0, // mov rcx, imm64 (hModule)
0xBA, 0x01, 0x00, 0x00, 0x00, // mov edx, 1 (DLL_PROCESS_ATTACH)
0x4D, 0x31, 0xC0, // xor r8, r8 (lpvReserved)
0x48, 0xB8, 0,0,0,0,0,0,0,0, // mov rax, imm64 (entry point)
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 (16 + shadow space)
0xFF, 0xD0, // call rax
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
0xC3 // ret
};
ULONGLONG hmod = reinterpret_cast<ULONGLONG>(remote_image);
ULONGLONG entry = hmod + nt->OptionalHeader.AddressOfEntryPoint;
std::memcpy(shellcode + 2, &hmod, sizeof hmod);
std::memcpy(shellcode + 20, &entry, sizeof entry);
LPVOID remote_sc = VirtualAllocEx(process, nullptr, sizeof shellcode,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remote_sc) {
DWORD err = GetLastError();
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
CloseHandle(process);
return fmt_err(L"VirtualAllocEx (shellcode)", err);
}
if (!WriteProcessMemory(process, remote_sc, shellcode, sizeof shellcode, &written)
|| written != sizeof shellcode) {
DWORD err = GetLastError();
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
CloseHandle(process);
return fmt_err(L"WriteProcessMemory (shellcode)", err);
}
HANDLE thread = CreateRemoteThread(process, nullptr, 0,
reinterpret_cast<LPTHREAD_START_ROUTINE>(remote_sc), nullptr, 0, nullptr);
if (!thread) {
DWORD err = GetLastError();
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
CloseHandle(process);
return fmt_err(L"CreateRemoteThread", err);
}
WaitForSingleObject(thread, 30000);
CloseHandle(thread);
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
// DllMain returning only means the DLL is mapped; the Java/bootstrap work
// runs on a detached thread inside the target. Poll the exported
// OpenZenBootstrapResult volatile so the caller gets a real verdict
// ("bootstrap complete" vs "Minecraft class not found") instead of a
// false success.
std::wstring verdict_err;
{
DWORD rva = find_export_rva(local_image.data(), nt,
"OpenZenBootstrapResult");
if (rva != 0) {
LPVOID remote_addr = static_cast<BYTE*>(remote_image) + rva;
bool done = false;
for (int i = 0; i < 150; ++i) { // up to 30 s
LONG val = -1;
SIZE_T read = 0;
if (ReadProcessMemory(process, remote_addr, &val, sizeof val,
&read) && read == sizeof val) {
if (val != -1) {
done = true;
if (val != 0) {
verdict_err =
L"Java bootstrap failed: " +
bootstrap_verdict(static_cast<DWORD>(val));
}
break;
}
}
Sleep(200);
}
if (!done) {
verdict_err =
L"Timed out waiting for the Java bootstrap; "
L"the native log (%TEMP%\\openzen-<pid>-*.log) has details";
}
}
}
// Leave remote_image allocated - the DLL stays mapped in the target's
// address space for the lifetime of the process.
CloseHandle(process);
return verdict_err;
}
} // namespace loader
+16
查看文件
@@ -0,0 +1,16 @@
#pragma once
#include <windows.h>
#include <string>
namespace loader {
// Map a DLL image directly into the target process and invoke its entry point,
// without writing the DLL to disk first. Implements a minimal PE loader:
// VirtualAllocEx + relocations + import resolution + DllMain stub via
// CreateRemoteThread shellcode.
//
// Returns an empty string on success, or a human-readable error message.
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size);
} // namespace loader
+303
查看文件
@@ -0,0 +1,303 @@
#include "overlay_win.h"
#include "loader.h"
#include <algorithm>
#include <cmath>
#include <memory>
namespace ui {
namespace {
constexpr int kW = 460;
constexpr int kH = 240;
constexpr float kCornerRadius = 16.0f;
constexpr double kFadeIn = 0.22; // panel fade-in
constexpr double kSpinnerT = 1.10; // one revolution
constexpr double kCrawl = 0.90; // progress 0 -> 0.7
constexpr double kFinish = 0.26; // progress -> 1.0
constexpr double kMark = 0.38; // check/cross draw-in
constexpr double kHold = 0.70; // hold after mark
constexpr double kFadeOut = 0.26; // panel fade-out
constexpr UINT WM_APP_INJECT_RESULT = WM_APP + 1;
float Scl(float v) { return static_cast<float>(v * g_scale); }
} // namespace
void OverlayWindow::Show(HWND centerOver, unsigned long pid,
const std::wstring& target) {
pid_ = pid;
target_ = target;
w_ = static_cast<int>(kW * g_scale);
h_ = static_cast<int>(kH * g_scale);
WNDCLASSW wc{};
wc.lpfnWndProc = &OverlayWindow::Thunk;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"OZLoaderOverlay";
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
RegisterClassW(&wc);
hwnd_ = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
wc.lpszClassName, L"", WS_POPUP, 0, 0, w_, h_,
nullptr, nullptr, wc.hInstance, this);
// Centre over the parent window if given, else the work area.
RECT rc{};
if (centerOver && GetWindowRect(centerOver, &rc)) {
int x = (rc.left + rc.right - w_) / 2;
int y = (rc.top + rc.bottom - h_) / 2;
SetWindowPos(hwnd_, HWND_TOPMOST, x, y, 0, 0,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
} else {
RECT wa{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0);
int x = (wa.left + wa.right - w_) / 2;
int y = (wa.top + wa.bottom - h_) / 2;
SetWindowPos(hwnd_, HWND_TOPMOST, x, y, 0, 0,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
t0_ = Now();
SetTimer(hwnd_, 1, 16, nullptr);
// Run the (synchronous, can-block) inject() on a worker thread so the
// overlay keeps animating; the result bounces back via PostMessage.
HWND targetWnd = hwnd_;
worker_ = std::make_unique<std::thread>([targetWnd, pid]() {
std::wstring err = loader::inject(pid);
auto* boxed = new std::wstring(std::move(err));
PostMessageW(targetWnd, WM_APP_INJECT_RESULT,
static_cast<WPARAM>(boxed->empty()),
reinterpret_cast<LPARAM>(boxed));
});
worker_->detach();
}
void OverlayWindow::Tick() {
if (!hwnd_) return;
double now = Now();
// Sequence end: fire completed() then destroy.
if (tResult_ >= 0.0) {
double t = now - tResult_;
if (t >= kFinish + kMark + kHold + kFadeOut) {
if (!completed_) {
completed_ = true;
if (onCompleted) onCompleted(ok_);
}
DestroyWindow(hwnd_);
return;
}
}
Render();
// Whole-window opacity: fade in, hold, fade out.
double winOp;
if (tResult_ < 0.0) {
winOp = EaseOutCubic(Since(t0_, kFadeIn));
} else {
double t = now - tResult_;
if (t < kFinish + kMark + kHold) {
winOp = 1.0;
} else {
winOp = 1.0 - EaseInCubic((t - kFinish - kMark - kHold) / kFadeOut);
}
}
canvas_.Present(hwnd_, static_cast<BYTE>(winOp * 255.0 + 0.5));
}
void OverlayWindow::Render() {
if (!canvas_.Resize(w_, h_)) return;
Gdiplus::Graphics& g = canvas_.g();
canvas_.Clear();
double now = Now();
double t = now - t0_;
Gdiplus::RectF rect(0.5f, 0.5f, w_ - 1.0f, h_ - 1.0f);
Gdiplus::GraphicsPath* panel =
RoundedRectPath(rect, Scl(kCornerRadius));
// Background.
{
Gdiplus::LinearGradientBrush bg(rect, Hex(0x1a1c22), Hex(0x0e1014),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&bg, panel);
}
// Accent glow behind the spinner.
{
float gcx = rect.X + rect.Width / 2.0f;
float gcy = rect.Y + rect.Height / 2.0f - Scl(30);
float r = rect.Width * 0.5f;
Gdiplus::GraphicsPath glowPath;
glowPath.AddEllipse(gcx - r, gcy - r, r * 2, r * 2);
Gdiplus::PathGradientBrush glow(&glowPath);
glow.SetCenterPoint(Gdiplus::PointF(gcx, gcy));
glow.SetCenterColor(Rgba(70, 130, 230, 90));
Gdiplus::Color surround[] = {Rgba(70, 130, 230, 0)};
INT surroundCount = 1;
glow.SetSurroundColors(surround, &surroundCount);
g.FillRectangle(&glow, rect);
}
// Spinner / status mark.
float cx = rect.X + rect.Width / 2.0f;
float cy = rect.Y + Scl(72.0f);
float r = Scl(26.0f);
bool injectDone = tResult_ >= 0.0;
if (!injectDone) {
Gdiplus::Pen ringPen(Rgba(255, 255, 255, 30), Scl(3));
g.DrawEllipse(&ringPen, cx - r, cy - r, r * 2, r * 2);
// Rotating 120-degree arc (GDI+ angles are clockwise from +x, which
// matches the visual sweep the Qt version produced).
double angle = fmod(t * 360.0 / kSpinnerT, 360.0);
Gdiplus::Pen arcPen(Hex(0x7fb0ff), Scl(3));
arcPen.SetStartCap(Gdiplus::LineCapRound);
arcPen.SetEndCap(Gdiplus::LineCapRound);
g.DrawArc(&arcPen, cx - r, cy - r, r * 2, r * 2,
static_cast<float>(angle), 120.0f);
} else {
Gdiplus::Color accent = ok_ ? Hex(0x3ecf6b) : Hex(0xe25656);
Gdiplus::SolidBrush markBrush(accent);
g.FillEllipse(&markBrush, cx - r, cy - r, r * 2, r * 2);
Gdiplus::Pen mark(Gdiplus::Color(255, 255, 255), Scl(3));
mark.SetStartCap(Gdiplus::LineCapRound);
mark.SetEndCap(Gdiplus::LineCapRound);
mark.SetLineJoin(Gdiplus::LineJoinRound);
double tm = Since(tResult_ + kFinish, kMark);
float tt = static_cast<float>(EaseOutBack(tm));
if (ok_) {
// Two-segment checkmark animated by t.
Gdiplus::PointF a(cx - Scl(11), cy + Scl(1));
Gdiplus::PointF b(cx - Scl(2), cy + Scl(9));
Gdiplus::PointF d(cx + Scl(12), cy - Scl(7));
if (tt <= 0.5f) {
float k = tt / 0.5f;
g.DrawLine(&mark, a, Gdiplus::PointF(a.X + (b.X - a.X) * k,
a.Y + (b.Y - a.Y) * k));
} else {
g.DrawLine(&mark, a, b);
float k = (tt - 0.5f) / 0.5f;
g.DrawLine(&mark, b, Gdiplus::PointF(b.X + (d.X - b.X) * k,
b.Y + (d.Y - b.Y) * k));
}
} else {
float off = Scl(11.0f) * tt;
g.DrawLine(&mark, cx - off, cy - off, cx + off, cy + off);
g.DrawLine(&mark, cx + off, cy - off, cx - off, cy + off);
}
}
// Status text.
{
Gdiplus::Font* f = Font(L"Segoe UI", Scl(12.0f * 96.0f / 72.0f), true);
float y = rect.Y + Scl(140) - LineHeight(g, f) / 2.0f;
DrawTextCentered(g, f, status_, cx, y, Hex(0xe3e7ef),
rect.Width - Scl(40));
}
// Target subtitle.
{
Gdiplus::Font* f = Font(L"Segoe UI", Scl(9.0f * 96.0f / 72.0f));
float y = rect.Y + Scl(162) - LineHeight(g, f) / 2.0f;
std::wstring sub =
L"PID " + std::to_wstring(pid_) + L" \u00B7 " + target_;
float maxW = rect.Width - Scl(60);
if (TextWidth(g, f, sub) > maxW) sub = ElideMiddle(g, f, sub, maxW);
DrawTextCentered(g, f, sub, cx, y, Hex(0x8a90a0), maxW + Scl(20));
}
// Progress bar.
{
float pbY = rect.GetBottom() - Scl(38);
float pbL = rect.X + Scl(64);
float pbR = rect.GetRight() - Scl(64);
float pbH = Scl(4);
Gdiplus::GraphicsPath* track =
RoundedRectPath(Gdiplus::RectF(pbL, pbY - pbH / 2, pbR - pbL, pbH),
pbH / 2);
Gdiplus::SolidBrush trackBrush(Rgba(255, 255, 255, 22));
g.FillPath(&trackBrush, track);
delete track;
// progress: crawl to 70% while waiting, then finish to 100%.
double p;
if (tResult_ < 0.0) {
p = 0.7 * EaseOutCubic(Since(t0_, kCrawl));
} else {
p = progressAtResult_ +
(1.0 - progressAtResult_) * EaseOutCubic(Since(tResult_, kFinish));
}
float fillW = (pbR - pbL) * static_cast<float>(p);
if (fillW > 0.5f) {
Gdiplus::GraphicsPath* fill = RoundedRectPath(
Gdiplus::RectF(pbL, pbY - pbH / 2, fillW, pbH), pbH / 2);
Gdiplus::LinearGradientBrush grad(
Gdiplus::RectF(pbL, pbY - pbH / 2, pbR - pbL, pbH),
injectDone && !ok_ ? Hex(0xe25656) : Hex(0x3d6fd1),
injectDone && !ok_ ? Hex(0xf08585) : Hex(0x7fb0ff),
Gdiplus::LinearGradientModeHorizontal);
g.FillPath(&grad, fill);
delete fill;
}
}
// Border.
{
Gdiplus::Pen border(Rgba(255, 255, 255, 28), 1.0f);
g.DrawPath(&border, panel);
}
delete panel;
}
LRESULT CALLBACK OverlayWindow::Thunk(HWND h, UINT m, WPARAM wp, LPARAM lp) {
if (m == WM_NCCREATE) {
auto* self = reinterpret_cast<OverlayWindow*>(
reinterpret_cast<CREATESTRUCTW*>(lp)->lpCreateParams);
SetWindowLongPtrW(h, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
self->hwnd_ = h;
}
auto* self =
reinterpret_cast<OverlayWindow*>(GetWindowLongPtrW(h, GWLP_USERDATA));
return self ? self->Handle(m, wp, lp) : DefWindowProcW(h, m, wp, lp);
}
LRESULT OverlayWindow::Handle(UINT m, WPARAM wp, LPARAM lp) {
switch (m) {
case WM_TIMER:
Tick();
return 0;
case WM_APP_INJECT_RESULT: {
auto* boxed = reinterpret_cast<std::wstring*>(lp);
std::wstring err = std::move(*boxed);
delete boxed;
if (tResult_ < 0.0) {
tResult_ = Now();
ok_ = wp != 0;
// Freeze the crawl value so the finish tween starts here.
double p = 0.7 * EaseOutCubic(Since(t0_, kCrawl));
progressAtResult_ = static_cast<float>(p);
status_ = ok_ ? L"Injection complete"
: L"Injection failed: " + err;
}
return 0;
}
case WM_DESTROY:
KillTimer(hwnd_, 1);
hwnd_ = nullptr;
return 0;
case WM_ERASEBKGND:
return 1;
default:
return DefWindowProcW(hwnd_, m, wp, lp);
}
}
} // namespace ui
+51
查看文件
@@ -0,0 +1,51 @@
#pragma once
//
// overlay_win.h — injection progress overlay (GDI+ replacement for
// InjectionOverlay).
//
// Frameless top-most layered window shown over the main window while the
// inject worker runs: spinner + crawling progress bar, then an animated
// check / cross mark, a short hold and a fade-out before completed() fires.
//
#include "wgfx.h"
#include <functional>
#include <memory>
#include <string>
#include <thread>
namespace ui {
class OverlayWindow {
public:
// Fired (on the UI thread) after the finish sequence, right before the
// window destroys itself.
std::function<void(bool ok)> onCompleted;
// `centerOver` — window to centre over (the main window).
void Show(HWND centerOver, unsigned long pid, const std::wstring& target);
private:
static LRESULT CALLBACK Thunk(HWND, UINT, WPARAM, LPARAM);
LRESULT Handle(UINT, WPARAM, LPARAM);
void Render();
void Tick();
HWND hwnd_ = nullptr;
LayeredCanvas canvas_;
std::unique_ptr<std::thread> worker_;
unsigned long pid_ = 0;
std::wstring target_;
std::wstring status_ = L"Injecting OpenZen…";
double t0_ = 0.0; // sequence start
double tResult_ = -1.0; // when the worker returned
float progressAtResult_ = 0.0f;
bool ok_ = false;
int w_ = 0, h_ = 0;
bool completed_ = false;
};
} // namespace ui
+71
查看文件
@@ -0,0 +1,71 @@
#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);
}
WindowInfo wi = window_info_for(jp.pid);
jp.window_title = std::move(wi.title);
jp.window_class = std::move(wi.class_name);
result.push_back(std::move(jp));
} while (Process32NextW(snap, &pe));
CloseHandle(snap);
return result;
}
} // namespace loader
+228
查看文件
@@ -0,0 +1,228 @@
#include "splash_win.h"
#include <cmath>
namespace ui {
namespace {
constexpr int kWidth = 520;
constexpr int kHeight = 280;
constexpr float kCornerRadius = 18.0f;
constexpr double kTWinIn = 0.18; // window fade-in duration
constexpr double kTLogoIn = 0.32; // wordmark alpha duration
constexpr double kTLogoZoom = 0.48; // wordmark scale duration
constexpr double kTGlow = 0.60; // glow pulse 0 -> 1 -> 0.35
constexpr double kTScan = 0.58; // scan rail head travel
constexpr double kTPause = 0.10; // hold between phase 1 and fade-out
constexpr double kTFadeOut = 0.22; // window fade-out duration
constexpr double kTTotal = kTGlow + kTPause + kTFadeOut;
} // namespace
void SplashWindow::Show() {
w_ = static_cast<int>(kWidth * g_scale);
h_ = static_cast<int>(kHeight * g_scale);
WNDCLASSW wc{};
wc.lpfnWndProc = &SplashWindow::Thunk;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = L"OZLoaderSplash";
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
RegisterClassW(&wc);
hwnd_ = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
wc.lpszClassName, L"", WS_POPUP, 0, 0, w_, h_,
nullptr, nullptr, wc.hInstance, this);
// Centre on the primary monitor's work area.
RECT wa{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0);
int x = (wa.left + wa.right - w_) / 2;
int y = (wa.top + wa.bottom - h_) / 2;
SetWindowPos(hwnd_, HWND_TOPMOST, x, y, w_, h_,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
t0_ = Now();
SetTimer(hwnd_, 1, 16, nullptr);
}
void SplashWindow::Tick() {
double t = Now() - t0_;
if (t >= kTTotal) {
if (!notified_) {
notified_ = true;
if (onFinished) onFinished();
}
DestroyWindow(hwnd_);
return;
}
Render();
// Whole-window opacity: fade-in, hold, fade-out.
double winOp;
if (t < kTWinIn) {
winOp = EaseOutCubic(t / kTWinIn);
} else if (t < kTGlow + kTPause) {
winOp = 1.0;
} else {
winOp = 1.0 - EaseInCubic((t - kTGlow - kTPause) / kTFadeOut);
}
canvas_.Present(hwnd_, static_cast<BYTE>(winOp * 255.0 + 0.5));
}
void SplashWindow::Render() {
if (!canvas_.Resize(w_, h_)) return;
Gdiplus::Graphics& g = canvas_.g();
canvas_.Clear();
double t = Now() - t0_;
double logoAlpha = Phase(t, 0.0, kTLogoIn, EaseOutCubic);
double logoScale = 0.55 + 0.45 * Phase(t, 0.0, kTLogoZoom, EaseOutBack);
double glowPulse;
if (t < kTGlow / 2.0) {
glowPulse = EaseInOutSine(t / (kTGlow / 2.0));
} else {
double u = Phase(t, kTGlow / 2.0, kTGlow, EaseInOutSine);
glowPulse = 1.0 - 0.65 * u; // 1 -> 0.35
}
double scan = Phase(t, 0.0, kTScan, EaseInOutQuad);
Gdiplus::RectF rect(0.5f, 0.5f, w_ - 1.0f, h_ - 1.0f);
Gdiplus::GraphicsPath* panel = RoundedRectPath(rect, kCornerRadius * static_cast<float>(g_scale));
// Layered background: dark vertical gradient + accent glow.
const float ccx = rect.X + rect.Width / 2.0f;
const float ccy = rect.Y + rect.Height / 2.0f;
{
Gdiplus::LinearGradientBrush bg(rect, Hex(0x16181f), Hex(0x0a0b0e),
Gdiplus::LinearGradientModeVertical);
g.FillPath(&bg, panel);
}
{
// Radial glow behind the wordmark; radius/alpha track glowPulse.
float r = rect.Width * static_cast<float>(0.45 + 0.07 * glowPulse);
float glowCy = ccy - 4.0f * static_cast<float>(g_scale);
Gdiplus::GraphicsPath glowPath;
glowPath.AddEllipse(ccx - r, glowCy - r, r * 2.0f, r * 2.0f);
int peak = static_cast<int>(120 * (0.45 + 0.55 * glowPulse));
Gdiplus::PathGradientBrush glow(&glowPath);
glow.SetCenterPoint(Gdiplus::PointF(ccx, glowCy));
glow.SetCenterColor(Rgba(85, 135, 235, peak));
Gdiplus::Color surround[] = {Rgba(85, 135, 235, peak / 4),
Rgba(85, 135, 235, 0)};
INT surroundCount = 2;
glow.SetSurroundColors(surround, &surroundCount);
g.FillRectangle(&glow, rect);
}
// Wordmark: scale around the centre, soft drop shadow for legibility.
{
float logoPx = 40.0f * (96.0f / 72.0f) * static_cast<float>(g_scale); // 40pt -> px
Gdiplus::Font* f = Font(L"Segoe UI", logoPx, /*bold=*/true);
const std::wstring text = L"OpenZen";
// Per-character advance * 1.02 reproduces Qt's 102% letter spacing.
float textW = 0.0f;
for (wchar_t c : text) {
std::wstring one(1, c);
textW += TextWidth(g, f, one) * 1.02f;
}
float lineH = LineHeight(g, f);
float cx = ccx;
float cy = ccy;
float left = cx - textW / 2.0f;
float baseY = cy - lineH / 2.0f - 1.0f * static_cast<float>(g_scale);
g.TranslateTransform(cx, cy);
g.ScaleTransform(static_cast<float>(logoScale), static_cast<float>(logoScale));
g.TranslateTransform(-cx, -cy);
{
Gdiplus::SolidBrush shadow(Rgba(0, 0, 0, static_cast<int>(90 * logoAlpha)));
float x = left;
for (wchar_t c : text) {
std::wstring one(1, c);
float w = TextWidth(g, f, one);
g.DrawString(one.c_str(), 1, f,
Gdiplus::RectF(x + 1.0f, baseY + 1.0f, w + 40.0f, lineH + 12.0f),
&NearFormat(), &shadow);
x += w * 1.02f;
}
}
{
Gdiplus::SolidBrush white(Gdiplus::Color(
static_cast<BYTE>(255 * logoAlpha), 255, 255, 255));
float x = left;
for (wchar_t c : text) {
std::wstring one(1, c);
float w = TextWidth(g, f, one);
g.DrawString(one.c_str(), 1, f,
Gdiplus::RectF(x, baseY, w + 40.0f, lineH + 12.0f),
&NearFormat(), &white);
x += w * 1.02f;
}
}
g.ResetTransform();
}
// Scan rail near the bottom: faint baseline, gradient trail, bright head.
{
float trackY = rect.GetBottom() - 38.0f * static_cast<float>(g_scale);
float trackL = rect.X + 70.0f * static_cast<float>(g_scale);
float trackR = rect.GetRight() - 70.0f * static_cast<float>(g_scale);
float trackW = trackR - trackL;
Gdiplus::Pen basePen(Rgba(255, 255, 255, 30), 1.0f);
g.DrawLine(&basePen, trackL, trackY, trackR, trackY);
float headX = trackL + trackW * static_cast<float>(scan);
float tailStart = headX - 140.0f * static_cast<float>(g_scale);
if (tailStart < trackL) tailStart = trackL;
Gdiplus::LinearGradientBrush trail(
Gdiplus::RectF(tailStart, trackY - 2.0f, headX - tailStart + 0.01f, 4.0f),
Rgba(80, 130, 230, 0), Rgba(130, 180, 255, 230),
Gdiplus::LinearGradientModeHorizontal);
Gdiplus::Pen trailPen(&trail, 2.0f);
if (headX > tailStart) g.DrawLine(&trailPen, tailStart, trackY, headX, trackY);
Gdiplus::SolidBrush head(Rgba(180, 210, 255, 230));
g.FillEllipse(&head, headX - 3.5f, trackY - 3.5f, 7.0f, 7.0f);
}
// Hairline border.
{
Gdiplus::Pen border(Rgba(255, 255, 255, 28), 1.0f);
g.DrawPath(&border, panel);
}
delete panel;
}
LRESULT CALLBACK SplashWindow::Thunk(HWND h, UINT m, WPARAM wp, LPARAM lp) {
if (m == WM_NCCREATE) {
auto* self = reinterpret_cast<SplashWindow*>(
reinterpret_cast<CREATESTRUCTW*>(lp)->lpCreateParams);
SetWindowLongPtrW(h, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
self->hwnd_ = h;
}
auto* self = reinterpret_cast<SplashWindow*>(GetWindowLongPtrW(h, GWLP_USERDATA));
return self ? self->Handle(m, wp, lp) : DefWindowProcW(h, m, wp, lp);
}
LRESULT SplashWindow::Handle(UINT m, WPARAM wp, LPARAM lp) {
switch (m) {
case WM_TIMER:
Tick();
return 0;
case WM_DESTROY:
KillTimer(hwnd_, 1);
hwnd_ = nullptr;
return 0;
case WM_ERASEBKGND:
return 1;
default:
return DefWindowProcW(hwnd_, m, wp, lp);
}
}
} // namespace ui
+37
查看文件
@@ -0,0 +1,37 @@
#pragma once
//
// splash_win.h — cold-start splash (GDI+ replacement for SplashScreen).
//
// Frameless, top-most, per-pixel-alpha layered window that animates the
// OpenZen wordmark (scale-in with overshoot + fade), a pulsing accent glow
// and a scanning rail, then fades out and invokes onFinished (~920 ms).
//
#include "wgfx.h"
#include <functional>
namespace ui {
class SplashWindow {
public:
// Invoked once when the splash sequence completes (before the window is
// destroyed). Wired by main.cpp to MainWindow::PlayEntrance().
std::function<void()> onFinished;
void Show();
private:
static LRESULT CALLBACK Thunk(HWND, UINT, WPARAM, LPARAM);
LRESULT Handle(UINT, WPARAM, LPARAM);
void Render();
void Tick();
HWND hwnd_ = nullptr;
LayeredCanvas canvas_;
double t0_ = 0.0;
bool notified_ = false;
int w_ = 0, h_ = 0;
};
} // namespace ui
+301
查看文件
@@ -0,0 +1,301 @@
#include "wgfx.h"
#include <chrono>
#include <cwchar>
#include <random>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
namespace ui {
double g_scale = 1.0;
void InitDpi() {
// Per-monitor v2 when available; plain aware as fallback. Either way the
// process stops getting bitmap-stretched by DWM and we can scale layout
// metrics by the real DPI.
using SetCtxFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT);
HMODULE user32 = GetModuleHandleW(L"user32.dll");
if (auto fn = reinterpret_cast<SetCtxFn>(
reinterpret_cast<void*>(GetProcAddress(
user32, "SetProcessDpiAwarenessContext")))) {
if (!fn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
fn(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
}
} else {
SetProcessDPIAware();
}
HDC dc = GetDC(nullptr);
g_scale = GetDeviceCaps(dc, LOGPIXELSX) / 96.0;
ReleaseDC(nullptr, dc);
}
double Now() {
using namespace std::chrono;
return duration<double>(steady_clock::now().time_since_epoch()).count();
}
double EaseLinear(double t) { return t; }
double EaseOutCubic(double t) {
double u = 1.0 - t;
return 1.0 - u * u * u;
}
double EaseInCubic(double t) { return t * t * t; }
double EaseInOutSine(double t) { return 0.5 - 0.5 * cos(t * 3.14159265358979); }
double EaseInOutQuad(double t) {
return t < 0.5 ? 2.0 * t * t : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0;
}
double EaseOutBack(double t) {
// Mirrors Qt's OutBack default overshoot (s = 1.70158).
const double s = 1.70158;
double u = t - 1.0;
return 1.0 + (s + 1.0) * u * u * u + s * u * u;
}
double Since(double start, double dur) {
if (dur <= 0.0) return 1.0;
double t = (Now() - start) / dur;
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
return t;
}
double Phase(double t, double t0, double t1, double (*ease)(double)) {
if (t <= t0) return 0.0;
if (t >= t1) return 1.0;
return ease((t - t0) / (t1 - t0));
}
Gdiplus::Color Rgba(int r, int g, int b, int a) {
return Gdiplus::Color(static_cast<BYTE>(a), static_cast<BYTE>(r),
static_cast<BYTE>(g), static_cast<BYTE>(b));
}
Gdiplus::Color Hex(unsigned rgb, int a) {
return Rgba((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff, a);
}
Gdiplus::GraphicsPath* RoundedRectPath(const Gdiplus::RectF& r, float radius) {
auto* p = new Gdiplus::GraphicsPath();
float rad = radius;
float m = r.Width < r.Height ? r.Width : r.Height;
m /= 2.0f;
if (rad > m) rad = m;
if (rad <= 0.0f) {
p->AddRectangle(r);
return p;
}
float d = rad * 2.0f;
p->AddArc(r.X, r.Y, d, d, 180.0f, 90.0f);
p->AddArc(r.X + r.Width - d, r.Y, d, d, 270.0f, 90.0f);
p->AddArc(r.X + r.Width - d, r.Y + r.Height - d, d, d, 0.0f, 90.0f);
p->AddArc(r.X, r.Y + r.Height - d, d, d, 90.0f, 90.0f);
p->CloseFigure();
return p;
}
namespace {
std::map<std::wstring, Gdiplus::Font*>& FontCache() {
static std::map<std::wstring, Gdiplus::Font*> cache;
return cache;
}
const Gdiplus::FontFamily& Family(const wchar_t* name) {
static std::map<std::wstring, Gdiplus::FontFamily*> fams;
auto it = fams.find(name);
if (it != fams.end()) return *it->second;
auto* f = new Gdiplus::FontFamily(name);
if (f->GetLastStatus() != Gdiplus::Ok) {
delete f;
f = new Gdiplus::FontFamily(L"Segoe UI"); // fallback
}
fams[name] = f;
return *f;
}
} // namespace
Gdiplus::StringFormat& NearFormat() {
static StringFormat sf(StringFormat::GenericDefault());
return sf;
}
Gdiplus::Font* Font(const wchar_t* family, float px, bool bold, bool italic) {
int style = bold ? FontStyleBold : FontStyleRegular;
if (italic) style |= FontStyleItalic;
wchar_t key[128];
_snwprintf_s(key, _TRUNCATE, L"%s|%.1f|%d", family, px, style);
auto& cache = FontCache();
auto it = cache.find(key);
if (it != cache.end()) return it->second;
auto* f = new Gdiplus::Font(&Family(family), px, style, UnitPixel);
cache[key] = f;
return f;
}
float LineHeight(Gdiplus::Graphics& g, Gdiplus::Font* f) {
return f->GetHeight(&g);
}
float TextWidth(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s) {
Gdiplus::RectF bounds;
g.MeasureString(s.c_str(), static_cast<INT>(s.size()), f,
Gdiplus::RectF(0, 0, 10000, 10000), &NearFormat(), &bounds);
return bounds.Width;
}
void DrawText(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s,
float x, float y, const Gdiplus::Color& c, float wrapWidth) {
Gdiplus::SolidBrush brush(c);
Gdiplus::RectF layout(x, y, wrapWidth, LineHeight(g, f) * 3.0f + 4.0f);
g.DrawString(s.c_str(), static_cast<INT>(s.size()), f, layout,
&NearFormat(), &brush);
}
void DrawTextCentered(Gdiplus::Graphics& g, Gdiplus::Font* f,
const std::wstring& s, float centerX, float y,
const Gdiplus::Color& c, float wrapWidth) {
Gdiplus::SolidBrush brush(c);
Gdiplus::RectF layout(centerX - wrapWidth / 2.0f, y, wrapWidth,
LineHeight(g, f) * 3.0f + 4.0f);
StringFormat sf(&NearFormat());
sf.SetAlignment(StringAlignmentCenter);
g.DrawString(s.c_str(), static_cast<INT>(s.size()), f, layout, &sf,
&brush);
}
std::wstring ElideMiddle(Gdiplus::Graphics& g, Gdiplus::Font* f,
const std::wstring& s, float maxW) {
if (TextWidth(g, f, s) <= maxW || s.size() < 3) return s;
size_t head = s.size() / 2, tail = s.size() / 2;
// Shrink the middle one char at a time until it fits, then insert an
// ellipsis in the gap.
while (head > 0 && tail < s.size()) {
std::wstring cut = s.substr(0, head) + L'\u2026' + s.substr(tail);
if (TextWidth(g, f, cut) <= maxW) return cut;
// Trim alternately from the middle outwards.
if (head > tail - s.size() / 2) --head; else ++tail;
}
return s.substr(0, head) + L'\u2026';
}
// ----- LayeredCanvas -----
LayeredCanvas::~LayeredCanvas() { Free(); }
void LayeredCanvas::Free() {
delete gfx_;
delete wrap_;
if (bmp_) DeleteObject(bmp_);
if (dc_) DeleteDC(dc_);
gfx_ = nullptr;
wrap_ = nullptr;
bmp_ = nullptr;
dc_ = nullptr;
bits_ = nullptr;
w_ = h_ = 0;
}
bool LayeredCanvas::Resize(int w, int h) {
if (w < 1 || h < 1) return false;
if (w == w_ && h == h_) return true;
Free();
BITMAPINFO bi{};
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bi.bmiHeader.biWidth = w;
bi.bmiHeader.biHeight = -h; // top-down
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = 32;
bi.bmiHeader.biCompression = BI_RGB;
dc_ = CreateCompatibleDC(nullptr);
bmp_ = CreateDIBSection(nullptr, &bi, DIB_RGB_COLORS, &bits_, nullptr, 0);
if (!dc_ || !bmp_ || !bits_) {
Free();
return false;
}
SelectObject(dc_, bmp_);
// Wrap the DIB memory as a premultiplied-ARGB GDI+ bitmap: everything
// drawn through gfx_ lands directly in the DIB, already in the exact
// format UpdateLayeredWindow expects.
wrap_ = new Gdiplus::Bitmap(w, h, w * 4, PixelFormat32bppPARGB,
static_cast<BYTE*>(bits_));
gfx_ = new Gdiplus::Graphics(wrap_);
if (gfx_->GetLastStatus() != Gdiplus::Ok) {
Free();
return false;
}
gfx_->SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
gfx_->SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
gfx_->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
w_ = w;
h_ = h;
return true;
}
void LayeredCanvas::Clear() {
if (gfx_) gfx_->Clear(Gdiplus::Color(0, 0, 0, 0));
}
Gdiplus::Graphics& LayeredCanvas::g() { return *gfx_; }
bool LayeredCanvas::Present(HWND hwnd, BYTE constAlpha) {
if (!dc_ || !hwnd) return false;
HDC screen = GetDC(nullptr);
POINT src{0, 0};
SIZE sz{w_, h_};
BLENDFUNCTION bf{AC_SRC_OVER, 0, constAlpha, AC_SRC_ALPHA};
BOOL ok = UpdateLayeredWindow(hwnd, screen, nullptr, &sz, dc_, &src, 0,
&bf, ULW_ALPHA);
ReleaseDC(nullptr, screen);
return ok != FALSE;
}
std::wstring RandomIdent(int minLen, int maxLen) {
static const wchar_t kAlphabet[] =
L"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const int kLetters = 52;
const int kAll = static_cast<int>(wcslen(kAlphabet));
std::mt19937 rng(static_cast<unsigned>(
std::random_device{}() ^
(std::hash<double>{}(Now()) << 1)));
int len = minLen + static_cast<int>(rng() % (maxLen - minLen + 1));
std::wstring s;
s.reserve(len);
s += kAlphabet[rng() % kLetters];
for (int i = 1; i < len; ++i) s += kAlphabet[rng() % kAll];
return s;
}
void LogLine(const std::wstring& msg) {
wchar_t temp[MAX_PATH]{};
GetTempPathW(MAX_PATH, temp);
std::wstring path = std::wstring(temp) + L"openzen-loader.log";
HANDLE f = CreateFileW(path.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ,
nullptr, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (f == INVALID_HANDLE_VALUE) return;
SYSTEMTIME st{};
GetLocalTime(&st);
wchar_t line[1024];
_snwprintf_s(line, _TRUNCATE,
L"[%02u:%02u:%02u.%03u] %s\r\n",
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
msg.c_str());
DWORD written = 0;
WriteFile(f, line, static_cast<DWORD>(wcslen(line) * sizeof(wchar_t)),
&written, nullptr);
CloseHandle(f);
}
} // namespace ui
+112
查看文件
@@ -0,0 +1,112 @@
#pragma once
//
// wgfx.h — minimal GDI+ drawing toolkit for the Win32 OpenZen loader UI.
//
// The loader GUI used to be Qt Widgets (static Qt6 via vcpkg). That was the
// single heaviest build dependency (30+ min first build, mirror workarounds
// for github downloads). The whole UI is now owner-drawn with GDI+ — which
// ships with Windows — onto per-pixel-alpha layered windows, reproducing the
// old look: rounded gradient panels, hover animations, pulsing indicators,
// spinner/progress overlay, splash, etc. No third-party dependencies remain.
//
#include <windows.h>
#include <objidl.h> // IStream etc. — skipped by WIN32_LEAN_AND_MEAN, gdiplus needs it
#include <gdiplus.h>
#include <functional>
#include <map>
#include <string>
namespace ui {
// Global UI scale (device pixels per 96-dpi logical pixel).
extern double g_scale;
// Enables per-monitor DPI awareness and fills in g_scale. Call once before
// creating any windows.
void InitDpi();
// Seconds from a monotonic clock (animation timelines).
double Now();
// --- easing curves (same shapes as the QEasingCurve values the Qt UI used) ---
double EaseLinear(double t);
double EaseOutCubic(double t);
double EaseInCubic(double t);
double EaseInOutSine(double t);
double EaseInOutQuad(double t);
double EaseOutBack(double t);
// Clamped 0..1 progress of `t` seconds elapsed since `start`.
double Since(double start, double dur);
// Maps 0..1 sub-range [t0,t1] of `t` through `ease`.
double Phase(double t, double t0, double t1, double (*ease)(double));
Gdiplus::Color Rgba(int r, int g, int b, int a = 255);
// Parsed "#rrggbb".
Gdiplus::Color Hex(unsigned rgb, int a = 255);
// Rounded-rectangle path (caller deletes).
Gdiplus::GraphicsPath* RoundedRectPath(const Gdiplus::RectF& r, float radius);
// --- fonts (cached) ---
Gdiplus::Font* Font(const wchar_t* family, float px,
bool bold = false, bool italic = false);
Gdiplus::StringFormat& NearFormat();
float LineHeight(Gdiplus::Graphics& g, Gdiplus::Font* f);
float TextWidth(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s);
void DrawText(Gdiplus::Graphics& g, Gdiplus::Font* f, const std::wstring& s,
float x, float y, const Gdiplus::Color& c,
float wrapWidth = 10000.0f);
void DrawTextCentered(Gdiplus::Graphics& g, Gdiplus::Font* f,
const std::wstring& s, float centerX, float y,
const Gdiplus::Color& c, float wrapWidth = 10000.0f);
// Middle ellipsis when the string measures wider than maxW.
std::wstring ElideMiddle(Gdiplus::Graphics& g, Gdiplus::Font* f,
const std::wstring& s, float maxW);
// A layered-window canvas: draws into a 32bpp PARGB DIB section and composes
// it with UpdateLayeredWindow (per-pixel alpha — the Win32 equivalent of
// Qt::FramelessWindowHint + WA_TranslucentBackground).
class LayeredCanvas {
public:
LayeredCanvas() = default;
~LayeredCanvas();
LayeredCanvas(const LayeredCanvas&) = delete;
LayeredCanvas& operator=(const LayeredCanvas&) = delete;
bool Resize(int w, int h); // (re)allocate the ARGB surface
void Clear(); // fill fully transparent
Gdiplus::Graphics& g(); // draw here after Clear()
// Composites the surface onto `hwnd` at its current position with an
// extra whole-window constant alpha (0..255) used for fades.
bool Present(HWND hwnd, BYTE constAlpha = 255);
int width() const { return w_; }
int height() const { return h_; }
private:
void Free();
HDC dc_ = nullptr;
HBITMAP bmp_ = nullptr;
void* bits_ = nullptr;
Gdiplus::Bitmap* wrap_ = nullptr; // GDI+ view over the DIB memory
Gdiplus::Graphics* gfx_ = nullptr;
int w_ = 0, h_ = 0;
};
// Random alphanumeric identifier (first char a letter), used for the
// non-constant Win32 window titles that defeat fixed-string scanners.
std::wstring RandomIdent(int minLen, int maxLen);
// Appends a line to %TEMP%\openzen-loader.log (startup diagnostics).
void LogLine(const std::wstring& msg);
} // namespace ui
+46
查看文件
@@ -0,0 +1,46 @@
#include "loader.h"
namespace loader {
namespace {
struct Search {
DWORD pid;
WindowInfo 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.title.size()) {
wchar_t cls[256] = {0};
GetClassNameW(hwnd, cls, 256);
s->best.title = std::move(title);
s->best.class_name = cls;
}
return TRUE;
}
}
WindowInfo window_info_for(DWORD pid) {
Search s{pid, {}};
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
return s.best;
}
} // namespace loader