Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
Write-Host "Exported VCPKG_ROOT=$vcpkg to GITHUB_ENV"
|
||||
|
||||
# Cache the resolved Qt install + the file-backed binary archive.
|
||||
# Key is keyed on vcpkg.json so any dependency change invalidates;
|
||||
# restore-keys lets a vcpkg.json bump still warm-start from the
|
||||
# previous build's archives. installed/ alone makes CMake's manifest
|
||||
# mode skip rebuilding qtbase; archives/ also fills back transitive
|
||||
# deps if a partial rebuild is needed.
|
||||
- name: Cache vcpkg installed + archives
|
||||
# Cache the vcpkg *binary archives* — the only thing that lets vcpkg skip
|
||||
# rebuilding qtbase. The build runs `gradlew clean`, which deletes
|
||||
# native/build/ (and thus the manifest install dir native/build/vcpkg_installed),
|
||||
# so on every run vcpkg re-resolves the manifest and restores packages from
|
||||
# this file-backed binary cache. If it's empty, qtbase is compiled from
|
||||
# source (~32 min). (We previously cached vcpkg\installed too, but in
|
||||
# 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
|
||||
with:
|
||||
path: |
|
||||
vcpkg\installed
|
||||
vcpkg-archives
|
||||
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}
|
||||
path: vcpkg-archives
|
||||
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-
|
||||
vcpkg-qt-${{ runner.os }}-
|
||||
|
||||
- name: Install UPX
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Open Zen
|
||||
|
||||
> 欢迎加群 523522206 讨论该项目。
|
||||
> 由于老群被精神马来西亚人的小狗疯狂举报,因此无法与您继续互动。欢迎加新群 660304532 讨论本项目。
|
||||
|
||||
> 截止至目前,Open Zen仍然处于早期构建阶段。大量特性可能仍不可用,请谅解!当您发现有不可用的功能时,欢迎开Issues描述或直接提交Pull Request,我们将不胜感激!
|
||||
> 当您发现有不可用的功能时,欢迎开 Issues 描述或直接提交 Pull Request,我们将不胜感激!
|
||||
|
||||
**Open Zen** 是 *Zen* Minecraft 客户端的反混淆源码版本,几乎是由 [Claude](https://claude.com/)协助逆向得到。目标版本为 **Minecraft 1.20.1 + Forge 47.4.20**。
|
||||
**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。
|
||||
原始 Jar 经过完整混淆:类/字段/方法重命名、控制流扁平化。我使用 Opus 4.7 对其进行了反混淆,并结合 [Enigma MCP](https://github.com/Margele/Enigma-MCP) 和 Sonnet 4.6 对其类/字段/方法重命名进行猜测,最后将其还原为可读的 Java。最终产物是一个可以直接用 Gradle 构建的工程,而不是一个二进制 blob。
|
||||
|
||||
> ⚠️ 本仓库**仅供学习与研究目的发布** —— 用于研究客户端侧游戏改造、ASM 字节码补丁和混淆/反混淆技术。在你不拥有的服务器上使用作弊客户端违反绝大多数服务器规则,请自行承担后果。
|
||||
|
||||
@@ -14,19 +14,104 @@
|
||||
原始混淆字节码未授予任何许可。本仓库中的反混淆产物、构建脚本与文档**仅供研究与学习使用**。如果你是 Zen 的原作者并希望本仓库下架或重新授权,请提 Issues。虽然提了也不会搭理你。
|
||||
|
||||
## 截图
|
||||

|
||||

|
||||
|
||||
## 精神马来西亚人
|
||||
或许是由于Zen的作者可能由于常年惨遭家暴,亦可能是由于常年沉迷于米哈游大作导致大脑退化完成义务教育后无法进行进一步的大脑升级。精神马来西亚人不得不前往马来西亚,以进一步大脑升级为高中毕业学历。在精神马来西亚人抵达马来西亚后,精神马来人似乎找到了自己的归宿。
|
||||
|
||||

|
||||
|
||||
自此,精神马来人正式成为精神马来人。开始称中国人“支那猪”,称中国[“支那”](https://zh.wikipedia.org/wiki/%E6%94%AF%E9%82%A3)。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
除此之外,精神马来人还会发表更多奇异搞笑言论。当你购买精神马来西亚人的外挂后,你必须要阅读#rules后才可以使用其外挂,其中包括“承认台湾是一个国家”、“承认新疆、西藏、香港、澳门同样都是独立国家”等奇异搞笑言论。因此笔者很难想象其外挂用户的政治立场。
|
||||
|
||||

|
||||
|
||||
除此之外,精神马来西亚人称惨绝人寰的[南京大屠杀](https://zh.wikipedia.org/wiki/%E5%8D%97%E4%BA%AC%E5%A4%A7%E5%B1%A0%E6%AE%BA)事件**晦气**。
|
||||
|
||||

|
||||
|
||||
## 开挂死妈
|
||||

|
||||
|
||||
精神马来西亚人认为,笔者在游玩《三角洲行动》时使用了外挂程序是疑似右手残疾的表现,正确的做法是练习枪法。因此使用本项目在《我的世界》中作弊可能会导致右手残疾,在使用本项目进行作弊前,请确认您的右手没有残疾!本项目不会对您的右手残疾付任何责任。
|
||||
|
||||
如果您在使用本项目时出现了疑似右手残疾的症状(如右手无力等),请及时关闭键盘声音以避免自己的生物爹对自己进行家暴行为。
|
||||
|
||||
[不是我咋掉线了操](./img/不是我咋掉线了操.mp4)
|
||||
|
||||
## 我有抑郁症
|
||||
众所周知,精神马来西亚人患有严重的精神疾病。结合此前精神马来人对其他亲朋好友的倾诉,笔者得知精神马来西亚人曾在群直播自己使用作弊客户端游玩《我的世界》游戏。但是突然下播,在长达半个小时的等待时间后,精神马来西亚人称自己由于键盘声音过大而惨遭家暴。
|
||||
|
||||

|
||||
|
||||
因此精神马来西亚人长期通过自残、过量使用药物等行为缓解自己长期惨遭家暴的事实。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
或许是出自自卑, 精神马来西亚人在公开时称嗑药是跟风行为。对于精神马来人对过量使用药物的态度,笔者暂且蒙在鼓里。
|
||||
|
||||
## 大孝子
|
||||
可能由于常年的家暴,导致精神马来西亚人的认知出现了错乱。又或许是长期多次的家暴导致精神马来西亚人患上了[创伤后应激障碍](https://zh.wikipedia.org/wiki/%E5%89%B5%E5%82%B7%E5%BE%8C%E5%A3%93%E5%8A%9B%E7%97%87),精神马来西亚人认为自己**滚刀爹妈**。笔者尚不明确精神马来西亚人所述的滚刀爹妈是何意味,但是笔者希望精神马来西亚人早日康复。
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## 发送低保
|
||||

|
||||
|
||||
当精神马来西亚人急眼时,将会自动查询你爹妈的户籍并且强制向你爹妈发送两份低保。
|
||||
|
||||
尽管[关于印发《山东省最低生活保障管理办法》的通知](http://mzt.shandong.gov.cn/art/2021/9/30/art_15335_10291529.html)明确规定了:
|
||||
|
||||
```
|
||||
第二十三条 家庭财产状况有下列情形之一的,原则上不纳入低保范围:
|
||||
|
||||
(一)人均金融资产超过当地年低保标准2倍的;
|
||||
(二)拥有机动车辆(普通二轮和三轮摩托车、残疾人用于功能型补偿代步的机动车辆除外)、船舶、大型农机具的;
|
||||
(三)拥有两套及以上住房且住房总面积超过当地住房保障标准面积2倍,或者申请低保之前1年内以及享受低保期间购买超过当地住房保障标准面积商品房的;申请低保之前1年内或者享受低保期间,兴建、购买非居住用房或者高标准装修住房的;
|
||||
(四)具有投资行为且人均投资数额超过当地年低保标准2倍的;
|
||||
(五)雇佣他人从事经营性活动的;
|
||||
(六)实际生活水平明显高于当地低保标准的。
|
||||
|
||||
设区的市可根据各自实际和财力条件,对家庭财产状况规定进行细化和调整,增加的支出由当地筹集安排。
|
||||
```
|
||||
|
||||

|
||||
|
||||
但仅笔者一人,2026年个人所得税申报仅有约四十万人民币,笔者家庭明显符合不纳入低保范围的条件。笔者暂且不清楚精神马来西亚人向笔者全家发送低保的动机,可能是因为精神马来人惨遭家暴精神错乱致使其认为拥有低保是一件非常令人羞耻的事情。笔者建议精神马来西亚人早日纠正错误想法。
|
||||
|
||||
## 后门
|
||||
> 警告!在阅读以下内容时,您可能会感到不适!如有不适,请及时关闭本页面,以避免自己笑出声音而导致惨遭生物爹家暴。
|
||||
|
||||
我们在逆向时发现原版Zen存在大量后门,例如上报QQ、屏幕截图、扫描文件、上传文件、远程执行命令等。因此我们**不推荐**任何用户继续使用原版Zen,除非你愿意现在把你身上的衣服脱掉然后去本地人最多的广场裸舞,然后把自己裸舞的视频发送到Zen的群内。
|
||||
|
||||
当Zen被注入后,会自动触发截图并上传至服务器。精神马来西亚人回应如下:
|
||||
|
||||

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

|
||||
|
||||
对此,笔者综合精神马来西亚人由于半夜玩电脑惨遭自己生物爹家暴的事实猜测:精神马来西亚人的生物爹和生物妈可能对精神马来西亚人的控制欲极强,因此精神马来西亚人的生活空间内可能存在十万甚至九万个摄像头,对精神马来西亚人的生活进行了无孔不入的监控。因此,精神马来西亚人在拉屎、自慰、睡觉、上课时都时时刻刻被监控,所以自然认为截图用户是正常且合理的行为。
|
||||
|
||||
笔者在此提醒:对用户的电脑进行无提醒、未通知用户的全屏幕截图,是不合理的行为。建议精神马来人端正自己对这个世界的认知,从自己过往被家暴经历中走出来,祝你早日康复!
|
||||
|
||||
### 分析
|
||||
当Zen启动时,会自动调用 `iIiIiIiIIIiIiI/Ʊ Đ()Ljava/awt/image/BufferedImage` ([Mapping](./mapping/zen.mapping#L140)),可能由于精神马来西亚人自知是后门,因此精神马来西亚人将此方法严防死守,惨遭没有逼卵子用的Native混淆。
|
||||
|
||||
@@ -44,7 +129,7 @@
|
||||
|
||||
随后,笔者继续分析。由于该类继承了 `iIiIiIiIIIiIiI/ɰ` (`Packet`) ([Mapping](./mapping/zen.mapping#L2459)),我们分析了所有该类的子类。
|
||||
|
||||
遗憾的,其他类由于没有添加Lombok标识,我们我们不得不通过其他方式Trace这些类的具体用途。经过我们不懈努力的调试和追踪,我们还原出了我们认为可疑的部分行为。
|
||||
遗憾的是,其他类由于没有添加 Lombok 标识,我们不得不通过其他方式 Trace 这些类的具体用途。经过我们不懈努力的调试和追踪,我们还原出了我们认为可疑的部分行为。
|
||||
|
||||
- 远程命令执行 `iIiIiIiIIIiIiI/ʔ`
|
||||
- 远程文件下发 `iIiIiIiIIIiIiI/ʏ`
|
||||
@@ -67,10 +152,25 @@
|
||||
由于精神马来西亚人嘴硬,所以到底具体有没有实现,笔者暂且蒙古。
|
||||
|
||||
### 父子相爱相杀
|
||||
精神马来西亚人在QQ群中指出,欣欣使用过外挂后门远程读取他人文件。不知作为德州骡子的许锦良见自己的亲生父亲如此指认自己作何感想。
|
||||
|
||||

|
||||
|
||||
在很久之前,作为知名野狗的许锦良曾对精神马来西亚人进行过攻击:许锦良认为精神马来西亚人是他儿子,去日本是为了成为慰安妇,抚平自己被家暴的过去。
|
||||
|
||||

|
||||
|
||||
但很明显,在本项目发布后,许锦良对OpenZen交流群创下了高达十进十出的历史记录。笔者在此猜测,精神马来西亚人曾因为自己半夜玩电脑由于敲键盘声音过大惨遭家暴的事情中迟迟无法走出阴影,因此自小时便缺失来自生物爹的父爱。而许锦良称精神马来西亚人为儿子,因此刚好补上了自己缺失的父爱这一块,私下便偷偷称许锦良为自己的父亲。自始,二人幸终。
|
||||
|
||||
笔者在惨遭许锦良十进十出狗叫时,认为许锦良可能已经完成[前脑叶白质切除术](https://zh.wikipedia.org/wiki/%E8%84%91%E7%99%BD%E8%B4%A8%E5%88%87%E9%99%A4%E6%9C%AF),许锦良坚持认为两张照片是同一个人,对此许锦良掏出了以下证据:
|
||||
|
||||

|
||||
|
||||
由此可见,许锦良并没有思考的能力,结合自己亲儿子精神马来西亚人患有多种精神疾病的事实与精神马来西亚人认为Telegram查询机器人的事实,证实了笔者在前提到的前脑叶白质切除术。笔者在此希望许锦良父子能够早日康复。
|
||||
|
||||

|
||||
|
||||
但精神马来西亚人在QQ群中指出,欣欣使用过外挂后门远程读取他人文件。不知作为德州骡子的许锦良见自己的亲生儿子如此指认自己作何感想。
|
||||
|
||||
## 原始 Jar + Mapping
|
||||
[原始Jar](./mapping/zen-orignial.jar)
|
||||
[Mapping](./mapping/zen.mapping)
|
||||
@@ -78,7 +178,7 @@
|
||||
需要说明的是,部分喜欢裸舞的忠实Zen用户认为本源码逆向自比较旧的Zen版本。可能是因为这部分用户的脑容量只允许自己导入其他配置,因此不认识Zen的老旧UI。因此必须要说明,此源码使用的原始Jar截止至2026年5月21日是最新的。
|
||||
|
||||
## 细节
|
||||
经过Opus 4.7长达18秒的分析,Opus认为所有的类由惨遭魔改的Zelix KlassMaster混淆。除了Zelix的Invoke Dynamic和String Encryption外,还有部分未参与任何计算的Interger \ Long变量花指令代码和仅在部分方法中出现的Flow混淆。
|
||||
经过Opus 4.7长达18秒的分析,Opus认为所有的类由惨遭魔改的Zelix KlassMaster混淆。除了Zelix的Invoke Dynamic和String Encryption外,还有部分未参与任何计算的 Integer / Long 变量花指令代码和仅在部分方法中出现的 Flow 混淆。
|
||||
|
||||
其中大部分类都可以经过小修小补的现有Zelix反混淆器完成,关键部分的`cinit`被Native保护,导致在Java层中没有对应的Master Key可以对Invoke Dynamic和String反混淆。但可能由于精神马来西亚人的脑袋在马来西亚骑摩托被其他车创飞导致脑溢血,即使你没有通过客户端认证也可以完整加载Native并对Class进行注册。因此我们完整的还原了所有类的Invoke Dynamic和String混淆。
|
||||
|
||||
@@ -102,17 +202,17 @@ OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启
|
||||
|
||||
### 编译时类名混淆(重要)
|
||||
|
||||
每次构建,OpenZen 会**自动把所有自有类(`shit.zen.*` / `asm.patchify.*`)重命名为随机的 16 位名字**——包名和类名都随机,**每次构建都不一样**、互不重复,原始类名/包名一律不保留(连日志里残留的类名字符串也清理掉了)。引导链(Agent 入口、DLL 加载、`Class.forName`)会在构建时自动联动到新名字,无需手工处理。两种交付形式(jar / 注入器)都已混淆。
|
||||
每次构建,OpenZen 会**自动把所有自有类(`shit.zen.*` / `asm.patchify.*`)重命名为随机的 16 位名字**——包名和类名都随机,**每次构建都不一样**、互不重复,原始类名/包名一律不保留(连日志里残留的类名字符串也清理掉了)。引导链(Agent 入口、DLL 加载、`Class.forName`)会在构建时自动联动到新名字,无需手工处理。两种交付形式(jar / 注入器)都已混淆。
|
||||
|
||||
这是为了对抗按**类名黑名单**工作的反作弊(见下方[常见问题](#布吉岛反作弊绕过))。正因为名字每次构建随机:
|
||||
这是为了对抗按**类名黑名单**工作的反作弊(见下方[常见问题](#布吉岛反作弊绕过))。正因为名字每次构建随机:
|
||||
|
||||
> ⚠️ **从 GitHub Actions / Release 下载到的是预编译版本,所有人拿到的是同一套混淆名**——这套固定的名字随时可能被反作弊收录进黑名单。想要一套**别人都不知道、独一无二**的类名,请**自己编译**:
|
||||
> - **Fork 本仓库**,在你自己的 GitHub Actions 里跑一次构建(每次运行都生成全新随机名),下载你自己的 artifact;**或**
|
||||
> ⚠️ **从 GitHub Actions / Release 下载到的是预编译版本,所有人拿到的是同一套混淆名**——这套固定的名字随时可能被反作弊收录进黑名单。想要一套**别人都不知道、独一无二**的类名,请**自己编译**:
|
||||
> - **Fork 本仓库**,在你自己的 GitHub Actions 里跑一次构建(每次运行都生成全新随机名),下载你自己的 artifact;**或**
|
||||
> - **clone 到本地**自己 `gradlew jar` / `gradlew dll`(每次本地构建同样是全新随机名)。
|
||||
|
||||
每次构建的"旧名 → 新名"映射写在 `build/rename-mapping.txt`(CI 也会把它作为 artifact 上传、并附到 Release),这是反混淆崩溃日志的**唯一**依据。**注意它每次构建都不同,务必和对应产物一起保存。**
|
||||
每次构建的"旧名 → 新名"映射写在 `build/rename-mapping.txt`(CI 也会把它作为 artifact 上传、并附到 Release),这是反混淆崩溃日志的**唯一**依据。**注意它每次构建都不同,务必和对应产物一起保存。**
|
||||
|
||||
实现细节见 `build.gradle` 的 `ext.obfuscateJar`:用项目自带的 ASM 在 ForgeGradle `reobf` 之后对产出 jar 做 `ClassRemapper` 重命名,**只改类名、不动方法/字段名**(避免破坏反射、JNI、`@SerializedName` 等)。
|
||||
实现细节见 `build.gradle` 的 `ext.obfuscateJar`:用项目自带的 ASM 在 ForgeGradle `reobf` 之后对产出 jar 做 `ClassRemapper` 重命名,**只改类名、不动方法/字段名**(避免破坏反射、JNI、`@SerializedName` 等)。
|
||||
|
||||
### 共同前置
|
||||
|
||||
@@ -194,7 +294,7 @@ OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启
|
||||
~~截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。~~
|
||||
~~建议构建时修改类名。~~
|
||||
~~类名已黑名单,请在构建时修改类名。~~
|
||||
类名已黑名单。现在**每次构建都会自动随机化全部类名**(见上方[编译时类名混淆](#编译时类名混淆重要))——但**务必自己 Fork/clone 编译**,别直接用 GitHub Actions / Release 上的预编译版:那是固定的一套名字,会被拉黑。
|
||||
类名已黑名单。现在**每次构建都会自动随机化全部类名**(见上方[编译时类名混淆](#编译时类名混淆重要))——但**务必自己 Fork/clone 编译**,别直接用 GitHub Actions / Release 上的预编译版:那是固定的一套名字,会被拉黑。
|
||||
|
||||
## 致谢
|
||||
|
||||
@@ -203,4 +303,4 @@ OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启
|
||||
- [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)
|
||||
- [Enigma MCP](https://github.com/Margele/Enigma-MCP)
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -15,6 +15,7 @@
|
||||
#include <QPainterPath>
|
||||
#include <QParallelAnimationGroup>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QRandomGenerator>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
@@ -32,6 +33,28 @@ namespace loader {
|
||||
|
||||
namespace {
|
||||
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) {
|
||||
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
|
||||
@@ -60,8 +83,14 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setAttribute(Qt::WA_NoSystemBackground);
|
||||
setMinimumSize(760, 500);
|
||||
resize(760, 500);
|
||||
// Base size with a small random jitter (±kSizeJitter px) on each axis so the
|
||||
// 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();
|
||||
buildUi();
|
||||
@@ -75,12 +104,15 @@ MainWindow::MainWindow(QWidget* parent)
|
||||
|
||||
void MainWindow::buildUi() {
|
||||
#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));
|
||||
#else
|
||||
const QString winTitle = QStringLiteral("OpenZen Loader");
|
||||
const QString displayTitle = QStringLiteral("OpenZen Loader");
|
||||
#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);
|
||||
central->setAttribute(Qt::WA_TranslucentBackground);
|
||||
@@ -90,7 +122,7 @@ void MainWindow::buildUi() {
|
||||
root->setSpacing(0);
|
||||
|
||||
titleBar_ = new TitleBar(central);
|
||||
titleBar_->setTitleText(winTitle);
|
||||
titleBar_->setTitleText(displayTitle);
|
||||
root->addWidget(titleBar_);
|
||||
|
||||
auto* body = new QWidget(central);
|
||||
|
||||
@@ -8,5 +8,13 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
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()) {
|
||||
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
||||
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;
|
||||
try {
|
||||
target = ann.value();
|
||||
|
||||
@@ -42,7 +42,12 @@ public final class PatchClassFileTransformer implements ClassFileTransformer {
|
||||
for (Class<?> patchClass : PatchRegistry.getPatches()) {
|
||||
Patch patch = patchClass.getAnnotation(Patch.class);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,20 +72,41 @@ public final class PatchTransformer {
|
||||
if (patchAnnotation == null) {
|
||||
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<>();
|
||||
// Collect handlers whose desc is empty (name-only wildcards) separately
|
||||
List<Method> nameOnlyHandlers = new ArrayList<>();
|
||||
for (Method handler : patchClass.getDeclaredMethods()) {
|
||||
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget);
|
||||
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget, nameOnlyHandlers);
|
||||
}
|
||||
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
||||
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<String> nameOnlyMatched = new HashSet<>();
|
||||
for (MethodNode method : target.methods) {
|
||||
MethodKey key = new MethodKey(method.name, method.desc);
|
||||
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;
|
||||
matched.add(key);
|
||||
for (Method handler : handlers) {
|
||||
@@ -121,11 +142,34 @@ public final class PatchTransformer {
|
||||
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,
|
||||
Method handler,
|
||||
Map<MethodKey, List<Method>> handlersByTarget) {
|
||||
Map<MethodKey, List<Method>> handlersByTarget,
|
||||
List<Method> nameOnlyHandlers) {
|
||||
if (!(handler.isAnnotationPresent(Inject.class)
|
||||
|| handler.isAnnotationPresent(Overwrite.class)
|
||||
|| handler.isAnnotationPresent(Transform.class)
|
||||
@@ -176,7 +220,13 @@ public final class PatchTransformer {
|
||||
// production Forge environment the live class only has SRG names, so
|
||||
// remap before matching against ClassNode.methods.
|
||||
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) {
|
||||
@@ -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) {
|
||||
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);
|
||||
String handlerOwner = Type.getInternalName(handler.getDeclaringClass());
|
||||
String handlerName = handler.getName();
|
||||
@@ -700,7 +801,9 @@ public final class PatchTransformer {
|
||||
|
||||
private static String targetClassName(Method handler) {
|
||||
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,
|
||||
|
||||
@@ -12,6 +12,8 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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.EventTarget;
|
||||
import shit.zen.event.impl.TickEvent;
|
||||
@@ -22,6 +24,7 @@ import shit.zen.manager.HudManager;
|
||||
import shit.zen.manager.LagManager;
|
||||
import shit.zen.manager.ModuleManager;
|
||||
import shit.zen.manager.TargetManager;
|
||||
import shit.zen.patch.BlockOcclusionCachePatch;
|
||||
import shit.zen.patch.BlockPatch;
|
||||
import shit.zen.patch.ChatScreenPatch;
|
||||
import shit.zen.patch.ClientLevelPatch;
|
||||
@@ -209,6 +212,13 @@ public class ZenClient extends ClientBase {
|
||||
PatchRegistry.register(ItemPatch.class);
|
||||
PatchRegistry.register(PlayerTabOverlayPatch.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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
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();
|
||||
RenderTarget mask = maskTarget.get();
|
||||
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
|
||||
|
||||
@@ -19,6 +19,8 @@ public class ShaderProgram {
|
||||
@Getter
|
||||
private final int programId;
|
||||
@Getter
|
||||
private final boolean valid;
|
||||
@Getter
|
||||
private final Map<String, Integer> uniformCache = new HashMap<>();
|
||||
@Getter
|
||||
private final Matrix4Uniform modelViewUniform;
|
||||
@@ -50,6 +52,18 @@ public class ShaderProgram {
|
||||
this.programId = GL20.glCreateProgram();
|
||||
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
|
||||
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, vertexShader);
|
||||
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
|
||||
@@ -59,15 +73,25 @@ public class ShaderProgram {
|
||||
GL20.glLinkProgram(this.programId);
|
||||
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
|
||||
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(vertexShader);
|
||||
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
|
||||
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
public void use() {
|
||||
if (!this.valid) {
|
||||
return;
|
||||
}
|
||||
prevProgram = GL20.glGetInteger(35725);
|
||||
GL20.glUseProgram(this.programId);
|
||||
this.setModelView(RenderSystem.getModelViewMatrix());
|
||||
@@ -112,8 +136,9 @@ public class ShaderProgram {
|
||||
GL20.glShaderSource(shader, source);
|
||||
GL20.glCompileShader(shader);
|
||||
if (GL20.glGetShaderi(shader, 35713) == 0) {
|
||||
LOGGER.error(GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
||||
throw new IllegalStateException(String.format("Failed to compile shader! (Type: %s)", new Object[]{type}));
|
||||
LOGGER.error("Failed to compile shader (type {}):\n{}", type, GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
||||
GL20.glDeleteShader(shader);
|
||||
return 0;
|
||||
}
|
||||
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.SlabBlock;
|
||||
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.phys.AABB;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
@@ -122,6 +123,10 @@ extends ClientBase {
|
||||
if (block instanceof SlabBlock) {
|
||||
return false;
|
||||
}
|
||||
// Cobwebs are tactical utility items (AutoWebPlace ammo), never bridging blocks.
|
||||
if (block instanceof WebBlock) {
|
||||
return false;
|
||||
}
|
||||
return !blacklist.contains(block);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -141,6 +141,11 @@ extends ClientBase {
|
||||
if (roundedRectShader == null) {
|
||||
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();
|
||||
roundedRectShader.use();
|
||||
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
|
||||
@@ -222,6 +227,11 @@ extends ClientBase {
|
||||
if (blurShader == null) {
|
||||
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) {
|
||||
mainRenderTarget = mc.getMainRenderTarget();
|
||||
}
|
||||
|
||||