diff --git a/README.md b/README.md index 87c78e8..a985607 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/build.gradle b/build.gradle index fcb6348..9b89a44 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } } diff --git a/native/loader/CMakeLists.txt b/native/loader/CMakeLists.txt index 2b16d82..3aaa431 100644 --- a/native/loader/CMakeLists.txt +++ b/native/loader/CMakeLists.txt @@ -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'\"" ) diff --git a/native/loader/src/MainWindow.cpp b/native/loader/src/MainWindow.cpp new file mode 100644 index 0000000..cb7e5e0 --- /dev/null +++ b/native/loader/src/MainWindow.cpp @@ -0,0 +1,220 @@ +#include "MainWindow.h" +#include "loader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace loader { + +namespace { +QString fromW(const std::wstring& w) { + return QString::fromWCharArray(w.c_str(), static_cast(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 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 diff --git a/native/loader/src/MainWindow.h b/native/loader/src/MainWindow.h new file mode 100644 index 0000000..8273ba4 --- /dev/null +++ b/native/loader/src/MainWindow.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +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 rows_; +}; +} // namespace loader diff --git a/native/loader/src/loader.h b/native/loader/src/loader.h index 161a10b..47212ea 100644 --- a/native/loader/src/loader.h +++ b/native/loader/src/loader.h @@ -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 diff --git a/native/loader/src/main.cpp b/native/loader/src/main.cpp index 4c5cb3b..1f24ca6 100644 --- a/native/loader/src/main.cpp +++ b/native/loader/src/main.cpp @@ -1,5 +1,18 @@ -#include "loader.h" +#include "MainWindow.h" -int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) { - return loader::run_ui(hInstance); +#include +#include + +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(); } diff --git a/native/loader/src/ui.cpp b/native/loader/src/ui.cpp deleted file mode 100644 index 822365b..0000000 --- a/native/loader/src/ui.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include "loader.h" - -#include - -#include -#include - -#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 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(jp.image_name.c_str())); - ListView_SetItemText(g_list, row, 2, - const_cast(jp.window_title.c_str())); - ListView_SetItemText(g_list, row, 3, - const_cast(jp.window_class.c_str())); - ListView_SetItemText(g_list, row, 4, - const_cast(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(L"PID"); - ListView_InsertColumn(g_list, 0, &col); - col.cx = 100; col.pszText = const_cast(L"Image"); - ListView_InsertColumn(g_list, 1, &col); - col.cx = 260; col.pszText = const_cast(L"Window Title"); - ListView_InsertColumn(g_list, 2, &col); - col.cx = 160; col.pszText = const_cast(L"Window Class"); - ListView_InsertColumn(g_list, 3, &col); - col.cx = 220; col.pszText = const_cast(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 diff --git a/native/vcpkg.json b/native/vcpkg.json new file mode 100644 index 0000000..7bfe14f --- /dev/null +++ b/native/vcpkg.json @@ -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" ] + } + ] +} diff --git a/src/main/java/asm/patchify/loader/PatchTransformer.java b/src/main/java/asm/patchify/loader/PatchTransformer.java index ada95c5..80b4e63 100644 --- a/src/main/java/asm/patchify/loader/PatchTransformer.java +++ b/src/main/java/asm/patchify/loader/PatchTransformer.java @@ -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 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), diff --git a/src/main/java/shit/zen/modules/impl/movement/Scaffold.java b/src/main/java/shit/zen/modules/impl/movement/Scaffold.java index 9c7db45..8b0c4ab 100644 --- a/src/main/java/shit/zen/modules/impl/movement/Scaffold.java +++ b/src/main/java/shit/zen/modules/impl/movement/Scaffold.java @@ -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()) { diff --git a/src/main/java/shit/zen/patch/LivingEntityRendererPatch.java b/src/main/java/shit/zen/patch/LivingEntityRendererPatch.java index 04d292c..6884d41 100644 --- a/src/main/java/shit/zen/patch/LivingEntityRendererPatch.java +++ b/src/main/java/shit/zen/patch/LivingEntityRendererPatch.java @@ -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,