Compare commits
28 Commits
build-5745547
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 692c0ac7b0 | |||
| 9f094b4aa8 | |||
| 8f53eda70e | |||
| 941b69203b | |||
| f59d4c121c | |||
| b76ba33ea5 | |||
| 21508a6f34 | |||
| 55fe4699f2 | |||
| ba0fd3b702 | |||
| 42ca5e208c | |||
| 72d5f1415c | |||
| 6c842f3033 | |||
| 7576add38f | |||
| d3e4748966 | |||
| 07be28c11e | |||
| 5b70703db5 | |||
| fb681e7c83 | |||
| 91fb65c22f | |||
| 3263b0b4cb | |||
| 5b1d220280 | |||
| 8b16eb4972 | |||
| 9ad6a6b70c | |||
| 4958f6694c | |||
| 8d395a1d9f | |||
| 9dc1dcbcd5 | |||
| dea5cf6270 | |||
| ecc29d4e82 | |||
| 6529c3090b |
@@ -3,6 +3,19 @@ name: Build Loader
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
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: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
# contents: write so the [Release] commit-marker path can create a
|
# contents: write so the [Release] commit-marker path can create a
|
||||||
@@ -17,6 +30,12 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
timeout-minutes: 180
|
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:
|
env:
|
||||||
# NOTE: do not declare VCPKG_ROOT here. The windows-2022 runner
|
# NOTE: do not declare VCPKG_ROOT here. The windows-2022 runner
|
||||||
# ships VS Enterprise which pre-sets a system-wide VCPKG_ROOT
|
# ships VS Enterprise which pre-sets a system-wide VCPKG_ROOT
|
||||||
@@ -69,31 +88,52 @@ jobs:
|
|||||||
- name: Clone & bootstrap vcpkg
|
- name: Clone & bootstrap vcpkg
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
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'
|
$vcpkg = Join-Path $env:GITHUB_WORKSPACE 'vcpkg'
|
||||||
if (Test-Path $vcpkg) {
|
if (Test-Path $vcpkg) {
|
||||||
Write-Host "Reusing existing $vcpkg"
|
Write-Host "Reusing existing $vcpkg"
|
||||||
} else {
|
} 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\bootstrap-vcpkg.bat" -disableMetrics
|
||||||
& "$vcpkg\vcpkg.exe" version
|
& "$vcpkg\vcpkg.exe" version
|
||||||
"VCPKG_ROOT=$vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
"VCPKG_ROOT=$vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||||
Write-Host "Exported VCPKG_ROOT=$vcpkg to GITHUB_ENV"
|
Write-Host "Exported VCPKG_ROOT=$vcpkg to GITHUB_ENV"
|
||||||
|
|
||||||
# Cache the resolved Qt install + the file-backed binary archive.
|
# Cache the vcpkg *binary archives* — the only thing that lets vcpkg skip
|
||||||
# Key is keyed on vcpkg.json so any dependency change invalidates;
|
# rebuilding qtbase. The build runs `gradlew clean`, which deletes
|
||||||
# restore-keys lets a vcpkg.json bump still warm-start from the
|
# native/build/ (and thus the manifest install dir native/build/vcpkg_installed),
|
||||||
# previous build's archives. installed/ alone makes CMake's manifest
|
# so on every run vcpkg re-resolves the manifest and restores packages from
|
||||||
# mode skip rebuilding qtbase; archives/ also fills back transitive
|
# this file-backed binary cache. If it's empty, qtbase is compiled from
|
||||||
# deps if a partial rebuild is needed.
|
# source (~32 min). (We previously cached vcpkg\installed too, but in
|
||||||
- name: Cache vcpkg installed + archives
|
# manifest mode that tree is never consulted for the rebuild decision — only
|
||||||
|
# the binary archive is — so it was ~1.2 GB of dead weight.)
|
||||||
|
#
|
||||||
|
# The key MUST rotate per run: actions/cache never overwrites an existing
|
||||||
|
# key, so a static key froze whatever archives the first run captured (none)
|
||||||
|
# and every later run hit "cache hit, not saving" at Post — discarding the
|
||||||
|
# qtbase archive it had just rebuilt, guaranteeing a fresh 32-min rebuild
|
||||||
|
# forever. Appending run_id makes Post always save a new entry; the layered
|
||||||
|
# restore-keys warm-start from the newest prior archive so qtbase is
|
||||||
|
# restored, not rebuilt. (GitHub LRU-evicts old entries within the 10 GB
|
||||||
|
# budget; the archive is only a few hundred MB.)
|
||||||
|
- name: Cache vcpkg binary archives
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: vcpkg-archives
|
||||||
vcpkg\installed
|
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-${{ github.run_id }}
|
||||||
vcpkg-archives
|
|
||||||
key: vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
|
vcpkg-qt-${{ runner.os }}-${{ hashFiles('native/vcpkg.json') }}-
|
||||||
vcpkg-qt-${{ runner.os }}-
|
vcpkg-qt-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install UPX
|
- name: Install UPX
|
||||||
@@ -129,20 +169,28 @@ jobs:
|
|||||||
$sha = "${{ steps.rev.outputs.sha }}"
|
$sha = "${{ steps.rev.outputs.sha }}"
|
||||||
$exeSrc = "build\dist\OpenZenLoader.exe"
|
$exeSrc = "build\dist\OpenZenLoader.exe"
|
||||||
$jarSrc = "build\libs\hey-1.0.jar"
|
$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 $exeSrc)) { throw "missing $exeSrc" }
|
||||||
if (-not (Test-Path $jarSrc)) { throw "missing $jarSrc" }
|
if (-not (Test-Path $jarSrc)) { throw "missing $jarSrc" }
|
||||||
|
if (-not (Test-Path $mapSrc)) { throw "missing $mapSrc" }
|
||||||
|
|
||||||
$release = "build\release"
|
$release = "build\release"
|
||||||
New-Item -ItemType Directory -Force -Path $release | Out-Null
|
New-Item -ItemType Directory -Force -Path $release | Out-Null
|
||||||
$exeDst = Join-Path $release "OpenZenLoader-$sha.exe"
|
$exeDst = Join-Path $release "OpenZenLoader-$sha.exe"
|
||||||
$jarDst = Join-Path $release "OpenZen-$sha.jar"
|
$jarDst = Join-Path $release "OpenZen-$sha.jar"
|
||||||
|
$mapDst = Join-Path $release "OpenZen-$sha-mapping.txt"
|
||||||
Copy-Item -Force $exeSrc $exeDst
|
Copy-Item -Force $exeSrc $exeDst
|
||||||
Copy-Item -Force $jarSrc $jarDst
|
Copy-Item -Force $jarSrc $jarDst
|
||||||
|
Copy-Item -Force $mapSrc $mapDst
|
||||||
|
|
||||||
$exeSz = (Get-Item $exeDst).Length
|
$exeSz = (Get-Item $exeDst).Length
|
||||||
$jarSz = (Get-Item $jarDst).Length
|
$jarSz = (Get-Item $jarDst).Length
|
||||||
Write-Host ("OpenZenLoader-{0}.exe : {1:N0} bytes ({2:N2} MB)" -f $sha, $exeSz, ($exeSz/1MB))
|
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}.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
|
# NOTE: actions/upload-artifact always wraps its content in a zip; that
|
||||||
# is a platform limitation we cannot disable. By giving each artifact a
|
# is a platform limitation we cannot disable. By giving each artifact a
|
||||||
@@ -166,6 +214,14 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 30
|
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 =====
|
# ===== Optional GitHub Release publish =====
|
||||||
# If the HEAD commit message contains the literal marker "[Release]",
|
# If the HEAD commit message contains the literal marker "[Release]",
|
||||||
# cut a GitHub Release tagged build-<sha> and attach the exe + jar.
|
# cut a GitHub Release tagged build-<sha> and attach the exe + jar.
|
||||||
@@ -176,7 +232,9 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$msg = (git log -1 --pretty=%B HEAD | Out-String)
|
$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())" |
|
"is_release=$($isRelease.ToString().ToLower())" |
|
||||||
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||||
Write-Host "HEAD commit message:"
|
Write-Host "HEAD commit message:"
|
||||||
@@ -195,9 +253,30 @@ jobs:
|
|||||||
# Write notes via a file so quoting / [brackets] / newlines in the
|
# Write notes via a file so quoting / [brackets] / newlines in the
|
||||||
# commit message can't corrupt the gh command line.
|
# commit message can't corrupt the gh command line.
|
||||||
$notes = "release-notes.md"
|
$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 `
|
gh release create $tag `
|
||||||
--title $title `
|
--title $title `
|
||||||
--notes-file $notes `
|
--notes-file $notes `
|
||||||
"build/release/OpenZenLoader-$env:SHA.exe" `
|
"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"
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ forge*changelog.txt
|
|||||||
# Native DLL build artifacts (CMake out-of-source build + staged jar)
|
# Native DLL build artifacts (CMake out-of-source build + staged jar)
|
||||||
native/build/
|
native/build/
|
||||||
native/zen.jar
|
native/zen.jar
|
||||||
|
|
||||||
|
# Generated at build time by ext.obfuscateJar (holds the obfuscated bridge FQCN)
|
||||||
|
native/dll/src/generated_names.h
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Open Zen
|
# Open Zen
|
||||||
|
|
||||||
> 欢迎加群 523522206 讨论该项目。
|
> 由于老群被精神马来西亚人的小狗疯狂举报,因此无法与您继续互动。欢迎加新群 660304532 讨论本项目。
|
||||||
|
|
||||||
> 截止至目前,Open Zen仍然处于早期构建阶段。大量特性可能仍不可用,请谅解!当您发现有不可用的功能时,欢迎开Issues描述或直接提交Pull Request,我们将不胜感激!
|
> 当您发现有不可用的功能时,欢迎开 Issues 描述或直接提交 Pull Request,我们将不胜感激!
|
||||||
|
|
||||||
**Open Zen** 是 *Zen* Minecraft 客户端的反混淆源码版本,几乎是由 [Claude](https://claude.com/)协助逆向得到。目标版本为 **Minecraft 1.20.1 + Forge 47.4.20**。
|
**Open Zen** 是 *Zen* Minecraft 客户端的反混淆源码版本,几乎是由 [Claude](https://claude.com/) 协助逆向得到。目标版本为 **Minecraft 1.20.1 + Forge 47.4.20**。
|
||||||
|
|
||||||
原始 Jar 经过完整混淆:类/字段/方法重命名、控制流扁平化。我使用Opus 4.7对其进行了反混淆,并结合 [Enigma MCP](https://github.com/Margele/Enigma-MCP)和Sonnet 4.6对其的类/字段/方法重命名进行猜测,最后将其还原为了可读的 Java。最终产物是一个可以直接用 Gradle 构建的工程,而不是一个二进制 blob。
|
原始 Jar 经过完整混淆:类/字段/方法重命名、控制流扁平化。我使用 Opus 4.7 对其进行了反混淆,并结合 [Enigma MCP](https://github.com/Margele/Enigma-MCP) 和 Sonnet 4.6 对其类/字段/方法重命名进行猜测,最后将其还原为可读的 Java。最终产物是一个可以直接用 Gradle 构建的工程,而不是一个二进制 blob。
|
||||||
|
|
||||||
> ⚠️ 本仓库**仅供学习与研究目的发布** —— 用于研究客户端侧游戏改造、ASM 字节码补丁和混淆/反混淆技术。在你不拥有的服务器上使用作弊客户端违反绝大多数服务器规则,请自行承担后果。
|
> ⚠️ 本仓库**仅供学习与研究目的发布** —— 用于研究客户端侧游戏改造、ASM 字节码补丁和混淆/反混淆技术。在你不拥有的服务器上使用作弊客户端违反绝大多数服务器规则,请自行承担后果。
|
||||||
|
|
||||||
@@ -14,7 +14,162 @@
|
|||||||
原始混淆字节码未授予任何许可。本仓库中的反混淆产物、构建脚本与文档**仅供研究与学习使用**。如果你是 Zen 的原作者并希望本仓库下架或重新授权,请提 Issues。虽然提了也不会搭理你。
|
原始混淆字节码未授予任何许可。本仓库中的反混淆产物、构建脚本与文档**仅供研究与学习使用**。如果你是 Zen 的原作者并希望本仓库下架或重新授权,请提 Issues。虽然提了也不会搭理你。
|
||||||
|
|
||||||
## 截图
|
## 截图
|
||||||

|

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

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

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

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

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

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

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

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

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

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

|
||||||
|

|
||||||
|

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

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

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

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

|
||||||
|
|
||||||
|
对此,笔者综合精神马来西亚人由于半夜玩电脑惨遭自己生物爹家暴的事实猜测:精神马来西亚人的生物爹和生物妈可能对精神马来西亚人的控制欲极强,因此精神马来西亚人的生活空间内可能存在十万甚至九万个摄像头,对精神马来西亚人的生活进行了无孔不入的监控。因此,精神马来西亚人在拉屎、自慰、睡觉、上课时都时时刻刻被监控,所以自然认为截图用户是正常且合理的行为。
|
||||||
|
|
||||||
|
笔者在此提醒:对用户的电脑进行无提醒、未通知用户的全屏幕截图,是不合理的行为。建议精神马来人端正自己对这个世界的认知,从自己过往被家暴经历中走出来,祝你早日康复!
|
||||||
|
|
||||||
|
### 分析
|
||||||
|
当Zen启动时,会自动调用 `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写的*。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
由于精神马来西亚人嘴硬,所以到底具体有没有实现,笔者暂且蒙古。
|
||||||
|
|
||||||
|
### 父子相爱相杀
|
||||||
|
|
||||||
|

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

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

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

