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:
@@ -84,6 +84,15 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
|||||||
1. `PATH` 上的 `cmake.exe`
|
1. `PATH` 上的 `cmake.exe`
|
||||||
2. 通过 `vswhere.exe` 找 VS 2022 自带的 CMake
|
2. 通过 `vswhere.exe` 找 VS 2022 自带的 CMake
|
||||||
3. 常见独立安装位置 (`%ProgramFiles%\CMake\bin\cmake.exe` 等)
|
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 参数)。
|
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
||||||
2. 双击 `OpenZenLoader.exe`。
|
2. 双击 `OpenZenLoader.exe`(会弹一次 UAC 提权)。
|
||||||
3. 列表里找到对应的 `javaw.exe`(看 Window Title 列,应是 `Minecraft 1.20.1` 一类)。
|
3. GUI 自动列出系统里**所有 Minecraft 实例**(按窗口标题 startsWith `Minecraft` 且不含 `Launcher` 过滤),每秒自动刷新一次。
|
||||||
4. 点 Inject。
|
4. **双击对应的行**就开始注入。
|
||||||
5. 看到 `Injection ok. Watch %TEMP%\openzen.log for Java side.` 即成功。Minecraft 内右 Shift 触发 ClickGUI 验证。
|
5. 状态栏出现 `Injected into PID ...` 即成功;Minecraft 内右 Shift 触发 ClickGUI 验证。
|
||||||
|
|
||||||
诊断日志:
|
诊断日志:
|
||||||
- Native 端:`%TEMP%\openzen.log`
|
- Native 端:`%TEMP%\openzen.log`
|
||||||
|
|||||||
+41
-2
@@ -311,6 +311,39 @@ def cmakeMissingMessage =
|
|||||||
' - Visual Studio 2022 with "C++ CMake tools for Windows" workload\n' +
|
' - Visual Studio 2022 with "C++ CMake tools for Windows" workload\n' +
|
||||||
'After installing, re-run ./gradlew dll'
|
'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) {
|
tasks.register('stageNativeJar', Copy) {
|
||||||
group = 'openzen'
|
group = 'openzen'
|
||||||
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
||||||
@@ -330,8 +363,14 @@ tasks.register('configureNative', Exec) {
|
|||||||
doFirst {
|
doFirst {
|
||||||
def cmake = findCmake()
|
def cmake = findCmake()
|
||||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||||
logger.lifecycle("Using CMake: ${cmake}")
|
def vcpkg = findVcpkg()
|
||||||
commandLine cmake, '-S', '.', '-B', 'build', '-A', 'x64'
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# 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).
|
# can pick it up as an RCDATA resource (embedded in the .exe).
|
||||||
set(EMBED_DIR "${CMAKE_CURRENT_BINARY_DIR}/embedded_dll")
|
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_custom_target(stage_dll_for_loader DEPENDS "${EMBED_DIR}/OpenZen.dll")
|
||||||
|
|
||||||
add_executable(OpenZenLoader WIN32
|
qt_add_executable(OpenZenLoader WIN32
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
src/MainWindow.cpp
|
||||||
|
src/MainWindow.h
|
||||||
src/process_list.cpp
|
src/process_list.cpp
|
||||||
src/injector.cpp
|
src/injector.cpp
|
||||||
src/manual_map.cpp
|
src/manual_map.cpp
|
||||||
src/embedded_dll.cpp
|
src/embedded_dll.cpp
|
||||||
src/window_title.cpp
|
src/window_title.cpp
|
||||||
src/ui.cpp
|
|
||||||
res/loader.rc
|
res/loader.rc
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +43,7 @@ set_source_files_properties(res/loader.rc PROPERTIES
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(OpenZenLoader PRIVATE
|
target_link_libraries(OpenZenLoader PRIVATE
|
||||||
comctl32
|
Qt6::Widgets
|
||||||
psapi
|
psapi
|
||||||
shlwapi
|
shlwapi
|
||||||
user32
|
user32
|
||||||
@@ -46,12 +54,9 @@ set_target_properties(OpenZenLoader PROPERTIES
|
|||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
# requireAdministrator: ensures the loader can OpenProcess /
|
# requireAdministrator: lets the loader OpenProcess / VirtualAllocEx /
|
||||||
# VirtualAllocEx / CreateRemoteThread against javaw.exe even when the
|
# CreateRemoteThread against javaw.exe even when the launcher was
|
||||||
# Minecraft launcher (HMCL, MultiMC) was itself started elevated, and
|
# itself started elevated. UAC consent prompts once on launch.
|
||||||
# avoids the silent injection failure that happens on locked-down user
|
|
||||||
# accounts. The user will see one UAC consent prompt when launching
|
|
||||||
# OpenZenLoader.exe.
|
|
||||||
set_target_properties(OpenZenLoader PROPERTIES
|
set_target_properties(OpenZenLoader PROPERTIES
|
||||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\""
|
LINK_FLAGS "/SUBSYSTEM:WINDOWS /MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -37,6 +37,4 @@ bool get_embedded_dll(const void*& out_data, size_t& out_size);
|
|||||||
// Returns empty strings if none found.
|
// Returns empty strings if none found.
|
||||||
WindowInfo window_info_for(DWORD pid);
|
WindowInfo window_info_for(DWORD pid);
|
||||||
|
|
||||||
int run_ui(HINSTANCE hInstance);
|
|
||||||
|
|
||||||
} // namespace loader
|
} // namespace loader
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
#include "loader.h"
|
#include "MainWindow.h"
|
||||||
|
|
||||||
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
|
#include <QApplication>
|
||||||
return loader::run_ui(hInstance);
|
#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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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 targetOwner = split[0];
|
||||||
String targetMethod = Bootstrap.remapMethod(split[0], split[1], targetDesc);
|
String targetMethod = Bootstrap.remapMethod(split[0], split[1], targetDesc);
|
||||||
|
|
||||||
// Match what shit.zen.asm.PatchApplier did: accept either owner or name match,
|
// Prefer a strict owner+name+desc match. Falling back to the historical
|
||||||
// so targets pointing at the declaring class (e.g. Entity/getYRot) still hit
|
// (owner || name) matcher means a wrap aimed at Mth.lerp(FFF)F also gets
|
||||||
// bytecode that encodes the receiver as a subclass (LivingEntity/getYRot).
|
// 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(),
|
List<AbstractInsnNode> sites = collectInjectionPoints(method.instructions, wrap.slice(),
|
||||||
insn -> insn instanceof MethodInsnNode m
|
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));
|
&& 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()) {
|
if (sites.isEmpty()) {
|
||||||
LOGGER.warn("@WrapInvoke {}#{}{} found no call site of {}#{}{} — patch handler will not run.",
|
LOGGER.warn("@WrapInvoke {}#{}{} found no call site of {}#{}{} — patch handler will not run.",
|
||||||
handler.getDeclaringClass().getName(), handler.getName(), Type.getMethodDescriptor(handler),
|
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 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 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 snap = new BooleanSetting("Snap", true, () -> this.mode.is("Normal"));
|
||||||
public final BooleanSetting renderItemSpoof = new BooleanSetting("Render Item Spoof", true);
|
public final BooleanSetting renderItemSpoof = new BooleanSetting("Render Item Spoof", true);
|
||||||
public final NumberSetting rotationTick = new NumberSetting("Rotation Tick", 3, 1, 6, 1);
|
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.setYaw(RotationUtil.moveTowards((float) this.getBlockDistance(), this.rots.getYaw(), this.correctRotation.getYaw()));
|
||||||
}
|
}
|
||||||
this.rots.setPitch(this.correctRotation.getPitch());
|
this.rots.setPitch(this.correctRotation.getPitch());
|
||||||
if (this.advancedBlockSearch.getValue()) {
|
if (this.sneak.getValue()) {
|
||||||
this.eagleTimer++;
|
this.eagleTimer++;
|
||||||
if (this.eagleTimer == 18) {
|
if (this.eagleTimer == 18) {
|
||||||
if (mc.player.isSprinting()) {
|
if (mc.player.isSprinting()) {
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ public class LivingEntityRendererPatch {
|
|||||||
ZenClient.getInstance().getEventBus().call(post);
|
ZenClient.getInstance().getEventBus().call(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchApplier's wrap matcher is (owner ∥ name) && desc, so the filter accepts every
|
// PatchTransformer.wrapInvoke now tries strict owner+name+desc matching first,
|
||||||
// Mth.*(FFF)F call. There are four in render — see onRenderPitchLerp below for the full
|
// so this slice index is independent of the other Mth.* wraps in this patch.
|
||||||
// map. We want the 2nd, the head yaw rotLerp.
|
// 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(
|
@WrapInvoke(
|
||||||
method = "render",
|
method = "render",
|
||||||
desc = "(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
|
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());
|
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(
|
@WrapInvoke(
|
||||||
method = "render",
|
method = "render",
|
||||||
desc = "(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
|
desc = "(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
|
||||||
target = "net/minecraft/util/Mth/lerp",
|
target = "net/minecraft/util/Mth/lerp",
|
||||||
targetDesc = "(FFF)F",
|
targetDesc = "(FFF)F",
|
||||||
slice = @Slice(startIndex = 4, endIndex = 4)
|
slice = @Slice(startIndex = 1, endIndex = 1)
|
||||||
)
|
)
|
||||||
public static float onRenderPitchLerp(
|
public static float onRenderPitchLerp(
|
||||||
LivingEntityRenderer<?, ?> renderer, LivingEntity entity, float entityYaw, float partialTick,
|
LivingEntityRenderer<?, ?> renderer, LivingEntity entity, float entityYaw, float partialTick,
|
||||||
|
|||||||
Reference in New Issue
Block a user