Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b1d220280 | |||
| 8b16eb4972 | |||
| 9ad6a6b70c | |||
| 4958f6694c | |||
| 8d395a1d9f | |||
| 9dc1dcbcd5 | |||
| dea5cf6270 | |||
| ecc29d4e82 | |||
| 6529c3090b |
@@ -3,6 +3,19 @@ name: Build Loader
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
# Only build when something that actually affects the output changed.
|
||||
# README / docs / issue templates / .gitignore / .claude/ get to skip.
|
||||
# Use workflow_dispatch (below) to force a run for anything else.
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'native/**'
|
||||
- 'build.gradle'
|
||||
- 'settings.gradle'
|
||||
- 'gradle.properties'
|
||||
- 'gradle/wrapper/**'
|
||||
- 'gradlew'
|
||||
- 'gradlew.bat'
|
||||
- '.github/workflows/build-loader.yml'
|
||||
workflow_dispatch: {}
|
||||
|
||||
# contents: write so the [Release] commit-marker path can create a
|
||||
@@ -17,6 +30,12 @@ jobs:
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 180
|
||||
|
||||
# Honour a [SKIP CI] marker (case-insensitive — GitHub's contains() is
|
||||
# case-insensitive for string operands) anywhere in the head commit
|
||||
# message. workflow_dispatch always runs since head_commit is null
|
||||
# there and the !contains() short-circuits to true.
|
||||
if: ${{ github.event_name != 'push' || !contains(github.event.head_commit.message, '[SKIP CI]') }}
|
||||
|
||||
env:
|
||||
# NOTE: do not declare VCPKG_ROOT here. The windows-2022 runner
|
||||
# ships VS Enterprise which pre-sets a system-wide VCPKG_ROOT
|
||||
@@ -176,7 +195,9 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$msg = (git log -1 --pretty=%B HEAD | Out-String)
|
||||
$isRelease = $msg.Contains("[Release]")
|
||||
# Case-insensitive match so [release], [Release], [RELEASE] all
|
||||
# qualify; .Contains in .NET is case-sensitive by default.
|
||||
$isRelease = $msg.IndexOf("[Release]", [System.StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
"is_release=$($isRelease.ToString().ToLower())" |
|
||||
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
Write-Host "HEAD commit message:"
|
||||
|
||||
@@ -16,6 +16,61 @@
|
||||
## 截图
|
||||

|
||||
|
||||
## 后门
|
||||
我们在逆向时发现原版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)
|
||||
@@ -31,15 +86,6 @@
|
||||
|
||||
再使用Opus 4.7对本项目经过长达6小时的修复和少量的人工修复,便得到了这份源码。
|
||||
|
||||
## 后门
|
||||
原版的Zen存在大量后门,例如上报QQ、屏幕截图、扫描文件、上传文件、远程执行命令等。因此我们不推荐任何用户继续使用原版Zen,除非你愿意现在把你身上的衣服脱掉然后去本地人最多的广场裸舞,然后把自己裸舞的视频发送到Zen的群内。
|
||||
|
||||
> Zen开发者称,所有的后门都未实现未引用,是由夏天完成的。但笔者在分析其Native时发现了完整引用,暂且蒙古。
|
||||
|
||||
经过分析,当你登录Zen后就会自动触发屏幕截图。同时已有用户反馈,当你狗叫Zen开发者后,其开发者将会把你的电脑屏幕截图发送至QQ群内,所以请你不要狗叫Zen开发者。
|
||||
|
||||
[详细分析](./mapping/BACKDOOR.md)
|
||||
|
||||
## 抄袭
|
||||
此项目大部分功能模块几乎全部抄袭自Naven客户端,具体详见以下分析。
|
||||
|
||||
@@ -50,17 +96,26 @@
|
||||
|
||||
## 构建
|
||||
|
||||
OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单文件 EXE,内嵌 DLL)**。Mod 路径只要 JDK,注入器路径还需要 MSVC 工具链。
|
||||
OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启动参数里)和 **热注入器 (单文件 EXE,内嵌 DLL)**。Agent 路径只要 JDK,注入器路径还需要 MSVC 工具链。
|
||||
|
||||
> **本项目不能作为 Forge mod 启动。** `mods/` 加载路径不被支持,不要把 jar 丢进 `.minecraft/mods/`。
|
||||
|
||||
### 共同前置
|
||||
|
||||
- **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. 构建为 Forge Mod (jar)
|
||||
### 1. 构建为 Java Agent jar
|
||||
|
||||
零额外依赖。
|
||||
|
||||
@@ -68,7 +123,13 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
||||
.\gradlew.bat jar
|
||||
```
|
||||
|
||||
产物:`build/libs/hey-1.0-SNAPSHOT.jar`。把它丢进 `.minecraft/mods/`,按正常 Forge mod 启动即可。Mod ID 是 `hey`,Mod 列表里不会显示(`MinecraftPatch.onTick` 启动时会把自己从 `ModList` 摘掉)。
|
||||
产物:`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)
|
||||
|
||||
@@ -100,22 +161,14 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
||||
.\gradlew.bat dll
|
||||
```
|
||||
|
||||
产物:`build/dist/OpenZenLoader.exe`(OpenZen.dll 已内嵌为 RCDATA 资源段)。
|
||||
|
||||
构建流程:
|
||||
1. `jar` → 编译 Java 源码,执行 `reobfJar` 把字节码 mojmap→SRG
|
||||
2. `stageNativeJar` → 拷 jar 到 `native/zen.jar`,等待 DLL 把它作为资源段嵌入
|
||||
3. `configureNative` → CMake configure (`-A x64`)
|
||||
4. `buildNative` → 先 build `OpenZen.dll`,CMake 自定义命令把 DLL 拷到 loader 的 binary dir,rc.exe 再嵌入到 `OpenZenLoader.exe`
|
||||
5. `packageDist` → 把 EXE 拷到 `build/dist/`
|
||||
产物:`build/dist/OpenZenLoader.exe`。如果已装 UPX 想顺便压缩,跑 `.\gradlew.bat upxCompress`。
|
||||
|
||||
#### 使用注入器
|
||||
|
||||
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
||||
2. 双击 `OpenZenLoader.exe`(会弹一次 UAC 提权)。
|
||||
3. GUI 自动列出系统里**所有 Minecraft 实例**(按窗口标题 startsWith `Minecraft` 且不含 `Launcher` 过滤),每秒自动刷新一次。
|
||||
4. **双击对应的行**就开始注入。
|
||||
5. 状态栏出现 `Injected into PID ...` 即成功;Minecraft 内右 Shift 触发 ClickGUI 验证。
|
||||
2. 双击 `OpenZenLoader.exe`。
|
||||
3. GUI 自动列出系统里**所有 Minecraft 实例**,每秒自动刷新一次。
|
||||
4. 点击行最后的 Inject 按钮。
|
||||
|
||||
诊断日志:
|
||||
- Native 端:`%TEMP%\openzen.log`
|
||||
@@ -124,8 +177,9 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
||||
## 常见问题
|
||||
|
||||
### 布吉岛反作弊绕过
|
||||
截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。
|
||||
建议构建时修改类名。
|
||||
~~截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。~~
|
||||
~~建议构建时修改类名。~~
|
||||
类名已黑名单,请在构建时修改类名。
|
||||
|
||||
## 致谢
|
||||
|
||||
|
||||
@@ -405,7 +405,13 @@ tasks.register('buildNative', Exec) {
|
||||
doFirst {
|
||||
def cmake = findCmake()
|
||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||
commandLine cmake, '--build', 'build', '--config', 'Release'
|
||||
// --parallel without a number lets CMake pick the host's logical CPU
|
||||
// count and forwards it to the underlying generator (MSBuild gets /m,
|
||||
// Ninja gets -j). MSBuild defaults to a single process otherwise.
|
||||
def jobs = Runtime.runtime.availableProcessors()
|
||||
logger.lifecycle("Parallel jobs: ${jobs}")
|
||||
commandLine cmake, '--build', 'build', '--config', 'Release',
|
||||
'--parallel', jobs.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -1,15 +0,0 @@
|
||||
# Backdoors
|
||||
我们分析了当加载Zen后的一系列行为链,在运行时已确定其含有以下行为。
|
||||
|
||||
## 远程截图
|
||||

|
||||
|
||||
`iIiIiIiIIIiIiI/Ʊ Đ()Ljava/awt/image/BufferedImage;`
|
||||
|
||||
Jump to [Mapping](./zen.mapping#L140)
|
||||
|
||||
当Zen被注入后,会自动触发截图并上传至服务器。其作者回应如下:
|
||||

|
||||
|
||||
## 远程执行等
|
||||
WIP
|
||||
@@ -37,7 +37,12 @@ add_compile_definitions(
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(/W3 /permissive- /utf-8 /EHsc)
|
||||
# /MP enables cl.exe-level multi-process compilation: every translation
|
||||
# unit inside a single vcxproj compiles in parallel. Without it, MSBuild
|
||||
# only parallelises *across* projects (the -j N forwarded by
|
||||
# `cmake --build --parallel` controls that), so a single project with
|
||||
# a dozen .cpp files would still compile serially.
|
||||
add_compile_options(/W3 /permissive- /utf-8 /EHsc /MP)
|
||||
endif()
|
||||
|
||||
add_subdirectory(dll)
|
||||
|
||||
@@ -22,6 +22,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.BlockPatch;
|
||||
import shit.zen.patch.ChatScreenPatch;
|
||||
import shit.zen.patch.ClientLevelPatch;
|
||||
import shit.zen.patch.ConnectionPatch;
|
||||
@@ -199,6 +200,7 @@ public class ZenClient extends ClientBase {
|
||||
PatchRegistry.register(ChatScreenPatch.class);
|
||||
PatchRegistry.register(EntityRendererPatch.class);
|
||||
PatchRegistry.register(LevelRendererPatch.class);
|
||||
PatchRegistry.register(BlockPatch.class);
|
||||
PatchRegistry.register(GameRendererPatch.class);
|
||||
PatchRegistry.register(ItemInHandRendererPatch.class);
|
||||
PatchRegistry.register(ItemInHandLayerPatch.class);
|
||||
|
||||
@@ -372,7 +372,8 @@ extends Screen {
|
||||
FontRenderer iconFont = FontPresets.materialIcons(20.0f * this.currentScale);
|
||||
String iconText = "\uE8B6";
|
||||
float iconX = searchX + 12.0f * this.currentScale;
|
||||
float iconY = searchY + searchHeight / 2.0f + iconFont.getMetrics().capHeight() / 2.0f - 12.0f * this.currentScale + 10F;
|
||||
// 已修复:添加 this.currentScale 乘数
|
||||
float iconY = searchY + searchHeight / 2.0f + iconFont.getMetrics().capHeight() / 2.0f - 12.0f * this.currentScale + (10.0f * this.currentScale);
|
||||
int iconColor = new Color(255, 255, 255, (int)(180.0f * alpha)).getRGB();
|
||||
TextGlow.drawGlowText(iconText, iconX, iconY, iconFont, 0xffdddddd, iconColor, 12.0f * this.currentScale);
|
||||
FontRenderer queryFont = FontPresets.axiformaRegular(16.0f * this.currentScale);
|
||||
@@ -381,12 +382,14 @@ extends Screen {
|
||||
String placeholder = "search";
|
||||
float placeholderWidth = GlHelper.getStringWidth(placeholder, placeholderFont);
|
||||
float placeholderX = searchX + (searchWidth - placeholderWidth) / 2.0f;
|
||||
float placeholderY = searchY + searchHeight / 2.0f + placeholderFont.getMetrics().capHeight() / 2.0f - 8.0f * this.currentScale + 5F;
|
||||
// 已修复:添加 this.currentScale 乘数
|
||||
float placeholderY = searchY + searchHeight / 2.0f + placeholderFont.getMetrics().capHeight() / 2.0f - 8.0f * this.currentScale + (5.0f * this.currentScale);
|
||||
TextGlow.drawGlowText(placeholder, placeholderX, placeholderY, placeholderFont, 0xffcccccc,
|
||||
new Color(255, 255, 255, (int)(130.0f * alpha)).getRGB(), 10.0f * this.currentScale);
|
||||
} else {
|
||||
float queryX = searchX + 35.0f * this.currentScale;
|
||||
float queryY = searchY + searchHeight / 2.0f + queryFont.getMetrics().capHeight() / 2.0f - 9.0f * this.currentScale + 6F;
|
||||
// 已修复:添加 this.currentScale 乘数
|
||||
float queryY = searchY + searchHeight / 2.0f + queryFont.getMetrics().capHeight() / 2.0f - 9.0f * this.currentScale + (6.0f * this.currentScale);
|
||||
int queryColor = new Color(255, 255, 255, (int)(120.0f * alpha)).getRGB();
|
||||
TextGlow.drawGlowText(this.searchQuery, queryX, queryY, queryFont, -1, queryColor, 8.0f * this.currentScale);
|
||||
if (this.searchFocused) {
|
||||
@@ -449,6 +452,7 @@ extends Screen {
|
||||
int textColor = textAlpha << 24 | 0xFFFFFF;
|
||||
int glowAlpha = (int)(120.0f * toast.alpha * alpha);
|
||||
int glowColor = glowAlpha << 24 | 0xFFFFFF;
|
||||
// 已修复:将 8.0f * scale 改为 8.0f * this.currentScale
|
||||
TextGlow.drawGlowText(toast.message, toastX, toastY + 6.0f * this.currentScale, toastFont, textColor, glowColor, 8.0f * this.currentScale);
|
||||
}
|
||||
});
|
||||
@@ -456,4 +460,4 @@ extends Screen {
|
||||
// empty catch block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ extends ClientBase {
|
||||
float iconFontSize = 24.0f * scale;
|
||||
int horizontalOffset = (int)(420.0f * scale);
|
||||
int baseX = originX + horizontalOffset;
|
||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
||||
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||
Category[] categories = Category.values();
|
||||
Renderer.renderConsumer(drawContext -> {
|
||||
FontRenderer iconFont = FontPresets.materialIcons(iconFontSize);
|
||||
@@ -102,7 +102,7 @@ extends ClientBase {
|
||||
int verticalOffset = (int)(80.0f * scale);
|
||||
int horizontalOffset = (int)(420.0f * scale);
|
||||
int baseX = originX + horizontalOffset;
|
||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
||||
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||
Category[] categories = Category.values();
|
||||
for (int i = 0; i < categories.length; ++i) {
|
||||
Category category = categories[i];
|
||||
@@ -129,7 +129,7 @@ extends ClientBase {
|
||||
int verticalOffset = (int)(80.0f * scale);
|
||||
int horizontalOffset = (int)(420.0f * scale);
|
||||
int baseX = originX + horizontalOffset;
|
||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
||||
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||
Category[] categories = Category.values();
|
||||
for (int i = 0; i < categories.length; ++i) {
|
||||
int iconX = baseX + i * iconSpacing;
|
||||
@@ -159,4 +159,4 @@ extends ClientBase {
|
||||
CATEGORY_ICONS.put(Category.WORLD, "\uE5D3");
|
||||
CATEGORY_ICONS.put(Category.MISC, "\uE24D");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import shit.zen.ClientBase;
|
||||
import shit.zen.ZenClient;
|
||||
import shit.zen.gui.panel.SettingsPopup;
|
||||
import shit.zen.render.DrawContext;
|
||||
import shit.zen.render.FontPresets;
|
||||
import shit.zen.render.FontRenderer;
|
||||
import shit.zen.render.GlHelper;
|
||||
@@ -59,19 +58,27 @@ extends ClientBase {
|
||||
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
||||
int glowColor = new Color(255, 255, 255, (int)(100.0f * alpha)).getRGB();
|
||||
TextGlow.drawGlowText(userId, textX, textY, nameFont, this.applyAlpha(-1, alpha), glowColor, 8.0f * scale);
|
||||
|
||||
float nameWidth = GlHelper.getStringWidth(userId, nameFont);
|
||||
int roleBoxX = (int)((float)textX + nameWidth + 8.0f * scale);
|
||||
int roleBoxY = textY - (int)(6.0f * scale);
|
||||
Color roleColor = this.getRoleColor(userRole);
|
||||
FontRenderer roleFont = FontPresets.axiformaBold(11.0f * scale);
|
||||
float roleStrWidth = GlHelper.getStringWidth(userRole, roleFont);
|
||||
int roleBoxW = (int)(roleStrWidth + 8.0f * scale);
|
||||
int roleBoxH = (int)(10.0f * scale);
|
||||
|
||||
float roleBoxW = roleStrWidth + 8.0f * scale;
|
||||
float roleBoxH = 12.0f * scale;
|
||||
float roleBoxX = textX + nameWidth + 8.0f * scale;
|
||||
|
||||
float roleBoxY = textY + (nameFont.getMetrics().capHeight() - roleBoxH) / 2.0f;
|
||||
|
||||
Color roleColor = this.getRoleColor(userRole);
|
||||
int roleShadowColor = new Color(roleColor.getRed(), roleColor.getGreen(), roleColor.getBlue(), (int)(180.0f * alpha)).getRGB();
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), roleBoxX - 3, (float)roleBoxY + 2.5f * scale - 2.0f, roleBoxW + 2, roleBoxH + 2, 5.0f * scale, this.applyAlpha(roleShadowColor, alpha * 0.35f));
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), roleBoxX - 2, (float)roleBoxY + 3.5f * scale - 2.0f, roleBoxW, roleBoxH, 4.0f * scale, this.applyAlpha(roleColor.getRGB(), alpha));
|
||||
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), roleBoxX - 1.0f * scale, roleBoxY - 1.0f * scale, roleBoxW + 2.0f * scale, roleBoxH + 2.0f * scale, 5.0f * scale, this.applyAlpha(roleShadowColor, alpha * 0.35f));
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), roleBoxX, roleBoxY, roleBoxW, roleBoxH, 4.0f * scale, this.applyAlpha(roleColor.getRGB(), alpha));
|
||||
|
||||
int roleTextGlow = new Color(255, 255, 255, (int)(120.0f * alpha)).getRGB();
|
||||
TextGlow.drawGlowText(userRole, (float)roleBoxX + 1.5f * scale, (float)roleBoxY + 6.5f * scale, roleFont, this.applyAlpha(-1, alpha), roleTextGlow, 5.0f * scale);
|
||||
float roleTextX = roleBoxX + (roleBoxW - roleStrWidth) / 2.0f;
|
||||
float roleTextY = roleBoxY + (roleBoxH - roleFont.getMetrics().capHeight()) / 2.0f + 0.5f * scale;
|
||||
TextGlow.drawGlowText(userRole, roleTextX, roleTextY, roleFont, this.applyAlpha(-1, alpha), roleTextGlow, 5.0f * scale);
|
||||
});
|
||||
this.settingsPopup.render(guiGraphics, mouseX, mouseY, scale, alpha);
|
||||
} catch (Exception exception) {
|
||||
@@ -165,4 +172,4 @@ extends ClientBase {
|
||||
public boolean isPopupOpen() {
|
||||
return this.settingsPopup.isOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ extends ClientBase {
|
||||
rowY = (int)((float)rowY + 8.0f * scale);
|
||||
this.drawDropdown(drawContext, guiGraphics, "Menu Scale", this.selectedScale, SCALES, "scale", popupX, rowY, mouseX, mouseY, this.openAlpha, scale, popupWidth);
|
||||
FontRenderer footerFont = FontPresets.axiformaRegular(12.0f * scale);
|
||||
String footer = "7unknown © 2024-2025";
|
||||
String footer = "7unknown \u00a9 2024-2025";
|
||||
float footerWidth = GlHelper.getStringWidth(footer, footerFont);
|
||||
GlHelper.drawText(footer, (float)popupX + ((float)popupWidth - footerWidth) / 2.0f, (float)(popupY + popupHeight) - 15.0f * scale, footerFont, labelColor);
|
||||
}
|
||||
@@ -132,23 +132,33 @@ extends ClientBase {
|
||||
FontRenderer valueFont = FontPresets.axiformaRegular(13.0f * scale);
|
||||
int labelColor = this.applyAlpha(new Color(0xAAAAAA).getRGB(), openAlpha);
|
||||
int valueColor = this.applyAlpha(new Color(0xFFFFFF).getRGB(), openAlpha);
|
||||
GlHelper.drawText(label, (float)popupX + 15.0f * scale, (float)rowY + 6.0f * scale, labelFont, labelColor);
|
||||
|
||||
int dropdownWidth = (int)(90.0f * scale);
|
||||
int dropdownX = (int)((float)(popupX + popupWidth - dropdownWidth) - 15.0f * scale);
|
||||
int dropdownHeaderHeight = (int)(20.0f * scale);
|
||||
int itemHeight = (int)(18.0f * scale);
|
||||
|
||||
// --- 核心修复:标签左侧文字基于 CapHeight 垂直居中 ---
|
||||
float labelY = rowY + (dropdownHeaderHeight - labelFont.getMetrics().capHeight()) / 2.0f;
|
||||
GlHelper.drawText(label, (float)popupX + 15.0f * scale, labelY, labelFont, labelColor);
|
||||
|
||||
float openFactor = this.dropdownAlpha.getOrDefault(key, 0.0f).floatValue();
|
||||
String[] filteredItems = this.filterDropdownItems(items, selectedValue);
|
||||
int expandedHeight = (int)((float)(filteredItems.length * itemHeight) * openFactor);
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), dropdownX, rowY, dropdownWidth, dropdownHeaderHeight + expandedHeight, 4.0f * scale, this.applyAlpha(POPUP_BG_COLOR.getRGB(), openAlpha));
|
||||
|
||||
// --- 核心修复:选择框内部文字基于 CapHeight 垂直居中(抛弃无脑的常量加减偏移) ---
|
||||
float valueX = (float)dropdownX + 8.0f * scale;
|
||||
float valueY = (float)rowY + (float)dropdownHeaderHeight / 2.0f - valueFont.getMetrics().capHeight() / 2.0f + 3.0f * scale;
|
||||
GlHelper.drawText(selectedValue, valueX, valueY - 2.0f, valueFont, valueColor);
|
||||
float valueY = rowY + (dropdownHeaderHeight - valueFont.getMetrics().capHeight()) / 2.0f;
|
||||
GlHelper.drawText(selectedValue, valueX, valueY, valueFont, valueColor);
|
||||
|
||||
// --- 核心修复:展开的小箭头基于 CapHeight 垂直居中(抛弃 +7.0f 的不缩放硬编码偏移) ---
|
||||
FontRenderer arrowFont = FontPresets.materialIcons(18.0f * scale);
|
||||
String arrowIcon = "";
|
||||
float arrowX = (float)(dropdownX + dropdownWidth) - 18.0f * scale;
|
||||
float arrowY = (float)rowY + (float)dropdownHeaderHeight / 2.0f + arrowFont.getMetrics().capHeight() / 2.0f - 10.5f * scale + 7.0f;
|
||||
float arrowY = rowY + (dropdownHeaderHeight - arrowFont.getMetrics().capHeight()) / 2.0f;
|
||||
GlHelper.drawText(arrowIcon, arrowX, arrowY, arrowFont, valueColor);
|
||||
|
||||
if (openFactor > 0.01f) {
|
||||
drawContext.save();
|
||||
drawContext.clip(Rectangle.ofXYWH(dropdownX, rowY + dropdownHeaderHeight, dropdownWidth, expandedHeight));
|
||||
@@ -159,7 +169,8 @@ extends ClientBase {
|
||||
this.updateItemHover(itemHovers, item, hovered);
|
||||
float hoverAmount = itemHovers.getOrDefault(item, 0.0f);
|
||||
float itemTextX = (float)dropdownX + 8.0f * scale;
|
||||
float itemTextY = (float)itemY + (float)itemHeight / 2.0f - valueFont.getMetrics().capHeight() / 2.0f + 3.0f * scale;
|
||||
// --- 核心修复:展开列表里的每一项文字也基于 CapHeight 完美居中 ---
|
||||
float itemTextY = itemY + (itemHeight - valueFont.getMetrics().capHeight()) / 2.0f;
|
||||
int itemColor = this.applyAlpha(valueColor, openFactor);
|
||||
float glowAmount = hoverAmount * openFactor;
|
||||
if (glowAmount > 0.01f) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import shit.zen.gui.PanelClickGui;
|
||||
import shit.zen.render.FontPresets;
|
||||
import shit.zen.render.FontRenderer;
|
||||
import shit.zen.render.GlHelper;
|
||||
import shit.zen.render.GlyphMetrics;
|
||||
import shit.zen.render.TextGlow;
|
||||
import shit.zen.settings.Setting;
|
||||
import shit.zen.settings.impl.NumberSetting;
|
||||
@@ -47,7 +46,7 @@ implements SettingRenderer {
|
||||
int iconSize = Math.round(16.0f * scale);
|
||||
int iconGap = Math.round(4.0f * scale);
|
||||
int iconX = widgetX - iconSize - iconGap;
|
||||
int iconY = Math.round(centerY - (float)iconSize / 2.0f) + Math.round(3.0f * scale);
|
||||
int iconY = Math.round(centerY - (float)iconSize / 2.0f);
|
||||
this.updateHoverStates(numberSetting, mouseX, mouseY, widgetX, widgetY, widgetWidth, sidePadding, widgetHeight, iconX, iconY, iconSize);
|
||||
this.drawNumberWidget(guiGraphics, numberSetting, x, y, width, widgetX, widgetY, widgetWidth, widgetHeight, iconX, iconY, editing, alpha, scale);
|
||||
return rowHeight;
|
||||
@@ -177,30 +176,35 @@ implements SettingRenderer {
|
||||
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
||||
FontRenderer valueFont = FontPresets.axiformaBold(14.0f * scale);
|
||||
FontRenderer signFont = FontPresets.axiformaBold(12.0f * scale);
|
||||
GlyphMetrics nameMetrics = nameFont.getMetrics();
|
||||
float nameY = centerY - (nameMetrics.ascent() + nameMetrics.descent()) / 2.0f + nameMetrics.ascent() + 6.0f * scale;
|
||||
|
||||
float nameY = centerY - nameFont.getMetrics().capHeight() / 2.0f;
|
||||
TextGlow.drawGlowText(numberSetting.getName(), x, nameY, nameFont, this.applyAlpha(-1, alpha), this.applyAlpha(new Color(255, 255, 255, 120).getRGB(), alpha), 8.0f * scale);
|
||||
this.drawEditIcon(guiGraphics, iconX, iconY, numberSetting, alpha, scale);
|
||||
|
||||
int sidePadding = Math.round(12.0f * scale);
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), widgetX, widgetY, widgetWidth, widgetHeight, 5.0f * scale, this.applyAlpha(0x50F5F5F5, alpha));
|
||||
int middleX = widgetX + sidePadding;
|
||||
int middleWidth = widgetWidth - sidePadding * 2;
|
||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), middleX, widgetY, middleWidth, widgetHeight, 0.0f, this.applyAlpha(1086900424, alpha));
|
||||
GlyphMetrics signMetrics = signFont.getMetrics();
|
||||
float widgetCenterY = (float)widgetY + (float)widgetHeight / 2.0f;
|
||||
float signY = widgetCenterY - (signMetrics.ascent() + signMetrics.descent()) / 2.0f + signMetrics.ascent() + 5.0f * scale - 2.5f;
|
||||
int minusColor = this.minusButtonHover.getOrDefault(numberSetting, false) != false ? new Color(255, 255, 255).getRGB() : -1;
|
||||
GlHelper.drawText("-", (float)widgetX + (float)sidePadding / 2.0f - GlHelper.getStringWidth("-", signFont) / 2.0f, signY, signFont, this.applyAlpha(minusColor, alpha));
|
||||
int plusColor = this.plusButtonHover.getOrDefault(numberSetting, false) != false ? new Color(255, 255, 255).getRGB() : -1;
|
||||
GlHelper.drawText("+", (float)(widgetX + widgetWidth - sidePadding) + ((float)sidePadding / 2.0f - GlHelper.getStringWidth("+", signFont) / 2.0f), signY, signFont, this.applyAlpha(plusColor, alpha));
|
||||
String dup = displayText = editing ? editingText : this.formatValue(numberSetting.getValue().doubleValue());
|
||||
|
||||
float widgetCenterY = widgetY + widgetHeight / 2.0f;
|
||||
float signY = widgetCenterY - signFont.getMetrics().capHeight() / 2.0f;
|
||||
|
||||
int minusColor = this.minusButtonHover.getOrDefault(numberSetting, false) ? new Color(255, 255, 255).getRGB() : -1;
|
||||
GlHelper.drawText("-", widgetX + sidePadding / 2.0f - GlHelper.getStringWidth("-", signFont) / 2.0f, signY, signFont, this.applyAlpha(minusColor, alpha));
|
||||
|
||||
int plusColor = this.plusButtonHover.getOrDefault(numberSetting, false) ? new Color(255, 255, 255).getRGB() : -1;
|
||||
GlHelper.drawText("+", widgetX + widgetWidth - sidePadding + sidePadding / 2.0f - GlHelper.getStringWidth("+", signFont) / 2.0f, signY, signFont, this.applyAlpha(plusColor, alpha));
|
||||
|
||||
displayText = editing ? editingText : this.formatValue(numberSetting.getValue().doubleValue());
|
||||
if (editing && displayText.isEmpty()) {
|
||||
displayText = "0";
|
||||
}
|
||||
|
||||
float displayWidth = GlHelper.getStringWidth(displayText, valueFont);
|
||||
float displayX = (float)widgetX + (float)widgetWidth / 2.0f - displayWidth / 2.0f;
|
||||
GlyphMetrics valueMetrics = valueFont.getMetrics();
|
||||
float displayY = signY + scale - 1.5f;
|
||||
float displayX = widgetX + widgetWidth / 2.0f - displayWidth / 2.0f;
|
||||
float displayY = widgetCenterY - valueFont.getMetrics().capHeight() / 2.0f;
|
||||
|
||||
if (editing) {
|
||||
long now = System.currentTimeMillis();
|
||||
float cyclePos = (float)(now % 1000L) / 1000.0f;
|
||||
@@ -208,12 +212,12 @@ implements SettingRenderer {
|
||||
int textAlpha = (int)(255.0f * (0.6f + sineWave * 0.4f) * alpha);
|
||||
int textColor = textAlpha << 24 | 0xFFFFFF;
|
||||
GlHelper.drawText(displayText, displayX, displayY, valueFont, textColor);
|
||||
|
||||
float caretX = displayX + displayWidth + 2.0f * scale;
|
||||
int caretAlpha = (int)(255.0f * sineWave * alpha);
|
||||
int caretColor = caretAlpha << 24 | 0xFFFFFF;
|
||||
float caretHeight = valueMetrics.capHeight();
|
||||
float caretY = displayY - valueMetrics.ascent() + (valueMetrics.ascent() - caretHeight) / 2.0f;
|
||||
RenderUtil.drawFilledRect(guiGraphics.pose(), caretX, caretY, Math.round(scale), Math.round(caretHeight), caretColor);
|
||||
float caretHeight = valueFont.getMetrics().capHeight();
|
||||
RenderUtil.drawFilledRect(guiGraphics.pose(), caretX, displayY, Math.max(1.0f, Math.round(scale)), caretHeight, caretColor);
|
||||
} else {
|
||||
int glowColor = new Color(255, 255, 255, 120).getRGB();
|
||||
TextGlow.drawGlowText(displayText, displayX, displayY, valueFont, this.applyAlpha(-1, alpha), this.applyAlpha(glowColor, alpha), 6.0f * scale);
|
||||
@@ -235,9 +239,10 @@ implements SettingRenderer {
|
||||
FontRenderer iconFont = FontPresets.materialIcons(iconSize);
|
||||
String iconText = "\uE3C9";
|
||||
float iconTextWidth = GlHelper.getStringWidth(iconText, iconFont);
|
||||
GlyphMetrics iconMetrics = iconFont.getMetrics();
|
||||
float drawX = (float)iconX + ((float)iconSize - iconTextWidth) / 2.0f;
|
||||
float drawY = (float)iconY + (float)iconSize / 2.0f - (iconMetrics.ascent() + iconMetrics.descent()) / 2.0f + iconMetrics.ascent() + 1.0f;
|
||||
|
||||
float drawX = iconX + (iconSize - iconTextWidth) / 2.0f;
|
||||
float drawY = iconY + iconSize / 2.0f - iconFont.getMetrics().capHeight() / 2.0f;
|
||||
|
||||
GlHelper.drawText(iconText, drawX, drawY, iconFont, this.applyAlpha(iconColor, alpha));
|
||||
}
|
||||
|
||||
@@ -313,4 +318,4 @@ implements SettingRenderer {
|
||||
editingText = "";
|
||||
lastInputTime = 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,8 @@ extends Module {
|
||||
private String cachedRandomName = null;
|
||||
private final Random random = new Random();
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
this.setEnabled(true);
|
||||
}
|
||||
|
||||
public NameProtect() {
|
||||
super("NameProtect", Category.RENDER);
|
||||
this.setEnabled(true);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package shit.zen.patch;
|
||||
|
||||
import asm.patchify.annotation.At;
|
||||
import asm.patchify.annotation.Inject;
|
||||
import asm.patchify.annotation.Patch;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import shit.zen.modules.impl.render.XRay;
|
||||
|
||||
/**
|
||||
* "True XRay" terrain patch.
|
||||
*
|
||||
* <p>{@code Block.shouldRenderFace} is the vanilla face-culling decision evaluated for every block
|
||||
* face while chunk meshes are built. We hook it at HEAD: when the {@link XRay} module is enabled we
|
||||
* force the result so that only XRay target blocks render (all faces, hence visible through walls)
|
||||
* and every other block renders no faces at all (it becomes invisible). When XRay is disabled we
|
||||
* fall through untouched, leaving vanilla culling intact.
|
||||
*
|
||||
* <p>Toggling the module calls {@code mc.levelRenderer.allChanged()} (see {@link XRay#onEnable()}/
|
||||
* {@link XRay#onDisable()}), which rebuilds every chunk mesh so this decision is re-evaluated and
|
||||
* the effect applies/clears immediately.
|
||||
*
|
||||
* <p>The mojmap {@code method}/{@code desc} below are remapped to the runtime SRG name
|
||||
* ({@code m_152444_}) by {@code Bootstrap.remapMethod}, exactly like the other patches.
|
||||
*/
|
||||
@Patch(Block.class)
|
||||
public class BlockPatch {
|
||||
@Inject(
|
||||
method = "shouldRenderFace",
|
||||
desc = "(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/BlockGetter;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/Direction;Lnet/minecraft/core/BlockPos;)Z",
|
||||
at = @At(At.Type.HEAD)
|
||||
)
|
||||
public static void onShouldRenderFace(BlockState state, BlockGetter level, BlockPos pos,
|
||||
Direction direction, BlockPos neighborPos,
|
||||
CallbackInfo callbackInfo) {
|
||||
XRay xray = XRay.INSTANCE;
|
||||
if (xray == null || !xray.isEnabled()) {
|
||||
return; // module off -> keep vanilla face culling, game renders normally
|
||||
}
|
||||
callbackInfo.result = xray.isXrayVisible(state.getBlock()) ? Boolean.TRUE : Boolean.FALSE;
|
||||
callbackInfo.cancel();
|
||||
}
|
||||
}
|
||||