feat(loader): Qt6 GUI + strict wrap matcher + scaffold tweaks

Loader UI
  - Replace the Win32 ListView loader window with a Qt6 Widgets
    one. main.cpp + new MainWindow.h/cpp: QTableWidget showing
    PID / Window Title / Window Class, QTimer auto-refresh every
    1 s, double-click a row to inject. Title filter is built in
    (startsWith "Minecraft" and not containing "Launcher"), so
    HMCL / MultiMC / generic Java apps never show up.
  - Dark Fusion theme + inline QSS so the single EXE looks the
    same on any Windows version.
  - native/vcpkg.json declares qtbase[widgets] and build.gradle's
    new findVcpkg() detects VCPKG_ROOT / C:/vcpkg / D:/vcpkg /
    ~/vcpkg / D:/vcpkg-<version> and passes
    -DCMAKE_TOOLCHAIN_FILE + -DVCPKG_TARGET_TRIPLET=
    x64-windows-static so Qt is fully statically linked. Final
    OpenZenLoader.exe stays single-file (~30 MB) with no Qt6*.dll
    or vcruntime/msvcp DLL imports beyond Windows system DLLs.
  - Drop the old ui.cpp and the now-unused run_ui() entry on
    loader.h.

Patch transformer
  - asm.patchify.loader.PatchTransformer.wrapInvoke now tries a
    strict owner+name+desc match first, falling back to the
    historical (owner || name) matcher only if strict turns up
    nothing. The old loose matcher counted every Mth.*(FFF)F call
    against a wrap aimed at Mth.lerp(FFF)F, so a same-method
    sibling wrap that deleted its own site shifted later slice
    indices and made onRenderPitchLerp miss.

LivingEntityRendererPatch
  - With the strict matcher in place, onRenderPitchLerp's slice
    drops from (4,4) to (1,1) - there is exactly one
    Mth.lerp(FFF)F call site in LivingEntityRenderer.render
    (pitch lerp), as verified via javap on the runtime srg jar.
    onRenderHeadYawLerp keeps slice (2,2) (2nd of three
    Mth.rotLerp calls = head yaw). No more "no call site of
    m_14179_" warning at boot.

Scaffold
  - Rename the BooleanSetting field from advancedBlockSearch to
    sneak so the Java name matches the user-facing label ("Sneak").

README
  - Add the vcpkg / Qt prerequisites and update the injector
    usage section to describe the auto-refreshing UI + double-
    click flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Shirona1337
