14 Commits

Author SHA1 Message Date
Shirona1337 07be28c11e chore(release): trigger obfuscated loader release [Release]
Touch build.gradle (a paths-filtered file) so the push runs Build Loader; HEAD carries [Release], so the workflow cuts a GitHub Release with the obfuscated jar + injector + de-obfuscation mapping. Empty commits are skipped by the workflow's paths filter, hence a real (harmless comment) change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:19:44 +08:00
Shirona1337 5b70703db5 chore(release): publish obfuscated loader [Release]
Build-time-obfuscated build — every class renamed to a fresh random 16-char name (random package + class). Ships the agent jar, the single-file injector, and the per-build de-obfuscation mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:15:24 +08:00
Shirona1337 fb681e7c83 fix(ci): repair corrupted native/vcpkg.json builtin-baseline
A stray '混淆器' had been appended to the builtin-baseline SHA, leaving the JSON string unterminated; vcpkg manifest parsing failed in CI (Unexpected EOF in middle of string). Restore the clean 40-hex baseline (tag 2026.04.27 / qtbase 6.10.2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:14:21 +08:00
Shirona1337 91fb65c22f feat(build): randomized build-time class-name obfuscation + pin Qt 6.10.2
Rename every shit.zen.* / asm.patchify.* class to a fresh random 16-char name in one random 16-char package on each build, via an ASM ClassRemapper pass (build.gradle ext.obfuscateJar, run after ForgeGradle reobfJar). Class names only; methods/fields preserved. Manifest Premain/Agent-Class, the DllBootstrap Class.forName string, and the native bridge name (generated_names.h OZ_BRIDGE_FQCN) are wired to the generated names. Residual original-name strings (loggers, log text, the asm.patchify.* property keys) scrubbed. Emits build/rename-mapping.txt.

Pin Qt to 6.10.2 (vcpkg builtin-baseline + CI tag 2026.04.27): MSVC 14.44 crashes building Qt 6.11.0. Build Qt single-threaded locally (VCPKG_MAX_CONCURRENCY=1, loader --parallel 1, gated off GitHub Actions) to dodge parallel-compilation compiler crashes on that toolset.

CI uploads rename-mapping.txt as an artifact and attaches it to the Release; the Release body warns that prebuilt artifacts share one fixed (blacklist-able) mapping and users should self-compile. README documents the obfuscation + the self-compile warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:09:28 +08:00
Shirona1337 3263b0b4cb ci(loader): pin vcpkg baseline so the Qt cache stops missing
The vcpkg install was cloned at a floating HEAD while native/vcpkg.json
carried no builtin-baseline, so every run resolved the qtbase port afresh.
Its package ABI drifted whenever upstream vcpkg moved, the restored
actions/cache entry no longer matched, and qtbase was rebuilt from source
(~31 min) on every push. Because the cache key is static (hash of
vcpkg.json, unchanged since 05-23) the rebuilt tree was never saved back
either, so the miss repeated forever.

Pin both sides to vcpkg tag 2026.05.25
(d015e31e90838a4c9dfa3eed45979bc70d9357fc, qtbase 6.11.0):
  - native/vcpkg.json: add "builtin-baseline"
  - workflow: clone --branch 2026.05.25

Adding the baseline also changes the vcpkg.json hash, so this run gets a
fresh cache key, cold-builds Qt once, and saves it; subsequent runs match
the now-deterministic ABI and skip the qtbase build entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:55:19 +08:00
Shirona1337 5b1d220280 feat(module): restore XRay with true see-through terrain patch [Release]
Restore the full XRay ore-scanner module (flood-fill vein detection,
anti-fake-ore scoring, packet-driven blind scan, wireframe ESP boxes and
on-screen tracers, ~27 settings) and add BlockPatch, which hooks
Block.shouldRenderFace so terrain turns transparent and only the target
ores render while XRay is enabled. Use @EqualsAndHashCode for BlockKey.

Closes #34

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:42:15 +08:00
Shirona1337 8b16eb4972 fix(module): default NameProtect to off [Release]
Don't auto-enable in the constructor — opt-in only, matching every
other render module. Pairs with the onDisable fix from 9ad6a6b so the
module starts off and can be toggled freely.
2026-05-31 16:34:58 +08:00
Shirona1337 9ad6a6b70c fix(module): allow NameProtect to be disabled (#38)
Drop the onDisable override that immediately re-enabled the module —
left-clicking the entry in the ClickGui now actually turns it off.
The constructor still defaults the module on, so the protection is
opt-out rather than opt-in.
2026-05-31 16:32:52 +08:00
Shirona1337 4958f6694c fix(gui): align panel ClickGui widget glyphs by capHeight (#37)
Reworks vertical placement in PanelClickGui / CategoryBar / ProfileWidget
/ SettingsPopup / NumberSettingRenderer so search-bar icon + placeholder,
category bar Y, role badge, dropdown header/value/arrow/items, and the
number widget label/sign/value/caret all derive their Y from CapHeight
instead of ascent+descent or hard-coded pixel offsets — fixes drift at
non-100% GUI scale.

Co-Authored-By: Koe <KoeNotPrism@proton.me>
2026-05-31 16:32:41 +08:00
Shirona1337 8d395a1d9f docs(readme): add 父子相爱相杀 subsection with xinxin screenshot
Adds a short subsection under 后门 documenting the upstream author's
QQ-group claim that another contributor used a backdoor to remotely
read user files, with the chat screenshot as evidence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 02:10:18 +08:00
Shirona1337 9dc1dcbcd5 docs(readme): inline the backdoor analysis with screenshots & traces
Promote the standalone mapping/BACKDOOR.md into the main README so the
backdoor discussion (screen capture, RCE / file download / file browse
packets, the upstream author's contradictory responses) is visible
without an extra click. Includes captured Trace screenshots, the
ToString-leaked CPacketSystemInfo class and the author's QQ-chat /
Bilibili rebuttals.

File moves:
- mapping/BACKDOOR.md       -> inlined into README
- mapping/screenshot-*.png  -> img/  (alongside the other readme assets)
- Several new images under img/  (RCE, backdoor*, CPacketSystemInfo, meme)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 02:06:05 +08:00
Shirona1337 dea5cf6270 docs(readme): replace Mod path with -javaagent, add optional UPX prereq
The Forge mod entry point no longer works in this project, so the
build section was steering people toward a path that wouldn't load.
Reframe the two delivery shapes as Java Agent jar and hot-injector
EXE, with a callout that mods/ is unsupported.

Other readme drift caught in this pass:
- UPX listed as an optional common prerequisite with install
  instructions (choco / upx.github.io) — the upxCompress task already
  no-ops when upx isn't on PATH, this just makes the option visible.
- 使用注入器 步骤 updated for the Inject-button UI (previously
  documented "double-click a row").
- 删除 stale "构建流程" 列表 in the injector section.
- 后门 段更新 + 布吉岛 段加删除线 reflecting current detection
  status (user-authored edits picked up in the same commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 01:16:27 +08:00
Shirona1337 ecc29d4e82 build(native): enable per-TU parallel compilation (cmake --parallel + /MP)
Before: MSBuild defaulted to a single process per project, so each
vcxproj compiled its .cpp files sequentially. Local clean rebuild of
OpenZen.dll + OpenZenLoader.exe measured ~14.6 s.

After: same cold rebuild measures ~4.8 s — 3x faster — by combining

- `cmake --build --parallel <Runtime.runtime.availableProcessors()>`
  in the buildNative task, which forwards a project-level `-m:N` to
  MSBuild and gets DLL / Loader to build concurrently where the
  dependency graph allows.
- `/MP` on the MSVC compile options, which is the actual win: it lets
  cl.exe spawn one compiler process per available core within a
  single vcxproj. The DLL+Loader together have ~13 .cpp files; this
  is where the bulk of wall-time was being spent serially.

CI runners get the same speed-up for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 01:06:02 +08:00
Shirona1337 6529c3090b ci(loader): widen build-trigger paths + [SKIP CI] gate + case-insensitive [Release]
Three workflow ergonomics tweaks:

- Path filter now also includes build.gradle / settings.gradle /
  gradle.properties / gradle/wrapper/** / gradlew(.bat) and the
  workflow file itself, so a wrapper bump or build-script change
  still triggers CI even though only src/ and native/ files compile
  into the final artifact.
- Job-level if: skips the entire build when the push commit message
  contains [SKIP CI] (Actions' contains() is case-insensitive for
  string operands, so [skip ci] and [SKIP CI] both qualify).
  workflow_dispatch is unaffected because head_commit is null there.
- [Release] marker detection in the PowerShell publish step now uses
  StringComparison.OrdinalIgnoreCase so [release], [Release] and
  [RELEASE] all cut a release; .NET's default .Contains is case-
  sensitive which made the existing rule too brittle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 00:52:16 +08:00
31 changed files with 1668 additions and 125 deletions
+73 -4
View File
@@ -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
@@ -69,11 +88,22 @@ jobs:
- name: Clone & bootstrap vcpkg
shell: pwsh
run: |
# Pin vcpkg to the SAME commit as native/vcpkg.json's
# "builtin-baseline". A floating HEAD clone made the qtbase port
# (and thus its package ABI) drift every time upstream vcpkg moved,
# so the restored Qt cache never matched and qtbase was rebuilt
# from source (~31 min) on every run. Tag 2026.04.27 ==
# 56bb2411609227288b70117ead2c47585ba07713 and ships qtbase 6.10.2.
# We pin 6.10.2 (not the newer 6.11.0 from 2026.05.25) because MSVC
# 14.44.x crashes with an internal compiler error (C1001) building
# Qt 6.11.0 from source; 6.10.2 builds cleanly on every toolset we use.
# Keep this tag and the builtin-baseline in vcpkg.json in lockstep.
$vcpkgTag = '2026.04.27'
$vcpkg = Join-Path $env:GITHUB_WORKSPACE 'vcpkg'
if (Test-Path $vcpkg) {
Write-Host "Reusing existing $vcpkg"
} else {
git clone --depth=1 https://github.com/microsoft/vcpkg.git $vcpkg
git clone --depth=1 --branch $vcpkgTag https://github.com/microsoft/vcpkg.git $vcpkg
}
& "$vcpkg\bootstrap-vcpkg.bat" -disableMetrics
& "$vcpkg\vcpkg.exe" version
@@ -129,20 +159,28 @@ jobs:
$sha = "${{ steps.rev.outputs.sha }}"
$exeSrc = "build\dist\OpenZenLoader.exe"
$jarSrc = "build\libs\hey-1.0.jar"
# The class-name obfuscator emits a fresh, random old->new mapping on every
# build; rename-mapping.txt is the ONLY way to de-obfuscate a stack trace, so
# ship it with the artifacts/release.
$mapSrc = "build\rename-mapping.txt"
if (-not (Test-Path $exeSrc)) { throw "missing $exeSrc" }
if (-not (Test-Path $jarSrc)) { throw "missing $jarSrc" }
if (-not (Test-Path $mapSrc)) { throw "missing $mapSrc" }
$release = "build\release"
New-Item -ItemType Directory -Force -Path $release | Out-Null
$exeDst = Join-Path $release "OpenZenLoader-$sha.exe"
$jarDst = Join-Path $release "OpenZen-$sha.jar"
$mapDst = Join-Path $release "OpenZen-$sha-mapping.txt"
Copy-Item -Force $exeSrc $exeDst
Copy-Item -Force $jarSrc $jarDst
Copy-Item -Force $mapSrc $mapDst
$exeSz = (Get-Item $exeDst).Length
$jarSz = (Get-Item $jarDst).Length
Write-Host ("OpenZenLoader-{0}.exe : {1:N0} bytes ({2:N2} MB)" -f $sha, $exeSz, ($exeSz/1MB))
Write-Host ("OpenZen-{0}.jar : {1:N0} bytes ({2:N2} MB)" -f $sha, $jarSz, ($jarSz/1MB))
Write-Host ("OpenZen-{0}-mapping.txt : {1:N0} bytes" -f $sha, (Get-Item $mapDst).Length)
# NOTE: actions/upload-artifact always wraps its content in a zip; that
# is a platform limitation we cannot disable. By giving each artifact a
@@ -166,6 +204,14 @@ jobs:
if-no-files-found: error
retention-days: 30
- name: Upload de-obfuscation mapping
uses: actions/upload-artifact@v4
with:
name: OpenZen-${{ steps.rev.outputs.sha }}-mapping.txt
path: build/release/OpenZen-${{ steps.rev.outputs.sha }}-mapping.txt
if-no-files-found: error
retention-days: 30
# ===== Optional GitHub Release publish =====
# If the HEAD commit message contains the literal marker "[Release]",
# cut a GitHub Release tagged build-<sha> and attach the exe + jar.
@@ -176,7 +222,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:"
@@ -195,9 +243,30 @@ jobs:
# Write notes via a file so quoting / [brackets] / newlines in the
# commit message can't corrupt the gh command line.
$notes = "release-notes.md"
git log -1 --pretty=%B HEAD | Out-File -FilePath $notes -Encoding utf8
# Prepend a PRE-BUILT warning to the release body: these artifacts all share
# one fixed obfuscation mapping, so an anti-cheat class-name blacklist can
# target them. Tell users to self-compile for unique, per-build random names.
# Build the banner as a string array (one line each) to avoid PowerShell
# here-string column-0 terminator issues inside a YAML block scalar.
$warn = @(
'> ⚠️ **这是预构建版本(PRE-BUILT)**'
'>'
'> 本 Release 里的 `OpenZenLoader.exe` / `OpenZen-*.jar` 是 GitHub Actions 编译的成品,**所有人下载到的是同一套混淆类名**。这套固定的名字随时可能被反作弊(如布吉岛)收录进**类名黑名单**而失效。'
'>'
'> 想要一套**独一无二、别人都不知道**的类名,请**自己编译**(每次构建都会生成全新随机类名):'
'> - **Fork 本仓库**,在你自己的 GitHub Actions 里跑 `Build Loader` 工作流,下载你自己的 artifact;**或**'
'> - **clone 到本地**自己 `gradlew jar` / `gradlew dll`。'
'>'
'> 详见仓库 README 的「编译时类名混淆」。`OpenZen-*-mapping.txt` 是本次构建的反混淆映射(每次构建都不同)。'
''
'---'
''
)
$warn | Out-File -FilePath $notes -Encoding utf8
git log -1 --pretty=%B HEAD | Out-File -FilePath $notes -Encoding utf8 -Append
gh release create $tag `
--title $title `
--notes-file $notes `
"build/release/OpenZenLoader-$env:SHA.exe" `
"build/release/OpenZen-$env:SHA.jar"
"build/release/OpenZen-$env:SHA.jar" `
"build/release/OpenZen-$env:SHA-mapping.txt"
+3
View File
@@ -27,3 +27,6 @@ forge*changelog.txt
# Native DLL build artifacts (CMake out-of-source build + staged jar)
native/build/
native/zen.jar
# Generated at build time by ext.obfuscateJar (holds the obfuscated bridge FQCN)
native/dll/src/generated_names.h
+96 -27
View File
@@ -16,6 +16,61 @@
## 截图
![Open Zen ClickGUI](img/screenshot.png)
## 后门
我们在逆向时发现原版Zen存在大量后门,例如上报QQ、屏幕截图、扫描文件、上传文件、远程执行命令等。因此我们**不推荐**任何用户继续使用原版Zen,除非你愿意现在把你身上的衣服脱掉然后去本地人最多的广场裸舞,然后把自己裸舞的视频发送到Zen的群内。
当Zen被注入后,会自动触发截图并上传至服务器。精神马来西亚人回应如下:
![Screenshot Response](./img/screenshot-response.png)
由于精神马来西亚人从小父母双亡,无父无母的精神马来西亚人自幼脑回路不正常。他认为虽然自己没有说自己的外挂会截图,但是由于自己截图,并没有遭到用户反对,所以所以用户都心甘情愿被截图**全屏**并上传到其服务器上。当然不排除所有Zen客户端用户都喜欢把身上的衣服脱掉然后去本地人最多的广场裸舞的可能性。
![Snapshot](./img/meme.jpeg)
### 分析
当Zen启动时,会自动调用 `iIiIiIiIIIiIiI/Ʊ Đ()Ljava/awt/image/BufferedImage` ([Mapping](./mapping/zen.mapping#L140)),可能由于精神马来西亚人自知是后门,因此精神马来西亚人将此方法严防死守,惨遭没有逼卵子用的Native混淆。
以下是对该方法的Trace。
![Screenshot Trace](./img/screenshot-trace.png)
可见,此方法调用了 `java/awt/Robot;createScreenCapture(Ljava/awt/Rectangle)`,会将用户的**全屏**截图后返回。
继续向下追踪,发现其新建了 `iIiIiIiIIIiIiI/ɿ` (`CPacketSystemInfo`) ([Mapping](./mapping/zen.mapping#L2552)) 对象,我们对该类反编译,发现精神马来西亚人妈妈死掉了所以忘记删除Lombok自动生成的`@ToString`方法,因此惨遭Claude还原。
![CPacketSystemInfo](./img/CPacketSystemInfo_ToString.png)
此包会上传用户处理器信息、模组列表、虚拟机参数、系统名称、上报截图等信息,但笔者认为除截图外,其他信息收集在**提前告知用户的前提下**是合理的,因此并无不妥。虽然精神马来西亚人没有提前告知用户。
随后,笔者继续分析。由于该类继承了 `iIiIiIiIIIiIiI/ɰ` (`Packet`) ([Mapping](./mapping/zen.mapping#L2459)),我们分析了所有该类的子类。
遗憾的,其他类由于没有添加Lombok标识,我们我们不得不通过其他方式Trace这些类的具体用途。经过我们不懈努力的调试和追踪,我们还原出了我们认为可疑的部分行为。
- 远程命令执行 `iIiIiIiIIIiIiI/ʔ`
- 远程文件下发 `iIiIiIiIIIiIiI/ʏ`
- 远程文件浏览 `iIiIiIiIIIiIiI/ʑ`
*以上不是全部*
![RCE](./img/RCE.png)
对于这些后门,精神马来西亚人作此解释。
![Backdoor](./img/backdoor.png)
精神马来西亚人称这些后门全部都是由**夏天233**制造,并非自己。并且这些后门并没有实现,所以可能是由于精神马来西亚人产生幻觉导致笔者抓到了Trace。而且并不能解释同是一套Network系统,为什么上传截图包实现了但其他方法没有实现。
其后其在[视频](https://www.bilibili.com/video/BV147L86TEEZ)中表示,是服务器在迁移时没有实现,而不是客户端没有实现。同时,精神马来西亚人在视频中表示*没有功能*,但是在QQ群中表示*是夏天233写的*。
![Backdoor](./img/backdoor_2.png)
由于精神马来西亚人嘴硬,所以到底具体有没有实现,笔者暂且蒙古。
### 父子相爱相杀
精神马来西亚人在QQ群中指出,欣欣使用过外挂后门远程读取他人文件。不知作为德州骡子的许锦良见自己的亲生父亲如此指认自己作何感想。
![Xinxin](./img/xinxin.png)
## 原始 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,40 @@
## 构建
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/`。
### 编译时类名混淆(重要)
每次构建,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. 构建为 Forge Mod (jar)
### 1. 构建为 Java Agent jar
零额外依赖。
@@ -68,7 +137,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,32 +175,26 @@ 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`
- Java 端:Minecraft 自己的 `logs/latest.log`(搜索 `OpenZen-Bootstrap`、`GameLoaderBridge`、`PatchAgent`、`ZenBootstrap`)
- Java 端:Minecraft 自己的 `logs/latest.log`(类名已被构建时混淆、logger 名是随机的,改用固定日志文案定位,如 `bootstrap.start`、`bridge.load`、`agent attached`、`Runtime mapping`)
## 常见问题
### 布吉岛反作弊绕过
截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。
建议构建时修改类名。
~~截止至目前(2026/05/23),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。~~
~~建议构建时修改类名。~~
~~类名已黑名单,请在构建时修改类名。~~
类名已黑名单。现在**每次构建都会自动随机化全部类名**(见上方[编译时类名混淆](#编译时类名混淆重要))——但**务必自己 Fork/clone 编译**,别直接用 GitHub Actions / Release 上的预编译版:那是固定的一套名字,会被拉黑。
## 致谢
+224 -2
View File
@@ -1,3 +1,27 @@
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.commons.Remapper
buildscript {
repositories {
mavenCentral()
}
dependencies {
// ASM is used by the build-time class-name obfuscation step (ext.obfuscateJar).
// It runs on the already-built jar, so it lives on the buildscript classpath
// rather than the project compile/runtime classpath.
//
// NOTE: the buildscript-classpath ASM also backs Groovy's build-script type
// resolution, so it must be able to read the class files of the JDK that runs
// Gradle. ASM 9.6 supports up to Java 21 (major version 65) — run Gradle on the
// project's JDK 17 (not a newer JDK like 25/major 69, which 9.6 cannot read).
classpath 'org.ow2.asm:asm-commons:9.6'
}
}
plugins {
id 'eclipse'
id 'idea'
@@ -209,6 +233,182 @@ tasks.named('jar', Jar).configure {
finalizedBy 'reobfJar'
}
// ===== Build-time class-name obfuscation =====
//
// Renames EVERY OpenZen class (shit.zen.** and asm.patchify.**) to an opaque
// generated name, so no original class name survives in the distributed jar.
// It runs on the reobf'd jar and only touches our two package trees —
// net.minecraft.** / net.minecraftforge.** references are left exactly as
// ForgeGradle's mojmap->SRG reobf produced them, so reobf and runtime
// remapping are unaffected.
//
// CLASS NAMES ONLY: method/field names are preserved, so the JNI method lookups
// (GameLoaderBridge.load, DllBootstrap.start), reflection, GSON @SerializedName
// wire keys and the manifest member contracts all keep working.
//
// NOTHING IS HARD-CODED. The three bootstrap classes referenced by string
// OUTSIDE the bytecode the remapper rewrites are kept in lockstep by
// propagating their freshly generated names:
// asm.patchify.loader.PatchAgent -> jar manifest Premain/Agent-Class (rewritten below)
// shit.zen.dll.DllBootstrap -> Class.forName(...) string literal (rewritten via Remapper.mapValue)
// shit.zen.dll.GameLoaderBridge -> native DLL loader, via a generated C++ header
// (native/dll/src/generated_names.h) emitted below.
ext.obfuscateJar = { File jarFile, File mappingOut ->
def owned = { String internal -> internal.startsWith('shit/zen/') || internal.startsWith('asm/patchify/') }
// Pass 1: enumerate the classes we own (every shit.zen.* / asm.patchify.* class).
def ownedNames = []
new java.util.zip.ZipFile(jarFile).withCloseable { zf ->
for (entry in Collections.list(zf.entries())) {
if (!entry.directory && entry.name.endsWith('.class')) {
def internal = entry.name.substring(0, entry.name.length() - 6)
if (owned(internal)) ownedNames << internal
}
}
}
if (ownedNames.isEmpty()) {
logger.lifecycle("obfuscateJar: no original class names in ${jarFile.name} (already obfuscated) — skipping")
return
}
// Pass 2: assign each owned class a FRESH RANDOM 16-char name, so the obfuscated
// names differ on every build and encode nothing. Uniqueness is enforced. The
// names are unpredictable, so build/rename-mapping.txt (written below) is the only
// way to de-obfuscate a stack trace — each build's mapping is different; keep it.
//
// ALL classes go into ONE shared (also random, 16-char) package. They must share a
// single package so that package-private (default-access) members originally used
// between same-package classes still resolve — flattening into one package only
// widens access, never breaks it. Per-class packages would turn those into illegal
// cross-package accesses (IllegalAccessError) unless every member were made public.
def typeMap = [:]
def usedNames = new HashSet()
def secureRandom = new java.security.SecureRandom()
def leadAlphabet = (('a'..'z') + ('A'..'Z')).join('') // first char: a letter
def nameAlphabet = (('a'..'z') + ('A'..'Z') + ('0'..'9')).join('') // rest: alphanumeric
def randomName = {
def sb = new StringBuilder(16)
sb.append(leadAlphabet.charAt(secureRandom.nextInt(leadAlphabet.length())))
15.times { sb.append(nameAlphabet.charAt(secureRandom.nextInt(nameAlphabet.length()))) }
sb.toString()
}
def obfPackage = randomName() // one random package for every class (see note above)
ownedNames.each { internal ->
def newName
while (true) { newName = randomName(); if (usedNames.add(newName)) break }
typeMap[internal] = obfPackage + '/' + newName
}
// String-constant remap table — catches class names embedded as String
// literals (e.g. Class.forName("shit.zen.dll.DllBootstrap")). Both the
// dotted (Class.forName) and slash (internal) spellings are covered.
def stringMap = [:]
typeMap.each { o, n ->
stringMap[o.replace('/', '.')] = n.replace('/', '.')
stringMap[o] = n
}
def remapper = new Remapper() {
String map(String internalName) {
def n = typeMap[internalName]
return n != null ? n : internalName
}
Object mapValue(Object value) {
if (value instanceof String) {
def repl = stringMap[value]
if (repl != null) return repl
}
return super.mapValue(value)
}
}
// Pass 2: rewrite into a temp jar, then atomically swap it in.
def tmp = new File(jarFile.parentFile, jarFile.name + '.obf')
tmp.delete()
new java.util.zip.ZipFile(jarFile).withCloseable { zf ->
new java.util.zip.ZipOutputStream(new FileOutputStream(tmp)).withCloseable { zos ->
for (entry in Collections.list(zf.entries())) {
if (entry.directory) continue
def name = entry.name
byte[] bytes = zf.getInputStream(entry).bytes
if (name.endsWith('.class')) {
def internal = name.substring(0, name.length() - 6)
if (owned(internal)) {
def cr = new ClassReader(bytes)
def cw = new ClassWriter(0)
// Drop the original SourceFile name ("ZenClient.java" etc.) but keep
// line numbers so stack traces still carry positions.
def stripSource = new ClassVisitor(Opcodes.ASM9, cw) {
void visitSource(String source, String debug) { super.visitSource(null, null) }
}
cr.accept(new ClassRemapper(stripSource, remapper), 0)
bytes = cw.toByteArray()
name = typeMap[internal] + '.class'
}
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bytes); zos.closeEntry()
} else if (name == 'META-INF/MANIFEST.MF') {
def mf = new java.util.jar.Manifest(new ByteArrayInputStream(bytes))
def attrs = mf.getMainAttributes()
['Premain-Class', 'Agent-Class'].each { key ->
def v = attrs.getValue(key)
if (v != null) {
def internal = v.replace('.', '/')
if (typeMap.containsKey(internal)) attrs.putValue(key, typeMap[internal].replace('/', '.'))
}
}
def bos = new ByteArrayOutputStream(); mf.write(bos)
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bos.toByteArray()); zos.closeEntry()
} else if (name.startsWith('META-INF/') &&
(name.endsWith('.SF') || name.endsWith('.RSA') || name.endsWith('.DSA') || name.endsWith('.EC'))) {
// Drop signature files — renaming class entries invalidates their digests.
} else {
// Resources verbatim: mapping.srg, webui/**, assets/**, fonts, mods.toml, pack.mcmeta.
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bytes); zos.closeEntry()
}
}
}
}
if (!jarFile.delete()) throw new GradleException("obfuscateJar: could not delete ${jarFile}")
if (!tmp.renameTo(jarFile)) throw new GradleException("obfuscateJar: could not move ${tmp} -> ${jarFile}")
mappingOut.parentFile.mkdirs()
mappingOut.withWriter('UTF-8') { w ->
typeMap.sort { it.key }.each { o, n -> w.writeLine("${o.replace('/', '.')} -> ${n.replace('/', '.')}") }
}
// Propagate the GameLoaderBridge name to the native DLL loader. class_loader.cpp
// #includes this generated header and loads the class by OZ_BRIDGE_FQCN, so the
// native side always matches whatever opaque name the bridge received — no
// hard-coded class name. Generated, never committed (see .gitignore).
def bridgeFqcn = typeMap['shit/zen/dll/GameLoaderBridge'].replace('/', '.')
def header = file('native/dll/src/generated_names.h')
header.parentFile.mkdirs()
header.text = """\
// AUTO-GENERATED by build.gradle (ext.obfuscateJar). DO NOT EDIT, DO NOT COMMIT.
// The build renames every OpenZen class to an opaque name; this captures the
// generated FQCN of the DLL bootstrap bridge (originally shit.zen.dll.GameLoaderBridge)
// so the native loader can request it by name. The bridge's load(String, ClassLoader)
// method name is preserved by the rename, so main.cpp's GetStaticMethodID still works.
#pragma once
#define OZ_BRIDGE_FQCN "${bridgeFqcn}"
"""
logger.lifecycle("obfuscateJar: renamed ${typeMap.size()} classes in ${jarFile.name}; " +
"bridge=${bridgeFqcn}; mapping -> ${mappingOut}")
}
tasks.register('obfuscateClasses') {
group = 'openzen'
description = 'Rename every OpenZen class to an opaque name in the built jar (class names only).'
dependsOn 'reobfJar'
doLast {
obfuscateJar(tasks.jar.archiveFile.get().asFile, file("$buildDir/rename-mapping.txt"))
}
}
// Auto-run after every reobf so `gradlew jar` / `build` / `dll` all emit obfuscated
// names. reobfJar is created lazily by ForgeGradle, so wire via matching/configureEach.
tasks.matching { it.name == 'reobfJar' }.configureEach { finalizedBy 'obfuscateClasses' }
// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing:
// tasks.named('publish').configure {
// dependsOn 'reobfJar'
@@ -366,7 +566,10 @@ tasks.register('stageNativeJar', Copy) {
group = 'openzen'
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
'resource compiler can embed it.'
dependsOn 'jar'
// obfuscateClasses (which dependsOn reobfJar, which dependsOn jar) rewrites
// the jar in place, so depend on it to guarantee the DLL embeds the
// obfuscated jar rather than racing the rename.
dependsOn 'obfuscateClasses'
from { tasks.jar.archiveFile }
into nativeDir
rename { 'zen.jar' }
@@ -378,6 +581,13 @@ tasks.register('configureNative', Exec) {
dependsOn 'stageNativeJar'
workingDir nativeDir
commandLine 'cmake', '-S', '.', '-B', 'build', '-A', 'x64'
// Local builds compile Qt single-threaded (vcpkg honours VCPKG_MAX_CONCURRENCY,
// which sets ninja -j for the qtbase port). This rules out parallel-compilation
// instability on the dev toolchain. CI is left parallel (fast) — it builds Qt
// fine, so we only serialise locally (off GitHub Actions).
if (System.getenv('GITHUB_ACTIONS') == null) {
environment 'VCPKG_MAX_CONCURRENCY', '1'
}
doFirst {
def cmake = findCmake()
if (!cmake) throw new GradleException(cmakeMissingMessage)
@@ -405,7 +615,15 @@ tasks.register('buildNative', Exec) {
doFirst {
def cmake = findCmake()
if (!cmake) throw new GradleException(cmakeMissingMessage)
commandLine cmake, '--build', 'build', '--config', 'Release'
// --parallel forwards a job count to the generator (MSBuild gets /m,
// Ninja gets -j). MSBuild defaults to a single process otherwise.
// Local builds use 1 job (single-threaded) to match the serialised Qt
// build above and rule out parallel-compilation instability; CI keeps
// full parallelism for speed.
def jobs = (System.getenv('GITHUB_ACTIONS') == null) ? 1 : Runtime.runtime.availableProcessors()
logger.lifecycle("Parallel jobs: ${jobs}")
commandLine cmake, '--build', 'build', '--config', 'Release',
'--parallel', jobs.toString()
}
}
@@ -495,3 +713,7 @@ tasks.register('cleanNative', Delete) {
tasks.named('clean').configure {
dependsOn 'cleanNative'
}
// Release builds are cut by .github/workflows/build-loader.yml when the head commit
// message contains [Release]. Every build emits fresh random class names — see
// ext.obfuscateJar above and the "编译时类名混淆" section in the README.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

BIN
View File
Binary file not shown.

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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

-15
View File
@@ -1,15 +0,0 @@
# Backdoors
我们分析了当加载Zen后的一系列行为链,在运行时已确定其含有以下行为。
## 远程截图
![Screenshot Trace](./screenshot-trace.png)
`iIiIiIiIIIiIiI/Ʊ Đ()Ljava/awt/image/BufferedImage;`
Jump to [Mapping](./zen.mapping#L140)
当Zen被注入后,会自动触发截图并上传至服务器。其作者回应如下:
![Screenshot Response](./screenshot-response.png)
## 远程执行等
WIP
+6 -1
View File
@@ -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)
+10 -2
View File
@@ -1,4 +1,5 @@
#include "openzen.h"
#include "generated_names.h" // OZ_BRIDGE_FQCN — generated by build.gradle ext.obfuscateJar
#include <cstring>
@@ -114,17 +115,24 @@ jclass load_dll_bootstrap(JNIEnv* env, jobject game_loader,
if (check_and_clear(env, "new URLClassLoader")) return nullptr;
log::info("URLClassLoader constructed with parent=gameLoader");
// urlcl.loadClass("shit.zen.dll.GameLoaderBridge")
// urlcl.loadClass(OZ_BRIDGE_FQCN) (the build-time-obfuscated GameLoaderBridge)
//
// We deliberately load GameLoaderBridge - not DllBootstrap - because the
// bridge's job is to re-define every class in zen.jar onto the game
// class loader so retransformed Minecraft classes can resolve our patch
// handlers (defining-loader equality). DllBootstrap is then loaded by
// the game loader from the bridge.
//
// The build renames every OpenZen class to an opaque generated name (see
// build.gradle ext.obfuscateJar). It also emits generated_names.h with the
// bridge's new FQCN as OZ_BRIDGE_FQCN, so this stays in lockstep with the
// embedded jar without any hard-coded class name. The bridge's
// load(String, ClassLoader) method name is preserved by the rename, so the
// GetStaticMethodID(bridge_cls, "load", ...) lookup in main.cpp still works.
jclass classLoaderCls = env->FindClass("java/lang/ClassLoader");
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;");
jstring name = env->NewStringUTF("shit.zen.dll.GameLoaderBridge");
jstring name = env->NewStringUTF(OZ_BRIDGE_FQCN);
jobject loaded = env->CallObjectMethod(urlcl, loadClass, name);
if (check_and_clear(env, "URLClassLoader.loadClass GameLoaderBridge")) return nullptr;
if (!loaded) {
+2 -1
View File
@@ -9,5 +9,6 @@
"default-features": false,
"features": [ "widgets" ]
}
]
],
"builtin-baseline": "56bb2411609227288b70117ead2c47585ba07713"
}
@@ -18,8 +18,8 @@ import org.apache.logging.log4j.Logger;
* regardless of which class loader it lives in.</p>
*/
public final class PatchAgent {
public static final String INSTRUMENTATION_KEY = "asm.patchify.instrumentation";
private static final Logger LOGGER = LogManager.getLogger("PatchAgent");
public static final String INSTRUMENTATION_KEY = "oz.instrumentation";
private static final Logger LOGGER = LogManager.getLogger(PatchAgent.class);
private static volatile boolean transformerInstalled = false;
@@ -40,7 +40,7 @@ public final class PatchAgent {
return;
}
System.getProperties().put(INSTRUMENTATION_KEY, inst);
LOGGER.info("PatchAgent attached, retransform supported = {}", inst.isRetransformClassesSupported());
LOGGER.info("agent attached, retransform supported = {}", inst.isRetransformClassesSupported());
}
/**
@@ -62,7 +62,7 @@ public final class PatchAgent {
}
Instrumentation inst = getInstrumentation();
if (inst == null) {
LOGGER.warn("PatchAgent not attached; cannot install patches");
LOGGER.warn("agent not attached; cannot install patches");
return;
}
PatchClassFileTransformer transformer = new PatchClassFileTransformer();
@@ -23,13 +23,13 @@ import org.objectweb.asm.tree.ClassNode;
* <p>Indexes the patches by the JVM-internal name of their target class and rewrites class
* bytes via ASM. Classes that have no patch are returned untouched (null).</p>
*
* <p>If the system property {@code asm.patchify.dumpDir} is set, every successfully
* <p>If the system property {@code oz.dumpDir} is set, every successfully
* transformed class is written under that directory as {@code <internalName>.class} for
* inspection (use {@code javap -v} or open in Recaf).</p>
*/
public final class PatchClassFileTransformer implements ClassFileTransformer {
private static final Logger LOGGER = LogManager.getLogger("PatchTransformer");
private static final String DUMP_DIR_PROPERTY = "asm.patchify.dumpDir";
private static final Logger LOGGER = LogManager.getLogger(PatchClassFileTransformer.class);
private static final String DUMP_DIR_PROPERTY = "oz.dumpDir";
private final Map<String, List<Class<?>>> patchesByTarget = new HashMap<>();
@@ -17,7 +17,7 @@ import org.apache.logging.log4j.Logger;
* lightweight no-op fallback when no transformer is installed.</p>
*/
public final class PatchRegistry {
private static final Logger LOGGER = LogManager.getLogger("PatchRegistry");
private static final Logger LOGGER = LogManager.getLogger(PatchRegistry.class);
private static final List<Class<?>> PATCHES = new ArrayList<>();
private PatchRegistry() {
+3 -1
View File
@@ -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;
@@ -108,7 +109,7 @@ public class ZenClient extends ClientBase {
if (PatchAgent.getInstrumentation() != null) {
PatchAgent.installPatchesAndRetransform();
} else {
logger.warn("PatchAgent not attached. Launch with `./gradlew runClient0` so the agent jvmArg is set.");
logger.warn("agent not attached. Launch with `./gradlew runClient0` so the agent jvmArg is set.");
}
isReady = true;
logger.info("{} v{} initialized.", CLIENT_NAME, VERSION);
@@ -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);
Binary file not shown.
+8 -8
View File
@@ -22,7 +22,7 @@ import shit.zen.asm.Bootstrap;
* onwards.</p>
*/
public final class DllBootstrap {
private static final Logger LOGGER = LogManager.getLogger("DllBootstrap");
private static final Logger LOGGER = LogManager.getLogger(DllBootstrap.class);
private static volatile boolean started = false;
private DllBootstrap() {
@@ -30,16 +30,16 @@ public final class DllBootstrap {
public static synchronized void start(String extractedJarPath) {
if (started) {
LOGGER.info("DllBootstrap.start ignored (already started)");
LOGGER.info("bootstrap.start ignored (already started)");
return;
}
started = true;
try {
LOGGER.info("DllBootstrap.start jar={}", extractedJarPath);
LOGGER.info("DllBootstrap loader = {}", DllBootstrap.class.getClassLoader());
LOGGER.info("ZenClient loader = {}", ZenClient.class.getClassLoader());
LOGGER.info("bootstrap.start jar={}", extractedJarPath);
LOGGER.info("bootstrap loader = {}", DllBootstrap.class.getClassLoader());
LOGGER.info("client loader = {}", ZenClient.class.getClassLoader());
LOGGER.info("Minecraft loader = {}", Minecraft.class.getClassLoader());
LOGGER.info("PatchAgent inst = {}", PatchAgent.getInstrumentation());
LOGGER.info("agent inst = {}", PatchAgent.getInstrumentation());
// Load mojmap → SRG mappings before we attempt any retransform.
Bootstrap.init();
@@ -53,9 +53,9 @@ public final class DllBootstrap {
ZenClient.registerPatches();
PatchAgent.installPatchesAndRetransform();
LOGGER.info("DllBootstrap done. ZenClient will be constructed on the next Minecraft.tick.");
LOGGER.info("bootstrap done. client will be constructed on the next tick.");
} catch (Throwable t) {
LOGGER.error("DllBootstrap.start failed", t);
LOGGER.error("bootstrap.start failed", t);
}
}
}
@@ -46,19 +46,19 @@ import org.apache.logging.log4j.Logger;
* </ol>
*/
public final class GameLoaderBridge {
private static final Logger LOGGER = LogManager.getLogger("GameLoaderBridge");
private static final Logger LOGGER = LogManager.getLogger(GameLoaderBridge.class);
public static final String RESOURCES_PROP = "openzen.resources";
private GameLoaderBridge() {
}
public static void load(String jarPath, ClassLoader gameLoader) throws Throwable {
LOGGER.info("GameLoaderBridge.load jar={} gameLoader={}", jarPath, gameLoader);
LOGGER.info("bridge.load jar={} gameLoader={}", jarPath, gameLoader);
long t0 = System.nanoTime();
Instrumentation inst = PatchAgent.getInstrumentation();
if (inst == null) {
throw new IllegalStateException("PatchAgent.getInstrumentation() returned null - "
throw new IllegalStateException("agent instrumentation returned null - "
+ "did instrument.dll Agent_OnAttach run?");
}
@@ -128,7 +128,7 @@ public final class GameLoaderBridge {
classCount, resourceCount, resourceDir, ms);
Class<?> bootstrapCls = Class.forName("shit.zen.dll.DllBootstrap", true, gameLoader);
LOGGER.info("DllBootstrap loader (should be gameLoader): {}", bootstrapCls.getClassLoader());
LOGGER.info("bootstrap loader (should be gameLoader): {}", bootstrapCls.getClassLoader());
Method start = bootstrapCls.getMethod("start", String.class);
start.invoke(null, jarPath);
}
@@ -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;
}
File diff suppressed because it is too large Load Diff
@@ -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();
}
}