feat(build): randomized build-time class-name obfuscation + pin Qt 6.10.2
Rename every shit.zen.* / asm.patchify.* class to a fresh random 16-char name in one random 16-char package on each build, via an ASM ClassRemapper pass (build.gradle ext.obfuscateJar, run after ForgeGradle reobfJar). Class names only; methods/fields preserved. Manifest Premain/Agent-Class, the DllBootstrap Class.forName string, and the native bridge name (generated_names.h OZ_BRIDGE_FQCN) are wired to the generated names. Residual original-name strings (loggers, log text, the asm.patchify.* property keys) scrubbed. Emits build/rename-mapping.txt. Pin Qt to 6.10.2 (vcpkg builtin-baseline + CI tag 2026.04.27): MSVC 14.44 crashes building Qt 6.11.0. Build Qt single-threaded locally (VCPKG_MAX_CONCURRENCY=1, loader --parallel 1, gated off GitHub Actions) to dodge parallel-compilation compiler crashes on that toolset. CI uploads rename-mapping.txt as an artifact and attaches it to the Release; the Release body warns that prebuilt artifacts share one fixed (blacklist-able) mapping and users should self-compile. README documents the obfuscation + the self-compile warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,10 +92,13 @@ jobs:
|
||||
# "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.05.25 ==
|
||||
# d015e31e90838a4c9dfa3eed45979bc70d9357fc and ships qtbase 6.11.0.
|
||||
# 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.05.25'
|
||||
$vcpkgTag = '2026.04.27'
|
||||
$vcpkg = Join-Path $env:GITHUB_WORKSPACE 'vcpkg'
|
||||
if (Test-Path $vcpkg) {
|
||||
Write-Host "Reusing existing $vcpkg"
|
||||
@@ -156,20 +159,28 @@ jobs:
|
||||
$sha = "${{ steps.rev.outputs.sha }}"
|
||||
$exeSrc = "build\dist\OpenZenLoader.exe"
|
||||
$jarSrc = "build\libs\hey-1.0.jar"
|
||||
# The class-name obfuscator emits a fresh, random old->new mapping on every
|
||||
# build; rename-mapping.txt is the ONLY way to de-obfuscate a stack trace, so
|
||||
# ship it with the artifacts/release.
|
||||
$mapSrc = "build\rename-mapping.txt"
|
||||
if (-not (Test-Path $exeSrc)) { throw "missing $exeSrc" }
|
||||
if (-not (Test-Path $jarSrc)) { throw "missing $jarSrc" }
|
||||
if (-not (Test-Path $mapSrc)) { throw "missing $mapSrc" }
|
||||
|
||||
$release = "build\release"
|
||||
New-Item -ItemType Directory -Force -Path $release | Out-Null
|
||||
$exeDst = Join-Path $release "OpenZenLoader-$sha.exe"
|
||||
$jarDst = Join-Path $release "OpenZen-$sha.jar"
|
||||
$mapDst = Join-Path $release "OpenZen-$sha-mapping.txt"
|
||||
Copy-Item -Force $exeSrc $exeDst
|
||||
Copy-Item -Force $jarSrc $jarDst
|
||||
Copy-Item -Force $mapSrc $mapDst
|
||||
|
||||
$exeSz = (Get-Item $exeDst).Length
|
||||
$jarSz = (Get-Item $jarDst).Length
|
||||
Write-Host ("OpenZenLoader-{0}.exe : {1:N0} bytes ({2:N2} MB)" -f $sha, $exeSz, ($exeSz/1MB))
|
||||
Write-Host ("OpenZen-{0}.jar : {1:N0} bytes ({2:N2} MB)" -f $sha, $jarSz, ($jarSz/1MB))
|
||||
Write-Host ("OpenZen-{0}-mapping.txt : {1:N0} bytes" -f $sha, (Get-Item $mapDst).Length)
|
||||
|
||||
# NOTE: actions/upload-artifact always wraps its content in a zip; that
|
||||
# is a platform limitation we cannot disable. By giving each artifact a
|
||||
@@ -193,6 +204,14 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload de-obfuscation mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: OpenZen-${{ steps.rev.outputs.sha }}-mapping.txt
|
||||
path: build/release/OpenZen-${{ steps.rev.outputs.sha }}-mapping.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# ===== Optional GitHub Release publish =====
|
||||
# If the HEAD commit message contains the literal marker "[Release]",
|
||||
# cut a GitHub Release tagged build-<sha> and attach the exe + jar.
|
||||
@@ -224,9 +243,30 @@ jobs:
|
||||
# Write notes via a file so quoting / [brackets] / newlines in the
|
||||
# commit message can't corrupt the gh command line.
|
||||
$notes = "release-notes.md"
|
||||
git log -1 --pretty=%B HEAD | Out-File -FilePath $notes -Encoding utf8
|
||||
# Prepend a PRE-BUILT warning to the release body: these artifacts all share
|
||||
# one fixed obfuscation mapping, so an anti-cheat class-name blacklist can
|
||||
# target them. Tell users to self-compile for unique, per-build random names.
|
||||
# Build the banner as a string array (one line each) to avoid PowerShell
|
||||
# here-string column-0 terminator issues inside a YAML block scalar.
|
||||
$warn = @(
|
||||
'> ⚠️ **这是预构建版本(PRE-BUILT)**'
|
||||
'>'
|
||||
'> 本 Release 里的 `OpenZenLoader.exe` / `OpenZen-*.jar` 是 GitHub Actions 编译的成品,**所有人下载到的是同一套混淆类名**。这套固定的名字随时可能被反作弊(如布吉岛)收录进**类名黑名单**而失效。'
|
||||
'>'
|
||||
'> 想要一套**独一无二、别人都不知道**的类名,请**自己编译**(每次构建都会生成全新随机类名):'
|
||||
'> - **Fork 本仓库**,在你自己的 GitHub Actions 里跑 `Build Loader` 工作流,下载你自己的 artifact;**或**'
|
||||
'> - **clone 到本地**自己 `gradlew jar` / `gradlew dll`。'
|
||||
'>'
|
||||
'> 详见仓库 README 的「编译时类名混淆」。`OpenZen-*-mapping.txt` 是本次构建的反混淆映射(每次构建都不同)。'
|
||||
''
|
||||
'---'
|
||||
''
|
||||
)
|
||||
$warn | Out-File -FilePath $notes -Encoding utf8
|
||||
git log -1 --pretty=%B HEAD | Out-File -FilePath $notes -Encoding utf8 -Append
|
||||
gh release create $tag `
|
||||
--title $title `
|
||||
--notes-file $notes `
|
||||
"build/release/OpenZenLoader-$env:SHA.exe" `
|
||||
"build/release/OpenZen-$env:SHA.jar"
|
||||
"build/release/OpenZen-$env:SHA.jar" `
|
||||
"build/release/OpenZen-$env:SHA-mapping.txt"
|
||||
|
||||
@@ -27,3 +27,6 @@ forge*changelog.txt
|
||||
# Native DLL build artifacts (CMake out-of-source build + staged jar)
|
||||
native/build/
|
||||
native/zen.jar
|
||||
|
||||
# Generated at build time by ext.obfuscateJar (holds the obfuscated bridge FQCN)
|
||||
native/dll/src/generated_names.h
|
||||
|
||||
@@ -100,6 +100,20 @@ OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启
|
||||
|
||||
> **本项目不能作为 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 任一)。
|
||||
@@ -172,14 +186,15 @@ OpenZen 支持两种交付形式:**Java Agent jar**(挂到 Minecraft JVM 启
|
||||
|
||||
诊断日志:
|
||||
- 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),布吉岛并未检测本项目,考虑其反作弊为黑名单类名机制。~~
|
||||
~~建议构建时修改类名。~~
|
||||
类名已黑名单,请在构建时修改类名。
|
||||
~~类名已黑名单,请在构建时修改类名。~~
|
||||
类名已黑名单。现在**每次构建都会自动随机化全部类名**(见上方[编译时类名混淆](#编译时类名混淆重要))——但**务必自己 Fork/clone 编译**,别直接用 GitHub Actions / Release 上的预编译版:那是固定的一套名字,会被拉黑。
|
||||
|
||||
## 致谢
|
||||
|
||||
|
||||
+216
-4
@@ -1,3 +1,27 @@
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassWriter
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.commons.ClassRemapper
|
||||
import org.objectweb.asm.commons.Remapper
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
// ASM is used by the build-time class-name obfuscation step (ext.obfuscateJar).
|
||||
// It runs on the already-built jar, so it lives on the buildscript classpath
|
||||
// rather than the project compile/runtime classpath.
|
||||
//
|
||||
// NOTE: the buildscript-classpath ASM also backs Groovy's build-script type
|
||||
// resolution, so it must be able to read the class files of the JDK that runs
|
||||
// Gradle. ASM 9.6 supports up to Java 21 (major version 65) — run Gradle on the
|
||||
// project's JDK 17 (not a newer JDK like 25/major 69, which 9.6 cannot read).
|
||||
classpath 'org.ow2.asm:asm-commons:9.6'
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'eclipse'
|
||||
id 'idea'
|
||||
@@ -209,6 +233,182 @@ tasks.named('jar', Jar).configure {
|
||||
finalizedBy 'reobfJar'
|
||||
}
|
||||
|
||||
// ===== Build-time class-name obfuscation =====
|
||||
//
|
||||
// Renames EVERY OpenZen class (shit.zen.** and asm.patchify.**) to an opaque
|
||||
// generated name, so no original class name survives in the distributed jar.
|
||||
// It runs on the reobf'd jar and only touches our two package trees —
|
||||
// net.minecraft.** / net.minecraftforge.** references are left exactly as
|
||||
// ForgeGradle's mojmap->SRG reobf produced them, so reobf and runtime
|
||||
// remapping are unaffected.
|
||||
//
|
||||
// CLASS NAMES ONLY: method/field names are preserved, so the JNI method lookups
|
||||
// (GameLoaderBridge.load, DllBootstrap.start), reflection, GSON @SerializedName
|
||||
// wire keys and the manifest member contracts all keep working.
|
||||
//
|
||||
// NOTHING IS HARD-CODED. The three bootstrap classes referenced by string
|
||||
// OUTSIDE the bytecode the remapper rewrites are kept in lockstep by
|
||||
// propagating their freshly generated names:
|
||||
// asm.patchify.loader.PatchAgent -> jar manifest Premain/Agent-Class (rewritten below)
|
||||
// shit.zen.dll.DllBootstrap -> Class.forName(...) string literal (rewritten via Remapper.mapValue)
|
||||
// shit.zen.dll.GameLoaderBridge -> native DLL loader, via a generated C++ header
|
||||
// (native/dll/src/generated_names.h) emitted below.
|
||||
ext.obfuscateJar = { File jarFile, File mappingOut ->
|
||||
def owned = { String internal -> internal.startsWith('shit/zen/') || internal.startsWith('asm/patchify/') }
|
||||
|
||||
// Pass 1: enumerate the classes we own (every shit.zen.* / asm.patchify.* class).
|
||||
def ownedNames = []
|
||||
new java.util.zip.ZipFile(jarFile).withCloseable { zf ->
|
||||
for (entry in Collections.list(zf.entries())) {
|
||||
if (!entry.directory && entry.name.endsWith('.class')) {
|
||||
def internal = entry.name.substring(0, entry.name.length() - 6)
|
||||
if (owned(internal)) ownedNames << internal
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ownedNames.isEmpty()) {
|
||||
logger.lifecycle("obfuscateJar: no original class names in ${jarFile.name} (already obfuscated) — skipping")
|
||||
return
|
||||
}
|
||||
// Pass 2: assign each owned class a FRESH RANDOM 16-char name, so the obfuscated
|
||||
// names differ on every build and encode nothing. Uniqueness is enforced. The
|
||||
// names are unpredictable, so build/rename-mapping.txt (written below) is the only
|
||||
// way to de-obfuscate a stack trace — each build's mapping is different; keep it.
|
||||
//
|
||||
// ALL classes go into ONE shared (also random, 16-char) package. They must share a
|
||||
// single package so that package-private (default-access) members originally used
|
||||
// between same-package classes still resolve — flattening into one package only
|
||||
// widens access, never breaks it. Per-class packages would turn those into illegal
|
||||
// cross-package accesses (IllegalAccessError) unless every member were made public.
|
||||
def typeMap = [:]
|
||||
def usedNames = new HashSet()
|
||||
def secureRandom = new java.security.SecureRandom()
|
||||
def leadAlphabet = (('a'..'z') + ('A'..'Z')).join('') // first char: a letter
|
||||
def nameAlphabet = (('a'..'z') + ('A'..'Z') + ('0'..'9')).join('') // rest: alphanumeric
|
||||
def randomName = {
|
||||
def sb = new StringBuilder(16)
|
||||
sb.append(leadAlphabet.charAt(secureRandom.nextInt(leadAlphabet.length())))
|
||||
15.times { sb.append(nameAlphabet.charAt(secureRandom.nextInt(nameAlphabet.length()))) }
|
||||
sb.toString()
|
||||
}
|
||||
def obfPackage = randomName() // one random package for every class (see note above)
|
||||
ownedNames.each { internal ->
|
||||
def newName
|
||||
while (true) { newName = randomName(); if (usedNames.add(newName)) break }
|
||||
typeMap[internal] = obfPackage + '/' + newName
|
||||
}
|
||||
// String-constant remap table — catches class names embedded as String
|
||||
// literals (e.g. Class.forName("shit.zen.dll.DllBootstrap")). Both the
|
||||
// dotted (Class.forName) and slash (internal) spellings are covered.
|
||||
def stringMap = [:]
|
||||
typeMap.each { o, n ->
|
||||
stringMap[o.replace('/', '.')] = n.replace('/', '.')
|
||||
stringMap[o] = n
|
||||
}
|
||||
|
||||
def remapper = new Remapper() {
|
||||
String map(String internalName) {
|
||||
def n = typeMap[internalName]
|
||||
return n != null ? n : internalName
|
||||
}
|
||||
|
||||
Object mapValue(Object value) {
|
||||
if (value instanceof String) {
|
||||
def repl = stringMap[value]
|
||||
if (repl != null) return repl
|
||||
}
|
||||
return super.mapValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: rewrite into a temp jar, then atomically swap it in.
|
||||
def tmp = new File(jarFile.parentFile, jarFile.name + '.obf')
|
||||
tmp.delete()
|
||||
new java.util.zip.ZipFile(jarFile).withCloseable { zf ->
|
||||
new java.util.zip.ZipOutputStream(new FileOutputStream(tmp)).withCloseable { zos ->
|
||||
for (entry in Collections.list(zf.entries())) {
|
||||
if (entry.directory) continue
|
||||
def name = entry.name
|
||||
byte[] bytes = zf.getInputStream(entry).bytes
|
||||
if (name.endsWith('.class')) {
|
||||
def internal = name.substring(0, name.length() - 6)
|
||||
if (owned(internal)) {
|
||||
def cr = new ClassReader(bytes)
|
||||
def cw = new ClassWriter(0)
|
||||
// Drop the original SourceFile name ("ZenClient.java" etc.) but keep
|
||||
// line numbers so stack traces still carry positions.
|
||||
def stripSource = new ClassVisitor(Opcodes.ASM9, cw) {
|
||||
void visitSource(String source, String debug) { super.visitSource(null, null) }
|
||||
}
|
||||
cr.accept(new ClassRemapper(stripSource, remapper), 0)
|
||||
bytes = cw.toByteArray()
|
||||
name = typeMap[internal] + '.class'
|
||||
}
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bytes); zos.closeEntry()
|
||||
} else if (name == 'META-INF/MANIFEST.MF') {
|
||||
def mf = new java.util.jar.Manifest(new ByteArrayInputStream(bytes))
|
||||
def attrs = mf.getMainAttributes()
|
||||
['Premain-Class', 'Agent-Class'].each { key ->
|
||||
def v = attrs.getValue(key)
|
||||
if (v != null) {
|
||||
def internal = v.replace('.', '/')
|
||||
if (typeMap.containsKey(internal)) attrs.putValue(key, typeMap[internal].replace('/', '.'))
|
||||
}
|
||||
}
|
||||
def bos = new ByteArrayOutputStream(); mf.write(bos)
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bos.toByteArray()); zos.closeEntry()
|
||||
} else if (name.startsWith('META-INF/') &&
|
||||
(name.endsWith('.SF') || name.endsWith('.RSA') || name.endsWith('.DSA') || name.endsWith('.EC'))) {
|
||||
// Drop signature files — renaming class entries invalidates their digests.
|
||||
} else {
|
||||
// Resources verbatim: mapping.srg, webui/**, assets/**, fonts, mods.toml, pack.mcmeta.
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(name)); zos.write(bytes); zos.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!jarFile.delete()) throw new GradleException("obfuscateJar: could not delete ${jarFile}")
|
||||
if (!tmp.renameTo(jarFile)) throw new GradleException("obfuscateJar: could not move ${tmp} -> ${jarFile}")
|
||||
|
||||
mappingOut.parentFile.mkdirs()
|
||||
mappingOut.withWriter('UTF-8') { w ->
|
||||
typeMap.sort { it.key }.each { o, n -> w.writeLine("${o.replace('/', '.')} -> ${n.replace('/', '.')}") }
|
||||
}
|
||||
|
||||
// Propagate the GameLoaderBridge name to the native DLL loader. class_loader.cpp
|
||||
// #includes this generated header and loads the class by OZ_BRIDGE_FQCN, so the
|
||||
// native side always matches whatever opaque name the bridge received — no
|
||||
// hard-coded class name. Generated, never committed (see .gitignore).
|
||||
def bridgeFqcn = typeMap['shit/zen/dll/GameLoaderBridge'].replace('/', '.')
|
||||
def header = file('native/dll/src/generated_names.h')
|
||||
header.parentFile.mkdirs()
|
||||
header.text = """\
|
||||
// AUTO-GENERATED by build.gradle (ext.obfuscateJar). DO NOT EDIT, DO NOT COMMIT.
|
||||
// The build renames every OpenZen class to an opaque name; this captures the
|
||||
// generated FQCN of the DLL bootstrap bridge (originally shit.zen.dll.GameLoaderBridge)
|
||||
// so the native loader can request it by name. The bridge's load(String, ClassLoader)
|
||||
// method name is preserved by the rename, so main.cpp's GetStaticMethodID still works.
|
||||
#pragma once
|
||||
#define OZ_BRIDGE_FQCN "${bridgeFqcn}"
|
||||
"""
|
||||
|
||||
logger.lifecycle("obfuscateJar: renamed ${typeMap.size()} classes in ${jarFile.name}; " +
|
||||
"bridge=${bridgeFqcn}; mapping -> ${mappingOut}")
|
||||
}
|
||||
|
||||
tasks.register('obfuscateClasses') {
|
||||
group = 'openzen'
|
||||
description = 'Rename every OpenZen class to an opaque name in the built jar (class names only).'
|
||||
dependsOn 'reobfJar'
|
||||
doLast {
|
||||
obfuscateJar(tasks.jar.archiveFile.get().asFile, file("$buildDir/rename-mapping.txt"))
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run after every reobf so `gradlew jar` / `build` / `dll` all emit obfuscated
|
||||
// names. reobfJar is created lazily by ForgeGradle, so wire via matching/configureEach.
|
||||
tasks.matching { it.name == 'reobfJar' }.configureEach { finalizedBy 'obfuscateClasses' }
|
||||
|
||||
// However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing:
|
||||
// tasks.named('publish').configure {
|
||||
// dependsOn 'reobfJar'
|
||||
@@ -366,7 +566,10 @@ tasks.register('stageNativeJar', Copy) {
|
||||
group = 'openzen'
|
||||
description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' +
|
||||
'resource compiler can embed it.'
|
||||
dependsOn 'jar'
|
||||
// obfuscateClasses (which dependsOn reobfJar, which dependsOn jar) rewrites
|
||||
// the jar in place, so depend on it to guarantee the DLL embeds the
|
||||
// obfuscated jar rather than racing the rename.
|
||||
dependsOn 'obfuscateClasses'
|
||||
from { tasks.jar.archiveFile }
|
||||
into nativeDir
|
||||
rename { 'zen.jar' }
|
||||
@@ -378,6 +581,13 @@ tasks.register('configureNative', Exec) {
|
||||
dependsOn 'stageNativeJar'
|
||||
workingDir nativeDir
|
||||
commandLine 'cmake', '-S', '.', '-B', 'build', '-A', 'x64'
|
||||
// Local builds compile Qt single-threaded (vcpkg honours VCPKG_MAX_CONCURRENCY,
|
||||
// which sets ninja -j for the qtbase port). This rules out parallel-compilation
|
||||
// instability on the dev toolchain. CI is left parallel (fast) — it builds Qt
|
||||
// fine, so we only serialise locally (off GitHub Actions).
|
||||
if (System.getenv('GITHUB_ACTIONS') == null) {
|
||||
environment 'VCPKG_MAX_CONCURRENCY', '1'
|
||||
}
|
||||
doFirst {
|
||||
def cmake = findCmake()
|
||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||
@@ -405,10 +615,12 @@ tasks.register('buildNative', Exec) {
|
||||
doFirst {
|
||||
def cmake = findCmake()
|
||||
if (!cmake) throw new GradleException(cmakeMissingMessage)
|
||||
// --parallel without a number lets CMake pick the host's logical CPU
|
||||
// count and forwards it to the underlying generator (MSBuild gets /m,
|
||||
// --parallel forwards a job count to the generator (MSBuild gets /m,
|
||||
// Ninja gets -j). MSBuild defaults to a single process otherwise.
|
||||
def jobs = Runtime.runtime.availableProcessors()
|
||||
// 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()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "openzen.h"
|
||||
#include "generated_names.h" // OZ_BRIDGE_FQCN — generated by build.gradle ext.obfuscateJar
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -114,17 +115,24 @@ jclass load_dll_bootstrap(JNIEnv* env, jobject game_loader,
|
||||
if (check_and_clear(env, "new URLClassLoader")) return nullptr;
|
||||
log::info("URLClassLoader constructed with parent=gameLoader");
|
||||
|
||||
// urlcl.loadClass("shit.zen.dll.GameLoaderBridge")
|
||||
// urlcl.loadClass(OZ_BRIDGE_FQCN) (the build-time-obfuscated GameLoaderBridge)
|
||||
//
|
||||
// We deliberately load GameLoaderBridge - not DllBootstrap - because the
|
||||
// bridge's job is to re-define every class in zen.jar onto the game
|
||||
// class loader so retransformed Minecraft classes can resolve our patch
|
||||
// handlers (defining-loader equality). DllBootstrap is then loaded by
|
||||
// the game loader from the bridge.
|
||||
//
|
||||
// The build renames every OpenZen class to an opaque generated name (see
|
||||
// build.gradle ext.obfuscateJar). It also emits generated_names.h with the
|
||||
// bridge's new FQCN as OZ_BRIDGE_FQCN, so this stays in lockstep with the
|
||||
// embedded jar without any hard-coded class name. The bridge's
|
||||
// load(String, ClassLoader) method name is preserved by the rename, so the
|
||||
// GetStaticMethodID(bridge_cls, "load", ...) lookup in main.cpp still works.
|
||||
jclass classLoaderCls = env->FindClass("java/lang/ClassLoader");
|
||||
jmethodID loadClass = env->GetMethodID(classLoaderCls, "loadClass",
|
||||
"(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
jstring name = env->NewStringUTF("shit.zen.dll.GameLoaderBridge");
|
||||
jstring name = env->NewStringUTF(OZ_BRIDGE_FQCN);
|
||||
jobject loaded = env->CallObjectMethod(urlcl, loadClass, name);
|
||||
if (check_and_clear(env, "URLClassLoader.loadClass GameLoaderBridge")) return nullptr;
|
||||
if (!loaded) {
|
||||
|
||||
+1
-2
@@ -10,5 +10,4 @@
|
||||
"features": [ "widgets" ]
|
||||
}
|
||||
],
|
||||
"builtin-baseline": "d015e31e90838a4c9dfa3eed45979bc70d9357fc"
|
||||
}
|
||||
"builtin-baseline": "56bb2411609227288b70117ead2c47585ba07713混淆器
|
||||
@@ -18,8 +18,8 @@ import org.apache.logging.log4j.Logger;
|
||||
* regardless of which class loader it lives in.</p>
|
||||
*/
|
||||
public final class PatchAgent {
|
||||
public static final String INSTRUMENTATION_KEY = "asm.patchify.instrumentation";
|
||||
private static final Logger LOGGER = LogManager.getLogger("PatchAgent");
|
||||
public static final String INSTRUMENTATION_KEY = "oz.instrumentation";
|
||||
private static final Logger LOGGER = LogManager.getLogger(PatchAgent.class);
|
||||
|
||||
private static volatile boolean transformerInstalled = false;
|
||||
|
||||
@@ -40,7 +40,7 @@ public final class PatchAgent {
|
||||
return;
|
||||
}
|
||||
System.getProperties().put(INSTRUMENTATION_KEY, inst);
|
||||
LOGGER.info("PatchAgent attached, retransform supported = {}", inst.isRetransformClassesSupported());
|
||||
LOGGER.info("agent attached, retransform supported = {}", inst.isRetransformClassesSupported());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +62,7 @@ public final class PatchAgent {
|
||||
}
|
||||
Instrumentation inst = getInstrumentation();
|
||||
if (inst == null) {
|
||||
LOGGER.warn("PatchAgent not attached; cannot install patches");
|
||||
LOGGER.warn("agent not attached; cannot install patches");
|
||||
return;
|
||||
}
|
||||
PatchClassFileTransformer transformer = new PatchClassFileTransformer();
|
||||
|
||||
@@ -23,13 +23,13 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
* <p>Indexes the patches by the JVM-internal name of their target class and rewrites class
|
||||
* bytes via ASM. Classes that have no patch are returned untouched (null).</p>
|
||||
*
|
||||
* <p>If the system property {@code asm.patchify.dumpDir} is set, every successfully
|
||||
* <p>If the system property {@code oz.dumpDir} is set, every successfully
|
||||
* transformed class is written under that directory as {@code <internalName>.class} for
|
||||
* inspection (use {@code javap -v} or open in Recaf).</p>
|
||||
*/
|
||||
public final class PatchClassFileTransformer implements ClassFileTransformer {
|
||||
private static final Logger LOGGER = LogManager.getLogger("PatchTransformer");
|
||||
private static final String DUMP_DIR_PROPERTY = "asm.patchify.dumpDir";
|
||||
private static final Logger LOGGER = LogManager.getLogger(PatchClassFileTransformer.class);
|
||||
private static final String DUMP_DIR_PROPERTY = "oz.dumpDir";
|
||||
|
||||
private final Map<String, List<Class<?>>> patchesByTarget = new HashMap<>();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.apache.logging.log4j.Logger;
|
||||
* lightweight no-op fallback when no transformer is installed.</p>
|
||||
*/
|
||||
public final class PatchRegistry {
|
||||
private static final Logger LOGGER = LogManager.getLogger("PatchRegistry");
|
||||
private static final Logger LOGGER = LogManager.getLogger(PatchRegistry.class);
|
||||
private static final List<Class<?>> PATCHES = new ArrayList<>();
|
||||
|
||||
private PatchRegistry() {
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ZenClient extends ClientBase {
|
||||
if (PatchAgent.getInstrumentation() != null) {
|
||||
PatchAgent.installPatchesAndRetransform();
|
||||
} else {
|
||||
logger.warn("PatchAgent not attached. Launch with `./gradlew runClient0` so the agent jvmArg is set.");
|
||||
logger.warn("agent not attached. Launch with `./gradlew runClient0` so the agent jvmArg is set.");
|
||||
}
|
||||
isReady = true;
|
||||
logger.info("{} v{} initialized.", CLIENT_NAME, VERSION);
|
||||
|
||||
Binary file not shown.
@@ -22,7 +22,7 @@ import shit.zen.asm.Bootstrap;
|
||||
* onwards.</p>
|
||||
*/
|
||||
public final class DllBootstrap {
|
||||
private static final Logger LOGGER = LogManager.getLogger("DllBootstrap");
|
||||
private static final Logger LOGGER = LogManager.getLogger(DllBootstrap.class);
|
||||
private static volatile boolean started = false;
|
||||
|
||||
private DllBootstrap() {
|
||||
@@ -30,16 +30,16 @@ public final class DllBootstrap {
|
||||
|
||||
public static synchronized void start(String extractedJarPath) {
|
||||
if (started) {
|
||||
LOGGER.info("DllBootstrap.start ignored (already started)");
|
||||
LOGGER.info("bootstrap.start ignored (already started)");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
try {
|
||||
LOGGER.info("DllBootstrap.start jar={}", extractedJarPath);
|
||||
LOGGER.info("DllBootstrap loader = {}", DllBootstrap.class.getClassLoader());
|
||||
LOGGER.info("ZenClient loader = {}", ZenClient.class.getClassLoader());
|
||||
LOGGER.info("bootstrap.start jar={}", extractedJarPath);
|
||||
LOGGER.info("bootstrap loader = {}", DllBootstrap.class.getClassLoader());
|
||||
LOGGER.info("client loader = {}", ZenClient.class.getClassLoader());
|
||||
LOGGER.info("Minecraft loader = {}", Minecraft.class.getClassLoader());
|
||||
LOGGER.info("PatchAgent inst = {}", PatchAgent.getInstrumentation());
|
||||
LOGGER.info("agent inst = {}", PatchAgent.getInstrumentation());
|
||||
|
||||
// Load mojmap → SRG mappings before we attempt any retransform.
|
||||
Bootstrap.init();
|
||||
@@ -53,9 +53,9 @@ public final class DllBootstrap {
|
||||
ZenClient.registerPatches();
|
||||
PatchAgent.installPatchesAndRetransform();
|
||||
|
||||
LOGGER.info("DllBootstrap done. ZenClient will be constructed on the next Minecraft.tick.");
|
||||
LOGGER.info("bootstrap done. client will be constructed on the next tick.");
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("DllBootstrap.start failed", t);
|
||||
LOGGER.error("bootstrap.start failed", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,19 +46,19 @@ import org.apache.logging.log4j.Logger;
|
||||
* </ol>
|
||||
*/
|
||||
public final class GameLoaderBridge {
|
||||
private static final Logger LOGGER = LogManager.getLogger("GameLoaderBridge");
|
||||
private static final Logger LOGGER = LogManager.getLogger(GameLoaderBridge.class);
|
||||
public static final String RESOURCES_PROP = "openzen.resources";
|
||||
|
||||
private GameLoaderBridge() {
|
||||
}
|
||||
|
||||
public static void load(String jarPath, ClassLoader gameLoader) throws Throwable {
|
||||
LOGGER.info("GameLoaderBridge.load jar={} gameLoader={}", jarPath, gameLoader);
|
||||
LOGGER.info("bridge.load jar={} gameLoader={}", jarPath, gameLoader);
|
||||
long t0 = System.nanoTime();
|
||||
|
||||
Instrumentation inst = PatchAgent.getInstrumentation();
|
||||
if (inst == null) {
|
||||
throw new IllegalStateException("PatchAgent.getInstrumentation() returned null - "
|
||||
throw new IllegalStateException("agent instrumentation returned null - "
|
||||
+ "did instrument.dll Agent_OnAttach run?");
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public final class GameLoaderBridge {
|
||||
classCount, resourceCount, resourceDir, ms);
|
||||
|
||||
Class<?> bootstrapCls = Class.forName("shit.zen.dll.DllBootstrap", true, gameLoader);
|
||||
LOGGER.info("DllBootstrap loader (should be gameLoader): {}", bootstrapCls.getClassLoader());
|
||||
LOGGER.info("bootstrap loader (should be gameLoader): {}", bootstrapCls.getClassLoader());
|
||||
Method start = bootstrapCls.getMethod("start", String.class);
|
||||
start.invoke(null, jarPath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user