|
||||||
|
|
||||||
|
但精神马来西亚人在QQ群中指出,欣欣使用过外挂后门远程读取他人文件。不知作为德州骡子的许锦良见自己的亲生儿子如此指认自己作何感想。
|
||||||
|
|
||||||
## 原始 Jar + Mapping
|
## 原始 Jar + Mapping
|
||||||
[原始Jar](./mapping/zen-orignial.jar)
|
[原始Jar](./mapping/zen-orignial.jar)
|
||||||
@@ -23,7 +178,7 @@
|
|||||||
需要说明的是,部分喜欢裸舞的忠实Zen用户认为本源码逆向自比较旧的Zen版本。可能是因为这部分用户的脑容量只允许自己导入其他配置,因此不认识Zen的老旧UI。因此必须要说明,此源码使用的原始Jar截止至2026年5月21日是最新的。
|
需要说明的是,部分喜欢裸舞的忠实Zen用户认为本源码逆向自比较旧的Zen版本。可能是因为这部分用户的脑容量只允许自己导入其他配置,因此不认识Zen的老旧UI。因此必须要说明,此源码使用的原始Jar截止至2026年5月21日是最新的。
|
||||||
|
|
||||||
## 细节
|
## 细节
|
||||||
经过Opus 4.7长达18秒的分析,Opus认为所有的类由惨遭魔改的Zelix KlassMaster混淆。除了Zelix的Invoke Dynamic和String Encryption外,还有部分未参与任何计算的Interger \ Long变量花指令代码和仅在部分方法中出现的Flow混淆。
|
经过Opus 4.7长达18秒的分析,Opus认为所有的类由惨遭魔改的Zelix KlassMaster混淆。除了Zelix的Invoke Dynamic和String Encryption外,还有部分未参与任何计算的 Integer / Long 变量花指令代码和仅在部分方法中出现的 Flow 混淆。
|
||||||
|
|
||||||
其中大部分类都可以经过小修小补的现有Zelix反混淆器完成,关键部分的`cinit`被Native保护,导致在Java层中没有对应的Master Key可以对Invoke Dynamic和String反混淆。但可能由于精神马来西亚人的脑袋在马来西亚骑摩托被其他车创飞导致脑溢血,即使你没有通过客户端认证也可以完整加载Native并对Class进行注册。因此我们完整的还原了所有类的Invoke Dynamic和String混淆。
|
其中大部分类都可以经过小修小补的现有Zelix反混淆器完成,关键部分的`cinit`被Native保护,导致在Java层中没有对应的Master Key可以对Invoke Dynamic和String反混淆。但可能由于精神马来西亚人的脑袋在马来西亚骑摩托被其他车创飞导致脑溢血,即使你没有通过客户端认证也可以完整加载Native并对Class进行注册。因此我们完整的还原了所有类的Invoke Dynamic和String混淆。
|
||||||
|
|
||||||
@@ -31,15 +186,6 @@
|
|||||||
|
|
||||||
再使用Opus 4.7对本项目经过长达6小时的修复和少量的人工修复,便得到了这份源码。
|
再使用Opus 4.7对本项目经过长达6小时的修复和少量的人工修复,便得到了这份源码。
|
||||||
|
|
||||||
## 后门
|
|
||||||
原版的Zen存在大量后门,例如上报QQ、屏幕截图、扫描文件、上传文件、远程执行命令等。因此我们不推荐任何用户继续使用原版Zen,除非你愿意现在把你身上的衣服脱掉然后去本地人最多的广场裸舞,然后把自己裸舞的视频发送到Zen的群内。
|
|
||||||
|
|
||||||
> Zen开发者称,所有的后门都未实现未引用,是由夏天完成的。但笔者在分析其Native时发现了完整引用,暂且蒙古。
|
|
||||||
|
|
||||||
经过分析,当你登录Zen后就会自动触发屏幕截图。同时已有用户反馈,当你狗叫Zen开发者后,其开发者将会把你的电脑屏幕截图发送至QQ群内,所以请你不要狗叫Zen开发者。
|
|
||||||
|
|
||||||
[详细分析](./mapping/BACKDOOR.md)
|
|
||||||
|
|
||||||
## 抄袭
|
## 抄袭
|
||||||
此项目大部分功能模块几乎全部抄袭自Naven客户端,具体详见以下分析。
|
此项目大部分功能模块几乎全部抄袭自Naven客户端,具体详见以下分析。
|
||||||
|
|
||||||
@@ -50,17 +196,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 任一)。
|
- **JDK 17**(推荐 Microsoft Build of OpenJDK / Temurin / Azul Zulu 任一)。
|
||||||
- 必须设置 `JAVA_HOME` 环境变量指向该 JDK 安装目录(PowerShell 验证:`echo $env:JAVA_HOME`)。
|
- 必须设置 `JAVA_HOME` 环境变量指向该 JDK 安装目录(PowerShell 验证:`echo $env:JAVA_HOME`)。
|
||||||
- 仓库根目录用 `gradlew.bat` 即可,**不需要**单独安装 Gradle。
|
- 仓库根目录用 `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 和依赖,耗时几分钟到十几分钟,取决于网络。
|
首次执行会从 ForgeMaven 下载 1.20.1 + Forge 47.4.20 的 mappings 和依赖,耗时几分钟到十几分钟,取决于网络。
|
||||||
|
|
||||||
### 1. 构建为 Forge Mod (jar)
|
### 1. 构建为 Java Agent jar
|
||||||
|
|
||||||
零额外依赖。
|
零额外依赖。
|
||||||
|
|
||||||
@@ -68,7 +237,13 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
|||||||
.\gradlew.bat 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)
|
### 2. 构建为热注入器 (单文件 EXE)
|
||||||
|
|
||||||
@@ -100,32 +275,26 @@ OpenZen 支持两种交付形式:**Forge Mod (jar)** 和 **热注入器 (单
|
|||||||
.\gradlew.bat dll
|
.\gradlew.bat dll
|
||||||
```
|
```
|
||||||
|
|
||||||
产物:`build/dist/OpenZenLoader.exe`(OpenZen.dll 已内嵌为 RCDATA 资源段)。
|
产物:`build/dist/OpenZenLoader.exe`。如果已装 UPX 想顺便压缩,跑 `.\gradlew.bat upxCompress`。
|
||||||
|
|
||||||
构建流程:
|
|
||||||
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/`
|
|
||||||
|
|
||||||
#### 使用注入器
|
#### 使用注入器
|
||||||
|
|
||||||
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
1. 用 HMCL / Forge 启动器正常启动 Minecraft 1.20.1 Forge(**不需要**任何特殊 JVM 参数)。
|
||||||
2. 双击 `OpenZenLoader.exe`(会弹一次 UAC 提权)。
|
2. 双击 `OpenZenLoader.exe`。
|
||||||
3. GUI 自动列出系统里**所有 Minecraft 实例**(按窗口标题 startsWith `Minecraft` 且不含 `Launcher` 过滤),每秒自动刷新一次。
|
3. GUI 自动列出系统里**所有 Minecraft 实例**,每秒自动刷新一次。
|
||||||
4. **双击对应的行**就开始注入。
|
4. 点击行最后的 Inject 按钮。
|
||||||
5. 状态栏出现 `Injected into PID ...` 即成功;Minecraft 内右 Shift 触发 ClickGUI 验证。
|
|
||||||
|
|
||||||
诊断日志:
|
诊断日志:
|
||||||
- Native 端:`%TEMP%\openzen.log`
|
- 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 上的预编译版:那是固定的一套名字,会被拉黑。
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
plugins {
|
||||||
id 'eclipse'
|
id 'eclipse'
|
||||||
id 'idea'
|
id 'idea'
|
||||||
@@ -209,6 +233,182 @@ tasks.named('jar', Jar).configure {
|
|||||||
finalizedBy 'reobfJar'
|
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:
|
// 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 {
|
// tasks.named('publish').configure {
|
||||||
// dependsOn 'reobfJar'
|
// dependsOn 'reobfJar'
|
||||||
@@ -366,7 +566,10 @@ tasks.register('stageNativeJar', Copy) {
|
|||||||
group = 'openzen'
|
group = 'openzen'
|
||||||
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
||||||
'resource compiler can embed it.'
|
'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 }
|
from { tasks.jar.archiveFile }
|
||||||
into nativeDir
|
into nativeDir
|
||||||
rename { 'zen.jar' }
|
rename { 'zen.jar' }
|
||||||
@@ -378,6 +581,13 @@ tasks.register('configureNative', Exec) {
|
|||||||
dependsOn 'stageNativeJar'
|
dependsOn 'stageNativeJar'
|
||||||
workingDir nativeDir
|
workingDir nativeDir
|
||||||
commandLine 'cmake', '-S', '.', '-B', 'build', '-A', 'x64'
|
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 {
|
doFirst {
|
||||||
def cmake = findCmake()
|
def cmake = findCmake()
|
||||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||||
@@ -405,7 +615,15 @@ tasks.register('buildNative', Exec) {
|
|||||||
doFirst {
|
doFirst {
|
||||||
def cmake = findCmake()
|
def cmake = findCmake()
|
||||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
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 {
|
tasks.named('clean').configure {
|
||||||
dependsOn 'cleanNative'
|
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.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -1,15 +0,0 @@
|
|||||||
# Backdoors
|
|
||||||
我们分析了当加载Zen后的一系列行为链,在运行时已确定其含有以下行为。
|
|
||||||
|
|
||||||
## 远程截图
|
|
||||||

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

|
|
||||||
|
|
||||||
## 远程执行等
|
|
||||||
WIP
|
|
||||||
|
Before Width: | Height: | Size: 126 KiB |
@@ -37,7 +37,12 @@ add_compile_definitions(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if(MSVC)
|
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()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(dll)
|
add_subdirectory(dll)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "openzen.h"
|
#include "openzen.h"
|
||||||
|
#include "generated_names.h" // OZ_BRIDGE_FQCN — generated by build.gradle ext.obfuscateJar
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -114,17 +115,24 @@ jclass load_dll_bootstrap(JNIEnv* env, jobject game_loader,
|
|||||||
if (check_and_clear(env, "new URLClassLoader")) return nullptr;
|
if (check_and_clear(env, "new URLClassLoader")) return nullptr;
|
||||||
log::info("URLClassLoader constructed with parent=gameLoader");
|
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
|
// We deliberately load GameLoaderBridge - not DllBootstrap - because the
|
||||||
// bridge's job is to re-define every class in zen.jar onto the game
|
// 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
|
// class loader so retransformed Minecraft classes can resolve our patch
|
||||||
// handlers (defining-loader equality). DllBootstrap is then loaded by
|
// handlers (defining-loader equality). DllBootstrap is then loaded by
|
||||||
// the game loader from the bridge.
|
// 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");
|
jclass classLoaderCls = env->FindClass("java/lang/ClassLoader");
|
||||||
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
|
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
|
||||||
"(Ljava/lang/String;)Ljava/lang/Class;");
|
"(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);
|
jobject loaded = env->CallObjectMethod(urlcl, loadClass, name);
|
||||||
if (check_and_clear(env, "URLClassLoader.loadClass GameLoaderBridge")) return nullptr;
|
if (check_and_clear(env, "URLClassLoader.loadClass GameLoaderBridge")) return nullptr;
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QParallelAnimationGroup>
|
#include <QParallelAnimationGroup>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
|
#include <QRandomGenerator>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -32,6 +33,28 @@ namespace loader {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kCornerRadius = 12;
|
constexpr int kCornerRadius = 12;
|
||||||
|
constexpr int kBaseWidth = 760;
|
||||||
|
constexpr int kBaseHeight = 500;
|
||||||
|
constexpr int kSizeJitter = 10; // ± px of random size jitter applied per launch
|
||||||
|
|
||||||
|
// Random alphanumeric identifier (first char always a letter). Used to give the
|
||||||
|
// loader window a non-constant Win32 title each launch so a scanner can't match
|
||||||
|
// it against a fixed window-text string.
|
||||||
|
QString randomIdent(int minLen, int maxLen) {
|
||||||
|
static const char kAlphabet[] =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
constexpr int kLetters = 52; // leading a-zA-Z only
|
||||||
|
constexpr int kAll = int(sizeof(kAlphabet)) - 1;
|
||||||
|
QRandomGenerator* rng = QRandomGenerator::global();
|
||||||
|
const int len = minLen + int(rng->bounded(quint32(maxLen - minLen + 1)));
|
||||||
|
QString s;
|
||||||
|
s.reserve(len);
|
||||||
|
s.append(QLatin1Char(kAlphabet[rng->bounded(kLetters)]));
|
||||||
|
for (int i = 1; i < len; ++i) {
|
||||||
|
s.append(QLatin1Char(kAlphabet[rng->bounded(kAll)]));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
QString fromW(const std::wstring& w) {
|
QString fromW(const std::wstring& w) {
|
||||||
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
|
return QString::fromWCharArray(w.c_str(), static_cast<int>(w.size()));
|
||||||
@@ -60,8 +83,14 @@ MainWindow::MainWindow(QWidget* parent)
|
|||||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
setAttribute(Qt::WA_NoSystemBackground);
|
setAttribute(Qt::WA_NoSystemBackground);
|
||||||
setMinimumSize(760, 500);
|
// Base size with a small random jitter (±kSizeJitter px) on each axis so the
|
||||||
resize(760, 500);
|
// window dimensions aren't a constant fingerprint. Minimum is lowered by the
|
||||||
|
// jitter so a negative jitter isn't clamped back to the base size.
|
||||||
|
setMinimumSize(kBaseWidth - kSizeJitter, kBaseHeight - kSizeJitter);
|
||||||
|
QRandomGenerator* rng = QRandomGenerator::global();
|
||||||
|
const int jitterW = int(rng->bounded(quint32(2 * kSizeJitter + 1))) - kSizeJitter;
|
||||||
|
const int jitterH = int(rng->bounded(quint32(2 * kSizeJitter + 1))) - kSizeJitter;
|
||||||
|
resize(kBaseWidth + jitterW, kBaseHeight + jitterH);
|
||||||
|
|
||||||
styleApp();
|
styleApp();
|
||||||
buildUi();
|
buildUi();
|
||||||
@@ -75,12 +104,15 @@ MainWindow::MainWindow(QWidget* parent)
|
|||||||
|
|
||||||
void MainWindow::buildUi() {
|
void MainWindow::buildUi() {
|
||||||
#ifdef OPENZEN_BUILD_REVISION
|
#ifdef OPENZEN_BUILD_REVISION
|
||||||
const QString winTitle = QStringLiteral("OpenZen Loader · build %1")
|
const QString displayTitle = QStringLiteral("OpenZen Loader · build %1")
|
||||||
.arg(QString::fromLatin1(OPENZEN_BUILD_REVISION).left(7));
|
.arg(QString::fromLatin1(OPENZEN_BUILD_REVISION).left(7));
|
||||||
#else
|
#else
|
||||||
const QString winTitle = QStringLiteral("OpenZen Loader");
|
const QString displayTitle = QStringLiteral("OpenZen Loader");
|
||||||
#endif
|
#endif
|
||||||
setWindowTitle(winTitle);
|
// The OS-level window title (what GetWindowTextW and window scanners read) is
|
||||||
|
// randomised on every launch so it can't be matched against a fixed string.
|
||||||
|
// The visible custom title bar below still shows the branded name.
|
||||||
|
setWindowTitle(randomIdent(8, 16));
|
||||||
|
|
||||||
auto* central = new QWidget(this);
|
auto* central = new QWidget(this);
|
||||||
central->setAttribute(Qt::WA_TranslucentBackground);
|
central->setAttribute(Qt::WA_TranslucentBackground);
|
||||||
@@ -90,7 +122,7 @@ void MainWindow::buildUi() {
|
|||||||
root->setSpacing(0);
|
root->setSpacing(0);
|
||||||
|
|
||||||
titleBar_ = new TitleBar(central);
|
titleBar_ = new TitleBar(central);
|
||||||
titleBar_->setTitleText(winTitle);
|
titleBar_->setTitleText(displayTitle);
|
||||||
root->addWidget(titleBar_);
|
root->addWidget(titleBar_);
|
||||||
|
|
||||||
auto* body = new QWidget(central);
|
auto* body = new QWidget(central);
|
||||||
|
|||||||
@@ -9,5 +9,6 @@
|
|||||||
"default-features": false,
|
"default-features": false,
|
||||||
"features": [ "widgets" ]
|
"features": [ "widgets" ]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"builtin-baseline": "56bb2411609227288b70117ead2c47585ba07713"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,13 @@ import java.lang.annotation.Target;
|
|||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface Patch {
|
public @interface Patch {
|
||||||
Class<?> value();
|
Class<?> value() default void.class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative to {@link #value()} for targeting classes that are not available at compile time.
|
||||||
|
* When non-empty, takes precedence over {@code value()}. This is useful for targeting classes from
|
||||||
|
* optional mods (e.g. Embeddium/Sodium).
|
||||||
|
* <p>Use the fully qualified JVM class name (e.g. {@code "me.jellysquid.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache"}).</p>
|
||||||
|
*/
|
||||||
|
String className() default "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import org.apache.logging.log4j.Logger;
|
|||||||
* regardless of which class loader it lives in.</p>
|
* regardless of which class loader it lives in.</p>
|
||||||
*/
|
*/
|
||||||
public final class PatchAgent {
|
public final class PatchAgent {
|
||||||
public static final String INSTRUMENTATION_KEY = "asm.patchify.instrumentation";
|
public static final String INSTRUMENTATION_KEY = "oz.instrumentation";
|
||||||
private static final Logger LOGGER = LogManager.getLogger("PatchAgent");
|
private static final Logger LOGGER = LogManager.getLogger(PatchAgent.class);
|
||||||
|
|
||||||
private static volatile boolean transformerInstalled = false;
|
private static volatile boolean transformerInstalled = false;
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ public final class PatchAgent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
System.getProperties().put(INSTRUMENTATION_KEY, inst);
|
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();
|
Instrumentation inst = getInstrumentation();
|
||||||
if (inst == null) {
|
if (inst == null) {
|
||||||
LOGGER.warn("PatchAgent not attached; cannot install patches");
|
LOGGER.warn("agent not attached; cannot install patches");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PatchClassFileTransformer transformer = new PatchClassFileTransformer();
|
PatchClassFileTransformer transformer = new PatchClassFileTransformer();
|
||||||
@@ -72,6 +72,29 @@ public final class PatchAgent {
|
|||||||
for (Class<?> patch : PatchRegistry.getPatches()) {
|
for (Class<?> patch : PatchRegistry.getPatches()) {
|
||||||
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
|
||||||
if (ann == null) continue;
|
if (ann == null) continue;
|
||||||
|
if (!ann.className().isEmpty()) {
|
||||||
|
// className-based patches target optional mod classes.
|
||||||
|
// Check if the class is already loaded via Instrumentation.
|
||||||
|
boolean found = false;
|
||||||
|
for (Class<?> loaded : inst.getAllLoadedClasses()) {
|
||||||
|
if (loaded.getName().equals(ann.className())) {
|
||||||
|
LOGGER.debug("Found already-loaded target {} for className-based patch {}",
|
||||||
|
ann.className(), patch.getName());
|
||||||
|
if (inst.isModifiableClass(loaded)) {
|
||||||
|
retransform.add(loaded);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("Cannot retransform unmodifiable target {}", ann.className());
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
LOGGER.debug("Target {} not yet loaded — transformer will catch it at class-load time",
|
||||||
|
ann.className());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Class<?> target;
|
Class<?> target;
|
||||||
try {
|
try {
|
||||||
target = ann.value();
|
target = ann.value();
|
||||||
|
|||||||
@@ -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
|
* <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>
|
* 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
|
* transformed class is written under that directory as {@code <internalName>.class} for
|
||||||
* inspection (use {@code javap -v} or open in Recaf).</p>
|
* inspection (use {@code javap -v} or open in Recaf).</p>
|
||||||
*/
|
*/
|
||||||
public final class PatchClassFileTransformer implements ClassFileTransformer {
|
public final class PatchClassFileTransformer implements ClassFileTransformer {
|
||||||
private static final Logger LOGGER = LogManager.getLogger("PatchTransformer");
|
private static final Logger LOGGER = LogManager.getLogger(PatchClassFileTransformer.class);
|
||||||
private static final String DUMP_DIR_PROPERTY = "asm.patchify.dumpDir";
|
private static final String DUMP_DIR_PROPERTY = "oz.dumpDir";
|
||||||
|
|
||||||
private final Map<String, List<Class<?>>> patchesByTarget = new HashMap<>();
|
private final Map<String, List<Class<?>>> patchesByTarget = new HashMap<>();
|
||||||
|
|
||||||
@@ -42,7 +42,12 @@ public final class PatchClassFileTransformer implements ClassFileTransformer {
|
|||||||
for (Class<?> patchClass : PatchRegistry.getPatches()) {
|
for (Class<?> patchClass : PatchRegistry.getPatches()) {
|
||||||
Patch patch = patchClass.getAnnotation(Patch.class);
|
Patch patch = patchClass.getAnnotation(Patch.class);
|
||||||
if (patch == null) continue;
|
if (patch == null) continue;
|
||||||
String internalName = patch.value().getName().replace('.', '/');
|
String internalName;
|
||||||
|
if (!patch.className().isEmpty()) {
|
||||||
|
internalName = patch.className().replace('.', '/');
|
||||||
|
} else {
|
||||||
|
internalName = patch.value().getName().replace('.', '/');
|
||||||
|
}
|
||||||
patchesByTarget.computeIfAbsent(internalName, k -> new ArrayList<>()).add(patchClass);
|
patchesByTarget.computeIfAbsent(internalName, k -> new ArrayList<>()).add(patchClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
* lightweight no-op fallback when no transformer is installed.</p>
|
* lightweight no-op fallback when no transformer is installed.</p>
|
||||||
*/
|
*/
|
||||||
public final class PatchRegistry {
|
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 static final List<Class<?>> PATCHES = new ArrayList<>();
|
||||||
|
|
||||||
private PatchRegistry() {
|
private PatchRegistry() {
|
||||||
|
|||||||
@@ -72,20 +72,41 @@ public final class PatchTransformer {
|
|||||||
if (patchAnnotation == null) {
|
if (patchAnnotation == null) {
|
||||||
throw new IllegalArgumentException(patchClass.getName() + " is not @Patch");
|
throw new IllegalArgumentException(patchClass.getName() + " is not @Patch");
|
||||||
}
|
}
|
||||||
String patchTargetOwner = Type.getInternalName(patchAnnotation.value());
|
String patchTargetOwner;
|
||||||
|
if (!patchAnnotation.className().isEmpty()) {
|
||||||
|
patchTargetOwner = patchAnnotation.className().replace('.', '/');
|
||||||
|
} else {
|
||||||
|
patchTargetOwner = Type.getInternalName(patchAnnotation.value());
|
||||||
|
}
|
||||||
|
|
||||||
Map<MethodKey, List<Method>> handlersByTarget = new HashMap<>();
|
Map<MethodKey, List<Method>> handlersByTarget = new HashMap<>();
|
||||||
|
// Collect handlers whose desc is empty (name-only wildcards) separately
|
||||||
|
List<Method> nameOnlyHandlers = new ArrayList<>();
|
||||||
for (Method handler : patchClass.getDeclaredMethods()) {
|
for (Method handler : patchClass.getDeclaredMethods()) {
|
||||||
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget);
|
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget, nameOnlyHandlers);
|
||||||
}
|
}
|
||||||
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
|
||||||
patchClass.getName(), target.name,
|
patchClass.getName(), target.name,
|
||||||
handlersByTarget.values().stream().mapToInt(List::size).sum());
|
handlersByTarget.values().stream().mapToInt(List::size).sum() + nameOnlyHandlers.size());
|
||||||
|
|
||||||
Set<MethodKey> matched = new HashSet<>();
|
Set<MethodKey> matched = new HashSet<>();
|
||||||
|
Set<String> nameOnlyMatched = new HashSet<>();
|
||||||
for (MethodNode method : target.methods) {
|
for (MethodNode method : target.methods) {
|
||||||
MethodKey key = new MethodKey(method.name, method.desc);
|
MethodKey key = new MethodKey(method.name, method.desc);
|
||||||
List<Method> handlers = handlersByTarget.get(key);
|
List<Method> handlers = handlersByTarget.get(key);
|
||||||
|
if (handlers == null && !nameOnlyHandlers.isEmpty()) {
|
||||||
|
// Try name-only match for handlers registered with empty desc.
|
||||||
|
// Works for every handler kind (Inject/Overwrite/Transform/WrapInvoke/
|
||||||
|
// ModifyLocals), not just Inject/Overwrite.
|
||||||
|
for (Method candidate : nameOnlyHandlers) {
|
||||||
|
String targetName = getHandlerTargetMethodName(candidate);
|
||||||
|
if (targetName.equals(method.name)) {
|
||||||
|
if (handlers == null) handlers = new ArrayList<>();
|
||||||
|
handlers.add(candidate);
|
||||||
|
nameOnlyMatched.add(method.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (handlers == null) continue;
|
if (handlers == null) continue;
|
||||||
matched.add(key);
|
matched.add(key);
|
||||||
for (Method handler : handlers) {
|
for (Method handler : handlers) {
|
||||||
@@ -121,11 +142,34 @@ public final class PatchTransformer {
|
|||||||
target.name, entry.getKey().name(), entry.getKey().desc(), target.name);
|
target.name, entry.getKey().name(), entry.getKey().desc(), target.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Warn about unmatched name-only handlers
|
||||||
|
for (Method handler : nameOnlyHandlers) {
|
||||||
|
if (!nameOnlyMatched.contains(getHandlerTargetMethodName(handler))) {
|
||||||
|
LOGGER.warn("Patch handler {}#{} is name-only (empty desc) but no method named \"{}\" exists on {} — handler will not run.",
|
||||||
|
handler.getDeclaringClass().getName(), handler.getName(),
|
||||||
|
getHandlerTargetMethodName(handler), target.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getHandlerTargetMethodName(Method handler) {
|
||||||
|
Inject inject = handler.getAnnotation(Inject.class);
|
||||||
|
if (inject != null) return inject.method();
|
||||||
|
Overwrite overwrite = handler.getAnnotation(Overwrite.class);
|
||||||
|
if (overwrite != null) return overwrite.method();
|
||||||
|
Transform transform = handler.getAnnotation(Transform.class);
|
||||||
|
if (transform != null) return transform.method();
|
||||||
|
WrapInvoke wrap = handler.getAnnotation(WrapInvoke.class);
|
||||||
|
if (wrap != null) return wrap.method();
|
||||||
|
ModifyLocals modify = handler.getAnnotation(ModifyLocals.class);
|
||||||
|
if (modify != null) return modify.method();
|
||||||
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectHandler(Class<?> patchClass, String patchTargetOwner,
|
private static void collectHandler(Class<?> patchClass, String patchTargetOwner,
|
||||||
Method handler,
|
Method handler,
|
||||||
Map<MethodKey, List<Method>> handlersByTarget) {
|
Map<MethodKey, List<Method>> handlersByTarget,
|
||||||
|
List<Method> nameOnlyHandlers) {
|
||||||
if (!(handler.isAnnotationPresent(Inject.class)
|
if (!(handler.isAnnotationPresent(Inject.class)
|
||||||
|| handler.isAnnotationPresent(Overwrite.class)
|
|| handler.isAnnotationPresent(Overwrite.class)
|
||||||
|| handler.isAnnotationPresent(Transform.class)
|
|| handler.isAnnotationPresent(Transform.class)
|
||||||
@@ -176,8 +220,14 @@ public final class PatchTransformer {
|
|||||||
// production Forge environment the live class only has SRG names, so
|
// production Forge environment the live class only has SRG names, so
|
||||||
// remap before matching against ClassNode.methods.
|
// remap before matching against ClassNode.methods.
|
||||||
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
|
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
|
||||||
|
if (desc.isEmpty()) {
|
||||||
|
// Empty desc = match by method name only (wildcard for mod classes
|
||||||
|
// whose exact descriptor may vary between Yarn and Mojmap mappings).
|
||||||
|
nameOnlyHandlers.add(handler);
|
||||||
|
} else {
|
||||||
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
|
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateInjectSignature(Class<?> patchClass, Method handler, Inject inject) {
|
private static void validateInjectSignature(Class<?> patchClass, Method handler, Inject inject) {
|
||||||
if (handler.getReturnType() != void.class) {
|
if (handler.getReturnType() != void.class) {
|
||||||
@@ -216,7 +266,58 @@ public final class PatchTransformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a HEAD-inject handler's parameter list is bytecode-compatible with the
|
||||||
|
* {@code (receiver?, args..., CallbackInfo)} sequence that {@link #injectHead} forwards.
|
||||||
|
*
|
||||||
|
* <p>This matters most for name-only ({@code desc=""}) patches: those match a target by
|
||||||
|
* method name alone, so a handler can end up bound to a method whose arity or primitive
|
||||||
|
* parameter types differ from what it declares. Forwarding into such a handler would emit a
|
||||||
|
* call site that fails JVM verification ({@code VerifyError}) at class load. Returns
|
||||||
|
* {@code null} when compatible, otherwise a human-readable reason.</p>
|
||||||
|
*/
|
||||||
|
private static String headHandlerMismatch(MethodNode method, Method handler) {
|
||||||
|
List<Type> forwarded = new ArrayList<>();
|
||||||
|
if (!Modifier.isStatic(method.access)) {
|
||||||
|
// Receiver is always a reference; only its primitive-ness is checked below.
|
||||||
|
forwarded.add(Type.getObjectType("java/lang/Object"));
|
||||||
|
}
|
||||||
|
forwarded.addAll(Arrays.asList(Type.getArgumentTypes(method.desc)));
|
||||||
|
|
||||||
|
// The last handler parameter is CallbackInfo (enforced by validateInjectSignature);
|
||||||
|
// the remaining params receive the forwarded receiver + args.
|
||||||
|
int expected = handler.getParameterCount() - 1;
|
||||||
|
if (forwarded.size() != expected) {
|
||||||
|
return "handler takes " + expected + " forwarded parameter(s) but target supplies "
|
||||||
|
+ forwarded.size() + " (receiver + args)";
|
||||||
|
}
|
||||||
|
Class<?>[] params = handler.getParameterTypes();
|
||||||
|
for (int i = 0; i < forwarded.size(); i++) {
|
||||||
|
Type arg = forwarded.get(i);
|
||||||
|
Class<?> p = params[i];
|
||||||
|
boolean argIsPrimitive = arg.getSort() != Type.OBJECT && arg.getSort() != Type.ARRAY;
|
||||||
|
if (argIsPrimitive) {
|
||||||
|
// Primitives cannot widen to Object — the handler param must be the exact type.
|
||||||
|
if (!p.isPrimitive() || !p.getName().equals(arg.getClassName())) {
|
||||||
|
return "parameter " + i + " is primitive " + arg.getClassName()
|
||||||
|
+ " but handler declares " + p.getName();
|
||||||
|
}
|
||||||
|
} else if (p.isPrimitive()) {
|
||||||
|
// A reference arg cannot be passed into a primitive parameter.
|
||||||
|
return "parameter " + i + " is a reference but handler declares primitive " + p.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static void injectHead(MethodNode method, Method handler) {
|
private static void injectHead(MethodNode method, Method handler) {
|
||||||
|
String mismatch = headHandlerMismatch(method, handler);
|
||||||
|
if (mismatch != null) {
|
||||||
|
LOGGER.warn("@Inject(HEAD) {}#{} is incompatible with matched target {}{} — {} — skipping injection to avoid invalid bytecode.",
|
||||||
|
handler.getDeclaringClass().getName(), handler.getName(),
|
||||||
|
method.name, method.desc, mismatch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Type returnType = Type.getReturnType(method.desc);
|
Type returnType = Type.getReturnType(method.desc);
|
||||||
String handlerOwner = Type.getInternalName(handler.getDeclaringClass());
|
String handlerOwner = Type.getInternalName(handler.getDeclaringClass());
|
||||||
String handlerName = handler.getName();
|
String handlerName = handler.getName();
|
||||||
@@ -700,7 +801,9 @@ public final class PatchTransformer {
|
|||||||
|
|
||||||
private static String targetClassName(Method handler) {
|
private static String targetClassName(Method handler) {
|
||||||
Patch patch = handler.getDeclaringClass().getAnnotation(Patch.class);
|
Patch patch = handler.getDeclaringClass().getAnnotation(Patch.class);
|
||||||
return patch == null ? "?" : Type.getInternalName(patch.value());
|
if (patch == null) return "?";
|
||||||
|
if (!patch.className().isEmpty()) return patch.className().replace('.', '/');
|
||||||
|
return Type.getInternalName(patch.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AbstractInsnNode> collectInjectionPoints(InsnList insns, Slice slice,
|
private static List<AbstractInsnNode> collectInjectionPoints(InsnList insns, Slice slice,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import shit.zen.event.EventBus;
|
import shit.zen.event.EventBus;
|
||||||
import shit.zen.event.EventTarget;
|
import shit.zen.event.EventTarget;
|
||||||
import shit.zen.event.impl.TickEvent;
|
import shit.zen.event.impl.TickEvent;
|
||||||
@@ -22,6 +24,8 @@ import shit.zen.manager.HudManager;
|
|||||||
import shit.zen.manager.LagManager;
|
import shit.zen.manager.LagManager;
|
||||||
import shit.zen.manager.ModuleManager;
|
import shit.zen.manager.ModuleManager;
|
||||||
import shit.zen.manager.TargetManager;
|
import shit.zen.manager.TargetManager;
|
||||||
|
import shit.zen.patch.BlockOcclusionCachePatch;
|
||||||
|
import shit.zen.patch.BlockPatch;
|
||||||
import shit.zen.patch.ChatScreenPatch;
|
import shit.zen.patch.ChatScreenPatch;
|
||||||
import shit.zen.patch.ClientLevelPatch;
|
import shit.zen.patch.ClientLevelPatch;
|
||||||
import shit.zen.patch.ConnectionPatch;
|
import shit.zen.patch.ConnectionPatch;
|
||||||
@@ -108,7 +112,7 @@ public class ZenClient extends ClientBase {
|
|||||||
if (PatchAgent.getInstrumentation() != null) {
|
if (PatchAgent.getInstrumentation() != null) {
|
||||||
PatchAgent.installPatchesAndRetransform();
|
PatchAgent.installPatchesAndRetransform();
|
||||||
} else {
|
} 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;
|
isReady = true;
|
||||||
logger.info("{} v{} initialized.", CLIENT_NAME, VERSION);
|
logger.info("{} v{} initialized.", CLIENT_NAME, VERSION);
|
||||||
@@ -199,6 +203,7 @@ public class ZenClient extends ClientBase {
|
|||||||
PatchRegistry.register(ChatScreenPatch.class);
|
PatchRegistry.register(ChatScreenPatch.class);
|
||||||
PatchRegistry.register(EntityRendererPatch.class);
|
PatchRegistry.register(EntityRendererPatch.class);
|
||||||
PatchRegistry.register(LevelRendererPatch.class);
|
PatchRegistry.register(LevelRendererPatch.class);
|
||||||
|
PatchRegistry.register(BlockPatch.class);
|
||||||
PatchRegistry.register(GameRendererPatch.class);
|
PatchRegistry.register(GameRendererPatch.class);
|
||||||
PatchRegistry.register(ItemInHandRendererPatch.class);
|
PatchRegistry.register(ItemInHandRendererPatch.class);
|
||||||
PatchRegistry.register(ItemInHandLayerPatch.class);
|
PatchRegistry.register(ItemInHandLayerPatch.class);
|
||||||
@@ -207,6 +212,13 @@ public class ZenClient extends ClientBase {
|
|||||||
PatchRegistry.register(ItemPatch.class);
|
PatchRegistry.register(ItemPatch.class);
|
||||||
PatchRegistry.register(PlayerTabOverlayPatch.class);
|
PatchRegistry.register(PlayerTabOverlayPatch.class);
|
||||||
PatchRegistry.register(FriendlyByteBufPatch.class);
|
PatchRegistry.register(FriendlyByteBufPatch.class);
|
||||||
|
|
||||||
|
// Compatibility patch for Embeddium/Sodium's BlockOcclusionCache.
|
||||||
|
// Always registered so the transformer can catch the class when it
|
||||||
|
// first loads. We must NOT use Class.forName() here — that would
|
||||||
|
// load the class before our transformer is installed, preventing
|
||||||
|
// the patch from ever being applied.
|
||||||
|
PatchRegistry.register(BlockOcclusionCachePatch.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Minecraft getMcInstance() {
|
public static Minecraft getMcInstance() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import shit.zen.asm.Bootstrap;
|
|||||||
* onwards.</p>
|
* onwards.</p>
|
||||||
*/
|
*/
|
||||||
public final class DllBootstrap {
|
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 static volatile boolean started = false;
|
||||||
|
|
||||||
private DllBootstrap() {
|
private DllBootstrap() {
|
||||||
@@ -30,16 +30,16 @@ public final class DllBootstrap {
|
|||||||
|
|
||||||
public static synchronized void start(String extractedJarPath) {
|
public static synchronized void start(String extractedJarPath) {
|
||||||
if (started) {
|
if (started) {
|
||||||
LOGGER.info("DllBootstrap.start ignored (already started)");
|
LOGGER.info("bootstrap.start ignored (already started)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
started = true;
|
started = true;
|
||||||
try {
|
try {
|
||||||
LOGGER.info("DllBootstrap.start jar={}", extractedJarPath);
|
LOGGER.info("bootstrap.start jar={}", extractedJarPath);
|
||||||
LOGGER.info("DllBootstrap loader = {}", DllBootstrap.class.getClassLoader());
|
LOGGER.info("bootstrap loader = {}", DllBootstrap.class.getClassLoader());
|
||||||
LOGGER.info("ZenClient loader = {}", ZenClient.class.getClassLoader());
|
LOGGER.info("client loader = {}", ZenClient.class.getClassLoader());
|
||||||
LOGGER.info("Minecraft loader = {}", Minecraft.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.
|
// Load mojmap → SRG mappings before we attempt any retransform.
|
||||||
Bootstrap.init();
|
Bootstrap.init();
|
||||||
@@ -53,9 +53,9 @@ public final class DllBootstrap {
|
|||||||
ZenClient.registerPatches();
|
ZenClient.registerPatches();
|
||||||
PatchAgent.installPatchesAndRetransform();
|
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) {
|
} 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>
|
* </ol>
|
||||||
*/
|
*/
|
||||||
public final class GameLoaderBridge {
|
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";
|
public static final String RESOURCES_PROP = "openzen.resources";
|
||||||
|
|
||||||
private GameLoaderBridge() {
|
private GameLoaderBridge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void load(String jarPath, ClassLoader gameLoader) throws Throwable {
|
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();
|
long t0 = System.nanoTime();
|
||||||
|
|
||||||
Instrumentation inst = PatchAgent.getInstrumentation();
|
Instrumentation inst = PatchAgent.getInstrumentation();
|
||||||
if (inst == null) {
|
if (inst == null) {
|
||||||
throw new IllegalStateException("PatchAgent.getInstrumentation() returned null - "
|
throw new IllegalStateException("agent instrumentation returned null - "
|
||||||
+ "did instrument.dll Agent_OnAttach run?");
|
+ "did instrument.dll Agent_OnAttach run?");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ public final class GameLoaderBridge {
|
|||||||
classCount, resourceCount, resourceDir, ms);
|
classCount, resourceCount, resourceDir, ms);
|
||||||
|
|
||||||
Class<?> bootstrapCls = Class.forName("shit.zen.dll.DllBootstrap", true, gameLoader);
|
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);
|
Method start = bootstrapCls.getMethod("start", String.class);
|
||||||
start.invoke(null, jarPath);
|
start.invoke(null, jarPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,7 +372,8 @@ extends Screen {
|
|||||||
FontRenderer iconFont = FontPresets.materialIcons(20.0f * this.currentScale);
|
FontRenderer iconFont = FontPresets.materialIcons(20.0f * this.currentScale);
|
||||||
String iconText = "\uE8B6";
|
String iconText = "\uE8B6";
|
||||||
float iconX = searchX + 12.0f * this.currentScale;
|
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();
|
int iconColor = new Color(255, 255, 255, (int)(180.0f * alpha)).getRGB();
|
||||||
TextGlow.drawGlowText(iconText, iconX, iconY, iconFont, 0xffdddddd, iconColor, 12.0f * this.currentScale);
|
TextGlow.drawGlowText(iconText, iconX, iconY, iconFont, 0xffdddddd, iconColor, 12.0f * this.currentScale);
|
||||||
FontRenderer queryFont = FontPresets.axiformaRegular(16.0f * this.currentScale);
|
FontRenderer queryFont = FontPresets.axiformaRegular(16.0f * this.currentScale);
|
||||||
@@ -381,12 +382,14 @@ extends Screen {
|
|||||||
String placeholder = "search";
|
String placeholder = "search";
|
||||||
float placeholderWidth = GlHelper.getStringWidth(placeholder, placeholderFont);
|
float placeholderWidth = GlHelper.getStringWidth(placeholder, placeholderFont);
|
||||||
float placeholderX = searchX + (searchWidth - placeholderWidth) / 2.0f;
|
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,
|
TextGlow.drawGlowText(placeholder, placeholderX, placeholderY, placeholderFont, 0xffcccccc,
|
||||||
new Color(255, 255, 255, (int)(130.0f * alpha)).getRGB(), 10.0f * this.currentScale);
|
new Color(255, 255, 255, (int)(130.0f * alpha)).getRGB(), 10.0f * this.currentScale);
|
||||||
} else {
|
} else {
|
||||||
float queryX = searchX + 35.0f * this.currentScale;
|
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();
|
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);
|
TextGlow.drawGlowText(this.searchQuery, queryX, queryY, queryFont, -1, queryColor, 8.0f * this.currentScale);
|
||||||
if (this.searchFocused) {
|
if (this.searchFocused) {
|
||||||
@@ -449,6 +452,7 @@ extends Screen {
|
|||||||
int textColor = textAlpha << 24 | 0xFFFFFF;
|
int textColor = textAlpha << 24 | 0xFFFFFF;
|
||||||
int glowAlpha = (int)(120.0f * toast.alpha * alpha);
|
int glowAlpha = (int)(120.0f * toast.alpha * alpha);
|
||||||
int glowColor = glowAlpha << 24 | 0xFFFFFF;
|
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);
|
TextGlow.drawGlowText(toast.message, toastX, toastY + 6.0f * this.currentScale, toastFont, textColor, glowColor, 8.0f * this.currentScale);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ extends ClientBase {
|
|||||||
float iconFontSize = 24.0f * scale;
|
float iconFontSize = 24.0f * scale;
|
||||||
int horizontalOffset = (int)(420.0f * scale);
|
int horizontalOffset = (int)(420.0f * scale);
|
||||||
int baseX = originX + horizontalOffset;
|
int baseX = originX + horizontalOffset;
|
||||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||||
Category[] categories = Category.values();
|
Category[] categories = Category.values();
|
||||||
Renderer.renderConsumer(drawContext -> {
|
Renderer.renderConsumer(drawContext -> {
|
||||||
FontRenderer iconFont = FontPresets.materialIcons(iconFontSize);
|
FontRenderer iconFont = FontPresets.materialIcons(iconFontSize);
|
||||||
@@ -102,7 +102,7 @@ extends ClientBase {
|
|||||||
int verticalOffset = (int)(80.0f * scale);
|
int verticalOffset = (int)(80.0f * scale);
|
||||||
int horizontalOffset = (int)(420.0f * scale);
|
int horizontalOffset = (int)(420.0f * scale);
|
||||||
int baseX = originX + horizontalOffset;
|
int baseX = originX + horizontalOffset;
|
||||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||||
Category[] categories = Category.values();
|
Category[] categories = Category.values();
|
||||||
for (int i = 0; i < categories.length; ++i) {
|
for (int i = 0; i < categories.length; ++i) {
|
||||||
Category category = categories[i];
|
Category category = categories[i];
|
||||||
@@ -129,7 +129,7 @@ extends ClientBase {
|
|||||||
int verticalOffset = (int)(80.0f * scale);
|
int verticalOffset = (int)(80.0f * scale);
|
||||||
int horizontalOffset = (int)(420.0f * scale);
|
int horizontalOffset = (int)(420.0f * scale);
|
||||||
int baseX = originX + horizontalOffset;
|
int baseX = originX + horizontalOffset;
|
||||||
int baseY = originY + verticalOffset - (int)(65.0f * scale);
|
int baseY = originY + verticalOffset - (int)(60.0f * scale);
|
||||||
Category[] categories = Category.values();
|
Category[] categories = Category.values();
|
||||||
for (int i = 0; i < categories.length; ++i) {
|
for (int i = 0; i < categories.length; ++i) {
|
||||||
int iconX = baseX + i * iconSpacing;
|
int iconX = baseX + i * iconSpacing;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import net.minecraft.client.player.AbstractClientPlayer;
|
|||||||
import shit.zen.ClientBase;
|
import shit.zen.ClientBase;
|
||||||
import shit.zen.ZenClient;
|
import shit.zen.ZenClient;
|
||||||
import shit.zen.gui.panel.SettingsPopup;
|
import shit.zen.gui.panel.SettingsPopup;
|
||||||
import shit.zen.render.DrawContext;
|
|
||||||
import shit.zen.render.FontPresets;
|
import shit.zen.render.FontPresets;
|
||||||
import shit.zen.render.FontRenderer;
|
import shit.zen.render.FontRenderer;
|
||||||
import shit.zen.render.GlHelper;
|
import shit.zen.render.GlHelper;
|
||||||
@@ -59,19 +58,27 @@ extends ClientBase {
|
|||||||
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
||||||
int glowColor = new Color(255, 255, 255, (int)(100.0f * alpha)).getRGB();
|
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);
|
TextGlow.drawGlowText(userId, textX, textY, nameFont, this.applyAlpha(-1, alpha), glowColor, 8.0f * scale);
|
||||||
|
|
||||||
float nameWidth = GlHelper.getStringWidth(userId, nameFont);
|
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);
|
FontRenderer roleFont = FontPresets.axiformaBold(11.0f * scale);
|
||||||
float roleStrWidth = GlHelper.getStringWidth(userRole, roleFont);
|
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();
|
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();
|
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);
|
this.settingsPopup.render(guiGraphics, mouseX, mouseY, scale, alpha);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ extends ClientBase {
|
|||||||
rowY = (int)((float)rowY + 8.0f * scale);
|
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);
|
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);
|
FontRenderer footerFont = FontPresets.axiformaRegular(12.0f * scale);
|
||||||
String footer = "7unknown © 2024-2025";
|
String footer = "7unknown \u00a9 2024-2025";
|
||||||
float footerWidth = GlHelper.getStringWidth(footer, footerFont);
|
float footerWidth = GlHelper.getStringWidth(footer, footerFont);
|
||||||
GlHelper.drawText(footer, (float)popupX + ((float)popupWidth - footerWidth) / 2.0f, (float)(popupY + popupHeight) - 15.0f * scale, footerFont, labelColor);
|
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);
|
FontRenderer valueFont = FontPresets.axiformaRegular(13.0f * scale);
|
||||||
int labelColor = this.applyAlpha(new Color(0xAAAAAA).getRGB(), openAlpha);
|
int labelColor = this.applyAlpha(new Color(0xAAAAAA).getRGB(), openAlpha);
|
||||||
int valueColor = this.applyAlpha(new Color(0xFFFFFF).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 dropdownWidth = (int)(90.0f * scale);
|
||||||
int dropdownX = (int)((float)(popupX + popupWidth - dropdownWidth) - 15.0f * scale);
|
int dropdownX = (int)((float)(popupX + popupWidth - dropdownWidth) - 15.0f * scale);
|
||||||
int dropdownHeaderHeight = (int)(20.0f * scale);
|
int dropdownHeaderHeight = (int)(20.0f * scale);
|
||||||
int itemHeight = (int)(18.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();
|
float openFactor = this.dropdownAlpha.getOrDefault(key, 0.0f).floatValue();
|
||||||
String[] filteredItems = this.filterDropdownItems(items, selectedValue);
|
String[] filteredItems = this.filterDropdownItems(items, selectedValue);
|
||||||
int expandedHeight = (int)((float)(filteredItems.length * itemHeight) * openFactor);
|
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));
|
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 valueX = (float)dropdownX + 8.0f * scale;
|
||||||
float valueY = (float)rowY + (float)dropdownHeaderHeight / 2.0f - valueFont.getMetrics().capHeight() / 2.0f + 3.0f * scale;
|
float valueY = rowY + (dropdownHeaderHeight - valueFont.getMetrics().capHeight()) / 2.0f;
|
||||||
GlHelper.drawText(selectedValue, valueX, valueY - 2.0f, valueFont, valueColor);
|
GlHelper.drawText(selectedValue, valueX, valueY, valueFont, valueColor);
|
||||||
|
|
||||||
|
// --- 核心修复:展开的小箭头基于 CapHeight 垂直居中(抛弃 +7.0f 的不缩放硬编码偏移) ---
|
||||||
FontRenderer arrowFont = FontPresets.materialIcons(18.0f * scale);
|
FontRenderer arrowFont = FontPresets.materialIcons(18.0f * scale);
|
||||||
String arrowIcon = "";
|
String arrowIcon = "";
|
||||||
float arrowX = (float)(dropdownX + dropdownWidth) - 18.0f * scale;
|
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);
|
GlHelper.drawText(arrowIcon, arrowX, arrowY, arrowFont, valueColor);
|
||||||
|
|
||||||
if (openFactor > 0.01f) {
|
if (openFactor > 0.01f) {
|
||||||
drawContext.save();
|
drawContext.save();
|
||||||
drawContext.clip(Rectangle.ofXYWH(dropdownX, rowY + dropdownHeaderHeight, dropdownWidth, expandedHeight));
|
drawContext.clip(Rectangle.ofXYWH(dropdownX, rowY + dropdownHeaderHeight, dropdownWidth, expandedHeight));
|
||||||
@@ -159,7 +169,8 @@ extends ClientBase {
|
|||||||
this.updateItemHover(itemHovers, item, hovered);
|
this.updateItemHover(itemHovers, item, hovered);
|
||||||
float hoverAmount = itemHovers.getOrDefault(item, 0.0f);
|
float hoverAmount = itemHovers.getOrDefault(item, 0.0f);
|
||||||
float itemTextX = (float)dropdownX + 8.0f * scale;
|
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);
|
int itemColor = this.applyAlpha(valueColor, openFactor);
|
||||||
float glowAmount = hoverAmount * openFactor;
|
float glowAmount = hoverAmount * openFactor;
|
||||||
if (glowAmount > 0.01f) {
|
if (glowAmount > 0.01f) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import shit.zen.gui.PanelClickGui;
|
|||||||
import shit.zen.render.FontPresets;
|
import shit.zen.render.FontPresets;
|
||||||
import shit.zen.render.FontRenderer;
|
import shit.zen.render.FontRenderer;
|
||||||
import shit.zen.render.GlHelper;
|
import shit.zen.render.GlHelper;
|
||||||
import shit.zen.render.GlyphMetrics;
|
|
||||||
import shit.zen.render.TextGlow;
|
import shit.zen.render.TextGlow;
|
||||||
import shit.zen.settings.Setting;
|
import shit.zen.settings.Setting;
|
||||||
import shit.zen.settings.impl.NumberSetting;
|
import shit.zen.settings.impl.NumberSetting;
|
||||||
@@ -47,7 +46,7 @@ implements SettingRenderer {
|
|||||||
int iconSize = Math.round(16.0f * scale);
|
int iconSize = Math.round(16.0f * scale);
|
||||||
int iconGap = Math.round(4.0f * scale);
|
int iconGap = Math.round(4.0f * scale);
|
||||||
int iconX = widgetX - iconSize - iconGap;
|
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.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);
|
this.drawNumberWidget(guiGraphics, numberSetting, x, y, width, widgetX, widgetY, widgetWidth, widgetHeight, iconX, iconY, editing, alpha, scale);
|
||||||
return rowHeight;
|
return rowHeight;
|
||||||
@@ -177,30 +176,35 @@ implements SettingRenderer {
|
|||||||
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
FontRenderer nameFont = FontPresets.axiformaRegular(14.0f * scale);
|
||||||
FontRenderer valueFont = FontPresets.axiformaBold(14.0f * scale);
|
FontRenderer valueFont = FontPresets.axiformaBold(14.0f * scale);
|
||||||
FontRenderer signFont = FontPresets.axiformaBold(12.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);
|
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);
|
this.drawEditIcon(guiGraphics, iconX, iconY, numberSetting, alpha, scale);
|
||||||
|
|
||||||
int sidePadding = Math.round(12.0f * scale);
|
int sidePadding = Math.round(12.0f * scale);
|
||||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), widgetX, widgetY, widgetWidth, widgetHeight, 5.0f * scale, this.applyAlpha(0x50F5F5F5, alpha));
|
RenderUtil.drawRoundedRect(guiGraphics.pose(), widgetX, widgetY, widgetWidth, widgetHeight, 5.0f * scale, this.applyAlpha(0x50F5F5F5, alpha));
|
||||||
int middleX = widgetX + sidePadding;
|
int middleX = widgetX + sidePadding;
|
||||||
int middleWidth = widgetWidth - sidePadding * 2;
|
int middleWidth = widgetWidth - sidePadding * 2;
|
||||||
RenderUtil.drawRoundedRect(guiGraphics.pose(), middleX, widgetY, middleWidth, widgetHeight, 0.0f, this.applyAlpha(1086900424, alpha));
|
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 widgetCenterY = widgetY + widgetHeight / 2.0f;
|
||||||
float signY = widgetCenterY - (signMetrics.ascent() + signMetrics.descent()) / 2.0f + signMetrics.ascent() + 5.0f * scale - 2.5f;
|
float signY = widgetCenterY - signFont.getMetrics().capHeight() / 2.0f;
|
||||||
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 minusColor = this.minusButtonHover.getOrDefault(numberSetting, false) ? new Color(255, 255, 255).getRGB() : -1;
|
||||||
int plusColor = this.plusButtonHover.getOrDefault(numberSetting, false) != 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));
|
||||||
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());
|
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()) {
|
if (editing && displayText.isEmpty()) {
|
||||||
displayText = "0";
|
displayText = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
float displayWidth = GlHelper.getStringWidth(displayText, valueFont);
|
float displayWidth = GlHelper.getStringWidth(displayText, valueFont);
|
||||||
float displayX = (float)widgetX + (float)widgetWidth / 2.0f - displayWidth / 2.0f;
|
float displayX = widgetX + widgetWidth / 2.0f - displayWidth / 2.0f;
|
||||||
GlyphMetrics valueMetrics = valueFont.getMetrics();
|
float displayY = widgetCenterY - valueFont.getMetrics().capHeight() / 2.0f;
|
||||||
float displayY = signY + scale - 1.5f;
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
float cyclePos = (float)(now % 1000L) / 1000.0f;
|
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 textAlpha = (int)(255.0f * (0.6f + sineWave * 0.4f) * alpha);
|
||||||
int textColor = textAlpha << 24 | 0xFFFFFF;
|
int textColor = textAlpha << 24 | 0xFFFFFF;
|
||||||
GlHelper.drawText(displayText, displayX, displayY, valueFont, textColor);
|
GlHelper.drawText(displayText, displayX, displayY, valueFont, textColor);
|
||||||
|
|
||||||
float caretX = displayX + displayWidth + 2.0f * scale;
|
float caretX = displayX + displayWidth + 2.0f * scale;
|
||||||
int caretAlpha = (int)(255.0f * sineWave * alpha);
|
int caretAlpha = (int)(255.0f * sineWave * alpha);
|
||||||
int caretColor = caretAlpha << 24 | 0xFFFFFF;
|
int caretColor = caretAlpha << 24 | 0xFFFFFF;
|
||||||
float caretHeight = valueMetrics.capHeight();
|
float caretHeight = valueFont.getMetrics().capHeight();
|
||||||
float caretY = displayY - valueMetrics.ascent() + (valueMetrics.ascent() - caretHeight) / 2.0f;
|
RenderUtil.drawFilledRect(guiGraphics.pose(), caretX, displayY, Math.max(1.0f, Math.round(scale)), caretHeight, caretColor);
|
||||||
RenderUtil.drawFilledRect(guiGraphics.pose(), caretX, caretY, Math.round(scale), Math.round(caretHeight), caretColor);
|
|
||||||
} else {
|
} else {
|
||||||
int glowColor = new Color(255, 255, 255, 120).getRGB();
|
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);
|
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);
|
FontRenderer iconFont = FontPresets.materialIcons(iconSize);
|
||||||
String iconText = "\uE3C9";
|
String iconText = "\uE3C9";
|
||||||
float iconTextWidth = GlHelper.getStringWidth(iconText, iconFont);
|
float iconTextWidth = GlHelper.getStringWidth(iconText, iconFont);
|
||||||
GlyphMetrics iconMetrics = iconFont.getMetrics();
|
|
||||||
float drawX = (float)iconX + ((float)iconSize - iconTextWidth) / 2.0f;
|
float drawX = iconX + (iconSize - iconTextWidth) / 2.0f;
|
||||||
float drawY = (float)iconY + (float)iconSize / 2.0f - (iconMetrics.ascent() + iconMetrics.descent()) / 2.0f + iconMetrics.ascent() + 1.0f;
|
float drawY = iconY + iconSize / 2.0f - iconFont.getMetrics().capHeight() / 2.0f;
|
||||||
|
|
||||||
GlHelper.drawText(iconText, drawX, drawY, iconFont, this.applyAlpha(iconColor, alpha));
|
GlHelper.drawText(iconText, drawX, drawY, iconFont, this.applyAlpha(iconColor, alpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,8 @@ extends Module {
|
|||||||
private String cachedRandomName = null;
|
private String cachedRandomName = null;
|
||||||
private final Random random = new Random();
|
private final Random random = new Random();
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDisable() {
|
|
||||||
this.setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NameProtect() {
|
public NameProtect() {
|
||||||
super("NameProtect", Category.RENDER);
|
super("NameProtect", Category.RENDER);
|
||||||
this.setEnabled(true);
|
|
||||||
INSTANCE = this;
|
INSTANCE = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package shit.zen.patch;
|
||||||
|
|
||||||
|
import asm.patchify.annotation.At;
|
||||||
|
import asm.patchify.annotation.Inject;
|
||||||
|
import asm.patchify.annotation.Patch;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import shit.zen.modules.impl.render.XRay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility patch for Embeddium/Sodium's {@code BlockOcclusionCache}.
|
||||||
|
*
|
||||||
|
* <p>Sodium/Embeddium replaces vanilla's chunk mesh building with its own pipeline. The
|
||||||
|
* vanilla {@link Block#shouldRenderFace()} method — which our {@link BlockPatch} hooks
|
||||||
|
* — is never called by Embeddium's {@code BlockRenderer}. Instead, face visibility is
|
||||||
|
* decided by {@code BlockOcclusionCache.shouldDrawSide()}, which has its own entirely
|
||||||
|
* independent occlusion logic.</p>
|
||||||
|
*
|
||||||
|
* <p>This patch injects into {@code BlockOcclusionCache.shouldDrawSide()} so that when
|
||||||
|
* XRay is enabled the result is forced to match {@link XRay#isXrayVisible(Block)},
|
||||||
|
* exactly like our vanilla patch.</p>
|
||||||
|
*
|
||||||
|
* <p>The target class is referenced by {@link Patch#className()} rather than by
|
||||||
|
* {@link Patch#value()} because Embeddium is an optional mod — the class is not
|
||||||
|
* available at compile time. The patch is registered <em>unconditionally</em> in
|
||||||
|
* {@link shit.zen.ZenClient#registerPatches()} (we must <em>not</em> probe with
|
||||||
|
* {@link Class#forName(String)}, which would force the target class to load before our
|
||||||
|
* transformer is installed and thus defeat the patch). When Embeddium is absent the
|
||||||
|
* target class simply never loads, so the registered patch is harmless. The method
|
||||||
|
* descriptor is left empty (name-only match) because the parameter types in Embeddium's
|
||||||
|
* bytecode may differ between Yarn and Mojmap mappings depending on the Embeddium build
|
||||||
|
* and compatibility layer.</p>
|
||||||
|
*/
|
||||||
|
@Patch(className = "me.jellysquid.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache")
|
||||||
|
public class BlockOcclusionCachePatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injected at HEAD of {@code shouldDrawSide}. When XRay is enabled we cancel the
|
||||||
|
* original method and force the result: target blocks return {@code true} (all
|
||||||
|
* faces visible = rendered through walls) and non-target blocks return
|
||||||
|
* {@code false} (no faces visible = completely transparent).
|
||||||
|
*
|
||||||
|
* <p>All reference parameters are declared as {@link Object} rather than their actual
|
||||||
|
* types because Embeddium may be compiled with either Yarn or Mojmap mappings — every
|
||||||
|
* argument of {@code shouldDrawSide} is a reference type, so declaring them as
|
||||||
|
* {@code Object} lets the forwarded values widen cleanly regardless of the concrete
|
||||||
|
* runtime types. Note this only protects against <em>type</em> mismatches: the
|
||||||
|
* parameter <em>count</em> must still equal the target method's arity (receiver + args),
|
||||||
|
* otherwise the generated call site is invalid. {@code PatchTransformer.injectHead}
|
||||||
|
* now guards this and skips the injection with a warning rather than emitting bytecode
|
||||||
|
* that would fail verification. The only param we actually read is {@code selfState},
|
||||||
|
* which is cast to Mojmap {@link BlockState} (always correct at runtime since the loaded
|
||||||
|
* Minecraft classes are Mojmap-mapped in a Forge environment).</p>
|
||||||
|
*
|
||||||
|
* @param self the {@code BlockOcclusionCache} instance (unused)
|
||||||
|
* @param selfState the {@code BlockState} of the block being rendered
|
||||||
|
* @param view the {@code BlockView/BlockGetter} (unused)
|
||||||
|
* @param pos the {@code BlockPos} (unused)
|
||||||
|
* @param direction the facing {@code Direction} (unused)
|
||||||
|
* @param ci callback info — cancelled with redirected result
|
||||||
|
*/
|
||||||
|
@Inject(
|
||||||
|
method = "shouldDrawSide",
|
||||||
|
desc = "",
|
||||||
|
at = @At(At.Type.HEAD)
|
||||||
|
)
|
||||||
|
public static void onShouldDrawSide(Object self, Object selfState, Object view,
|
||||||
|
Object pos, Object direction, CallbackInfo ci) {
|
||||||
|
XRay xray = XRay.INSTANCE;
|
||||||
|
if (xray == null || !xray.isEnabled()) {
|
||||||
|
return; // XRay off -> keep original Embeddium occlusion logic
|
||||||
|
}
|
||||||
|
// Cast to Mojmap BlockState — always correct at runtime on Forge
|
||||||
|
boolean visible = xray.isXrayVisible(((BlockState) selfState).getBlock());
|
||||||
|
ci.result = visible ? Boolean.TRUE : Boolean.FALSE;
|
||||||
|
ci.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,12 @@ public class StencilHelper {
|
|||||||
private static final RenderTarget mainRenderTarget = ClientBase.mc.getMainRenderTarget();
|
private static final RenderTarget mainRenderTarget = ClientBase.mc.getMainRenderTarget();
|
||||||
|
|
||||||
public static void applyStencil(PoseStack poseStack, Runnable drawMask, Runnable drawContent, float opacity) {
|
public static void applyStencil(PoseStack poseStack, Runnable drawMask, Runnable drawContent, float opacity) {
|
||||||
|
if (!stencilShader.isValid()) {
|
||||||
|
// Stencil shader unavailable on this driver — skip the rounded composite and
|
||||||
|
// just draw the content directly so the panel still shows (without rounding).
|
||||||
|
drawContent.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
Matrix4f pose = poseStack.last().pose();
|
Matrix4f pose = poseStack.last().pose();
|
||||||
RenderTarget mask = maskTarget.get();
|
RenderTarget mask = maskTarget.get();
|
||||||
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
|
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public class ShaderProgram {
|
|||||||
@Getter
|
@Getter
|
||||||
private final int programId;
|
private final int programId;
|
||||||
@Getter
|
@Getter
|
||||||
|
private final boolean valid;
|
||||||
|
@Getter
|
||||||
private final Map<String, Integer> uniformCache = new HashMap<>();
|
private final Map<String, Integer> uniformCache = new HashMap<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final Matrix4Uniform modelViewUniform;
|
private final Matrix4Uniform modelViewUniform;
|
||||||
@@ -50,6 +52,18 @@ public class ShaderProgram {
|
|||||||
this.programId = GL20.glCreateProgram();
|
this.programId = GL20.glCreateProgram();
|
||||||
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
|
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
|
||||||
int vertexShader = ShaderProgram.compileShader(ShaderSource.getByFileName(vertexName + ".vsh").getSource(), 35633);
|
int vertexShader = ShaderProgram.compileShader(ShaderSource.getByFileName(vertexName + ".vsh").getSource(), 35633);
|
||||||
|
// Shader compilation can fail on some drivers (notably Intel forward-compatible
|
||||||
|
// GL contexts, which reject legacy GLSL such as texture2D). Degrade gracefully —
|
||||||
|
// disable this program so callers skip the effect — instead of crashing the game.
|
||||||
|
if (fragmentShader == 0 || vertexShader == 0) {
|
||||||
|
if (fragmentShader != 0) GL20.glDeleteShader(fragmentShader);
|
||||||
|
if (vertexShader != 0) GL20.glDeleteShader(vertexShader);
|
||||||
|
LOGGER.error("Shader '{}' disabled: compilation failed; the effect will be skipped.", fragmentName);
|
||||||
|
this.modelViewUniform = null;
|
||||||
|
this.projectionUniform = null;
|
||||||
|
this.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
GL20.glAttachShader(this.programId, fragmentShader);
|
GL20.glAttachShader(this.programId, fragmentShader);
|
||||||
GL20.glAttachShader(this.programId, vertexShader);
|
GL20.glAttachShader(this.programId, vertexShader);
|
||||||
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
|
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
|
||||||
@@ -59,15 +73,25 @@ public class ShaderProgram {
|
|||||||
GL20.glLinkProgram(this.programId);
|
GL20.glLinkProgram(this.programId);
|
||||||
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
|
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
|
||||||
LOGGER.error(GL20.glGetProgramInfoLog(this.programId, Short.MAX_VALUE));
|
LOGGER.error(GL20.glGetProgramInfoLog(this.programId, Short.MAX_VALUE));
|
||||||
throw new IllegalStateException("Failed to link shader program!");
|
LOGGER.error("Shader '{}' disabled: program link failed; the effect will be skipped.", fragmentName);
|
||||||
|
GL20.glDeleteShader(fragmentShader);
|
||||||
|
GL20.glDeleteShader(vertexShader);
|
||||||
|
this.modelViewUniform = null;
|
||||||
|
this.projectionUniform = null;
|
||||||
|
this.valid = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
GL20.glDeleteShader(fragmentShader);
|
GL20.glDeleteShader(fragmentShader);
|
||||||
GL20.glDeleteShader(vertexShader);
|
GL20.glDeleteShader(vertexShader);
|
||||||
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
|
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
|
||||||
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
|
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
|
||||||
|
this.valid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void use() {
|
public void use() {
|
||||||
|
if (!this.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
prevProgram = GL20.glGetInteger(35725);
|
prevProgram = GL20.glGetInteger(35725);
|
||||||
GL20.glUseProgram(this.programId);
|
GL20.glUseProgram(this.programId);
|
||||||
this.setModelView(RenderSystem.getModelViewMatrix());
|
this.setModelView(RenderSystem.getModelViewMatrix());
|
||||||
@@ -112,8 +136,9 @@ public class ShaderProgram {
|
|||||||
GL20.glShaderSource(shader, source);
|
GL20.glShaderSource(shader, source);
|
||||||
GL20.glCompileShader(shader);
|
GL20.glCompileShader(shader);
|
||||||
if (GL20.glGetShaderi(shader, 35713) == 0) {
|
if (GL20.glGetShaderi(shader, 35713) == 0) {
|
||||||
LOGGER.error(GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
LOGGER.error("Failed to compile shader (type {}):\n{}", type, GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
|
||||||
throw new IllegalStateException(String.format("Failed to compile shader! (Type: %s)", new Object[]{type}));
|
GL20.glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return shader;
|
return shader;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.LadderBlock;
|
|||||||
import net.minecraft.world.level.block.LiquidBlock;
|
import net.minecraft.world.level.block.LiquidBlock;
|
||||||
import net.minecraft.world.level.block.SlabBlock;
|
import net.minecraft.world.level.block.SlabBlock;
|
||||||
import net.minecraft.world.level.block.TntBlock;
|
import net.minecraft.world.level.block.TntBlock;
|
||||||
|
import net.minecraft.world.level.block.WebBlock;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
import net.minecraft.world.phys.AABB;
|
import net.minecraft.world.phys.AABB;
|
||||||
import net.minecraft.world.phys.shapes.Shapes;
|
import net.minecraft.world.phys.shapes.Shapes;
|
||||||
@@ -122,6 +123,10 @@ extends ClientBase {
|
|||||||
if (block instanceof SlabBlock) {
|
if (block instanceof SlabBlock) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Cobwebs are tactical utility items (AutoWebPlace ammo), never bridging blocks.
|
||||||
|
if (block instanceof WebBlock) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return !blacklist.contains(block);
|
return !blacklist.contains(block);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ extends ClientBase {
|
|||||||
if (roundedRectShader == null) {
|
if (roundedRectShader == null) {
|
||||||
roundedRectShader = new ShaderProgram("rounded_rect", "vertex_color", ShaderFormats.POSITION_UV_COLOR);
|
roundedRectShader = new ShaderProgram("rounded_rect", "vertex_color", ShaderFormats.POSITION_UV_COLOR);
|
||||||
}
|
}
|
||||||
|
if (!roundedRectShader.isValid()) {
|
||||||
|
// Rounded-rect shader unavailable on this driver — draw a plain rect instead of crashing.
|
||||||
|
RenderUtil.drawFilledRect(poseStack, x, y, width, height, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Matrix4f matrix4f = poseStack.last().pose();
|
Matrix4f matrix4f = poseStack.last().pose();
|
||||||
roundedRectShader.use();
|
roundedRectShader.use();
|
||||||
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
|
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
|
||||||
@@ -222,6 +227,11 @@ extends ClientBase {
|
|||||||
if (blurShader == null) {
|
if (blurShader == null) {
|
||||||
blurShader = new ShaderProgram("blur", ShaderFormats.POSITION_UV_COLOR);
|
blurShader = new ShaderProgram("blur", ShaderFormats.POSITION_UV_COLOR);
|
||||||
}
|
}
|
||||||
|
if (!blurShader.isValid()) {
|
||||||
|
// Blur shader unavailable on this driver — disable blur for the rest of the session.
|
||||||
|
blurFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (mainRenderTarget == null) {
|
if (mainRenderTarget == null) {
|
||||||
mainRenderTarget = mc.getMainRenderTarget();
|
mainRenderTarget = mc.getMainRenderTarget();
|
||||||
}
|
}
|
||||||
|
|||||||