2026-05-23 21:59:01 +08:00
parent bfb0c0e464
commit 9a72330c85
12 changed files with 383 additions and 272 deletions
+13 -4
View File
@@ -84,6 +84,15 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
1. `PATH` 上的 `cmake.exe`
2. 通过 `vswhere.exe` 找 VS 2022 自带的 CMake
3. 常见独立安装位置 (`%ProgramFiles%\CMake\bin\cmake.exe` 等)
4. **vcpkg**(注入器 GUI 用 Qt6,由 vcpkg 提供静态库)。一次性安装:
```powershell
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat
```
Gradle 检测顺序:环境变量 `VCPKG_ROOT` → `C:\vcpkg` → `D:\vcpkg` → `%USERPROFILE%\vcpkg`。
**首次** `./gradlew dll` 时 vcpkg 会按 `native/vcpkg.json` 编译静态 Qt6(30 分钟到 2 小时,看 CPU),之后增量 build 几分钟。Qt 完全静态链接进 EXE,所以分发依然单文件——`OpenZenLoader.exe` 自带 Qt6 + OpenZen.dll,零运行时依赖。
#### 构建命令
@@ -103,10 +112,10 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
#### 使用注入器
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
2. 双击 `OpenZenLoader.exe`。
3. 列表里找到对应的 `javaw.exe`(看 Window Title 列,应是 `Minecraft 1.20.1` 一类)。
4. 点 Inject。
5. 看到 `Injection ok. Watch %TEMP%\openzen.log for Java side.` 即成功。Minecraft 内右 Shift 触发 ClickGUI 验证。
2. 双击 `OpenZenLoader.exe`(会弹一次 UAC 提权)。
3. GUI 自动列出系统里**所有 Minecraft 实例**(按窗口标题 startsWith `Minecraft` 且不含 `Launcher` 过滤),每秒自动刷新一次。
4. **双击对应的行**就开始注入。
5. 状态栏出现 `Injected into PID ...` 即成功;Minecraft 内右 Shift 触发 ClickGUI 验证。
诊断日志:
- Native 端:`%TEMP%\openzen.log`
+41 -2
View File
@@ -311,6 +311,39 @@ def cmakeMissingMessage =
' - Visual Studio 2022 with "C++ CMake tools for Windows" workload\n' +
'After installing, re-run ./gradlew dll'
ext.findVcpkg = {
def envRoot = System.getenv('VCPKG_ROOT')
def candidates = []
if (envRoot) candidates << envRoot
candidates += ['C:/vcpkg', 'D:/vcpkg', "${System.getProperty('user.home')}/vcpkg"]
// Also accept versioned install dirs like C:/vcpkg-2026.03.18.
['C:/', 'D:/'].each { drive ->
def root = file(drive)
if (root.isDirectory()) {
root.listFiles().each { f ->
if (f.isDirectory() && f.name ==~ /^vcpkg(-.*)?$/) {
candidates << f.absolutePath.replace('\\', '/')
}
}
}
}
for (root in candidates) {
if (root && file("$root/scripts/buildsystems/vcpkg.cmake").exists()) {
return root
}
}
return null
}
def vcpkgMissingMessage =
'vcpkg not found. The Qt-based loader needs vcpkg for static Qt6.\n' +
'One-time setup:\n' +
' git clone https://github.com/microsoft/vcpkg.git C:/vcpkg\n' +
' C:/vcpkg/bootstrap-vcpkg.bat\n' +
'Optionally set VCPKG_ROOT, or put it at C:/vcpkg or D:/vcpkg.\n' +
'Then re-run ./gradlew dll - the first run will build static Qt\n' +
'(can take 30 min to 2 h); subsequent builds are fast.'
tasks.register('stageNativeJar', Copy) {
group = 'openzen'
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
@@ -330,8 +363,14 @@ tasks.register('configureNative', Exec) {
doFirst {
def cmake = findCmake()
if (!cmake) throw new GradleException(cmakeMissingMessage)
logger.lifecycle("Using CMake: ${cmake}")
commandLine cmake, '-S', '.', '-B', 'build', '-A', 'x64'
def vcpkg = findVcpkg()
if (!vcpkg) throw new GradleException(vcpkgMissingMessage)
logger.lifecycle("Using CMake : ${cmake}")
logger.lifecycle("Using vcpkg : ${vcpkg}")
commandLine cmake, '-S', '.', '-B', 'build', '-A', 'x64',
"-DCMAKE_TOOLCHAIN_FILE=${vcpkg}/scripts/buildsystems/vcpkg.cmake",
'-DVCPKG_TARGET_TRIPLET=x64-windows-static',
'-DVCPKG_HOST_TRIPLET=x64-windows-static'
}
}
+14 -9
View File
@@ -1,3 +1,10 @@
# Qt-side toggles: AUTOMOC for our Q_OBJECT classes, AUTORCC for any .qrc.
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 6.4 COMPONENTS Widgets REQUIRED)
# 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")
@@ -10,14 +17,15 @@ add_custom_command(
)
add_custom_target(stage_dll_for_loader DEPENDS "${EMBED_DIR}/OpenZen.dll")
add_executable(OpenZenLoader WIN32
qt_add_executable(OpenZenLoader WIN32
src/main.cpp
src/MainWindow.cpp
src/MainWindow.h
src/process_list.cpp
src/injector.cpp
src/manual_map.cpp
src/embedded_dll.cpp
src/window_title.cpp
src/ui.cpp
res/loader.rc
)
@@ -35,7 +43,7 @@ set_source_files_properties(res/loader.rc PROPERTIES
)
target_link_libraries(OpenZenLoader PRIVATE
comctl32
Qt6::Widgets
psapi
shlwapi
user32
@@ -46,12 +54,9 @@ set_target_properties(OpenZenLoader PROPERTIES
)
if(MSVC)
# requireAdministrator: ensures the loader can OpenProcess /
# VirtualAllocEx / CreateRemoteThread against javaw.exe even when the
# Minecraft launcher (HMCL, MultiMC) was itself started elevated, and
# avoids the silent injection failure that happens on locked-down user
# accounts. The user will see one UAC consent prompt when launching
# OpenZenLoader.exe.
# 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'\""
)
+220
View File
@@ -0,0 +1,220 @@
#include "MainWindow.h"
#include "loader.h"
#include <QApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QMessageBox>
#include <QString>
#include <QTableWidget>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <string>
namespace loader {
namespace {
QString fromW(const std::wstring& w) {
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
}
bool isMinecraft(const std::wstring& title) {
static const std::wstring prefix = L"Minecraft";
static const std::wstring banned = L"Launcher";
if (title.size() < prefix.size()) return false;
if (title.compare(0, prefix.size(), prefix) != 0) return false;
return title.find(banned) == std::wstring::npos;
}
} // namespace
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent) {
styleApp();
buildUi();
timer_ = new QTimer(this);
timer_->setInterval(1000);
connect(timer_, &QTimer::timeout, this, &MainWindow::refreshNow);
timer_->start();
refreshNow();
}
void MainWindow::buildUi() {
setWindowTitle(QStringLiteral("OpenZen Loader"));
resize(720, 460);
auto* central = new QWidget(this);
auto* layout = new QVBoxLayout(central);
layout->setContentsMargins(16, 16, 16, 12);
layout->setSpacing(10);
auto* title = new QLabel(QStringLiteral("Minecraft Instances"), central);
title->setObjectName("title");
hint_ = new QLabel(
QStringLiteral("Double-click a row to inject. List refreshes every second."),
central);
hint_->setObjectName("hint");
table_ = new QTableWidget(0, 3, central);
table_->setHorizontalHeaderLabels({
QStringLiteral("PID"),
QStringLiteral("Window Title"),
QStringLiteral("Window Class"),
});
table_->verticalHeader()->setVisible(false);
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
table_->setSelectionMode(QAbstractItemView::SingleSelection);
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
table_->setAlternatingRowColors(true);
table_->setShowGrid(false);
table_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
table_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
table_->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
table_->horizontalHeader()->setHighlightSections(false);
connect(table_, &QTableWidget::cellDoubleClicked,
this, &MainWindow::onRowDoubleClicked);
status_ = new QLabel(QStringLiteral("Ready."), central);
status_->setObjectName("status");
status_->setWordWrap(true);
layout->addWidget(title);
layout->addWidget(hint_);
layout->addWidget(table_, 1);
layout->addWidget(status_);
setCentralWidget(central);
}
void MainWindow::styleApp() {
// Modern dark palette via QSS - keeps the EXE self-contained (no theme
// file shipped separately) and looks consistent across Windows versions.
static const char* kQss = R"qss(
QMainWindow, QWidget {
background: #1c1d22;
color: #e3e5ea;
font-family: "Segoe UI", "Microsoft YaHei UI", sans-serif;
font-size: 13px;
}
QLabel#title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
QLabel#hint {
color: #8a8e98;
font-size: 12px;
}
QLabel#status {
color: #c2c6cf;
padding: 6px 10px;
border: 1px solid #2c2e35;
border-radius: 6px;
background: #23252b;
}
QTableWidget {
background: #23252b;
alternate-background-color: #1f2126;
gridline-color: #2c2e35;
border: 1px solid #2c2e35;
border-radius: 8px;
selection-background-color: #3d6fd1;
selection-color: #ffffff;
}
QTableWidget::item {
padding: 6px 8px;
}
QHeaderView::section {
background: #2a2c33;
color: #cfd2da;
padding: 6px 10px;
border: none;
border-right: 1px solid #1c1d22;
font-weight: 600;
}
QHeaderView::section:last {
border-right: none;
}
QTableCornerButton::section {
background: #2a2c33;
border: none;
}
QScrollBar:vertical {
background: #1c1d22;
width: 10px;
margin: 0;
}
QScrollBar::handle:vertical {
background: #3a3d45;
border-radius: 4px;
min-height: 24px;
}
QScrollBar::handle:vertical:hover {
background: #4a4e58;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0;
}
)qss";
qApp->setStyleSheet(QString::fromUtf8(kQss));
}
void MainWindow::refreshNow() {
auto procs = list_java_processes();
QVector<Row> filtered;
filtered.reserve(procs.size());
for (const auto& jp : procs) {
if (!isMinecraft(jp.window_title)) continue;
Row r{ jp.pid, fromW(jp.window_title), fromW(jp.window_class) };
filtered.push_back(std::move(r));
}
// Preserve selection by pid across refreshes.
unsigned long selectedPid = 0;
if (auto sel = table_->currentRow(); sel >= 0 && sel < rows_.size()) {
selectedPid = rows_[sel].pid;
}
rows_ = std::move(filtered);
table_->setRowCount(rows_.size());
int restoreRow = -1;
for (int i = 0; i < rows_.size(); ++i) {
const auto& r = rows_[i];
auto pidItem = new QTableWidgetItem(QString::number(r.pid));
auto titleItem = new QTableWidgetItem(r.title);
auto clsItem = new QTableWidgetItem(r.cls);
pidItem->setTextAlignment(Qt::AlignCenter);
table_->setItem(i, 0, pidItem);
table_->setItem(i, 1, titleItem);
table_->setItem(i, 2, clsItem);
if (selectedPid && r.pid == selectedPid) restoreRow = i;
}
if (restoreRow >= 0) table_->selectRow(restoreRow);
status_->setText(QStringLiteral("Watching %1 Minecraft instance(s).")
.arg(rows_.size()));
}
void MainWindow::onRowDoubleClicked(int row, int /*column*/) {
if (row < 0 || row >= rows_.size()) return;
const auto& r = rows_[row];
status_->setText(QStringLiteral("Mapping OpenZen.dll into PID %1 (%2)...")
.arg(r.pid).arg(r.title));
QApplication::processEvents();
std::wstring err = inject(r.pid);
if (err.empty()) {
status_->setText(QStringLiteral(
"Injected into PID %1. Check %TEMP%\\openzen.log for Java side.")
.arg(r.pid));
} else {
QString msg = fromW(err);
status_->setText(QStringLiteral("Injection failed: %1").arg(msg));
QMessageBox::critical(this, QStringLiteral("Injection failed"), msg);
}
}
} // namespace loader
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <QMainWindow>
#include <QString>
#include <QVector>
class QTableWidget;
class QTimer;
class QLabel;
namespace loader {
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void refreshNow();
void onRowDoubleClicked(int row, int column);
private:
void buildUi();
void styleApp();
QTableWidget* table_ = nullptr;
QTimer* timer_ = nullptr;
QLabel* status_ = nullptr;
QLabel* hint_ = nullptr;
struct Row {
unsigned long pid;
QString title;
QString cls;
};
QVector<Row> rows_;
};
} // namespace loader
-2
View File
@@ -37,6 +37,4 @@ bool get_embedded_dll(const void*& out_data, size_t& out_size);
// Returns empty strings if none found.
WindowInfo window_info_for(DWORD pid);
int run_ui(HINSTANCE hInstance);
} // namespace loader
+16 -3
View File
@@ -1,5 +1,18 @@
#include "loader.h"
#include "MainWindow.h"
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
return loader::run_ui(hInstance);
#include <QApplication>
#include <QStyleFactory>
int main(int argc, char** argv) {
QApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
QApplication app(argc, argv);
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
QApplication::setApplicationName(QStringLiteral("OpenZen Loader"));
QApplication::setOrganizationName(QStringLiteral("OpenZen"));
loader::MainWindow w;
w.show();
return app.exec();
}
-242
View File
@@ -1,242 +0,0 @@
#include "loader.h"
#include <commctrl.h>
#include <string>
#include <vector>
#pragma comment(lib, "comctl32.lib")
namespace loader {
namespace {
constexpr wchar_t WND_CLASS[] = L"OpenZenLoaderWnd";
enum {
ID_LIST = 1001,
ID_REFRESH,
ID_INJECT,
ID_MC_ONLY,
ID_LOG,
};
HWND g_list = nullptr;
HWND g_refresh = nullptr;
HWND g_inject = nullptr;
HWND g_mc_only = nullptr;
HWND g_log = nullptr;
std::vector<JavaProcess> g_processes;
bool is_minecraft_like(const JavaProcess& jp) {
// Title must start with "Minecraft" - in-game windows look like
// "Minecraft 1.20.1" / "Minecraft* 1.21" - and must not look like an
// external launcher (HMCL / MultiMC / "Minecraft Launcher" itself).
const std::wstring& t = jp.window_title;
if (t.rfind(L"Minecraft", 0) != 0) return false;
if (t.find(L"Launcher") != std::wstring::npos) return false;
return true;
}
void append_log(const std::wstring& line) {
int len = GetWindowTextLengthW(g_log);
SendMessageW(g_log, EM_SETSEL, len, len);
std::wstring text = line + L"\r\n";
SendMessageW(g_log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
}
void refresh_list() {
ListView_DeleteAllItems(g_list);
g_processes = list_java_processes();
bool mc_only = SendMessageW(g_mc_only, BM_GETCHECK, 0, 0) == BST_CHECKED;
int shown = 0;
int row_index = 0;
for (size_t i = 0; i < g_processes.size(); ++i) {
const auto& jp = g_processes[i];
if (mc_only && !is_minecraft_like(jp)) continue;
LVITEMW item{};
item.mask = LVIF_TEXT | LVIF_PARAM;
item.iItem = row_index++;
item.iSubItem = 0;
wchar_t pid_text[32];
std::swprintf(pid_text, 32, L"%lu", jp.pid);
item.pszText = pid_text;
item.lParam = (LPARAM)i;
int row = ListView_InsertItem(g_list, &item);
ListView_SetItemText(g_list, row, 1,
const_cast<LPWSTR>(jp.image_name.c_str()));
ListView_SetItemText(g_list, row, 2,
const_cast<LPWSTR>(jp.window_title.c_str()));
ListView_SetItemText(g_list, row, 3,
const_cast<LPWSTR>(jp.window_class.c_str()));
ListView_SetItemText(g_list, row, 4,
const_cast<LPWSTR>(jp.command_line.c_str()));
++shown;
}
wchar_t msg[128];
if (mc_only) {
std::swprintf(msg, 128, L"Refreshed: %d shown / %zu Java process(es) (Minecraft filter on)",
shown, g_processes.size());
} else {
std::swprintf(msg, 128, L"Refreshed: %zu Java process(es)", g_processes.size());
}
append_log(msg);
}
void do_inject() {
int sel = ListView_GetNextItem(g_list, -1, LVNI_SELECTED);
if (sel < 0) {
append_log(L"No process selected. Pick a row and try again.");
return;
}
LVITEMW item{};
item.mask = LVIF_PARAM;
item.iItem = sel;
ListView_GetItem(g_list, &item);
size_t idx = (size_t)item.lParam;
if (idx >= g_processes.size()) {
append_log(L"Stale selection. Refresh and retry.");
return;
}
const auto& jp = g_processes[idx];
wchar_t msg[256];
std::swprintf(msg, 256, L"Mapping embedded OpenZen.dll into pid %lu (%ls)",
jp.pid, jp.image_name.c_str());
append_log(msg);
std::wstring err = inject(jp.pid);
if (err.empty()) {
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
} else {
append_log(L"Injection failed: " + err);
}
}
void layout(HWND hwnd) {
RECT rc; GetClientRect(hwnd, &rc);
int W = rc.right - rc.left;
int H = rc.bottom - rc.top;
int pad = 8;
int btn_h = 24;
int row_h = 26;
// Button row only (no DLL path needed; the DLL lives inside this EXE
// and is mapped directly into the target process without touching disk).
int by = pad;
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
MoveWindow(g_inject, pad + 110, by, 100, btn_h, TRUE);
MoveWindow(g_mc_only, pad + 220, by, 180, btn_h, TRUE);
// List view
int list_y = by + row_h;
int list_h = H * 60 / 100 - list_y;
if (list_h < 100) list_h = 100;
MoveWindow(g_list, pad, list_y, W - pad*2, list_h, TRUE);
// Log area
int log_y = list_y + list_h + pad;
int log_h = H - log_y - pad;
if (log_h < 60) log_h = 60;
MoveWindow(g_log, pad, log_y, W - pad*2, log_h, TRUE);
}
LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_CREATE: {
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance;
g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
g_inject = CreateWindowW(L"BUTTON", L"Inject",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr);
g_mc_only = CreateWindowW(L"BUTTON", L"Minecraft Only",
WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
0, 0, 0, 0, hwnd, (HMENU)ID_MC_ONLY, hi, nullptr);
SendMessageW(g_mc_only, BM_SETCHECK, BST_CHECKED, 0);
g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL,
0, 0, 0, 0, hwnd, (HMENU)ID_LIST, hi, nullptr);
ListView_SetExtendedListViewStyle(g_list,
LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
LVCOLUMNW col{};
col.mask = LVCF_TEXT | LVCF_WIDTH;
col.cx = 70; col.pszText = const_cast<LPWSTR>(L"PID");
ListView_InsertColumn(g_list, 0, &col);
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
ListView_InsertColumn(g_list, 1, &col);
col.cx = 260; col.pszText = const_cast<LPWSTR>(L"Window Title");
ListView_InsertColumn(g_list, 2, &col);
col.cx = 160; col.pszText = const_cast<LPWSTR>(L"Window Class");
ListView_InsertColumn(g_list, 3, &col);
col.cx = 220; col.pszText = const_cast<LPWSTR>(L"Path");
ListView_InsertColumn(g_list, 4, &col);
g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE
| ES_AUTOVSCROLL | ES_READONLY,
0, 0, 0, 0, hwnd, (HMENU)ID_LOG, hi, nullptr);
layout(hwnd);
refresh_list();
append_log(L"OpenZen Loader ready. DLL is mapped in-memory; nothing touches disk.");
return 0;
}
case WM_SIZE: layout(hwnd); return 0;
case WM_COMMAND:
switch (LOWORD(wp)) {
case ID_REFRESH: refresh_list(); return 0;
case ID_INJECT: do_inject(); return 0;
case ID_MC_ONLY:
if (HIWORD(wp) == BN_CLICKED) refresh_list();
return 0;
}
break;
case WM_DESTROY: PostQuitMessage(0); return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
} // namespace
int run_ui(HINSTANCE hInstance) {
INITCOMMONCONTROLSEX icc{};
icc.dwSize = sizeof icc;
icc.dwICC = ICC_LISTVIEW_CLASSES | ICC_STANDARD_CLASSES;
InitCommonControlsEx(&icc);
WNDCLASSEXW wc{};
wc.cbSize = sizeof wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = wnd_proc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszClassName = WND_CLASS;
RegisterClassExW(&wc);
HWND hwnd = CreateWindowExW(0, WND_CLASS, L"OpenZen Loader",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 820, 560,
nullptr, nullptr, hInstance, nullptr);
if (!hwnd) return 1;
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return (int)msg.wParam;
}
} // namespace loader
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "openzen-loader",
"version-string": "1.0.0",
"description": "Native loader + DLL for the OpenZen Minecraft client",
"dependencies": [
{
"name": "qtbase",
"default-features": false,
"features": [ "widgets" ]
}
]
}
@@ -472,13 +472,25 @@ public final class PatchTransformer {
String targetOwner = split[0];
String targetMethod = Bootstrap.remapMethod(split[0], split[1], targetDesc);
// Match what shit.zen.asm.PatchApplier did: accept either owner or name match,
// so targets pointing at the declaring class (e.g. Entity/getYRot) still hit
// bytecode that encodes the receiver as a subclass (LivingEntity/getYRot).
// Prefer a strict owner+name+desc match. Falling back to the historical
// (owner || name) matcher means a wrap aimed at Mth.lerp(FFF)F also gets
// its slice index counted against every Mth.rotLerp(FFF)F call site,
// which silently shifts indices once an earlier wrap deletes its own
// site instruction. Strict matching keeps each wrap's slice index
// independent. The legacy loose matcher is still used as a fallback so
// patches that target an inherited method on a subclass receiver
// (Entity#getYRot resolved at LivingEntity#getYRot) keep working.
List<AbstractInsnNode> sites = collectInjectionPoints(method.instructions, wrap.slice(),
insn -> insn instanceof MethodInsnNode m
&& (m.owner.equals(targetOwner) || m.name.equals(targetMethod))
&& m.owner.equals(targetOwner)
&& m.name.equals(targetMethod)
&& m.desc.equals(targetDesc));
if (sites.isEmpty()) {
sites = collectInjectionPoints(method.instructions, wrap.slice(),
insn -> insn instanceof MethodInsnNode m
&& (m.owner.equals(targetOwner) || m.name.equals(targetMethod))
&& m.desc.equals(targetDesc));
}
if (sites.isEmpty()) {
LOGGER.warn("@WrapInvoke {}#{}{} found no call site of {}#{}{} — patch handler will not run.",
handler.getDeclaringClass().getName(), handler.getName(), Type.getMethodDescriptor(handler),
@@ -60,7 +60,7 @@ public class Scaffold extends Module {
public final ModeSetting mode = new ModeSetting("Mode", "Normal", "Telly Bridge", "Old Telly", "Keep Y").withDefault("Normal");
public final BooleanSetting eagle = new BooleanSetting("Eagle", true, () -> this.mode.is("Normal"));
public final BooleanSetting advancedBlockSearch = new BooleanSetting("Sneak", true);
public final BooleanSetting sneak = new BooleanSetting("Sneak", true);
public final BooleanSetting snap = new BooleanSetting("Snap", true, () -> this.mode.is("Normal"));
public final BooleanSetting renderItemSpoof = new BooleanSetting("Render Item Spoof", true);
public final NumberSetting rotationTick = new NumberSetting("Rotation Tick", 3, 1, 6, 1);
@@ -261,7 +261,7 @@ public class Scaffold extends Module {
this.rots.setYaw(RotationUtil.moveTowards((float) this.getBlockDistance(), this.rots.getYaw(), this.correctRotation.getYaw()));
}
this.rots.setPitch(this.correctRotation.getPitch());
if (this.advancedBlockSearch.getValue()) {
if (this.sneak.getValue()) {
this.eagleTimer++;
if (this.eagleTimer == 18) {
if (mc.player.isSprinting()) {
@@ -47,9 +47,12 @@ public class LivingEntityRendererPatch {
ZenClient.getInstance().getEventBus().call(post);
}
// PatchApplier's wrap matcher is (owner ∥ name) && desc, so the filter accepts every
// Mth.*(FFF)F call. There are four in render — see onRenderPitchLerp below for the full
// map. We want the 2nd, the head yaw rotLerp.
// PatchTransformer.wrapInvoke now tries strict owner+name+desc matching first,
// so this slice index is independent of the other Mth.* wraps in this patch.
// LivingEntityRenderer.render holds 3 Mth.rotLerp(FFF)F calls in order:
// 1: body yaw (Mth.rotLerp(g, yBodyRotO, yBodyRot))
// 2: head yaw (Mth.rotLerp(g, yHeadRotO, yHeadRot)) <- we want this
// 3: head yaw recalc in the conditional branch
@WrapInvoke(
method = "render",
desc = "(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
@@ -71,12 +74,16 @@ public class LivingEntityRendererPatch {
return Mth.rotLerp(delta, event.getLastYaw(), event.getYaw());
}
// Only one Mth.lerp(FFF)F call site in LivingEntityRenderer.render (pitch
// lerp: Mth.lerp(g, xRotO, getXRot())). The strict matcher in
// PatchTransformer.wrapInvoke filters out the three Mth.rotLerp calls, so
// slice (1,1) is the right pick.
@WrapInvoke(
method = "render",
desc = "(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
target = "net/minecraft/util/Mth/lerp",
targetDesc = "(FFF)F",
slice = @Slice(startIndex = 4, endIndex = 4)
slice = @Slice(startIndex = 1, endIndex = 1)
)
public static float onRenderPitchLerp(
LivingEntityRenderer<?, ?> renderer, LivingEntity entity, float entityYaw, float partialTick,