Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f7c466e2 | |||
| 954a2504f2 | |||
| 692c0ac7b0 | |||
| 9f094b4aa8 | |||
| 8f53eda70e | |||
| 941b69203b | |||
| f59d4c121c | |||
| b76ba33ea5 | |||
| 21508a6f34 | |||
| 55fe4699f2 | |||
| ba0fd3b702 | |||
| 42ca5e208c | |||
| 72d5f1415c | |||
| 6c842f3033 | |||
| 7576add38f | |||
| d3e4748966 |
@@ -110,20 +110,30 @@ jobs:
|
|||||||
"VCPKG_ROOT=$vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
"VCPKG_ROOT=$vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
Write-Host "Exported VCPKG_ROOT=$vcpkg to GITHUB_ENV"
|
Write-Host "Exported VCPKG_ROOT=$vcpkg to GITHUB_ENV"
|
||||||
|
|
||||||
# Cache the resolved Qt install + the file-backed binary archive.
|
# Cache the vcpkg *binary archives* — the only thing that lets vcpkg skip
|
||||||
# Key is keyed on vcpkg.json so any dependency change invalidates;
|
# rebuilding qtbase. The build runs `gradlew clean`, which deletes
|
||||||
# restore-keys lets a vcpkg.json bump still warm-start from the
|
# native/build/ (and thus the manifest install dir native/build/vcpkg_installed),
|
||||||
# previous build's archives. installed/ alone makes CMake's manifest
|
# so on every run vcpkg re-resolves the manifest and restores packages from
|
||||||
# mode skip rebuilding qtbase; archives/ also fills back transitive
|
# this file-backed binary cache. If it's empty, qtbase is compiled from
|
||||||
# deps if a partial rebuild is needed.
|
# source (~32 min). (We previously cached vcpkg\installed too, but in
|
||||||
- name: Cache vcpkg installed + archives
|
# manifest mode that tree is never consulted for the rebuild decision — only
|
||||||
|
# the binary archive is — so it was ~1.2 GB of dead weight.)
|
||||||
|
#
|
||||||
|
# The key MUST rotate per run: actions/cache never overwrites an existing
|
||||||
|
# key, so a static key froze whatever archives the first run captured (none)
|
||||||
|
# and every later run hit "cache hit, not saving" at Post — discarding the
|
||||||
|
# qtbase archive it had just rebuilt, guaranteeing a fresh 32-min rebuild
|
||||||
|
# forever. Appending run_id makes Post always save a new entry; the layered
|
||||||
|
# restore-keys warm-start from the newest prior archive so qtbase is
|
||||||
|
# restored, not rebuilt. (GitHub LRU-evicts old entries within the 10 GB
|
||||||
|
# budget; the archive is only a few hundred MB.)
|
||||||
|
- name: Cache vcpkg binary archives
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: vcpkg-archives
|
||||||
vcpkg\installed
|
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-${{ github.run_id }}
|
||||||
vcpkg-archives
|
|
||||||
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
|
vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-
|
||||||
vcpkg-qt-${{ runner.os }}-
|
vcpkg-qt-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install UPX
|
- name: Install UPX
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
# Open Zen
|
|
||||||
|
|
||||||
> 欢迎加群 523522206 讨论该项目。
|
|
||||||
|
|
||||||
> 截止至目前,Open Zen仍然处于早期构建阶段。大量特性可能仍不可用,请谅解!当您发现有不可用的功能时,欢迎开Issues描述或直接提交Pull Request,我们将不胜感激!
|
|
||||||
|
|
||||||
**Open Zen** 是 *Zen* Minecraft 客户端的反混淆源码版本,几乎是由 [Claude](https://claude.com/)协助逆向得到。目标版本为 **Minecraft 1.20.1 + Forge 47.4.20**。
|
|
||||||
|
|
||||||
原始 Jar 经过完整混淆:类/字段/方法重命名、控制流扁平化。我使用Opus 4.7对其进行了反混淆,并结合 [Enigma MCP](https://github.com/Margele/Enigma-MCP)和Sonnet 4.6对其的类/字段/方法重命名进行猜测,最后将其还原为了可读的 Java。最终产物是一个可以直接用 Gradle 构建的工程,而不是一个二进制 blob。
|
|
||||||
|
|
||||||
> ⚠️ 本仓库**仅供学习与研究目的发布** —— 用于研究客户端侧游戏改造、ASM 字节码补丁和混淆/反混淆技术。在你不拥有的服务器上使用作弊客户端违反绝大多数服务器规则,请自行承担后果。
|
|
||||||
|
|
||||||
## 许可
|
|
||||||
原始混淆字节码未授予任何许可。本仓库中的反混淆产物、构建脚本与文档**仅供研究与学习使用**。如果你是 Zen 的原作者并希望本仓库下架或重新授权,请提 Issues。虽然提了也不会搭理你。
|
|
||||||
|
|
||||||
## 截图
|
|
||||||

|
|
||||||
|
|
||||||
## 后门
|
|
||||||
我们在逆向时发现原版Zen存在大量后门,例如上报QQ、屏幕截图、扫描文件、上传文件、远程执行命令等。因此我们**不推荐**任何用户继续使用原版Zen,除非你愿意现在把你身上的衣服脱掉然后去本地人最多的广场裸舞,然后把自己裸舞的视频发送到Zen的群内。
|
|
||||||
|
|
||||||
当Zen被注入后,会自动触发截图并上传至服务器。精神马来西亚人回应如下:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
由于精神马来西亚人从小父母双亡,无父无母的精神马来西亚人自幼脑回路不正常。他认为虽然自己没有说自己的外挂会截图,但是由于自己截图,并没有遭到用户反对,所以所以用户都心甘情愿被截图**全屏**并上传到其服务器上。当然不排除所有Zen客户端用户都喜欢把身上的衣服脱掉然后去本地人最多的广场裸舞的可能性。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 分析
|
|
||||||
当Zen启动时,会自动调用 `iIiIiIiIIIiIiI/Ʊ Đ()Ljava/awt/image/BufferedImage` ([Mapping](./mapping/zen.mapping#L140)),可能由于精神马来西亚人自知是后门,因此精神马来西亚人将此方法严防死守,惨遭没有逼卵子用的Native混淆。
|
|
||||||
|
|
||||||
以下是对该方法的Trace。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
可见,此方法调用了 `java/awt/Robot;createScreenCapture(Ljava/awt/Rectangle)`,会将用户的**全屏**截图后返回。
|
|
||||||
|
|
||||||
继续向下追踪,发现其新建了 `iIiIiIiIIIiIiI/ɿ` (`CPacketSystemInfo`) ([Mapping](./mapping/zen.mapping#L2552)) 对象,我们对该类反编译,发现精神马来西亚人妈妈死掉了所以忘记删除Lombok自动生成的`@ToString`方法,因此惨遭Claude还原。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
此包会上传用户处理器信息、模组列表、虚拟机参数、系统名称、上报截图等信息,但笔者认为除截图外,其他信息收集在**提前告知用户的前提下**是合理的,因此并无不妥。虽然精神马来西亚人没有提前告知用户。
|
|
||||||
|
|
||||||
随后,笔者继续分析。由于该类继承了 `iIiIiIiIIIiIiI/ɰ` (`Packet`) ([Mapping](./mapping/zen.mapping#L2459)),我们分析了所有该类的子类。
|
|
||||||
|
|
||||||
遗憾的,其他类由于没有添加Lombok标识,我们我们不得不通过其他方式Trace这些类的具体用途。经过我们不懈努力的调试和追踪,我们还原出了我们认为可疑的部分行为。
|
|
||||||
|
|
||||||
- 远程命令执行 `iIiIiIiIIIiIiI/ʔ`
|
|
||||||
- 远程文件下发 `iIiIiIiIIIiIiI/ʏ`
|
|
||||||
- 远程文件浏览 `iIiIiIiIIIiIiI/ʑ`
|
|
||||||
|
|
||||||
*以上不是全部*
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
对于这些后门,精神马来西亚人作此解释。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
精神马来西亚人称这些后门全部都是由**夏天233**制造,并非自己。并且这些后门并没有实现,所以可能是由于精神马来西亚人产生幻觉导致笔者抓到了Trace。而且并不能解释同是一套Network系统,为什么上传截图包实现了但其他方法没有实现。
|
|
||||||
|
|
||||||
其后其在[视频](https://www.bilibili.com/video/BV147L86TEEZ)中表示,是服务器在迁移时没有实现,而不是客户端没有实现。同时,精神马来西亚人在视频中表示*没有功能*,但是在QQ群中表示*是夏天233写的*。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
由于精神马来西亚人嘴硬,所以到底具体有没有实现,笔者暂且蒙古。
|
|
||||||
|
|
||||||
### 父子相爱相杀
|
|
||||||
精神马来西亚人在QQ群中指出,欣欣使用过外挂后门远程读取他人文件。不知作为德州骡子的许锦良见自己的亲生父亲如此指认自己作何感想。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 原始 Jar + Mapping
|
|
||||||
[原始Jar](./mapping/zen-orignial.jar)
|
|
||||||
[Mapping](./mapping/zen.mapping)
|
|
||||||
|
|
||||||
需要说明的是,部分喜欢裸舞的忠实Zen用户认为本源码逆向自比较旧的Zen版本。可能是因为这部分用户的脑容量只允许自己导入其他配置,因此不认识Zen的老旧UI。因此必须要说明,此源码使用的原始Jar截止至2026年5月21日是最新的。
|
|
||||||
|
|
||||||
## 细节
|
|
||||||
经过Opus 4.7长达18秒的分析,Opus认为所有的类由惨遭魔改的Zelix KlassMaster混淆。除了Zelix的Invoke Dynamic和String Encryption外,还有部分未参与任何计算的Interger \ Long变量花指令代码和仅在部分方法中出现的Flow混淆。
|
|
||||||
|
|
||||||
其中大部分类都可以经过小修小补的现有Zelix反混淆器完成,关键部分的`cinit`被Native保护,导致在Java层中没有对应的Master Key可以对Invoke Dynamic和String反混淆。但可能由于精神马来西亚人的脑袋在马来西亚骑摩托被其他车创飞导致脑溢血,即使你没有通过客户端认证也可以完整加载Native并对Class进行注册。因此我们完整的还原了所有类的Invoke Dynamic和String混淆。
|
|
||||||
|
|
||||||
其他的混淆经过Opus长达30秒的分析,顺利写出了反混淆器。 但被Rename后的代码几乎不可读,因此我用Opus 4.7制作了[Enigma MCP](https://github.com/Margele/Enigma-MCP),接入Sonnet 4.6对其参照部分客户端进行了反混淆。
|
|
||||||
|
|
||||||
再使用Opus 4.7对本项目经过长达6小时的修复和少量的人工修复,便得到了这份源码。
|
|
||||||
|
|
||||||
## 抄袭
|
|
||||||
此项目大部分功能模块几乎全部抄袭自Naven客户端,具体详见以下分析。
|
|
||||||
|
|
||||||
[详细分析](./paste/README.md)
|
|
||||||
|
|
||||||
## 状态与注意事项
|
|
||||||
- 这是**尽力而为的反混淆结果**,部分符号是根据上下文重建的,可能与原作者的命名意图不一致。
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启动参数里)和 **热注入器 (单文件 EXE,内嵌 DLL)**。Agent 路径只要 JDK,注入器路径还需要 MSVC 工具链。
|
|
||||||
|
|
||||||
> **本项目不能作为 Forge mod 启动。** `mods/` 加载路径不被支持,不要把 jar 丢进 `.minecraft/mods/`。
|
|
||||||
|
|
||||||
### 编译时类名混淆(重要)
|
|
||||||
|
|
||||||
每次构建,OpenZen 会**自动把所有自有类(`shit.zen.*` / `asm.patchify.*`)重命名为随机的 16 位名字**——包名和类名都随机,**每次构建都不一样**、互不重复,原始类名/包名一律不保留(连日志里残留的类名字符串也清理掉了)。引导链(Agent 入口、DLL 加载、`Class.forName`)会在构建时自动联动到新名字,无需手工处理。两种交付形式(jar / 注入器)都已混淆。
|
|
||||||
|
|
||||||
这是为了对抗按**类名黑名单**工作的反作弊(见下方[常见问题](#布吉岛反作弊绕过))。正因为名字每次构建随机:
|
|
||||||
|
|
||||||
> ⚠️ **从 GitHub Actions / Release 下载到的是预编译版本,所有人拿到的是同一套混淆名**——这套固定的名字随时可能被反作弊收录进黑名单。想要一套**别人都不知道、独一无二**的类名,请**自己编译**:
|
|
||||||
> - **Fork 本仓库**,在你自己的 GitHub Actions 里跑一次构建(每次运行都生成全新随机名),下载你自己的 artifact;**或**
|
|
||||||
> - **clone 到本地**自己 `gradlew jar` / `gradlew dll`(每次本地构建同样是全新随机名)。
|
|
||||||
|
|
||||||
每次构建的"旧名 → 新名"映射写在 `build/rename-mapping.txt`(CI 也会把它作为 artifact 上传、并附到 Release),这是反混淆崩溃日志的**唯一**依据。**注意它每次构建都不同,务必和对应产物一起保存。**
|
|
||||||
|
|
||||||
实现细节见 `build.gradle` 的 `ext.obfuscateJar`:用项目自带的 ASM 在 ForgeGradle `reobf` 之后对产出 jar 做 `ClassRemapper` 重命名,**只改类名、不动方法/字段名**(避免破坏反射、JNI、`@SerializedName` 等)。
|
|
||||||
|
|
||||||
### 共同前置
|
|
||||||
|
|
||||||
- **JDK 17**(推荐 Microsoft Build of OpenJDK / Temurin / Azul Zulu 任一)。
|
|
||||||
- 必须设置 `JAVA_HOME` 环境变量指向该 JDK 安装目录(PowerShell 验证:`echo $env:JAVA_HOME`)。
|
|
||||||
- 仓库根目录用 `gradlew.bat` 即可,**不需要**单独安装 Gradle。
|
|
||||||
- **可选:UPX** —— 仅热注入器路径会用到,作用是把最终的 `OpenZenLoader.exe` 从 ~32 MB 压到 ~10 MB。在 `PATH` 上检测到 `upx` 时 `./gradlew upxCompress` 会自动跑 `--best --lzma`;找不到就只打一条 warning 直接跳过,不影响功能。安装方式:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
choco install upx -y
|
|
||||||
```
|
|
||||||
|
|
||||||
或者从 <https://upx.github.io/> 下载 ZIP 并把 `upx.exe` 加进 `PATH`。
|
|
||||||
|
|
||||||
首次执行会从 ForgeMaven 下载 1.20.1 + Forge 47.4.20 的 mappings 和依赖,耗时几分钟到十几分钟,取决于网络。
|
|
||||||
|
|
||||||
### 1. 构建为 Java Agent jar
|
|
||||||
|
|
||||||
零额外依赖。
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\gradlew.bat jar
|
|
||||||
```
|
|
||||||
|
|
||||||
产物:`build/libs/hey-1.0.jar`。在 Forge 启动器的 JVM 启动参数里加上:
|
|
||||||
|
|
||||||
```
|
|
||||||
-javaagent:"完整\路径\到\hey-1.0.jar" -Djdk.attach.allowAttachSelf=true
|
|
||||||
```
|
|
||||||
|
|
||||||
`PatchAgent.premain` 会在 Minecraft 类加载之前装载所有 ASM 补丁;`-Djdk.attach.allowAttachSelf=true` 是让 `installPatchesAndRetransform` 在需要时能兜底走 Attach API。
|
|
||||||
|
|
||||||
### 2. 构建为热注入器 (单文件 EXE)
|
|
||||||
|
|
||||||
产出一个独立的 `OpenZenLoader.exe`,DLL 已经作为资源段嵌入 EXE 内部。用户分发只需要这一个文件,运行后 GUI 列出当前所有 `javaw.exe` 进程(含 Minecraft 窗口标题),选中后点 Inject 即可。
|
|
||||||
|
|
||||||
#### 额外前置 — 必须项
|
|
||||||
|
|
||||||
1. **Visual Studio 2022**(Community 版即可,免费)。安装时勾选:
|
|
||||||
- **"使用 C++ 的桌面开发"** 工作负载
|
|
||||||
- 该工作负载的可选组件里勾上 **"适用于 Windows 的 C++ CMake 工具"**("C++ CMake tools for Windows")
|
|
||||||
2. **`JAVA_HOME` 必须指向 JDK 17**(不只是 JRE)。CMake 需要它定位 `<JAVA_HOME>/include/jni.h` 和 `<JAVA_HOME>/include/win32/jvmti.h`。
|
|
||||||
3. **CMake**:VS 2022 自带,Gradle 会自动检测——也可以独立安装 [CMake](https://cmake.org/download/) 并加入 PATH。Gradle 的检测顺序:
|
|
||||||
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,零运行时依赖。
|
|
||||||
|
|
||||||
#### 构建命令
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\gradlew.bat dll
|
|
||||||
```
|
|
||||||
|
|
||||||
产物:`build/dist/OpenZenLoader.exe`。如果已装 UPX 想顺便压缩,跑 `.\gradlew.bat upxCompress`。
|
|
||||||
|
|
||||||
#### 使用注入器
|
|
||||||
|
|
||||||
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
|
||||||
2. 双击 `OpenZenLoader.exe`。
|
|
||||||
3. GUI 自动列出系统里**所有 Minecraft 实例**,每秒自动刷新一次。
|
|
||||||
4. 点击行最后的 Inject 按钮。
|
|
||||||
|
|
||||||
诊断日志:
|
|
||||||
- Native 端:`%TEMP%\openzen.log`
|
|
||||||
- Java 端:Minecraft 自己的 `logs/latest.log`(类名已被构建时混淆、logger 名是随机的,改用固定日志文案定位,如 `bootstrap.start`、`bridge.load`、`agent attached`、`Runtime mapping`)
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### 布吉岛反作弊绕过
|
|
||||||
~~截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。~~
|
|
||||||
~~建议构建时修改类名。~~
|
|
||||||
~~类名已黑名单,请在构建时修改类名。~~
|
|
||||||
类名已黑名单。现在**每次构建都会自动随机化全部类名**(见上方[编译时类名混淆](#编译时类名混淆重要))——但**务必自己 Fork/clone 编译**,别直接用 GitHub Actions / Release 上的预编译版:那是固定的一套名字,会被拉黑。
|
|
||||||
|
|
||||||
## 致谢
|
|
||||||
|
|
||||||
- 原始混淆客户端:**Zen**。
|
|
||||||
- 反混淆、符号还原与工程脚手架:**Claude** 在人工监督下完成。
|
|
||||||
- [Java Deobfuscator](https://github.com/java-deobfuscator/deobfuscator)
|
|
||||||
- 从古墓中挖出的 [Themida](https://www.oreans.com/Themida.php)
|
|
||||||
- 惨遭魔改的 [Zelix](https://www.zelix.com/)
|
|
||||||
- [Enigma MCP](https://github.com/Margele/Enigma-MCP)
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 88 KiB |
@@ -15,6 +15,7 @@
|
|||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QParallelAnimationGroup>
|
#include <QParallelAnimationGroup>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
|
#include <QRandomGenerator>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -32,6 +33,28 @@ namespace loader {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kCornerRadius = 12;
|
constexpr int kCornerRadius = 12;
|
||||||
|
constexpr int kBaseWidth = 760;
|
||||||
|
constexpr int kBaseHeight = 500;
|
||||||
|
constexpr int kSizeJitter = 10; // ± px of random size jitter applied per launch
|
||||||
|
|
||||||
|
// Random alphanumeric identifier (first char always a letter). Used to give the
|
||||||
|
// loader window a non-constant Win32 title each launch so a scanner can't match
|
||||||
|
// it against a fixed window-text string.
|
||||||
|
QString randomIdent(int minLen, int maxLen) {
|
||||||
|
static const char kAlphabet[] =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
constexpr int kLetters = 52; // leading a-zA-Z only
|
||||||
|
constexpr int kAll = int(sizeof(kAlphabet)) - 1;
|
||||||
|
QRandomGenerator* rng = QRandomGenerator::global();
|
||||||
|
const int len = minLen + int(rng->bounded(quint32(maxLen - minLen + 1)));
|
||||||
|
QString s;
|
||||||
|
s.reserve(len);
|
||||||
|
s.append(QLatin1Char(kAlphabet[rng->bounded(kLetters)]));
|
||||||
|
for (int i = 1; i < len; ++i) {
|
||||||
|
s.append(QLatin1Char(kAlphabet[rng->bounded(kAll)]));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
QString fromW(const std::wstring& w) {
|
QString fromW(const std::wstring& w) {
|
||||||
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
|
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
|
||||||
@@ -60,8 +83,14 @@ MainWindow::MainWindow(QWidget* parent)
|
|||||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
setAttribute(Qt::WA_NoSystemBackground);
|
setAttribute(Qt::WA_NoSystemBackground);
|
||||||
setMinimumSize(760, 500);
|
// Base size with a small random jitter (±kSizeJitter px) on each axis so the
|
||||||
resize(760, 500);
|
// window dimensions aren't a constant fingerprint. Minimum is lowered by the
|
||||||
|
// jitter so a negative jitter isn't clamped back to the base size.
|
||||||
|
setMinimumSize(kBaseWidth - kSizeJitter, kBaseHeight - kSizeJitter);
|
||||||
|
QRandomGenerator* rng = QRandomGenerator::global();
|
||||||
|
const int jitterW = int(rng->bounded(quint32(2 * kSizeJitter + 1))) - kSizeJitter;
|
||||||
|
const int jitterH = int(rng->bounded(quint32(2 * kSizeJitter + 1))) - kSizeJitter;
|
||||||
|
resize(kBaseWidth + jitterW, kBaseHeight + jitterH);
|
||||||
|
|
||||||
styleApp();
|
styleApp();
|
||||||
buildUi();
|
buildUi();
|
||||||
@@ -75,12 +104,15 @@ MainWindow::MainWindow(QWidget* parent)
|
|||||||
|
|
||||||
void MainWindow::buildUi() {
|
void MainWindow::buildUi() {
|
||||||
#ifdef OPENZEN_BUILD_REVISION
|
#ifdef OPENZEN_BUILD_REVISION
|
||||||
const QString winTitle = QStringLiteral("OpenZen Loader · build %1")
|
const QString displayTitle = QStringLiteral("OpenZen Loader · build %1")
|
||||||
.arg(QString::fromLatin1(OPENZEN_BUILD_REVISION).left(7));
|
.arg(QString::fromLatin1(OPENZEN_BUILD_REVISION).left(7));
|
||||||
#else
|
#else
|
||||||
const QString winTitle = QStringLiteral("OpenZen Loader");
|
const QString displayTitle = QStringLiteral("OpenZen Loader");
|
||||||
#endif
|
#endif
|
||||||
setWindowTitle(winTitle);
|
// The OS-level window title (what GetWindowTextW and window scanners read) is
|
||||||
|
// randomised on every launch so it can't be matched against a fixed string.
|
||||||
|
// The visible custom title bar below still shows the branded name.
|
||||||
|
setWindowTitle(randomIdent(8, 16));
|
||||||
|
|
||||||
auto* central = new QWidget(this);
|
auto* central = new QWidget(this);
|
||||||
central->setAttribute(Qt::WA_TranslucentBackground);
|
central->setAttribute(Qt::WA_TranslucentBackground);
|
||||||
@@ -90,7 +122,7 @@ void MainWindow::buildUi() {
|
|||||||
root->setSpacing(0);
|
root->setSpacing(0);
|
||||||
|
|
||||||
titleBar_ = new TitleBar(central);
|
titleBar_ = new TitleBar(central);
|
||||||
titleBar_->setTitleText(winTitle);
|
titleBar_->setTitleText(displayTitle);
|
||||||
root->addWidget(titleBar_);
|
root->addWidget(titleBar_);
|
||||||
|
|
||||||
auto* body = new QWidget(central);
|
auto* body = new QWidget(central);
|
||||||
|
|||||||
@@ -8,5 +8,13 @@ import java.lang.annotation.Target;
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface Patch {
|
public @interface Patch {
|
||||||
Class<?> value();
|
Class<?> value() default void.class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative to {@link #value()} for targeting classes that are not available at compile time.
|
||||||
|
* When non-empty, takes precedence over {@code value()}. This is useful for targeting classes from
|
||||||
|
* optional mods (e.g. Embeddium/Sodium).
|
||||||
|
* <p>Use the fully qualified JVM class name (e.g. {@code "me.jellysquid.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache"}).</p>
|
||||||
|
*/
|
||||||
|
String className() default "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,29 @@ public final class PatchAgent {
|
|||||||
for (Class<?> patch : PatchRegistry.getPatches()) {
|
for (Class<?> patch : PatchRegistry.getPatches()) {
|
||||||
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
||||||
if (ann == null) continue;
|
if (ann == null) continue;
|
||||||
|
if (!ann.className().isEmpty()) {
|
||||||
|
// className-based patches target optional mod classes.
|
||||||
|
// Check if the class is already loaded via Instrumentation.
|
||||||
|
boolean found = false;
|
||||||
|
for (Class<?> loaded : inst.getAllLoadedClasses()) {
|
||||||
|
if (loaded.getName().equals(ann.className())) {
|
||||||
|
LOGGER.debug("Found already-loaded target {} for className-based patch {}",
|
||||||
|
ann.className(), patch.getName());
|
||||||
|
if (inst.isModifiableClass(loaded)) {
|
||||||
|
retransform.add(loaded);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("Cannot retransform unmodifiable target {}", ann.className());
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
LOGGER.debug("Target {} not yet loaded — transformer will catch it at class-load time",
|
||||||
|
ann.className());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Class<?> target;
|
Class<?> target;
|
||||||
try {
|
try {
|
||||||
target = ann.value();
|
target = ann.value();
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ public final class PatchClassFileTransformer implements ClassFileTransformer {
|
|||||||
for (Class<?> patchClass : PatchRegistry.getPatches()) {
|
for (Class<?> patchClass : PatchRegistry.getPatches()) {
|
||||||
Patch patch = patchClass.getAnnotation(Patch.class);
|
Patch patch = patchClass.getAnnotation(Patch.class);
|
||||||
if (patch == null) continue;
|
if (patch == null) continue;
|
||||||
String internalName = patch.value().getName().replace('.', '/');
|
String internalName;
|
||||||
|
if (!patch.className().isEmpty()) {
|
||||||
|
internalName = patch.className().replace('.', '/');
|
||||||
|
} else {
|
||||||
|
internalName = patch.value().getName().replace('.', '/');
|
||||||
|
}
|
||||||
patchesByTarget.computeIfAbsent(internalName, k -> new ArrayList<>()).add(patchClass);
|
patchesByTarget.computeIfAbsent(internalName, k -> new ArrayList<>()).add(patchClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,20 +72,41 @@ public final class PatchTransformer {
|
|||||||
if (patchAnnotation == null) {
|
if (patchAnnotation == null) {
|
||||||
throw new IllegalArgumentException(patchClass.getName() + " is not @Patch");
|
throw new IllegalArgumentException(patchClass.getName() + " is not @Patch");
|
||||||
}
|
}
|
||||||
String patchTargetOwner = Type.getInternalName(patchAnnotation.value());
|
String patchTargetOwner;
|
||||||
|
if (!patchAnnotation.className().isEmpty()) {
|
||||||
|
patchTargetOwner = patchAnnotation.className().replace('.', '/');
|
||||||
|
} else {
|
||||||
|
patchTargetOwner = Type.getInternalName(patchAnnotation.value());
|
||||||
|
}
|
||||||
|
|
||||||
Map<MethodKey, List<Method>> handlersByTarget = new HashMap<>();
|
Map<MethodKey, List<Method>> handlersByTarget = new HashMap<>();
|
||||||
|
// Collect handlers whose desc is empty (name-only wildcards) separately
|
||||||
|
List<Method> nameOnlyHandlers = new ArrayList<>();
|
||||||
for (Method handler : patchClass.getDeclaredMethods()) {
|
for (Method handler : patchClass.getDeclaredMethods()) {
|
||||||
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget);
|
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget, nameOnlyHandlers);
|
||||||
}
|
}
|
||||||
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
||||||
patchClass.getName(), target.name,
|
patchClass.getName(), target.name,
|
||||||
handlersByTarget.values().stream().mapToInt(List::size).sum());
|
handlersByTarget.values().stream().mapToInt(List::size).sum() + nameOnlyHandlers.size());
|
||||||
|
|
||||||
Set<MethodKey> matched = new HashSet<>();
|
Set<MethodKey> matched = new HashSet<>();
|
||||||
|
Set<String> nameOnlyMatched = new HashSet<>();
|
||||||
for (MethodNode method : target.methods) {
|
for (MethodNode method : target.methods) {
|
||||||
MethodKey key = new MethodKey(method.name, method.desc);
|
MethodKey key = new MethodKey(method.name, method.desc);
|
||||||
List<Method> handlers = handlersByTarget.get(key);
|
List<Method> handlers = handlersByTarget.get(key);
|
||||||
|
if (handlers == null && !nameOnlyHandlers.isEmpty()) {
|
||||||
|
// Try name-only match for handlers registered with empty desc.
|
||||||
|
// Works for every handler kind (Inject/Overwrite/Transform/WrapInvoke/
|
||||||
|
// ModifyLocals), not just Inject/Overwrite.
|
||||||
|
for (Method candidate : nameOnlyHandlers) {
|
||||||
|
String targetName = getHandlerTargetMethodName(candidate);
|
||||||
|
if (targetName.equals(method.name)) {
|
||||||
|
if (handlers == null) handlers = new ArrayList<>();
|
||||||
|
handlers.add(candidate);
|
||||||
|
nameOnlyMatched.add(method.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (handlers == null) continue;
|
if (handlers == null) continue;
|
||||||
matched.add(key);
|
matched.add(key);
|
||||||
for (Method handler : handlers) {
|
for (Method handler : handlers) {
|
||||||
@@ -121,11 +142,34 @@ public final class PatchTransformer {
|
|||||||
target.name, entry.getKey().name(), entry.getKey().desc(), target.name);
|
target.name, entry.getKey().name(), entry.getKey().desc(), target.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Warn about unmatched name-only handlers
|
||||||
|
for (Method handler : nameOnlyHandlers) {
|
||||||
|
if (!nameOnlyMatched.contains(getHandlerTargetMethodName(handler))) {
|
||||||
|
LOGGER.warn("Patch handler {}#{} is name-only (empty desc) but no method named \"{}\" exists on {} — handler will not run.",
|
||||||
|
handler.getDeclaringClass().getName(), handler.getName(),
|
||||||
|
getHandlerTargetMethodName(handler), target.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getHandlerTargetMethodName(Method handler) {
|
||||||
|
Inject inject = handler.getAnnotation(Inject.class);
|
||||||
|
if (inject != null) return inject.method();
|
||||||
|
Overwrite overwrite = handler.getAnnotation(Overwrite.class);
|
||||||
|
if (overwrite != null) return overwrite.method();
|
||||||
|
Transform transform = handler.getAnnotation(Transform.class);
|
||||||
|
if (transform != null) return transform.method();
|
||||||
|
WrapInvoke wrap = handler.getAnnotation(WrapInvoke.class);
|
||||||
|
if (wrap != null) return wrap.method();
|
||||||
|
ModifyLocals modify = handler.getAnnotation(ModifyLocals.class);
|
||||||
|
if (modify != null) return modify.method();
|
||||||
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectHandler(Class<?> patchClass, String patchTargetOwner,
|
private static void collectHandler(Class<?> patchClass, String patchTargetOwner,
|
||||||
Method handler,
|
Method handler,
|
||||||
Map<MethodKey, List<Method>> handlersByTarget) {
|
Map<MethodKey, List<Method>> handlersByTarget,
|
||||||
|
List<Method> nameOnlyHandlers) {
|
||||||
if (!(handler.isAnnotationPresent(Inject.class)
|
if (!(handler.isAnnotationPresent(Inject.class)
|
||||||
|| handler.isAnnotationPresent(Overwrite.class)
|
|| handler.isAnnotationPresent(Overwrite.class)
|
||||||
|| handler.isAnnotationPresent(Transform.class)
|
|| handler.isAnnotationPresent(Transform.class)
|
||||||
@@ -176,7 +220,13 @@ public final class PatchTransformer {
|
|||||||
// production Forge environment the live class only has SRG names, so
|
// production Forge environment the live class only has SRG names, so
|
||||||
// remap before matching against ClassNode.methods.
|
// remap before matching against ClassNode.methods.
|
||||||
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
|
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
|
||||||
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
|
if (desc.isEmpty()) {
|
||||||
|
// Empty desc = match by method name only (wildcard for mod classes
|
||||||
|
// whose exact descriptor may vary between Yarn and Mojmap mappings).
|
||||||
|
nameOnlyHandlers.add(handler);
|
||||||
|
} else {
|
||||||
|
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateInjectSignature(Class<?> patchClass, Method handler, Inject inject) {
|
private static void validateInjectSignature(Class<?> patchClass, Method handler, Inject inject) {
|
||||||
@@ -216,7 +266,58 @@ public final class PatchTransformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a HEAD-inject handler's parameter list is bytecode-compatible with the
|
||||||
|
* {@code (receiver?, args..., CallbackInfo)} sequence that {@link #injectHead} forwards.
|
||||||
|
*
|
||||||
|
* <p>This matters most for name-only ({@code desc=""}) patches: those match a target by
|
||||||
|
* method name alone, so a handler can end up bound to a method whose arity or primitive
|
||||||
|
* parameter types differ from what it declares. Forwarding into such a handler would emit a
|
||||||
|
* call site that fails JVM verification ({@code VerifyError}) at class load. Returns
|
||||||
|
* {@code null} when compatible, otherwise a human-readable reason.</p>
|
||||||
|
*/
|
||||||
|
private static String headHandlerMismatch(MethodNode method, Method handler) {
|
||||||
|
List<Type> forwarded = new ArrayList<>();
|
||||||
|
if (!Modifier.isStatic(method.access)) {
|
||||||
|
// Receiver is always a reference; only its primitive-ness is checked below.
|
||||||
|
forwarded.add(Type.getObjectType("java/lang/Object"));
|
||||||
|
}
|
||||||
|
forwarded.addAll(Arrays.asList(Type.getArgumentTypes(method.desc)));
|
||||||
|
|
||||||
|
// The last handler parameter is CallbackInfo (enforced by validateInjectSignature);
|
||||||
|
// the remaining params receive the forwarded receiver + args.
|
||||||
|
int expected = handler.getParameterCount() - 1;
|
||||||
|
if (forwarded.size() != expected) {
|
||||||
|
return "handler takes " + expected + " forwarded parameter(s) but target supplies "
|
||||||
|
+ forwarded.size() + " (receiver + args)";
|
||||||
|
}
|
||||||
|
Class<?>[] params = handler.getParameterTypes();
|
||||||
|
for (int i = 0; i < forwarded.size(); i++) {
|
||||||
|
Type arg = forwarded.get(i);
|
||||||
|
Class<?> p = params[i];
|
||||||
|
boolean argIsPrimitive = arg.getSort() != Type.OBJECT && arg.getSort() != Type.ARRAY;
|
||||||
|
if (argIsPrimitive) {
|
||||||
|
// Primitives cannot widen to Object — the handler param must be the exact type.
|
||||||
|
if (!p.isPrimitive() || !p.getName().equals(arg.getClassName())) {
|
||||||
|
return "parameter " + i + " is primitive " + arg.getClassName()
|
||||||
|
+ " but handler declares " + p.getName();
|
||||||
|
}
|
||||||
|
} else if (p.isPrimitive()) {
|
||||||
|
// A reference arg cannot be passed into a primitive parameter.
|
||||||
|
return "parameter " + i + " is a reference but handler declares primitive " + p.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static void injectHead(MethodNode method, Method handler) {
|
private static void injectHead(MethodNode method, Method handler) {
|
||||||
|
String mismatch = headHandlerMismatch(method, handler);
|
||||||
|
if (mismatch != null) {
|
||||||
|
LOGGER.warn("@Inject(HEAD) {}#{} is incompatible with matched target {}{} — {} — skipping injection to avoid invalid bytecode.",
|
||||||
|
handler.getDeclaringClass().getName(), handler.getName(),
|
||||||
|
method.name, method.desc, mismatch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Type returnType = Type.getReturnType(method.desc);
|
Type returnType = Type.getReturnType(method.desc);
|
||||||
String handlerOwner = Type.getInternalName(handler.getDeclaringClass());
|
String handlerOwner = Type.getInternalName(handler.getDeclaringClass());
|
||||||
String handlerName = handler.getName();
|
String handlerName = handler.getName();
|
||||||
@@ -700,7 +801,9 @@ public final class PatchTransformer {
|
|||||||
|
|
||||||
private static String targetClassName(Method handler) {
|
private static String targetClassName(Method handler) {
|
||||||
Patch patch = handler.getDeclaringClass().getAnnotation(Patch.class);
|
Patch patch = handler.getDeclaringClass().getAnnotation(Patch.class);
|
||||||
return patch == null ? "?" : Type.getInternalName(patch.value());
|
if (patch == null) return "?";
|
||||||
|
if (!patch.className().isEmpty()) return patch.className().replace('.', '/');
|
||||||
|
return Type.getInternalName(patch.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AbstractInsnNode> collectInjectionPoints(InsnList insns, Slice slice,
|
private static List<AbstractInsnNode> collectInjectionPoints(InsnList insns, Slice slice,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import shit.zen.event.EventBus;
|
import shit.zen.event.EventBus;
|
||||||
import shit.zen.event.EventTarget;
|
import shit.zen.event.EventTarget;
|
||||||
import shit.zen.event.impl.TickEvent;
|
import shit.zen.event.impl.TickEvent;
|
||||||
@@ -22,6 +24,7 @@ import shit.zen.manager.HudManager;
|
|||||||
import shit.zen.manager.LagManager;
|
import shit.zen.manager.LagManager;
|
||||||
import shit.zen.manager.ModuleManager;
|
import shit.zen.manager.ModuleManager;
|
||||||
import shit.zen.manager.TargetManager;
|
import shit.zen.manager.TargetManager;
|
||||||
|
import shit.zen.patch.BlockOcclusionCachePatch;
|
||||||
import shit.zen.patch.BlockPatch;
|
import shit.zen.patch.BlockPatch;
|
||||||
import shit.zen.patch.ChatScreenPatch;
|
import shit.zen.patch.ChatScreenPatch;
|
||||||
import shit.zen.patch.ClientLevelPatch;
|
import shit.zen.patch.ClientLevelPatch;
|
||||||
@@ -209,6 +212,13 @@ public class ZenClient extends ClientBase {
|
|||||||
PatchRegistry.register(ItemPatch.class);
|
PatchRegistry.register(ItemPatch.class);
|
||||||
PatchRegistry.register(PlayerTabOverlayPatch.class);
|
PatchRegistry.register(PlayerTabOverlayPatch.class);
|
||||||
PatchRegistry.register(FriendlyByteBufPatch.class);
|
PatchRegistry.register(FriendlyByteBufPatch.class);
|
||||||
|
|
||||||
|
// Compatibility patch for Embeddium/Sodium's BlockOcclusionCache.
|
||||||
|
// Always registered so the transformer can catch the class when it
|
||||||
|
// first loads. We must NOT use Class.forName() here — that would
|
||||||
|
// load the class before our transformer is installed, preventing
|
||||||
|
// the patch from ever being applied.
|
||||||
|
PatchRegistry.register(BlockOcclusionCachePatch.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Minecraft getMcInstance() {
|
public static Minecraft getMcInstance() {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package shit.zen.patch;
|
||||||
|
|
||||||
|
import asm.patchify.annotation.At;
|
||||||
|
import asm.patchify.annotation.Inject;
|
||||||
|
import asm.patchify.annotation.Patch;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import shit.zen.modules.impl.render.XRay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility patch for Embeddium/Sodium's {@code BlockOcclusionCache}.
|
||||||
|
*
|
||||||
|
* <p>Sodium/Embeddium replaces vanilla's chunk mesh building with its own pipeline. The
|
||||||
|
* vanilla {@link Block#shouldRenderFace()} method — which our {@link BlockPatch} hooks
|
||||||
|
* — is never called by Embeddium's {@code BlockRenderer}. Instead, face visibility is
|
||||||
|
* decided by {@code BlockOcclusionCache.shouldDrawSide()}, which has its own entirely
|
||||||
|
* independent occlusion logic.</p>
|
||||||
|
*
|
||||||
|
* <p>This patch injects into {@code BlockOcclusionCache.shouldDrawSide()} so that when
|
||||||
|
* XRay is enabled the result is forced to match {@link XRay#isXrayVisible(Block)},
|
||||||
|
* exactly like our vanilla patch.</p>
|
||||||
|
*
|
||||||
|
* <p>The target class is referenced by {@link Patch#className()} rather than by
|
||||||
|
* {@link Patch#value()} because Embeddium is an optional mod — the class is not
|
||||||
|
* available at compile time. The patch is registered <em>unconditionally</em> in
|
||||||
|
* {@link shit.zen.ZenClient#registerPatches()} (we must <em>not</em> probe with
|
||||||
|
* {@link Class#forName(String)}, which would force the target class to load before our
|
||||||
|
* transformer is installed and thus defeat the patch). When Embeddium is absent the
|
||||||
|
* target class simply never loads, so the registered patch is harmless. The method
|
||||||
|
* descriptor is left empty (name-only match) because the parameter types in Embeddium's
|
||||||
|
* bytecode may differ between Yarn and Mojmap mappings depending on the Embeddium build
|
||||||
|
* and compatibility layer.</p>
|
||||||
|
*/
|
||||||
|
@Patch(className = "me.jellysquid.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache")
|
||||||
|
public class BlockOcclusionCachePatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injected at HEAD of {@code shouldDrawSide}. When XRay is enabled we cancel the
|
||||||
|
* original method and force the result: target blocks return {@code true} (all
|
||||||
|
* faces visible = rendered through walls) and non-target blocks return
|
||||||
|
* {@code false} (no faces visible = completely transparent).
|
||||||
|
*
|
||||||
|
* <p>All reference parameters are declared as {@link Object} rather than their actual
|
||||||
|
* types because Embeddium may be compiled with either Yarn or Mojmap mappings — every
|
||||||
|
* argument of {@code shouldDrawSide} is a reference type, so declaring them as
|
||||||
|
* {@code Object} lets the forwarded values widen cleanly regardless of the concrete
|
||||||
|
* runtime types. Note this only protects against <em>type</em> mismatches: the
|
||||||
|
* parameter <em>count</em> must still equal the target method's arity (receiver + args),
|
||||||
|
* otherwise the generated call site is invalid. {@code PatchTransformer.injectHead}
|
||||||
|
* now guards this and skips the injection with a warning rather than emitting bytecode
|
||||||
|
* that would fail verification. The only param we actually read is {@code selfState},
|
||||||
|
* which is cast to Mojmap {@link BlockState} (always correct at runtime since the loaded
|
||||||
|
* Minecraft classes are Mojmap-mapped in a Forge environment).</p>
|
||||||
|
*
|
||||||
|
* @param self the {@code BlockOcclusionCache} instance (unused)
|
||||||
|
* @param selfState the {@code BlockState} of the block being rendered
|
||||||
|
* @param view the {@code BlockView/BlockGetter} (unused)
|
||||||
|
* @param pos the {@code BlockPos} (unused)
|
||||||
|
* @param direction the facing {@code Direction} (unused)
|
||||||
|
* @param ci callback info — cancelled with redirected result
|
||||||
|
*/
|
||||||
|
@Inject(
|
||||||
|
method = "shouldDrawSide",
|
||||||
|
desc = "",
|
||||||
|
at = @At(At.Type.HEAD)
|
||||||
|
)
|
||||||
|
public static void onShouldDrawSide(Object self, Object selfState, Object view,
|
||||||
|
Object pos, Object direction, CallbackInfo ci) {
|
||||||
|
XRay xray = XRay.INSTANCE;
|
||||||
|
if (xray == null || !xray.isEnabled()) {
|
||||||
|
return; // XRay off -> keep original Embeddium occlusion logic
|
||||||
|
}
|
||||||
|
// Cast to Mojmap BlockState — always correct at runtime on Forge
|
||||||
|
boolean visible = xray.isXrayVisible(((BlockState) selfState).getBlock());
|
||||||
|
ci.result = visible ? Boolean.TRUE : Boolean.FALSE;
|
||||||
|
ci.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,12 @@ public class StencilHelper {
|
|||||||
private static final RenderTarget mainRenderTarget = ClientBase.mc.getMainRenderTarget();
|
private static final RenderTarget mainRenderTarget = ClientBase.mc.getMainRenderTarget();
|
||||||
|
|
||||||
public static void applyStencil(PoseStack poseStack, Runnable drawMask, Runnable drawContent, float opacity) {
|
public static void applyStencil(PoseStack poseStack, Runnable drawMask, Runnable drawContent, float opacity) {
|
||||||
|
if (!stencilShader.isValid()) {
|
||||||
|
// Stencil shader unavailable on this driver — skip the rounded composite and
|
||||||
|
// just draw the content directly so the panel still shows (without rounding).
|
||||||
|
drawContent.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
Matrix4f pose = poseStack.last().pose();
|
Matrix4f pose = poseStack.last().pose();
|
||||||
RenderTarget mask = maskTarget.get();
|
RenderTarget mask = maskTarget.get();
|
||||||
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
|
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public class ShaderProgram {
|
|||||||
@Getter
|
@Getter
|
||||||
private final int programId;
|
private final int programId;
|
||||||
@Getter
|
@Getter
|
||||||
|
private final boolean valid;
|
||||||
|
@Getter
|
||||||
private final Map<String, Integer> uniformCache = new HashMap<>();
|
private final Map<String, Integer> uniformCache = new HashMap<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final Matrix4Uniform modelViewUniform;
|
private final Matrix4Uniform modelViewUniform;
|
||||||
@@ -50,6 +52,18 @@ public class ShaderProgram {
|
|||||||
this.programId = GL20.glCreateProgram();
|
this.programId = GL20.glCreateProgram();
|
||||||
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
|
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
|
||||||
int vertexShader = ShaderProgram.compileShader(ShaderSource.getByFileName(vertexName + ".vsh").getSource(), 35633);
|
int vertexShader = ShaderProgram.compileShader(ShaderSource.getByFileName(vertexName + ".vsh").getSource(), 35633);
|
||||||
|
// Shader compilation can fail on some drivers (notably Intel forward-compatible
|
||||||
|
// GL contexts, which reject legacy GLSL such as texture2D). Degrade gracefully —
|
||||||
|
// disable this program so callers skip the effect — instead of crashing the game.
|
||||||
|
if (fragmentShader == 0 || vertexShader == 0) {
|
||||||
|
if (fragmentShader != 0) GL20.glDeleteShader(fragmentShader);
|
||||||
|
if (vertexShader != 0) GL20.glDeleteShader(vertexShader);
|
||||||
|
LOGGER.error("Shader '{}' disabled: compilation failed; the effect will be skipped.", fragmentName);
|
||||||
|
this.modelViewUniform = null;
|
||||||
|
this.projectionUniform = null;
|
||||||
|
this.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
GL20.glAttachShader(this.programId, fragmentShader);
|
GL20.glAttachShader(this.programId, fragmentShader);
|
||||||
GL20.glAttachShader(this.programId, vertexShader);
|
GL20.glAttachShader(this.programId, vertexShader);
|
||||||
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
|
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
|
||||||
@@ -59,15 +73,25 @@ public class ShaderProgram {
|
|||||||
GL20.glLinkProgram(this.programId);
|
GL20.glLinkProgram(this.programId);
|
||||||
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
|
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
|
||||||
LOGGER.error(GL20.glGetProgramInfoLog(this.programId, Short.MAX_VALUE));
|
LOGGER.error(GL20.glGetProgramInfoLog(this.programId, Short.MAX_VALUE));
|
||||||
throw new IllegalStateException("Failed to link shader program!");
|
LOGGER.error("Shader '{}' disabled: program link failed; the effect will be skipped.", fragmentName);
|
||||||
|
GL20.glDeleteShader(fragmentShader);
|
||||||
|
GL20.glDeleteShader(vertexShader);
|
||||||
|
this.modelViewUniform = null;
|
||||||
|
this.projectionUniform = null;
|
||||||
|
this.valid = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
GL20.glDeleteShader(fragmentShader);
|
GL20.glDeleteShader(fragmentShader);
|
||||||
GL20.glDeleteShader(vertexShader);
|
GL20.glDeleteShader(vertexShader);
|
||||||
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
|
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
|
||||||
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
|
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
|
||||||
|
this.valid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void use() {
|
public void use() {
|
||||||
|
if (!this.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
prevProgram = GL20.glGetInteger(35725);
|
prevProgram = GL20.glGetInteger(35725);
|
||||||
GL20.glUseProgram(this.programId);
|
GL20.glUseProgram(this.programId);
|
||||||
this.setModelView(RenderSystem.getModelViewMatrix());
|
this.setModelView(RenderSystem.getModelViewMatrix());
|
||||||
@@ -112,8 +136,9 @@ public class ShaderProgram {
|
|||||||
GL20.glShaderSource(shader, source);
|
GL20.glShaderSource(shader, source);
|
||||||
GL20.glCompileShader(shader);
|
GL20.glCompileShader(shader);
|
||||||
if (GL20.glGetShaderi(shader, 35713) == 0) {
|
if (GL20.glGetShaderi(shader, 35713) == 0) {
|
||||||
LOGGER.error(GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
LOGGER.error("Failed to compile shader (type {}):\n{}", type, GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
||||||
throw new IllegalStateException(String.format("Failed to compile shader! (Type: %s)", new Object[]{type}));
|
GL20.glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return shader;
|
return shader;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.LadderBlock;
|
|||||||
import net.minecraft.world.level.block.LiquidBlock;
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
import net.minecraft.world.level.block.SlabBlock;
|
import net.minecraft.world.level.block.SlabBlock;
|
||||||
import net.minecraft.world.level.block.TntBlock;
|
import net.minecraft.world.level.block.TntBlock;
|
||||||
|
import net.minecraft.world.level.block.WebBlock;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.phys.AABB;
|
import net.minecraft.world.phys.AABB;
|
||||||
import net.minecraft.world.phys.shapes.Shapes;
|
import net.minecraft.world.phys.shapes.Shapes;
|
||||||
@@ -122,6 +123,10 @@ extends ClientBase {
|
|||||||
if (block instanceof SlabBlock) {
|
if (block instanceof SlabBlock) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Cobwebs are tactical utility items (AutoWebPlace ammo), never bridging blocks.
|
||||||
|
if (block instanceof WebBlock) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return !blacklist.contains(block);
|
return !blacklist.contains(block);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ extends ClientBase {
|
|||||||
if (roundedRectShader == null) {
|
if (roundedRectShader == null) {
|
||||||
roundedRectShader = new ShaderProgram("rounded_rect", "vertex_color", ShaderFormats.POSITION_UV_COLOR);
|
roundedRectShader = new ShaderProgram("rounded_rect", "vertex_color", ShaderFormats.POSITION_UV_COLOR);
|
||||||
}
|
}
|
||||||
|
if (!roundedRectShader.isValid()) {
|
||||||
|
// Rounded-rect shader unavailable on this driver — draw a plain rect instead of crashing.
|
||||||
|
RenderUtil.drawFilledRect(poseStack, x, y, width, height, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Matrix4f matrix4f = poseStack.last().pose();
|
Matrix4f matrix4f = poseStack.last().pose();
|
||||||
roundedRectShader.use();
|
roundedRectShader.use();
|
||||||
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
|
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
|
||||||
@@ -222,6 +227,11 @@ extends ClientBase {
|
|||||||
if (blurShader == null) {
|
if (blurShader == null) {
|
||||||
blurShader = new ShaderProgram("blur", ShaderFormats.POSITION_UV_COLOR);
|
blurShader = new ShaderProgram("blur", ShaderFormats.POSITION_UV_COLOR);
|
||||||
}
|
}
|
||||||
|
if (!blurShader.isValid()) {
|
||||||
|
// Blur shader unavailable on this driver — disable blur for the rest of the session.
|
||||||
|
blurFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mainRenderTarget == null) {
|
if (mainRenderTarget == null) {
|
||||||
mainRenderTarget = mc.getMainRenderTarget();
|
mainRenderTarget = mc.getMainRenderTarget();
|
||||||
}
|
}
|
||||||
|
|||||||