A stray '混淆器' had been appended to the builtin-baseline SHA, leaving the JSON string unterminated; vcpkg manifest parsing failed in CI (Unexpected EOF in middle of string). Restore the clean 40-hex baseline (tag 2026.04.27 / qtbase 6.10.2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The vcpkg install was cloned at a floating HEAD while native/vcpkg.json
carried no builtin-baseline, so every run resolved the qtbase port afresh.
Its package ABI drifted whenever upstream vcpkg moved, the restored
actions/cache entry no longer matched, and qtbase was rebuilt from source
(~31 min) on every push. Because the cache key is static (hash of
vcpkg.json, unchanged since 05-23) the rebuilt tree was never saved back
either, so the miss repeated forever.
Pin both sides to vcpkg tag 2026.05.25
(d015e31e90838a4c9dfa3eed45979bc70d9357fc, qtbase 6.11.0):
- native/vcpkg.json: add "builtin-baseline"
- workflow: clone --branch 2026.05.25
Adding the baseline also changes the vcpkg.json hash, so this run gets a
fresh cache key, cold-builds Qt once, and saves it; subsequent runs match
the now-deterministic ABI and skip the qtbase build entirely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before: MSBuild defaulted to a single process per project, so each
vcxproj compiled its .cpp files sequentially. Local clean rebuild of
OpenZen.dll + OpenZenLoader.exe measured ~14.6 s.
After: same cold rebuild measures ~4.8 s — 3x faster — by combining
- `cmake --build --parallel <Runtime.runtime.availableProcessors()>`
in the buildNative task, which forwards a project-level `-m:N` to
MSBuild and gets DLL / Loader to build concurrently where the
dependency graph allows.
- `/MP` on the MSVC compile options, which is the actual win: it lets
cl.exe spawn one compiler process per available core within a
single vcxproj. The DLL+Loader together have ~13 .cpp files; this
is where the bulk of wall-time was being spent serially.
CI runners get the same speed-up for free.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the standard QMainWindow chrome and QTableWidget with a
purpose-built Qt UI built around hand-rolled widgets and animations:
- SplashScreen: frameless translucent splash with a wordmark fade/scale,
a sweeping scan line and a pulsing accent glow. ~950ms total cold-start
reveal that fades out into the main window.
- TitleBar: custom title bar replaces the OS frame, owns drag-to-move,
minimize/close buttons, and a breathing scan-status dot.
- InstanceList + InstanceRow: scrollable column of self-painted rows
(PID, title, Inject button). Rows are not selectable, double-click
has no special meaning - injection happens only through the button.
Hover animates each row's tint; new pids play a slide-in/opacity
entrance.
- InjectionOverlay: modal-style frameless overlay shown while inject()
runs in a worker thread. Spinner ring + progress bar advance during
injection; completion swaps to a checkmark / X, holds briefly, then
fades out. After it closes, the main window fades and the loader
self-quits.
- MainWindow: frameless + translucent panel painted by hand; entrance
fade/slide on launch; uniform playExitThenQuit() shared by the close
button and the post-injection path so every exit path gets a fade-out
instead of a hard vanish.
Also enables Win11 rounded corners via DwmSetWindowAttribute on show.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GitHub Actions (.github/workflows/build-loader.yml)
- Runs on every push to master and on workflow_dispatch.
- windows-2022 runner: setup-java 17 (Temurin), ilammy/msvc-dev-cmd
for MSVC x64, clones + bootstraps microsoft/vcpkg into the
workspace, installs UPX via choco, caches Gradle, then runs
`gradlew --no-daemon clean dll upxCompress` with VCPKG_ROOT and
OPENZEN_BUILD_REVISION exported.
- vcpkg's x-gha binary cache is wired in via VCPKG_BINARY_SOURCES +
ACTIONS_CACHE_URL / ACTIONS_RUNTIME_TOKEN, so the first run pays
the Qt static-build cost (~30 min - 2 h) and every push after
that pulls cached qtbase artifacts.
- Publishes build/dist/OpenZenLoader.exe as the
OpenZenLoader-<sha> artifact (30 day retention).
Git revision in window title
- build.gradle: gitShortRevision() prefers OPENZEN_BUILD_REVISION
from the environment (set by CI) and falls back to
`git rev-parse --short=7 HEAD` for local builds.
- configureNative passes -DOPENZEN_BUILD_REVISION=<sha> to CMake
and logs the resolved value.
- native/loader/CMakeLists.txt forwards it as a compile definition.
- MainWindow.buildUi sets the window title to
"OpenZen Loader (build abc1234)" when the macro is defined, plain
"OpenZen Loader" otherwise.
UPX compression
- New upxCompress Gradle task: locates upx on PATH, runs
`upx --best --lzma` on build/dist/OpenZenLoader.exe and logs the
before/after size. Skips with a warning (not an error) when upx
is missing so local builds keep working without it. CI's
`gradlew clean dll upxCompress` always shrinks the artifact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
build.gradle
- Rewrite packageDist: drop the Copy task type and use a plain
task with `copy { from src; into ... }` inside doLast. The
Copy task snapshots its source set at configuration time, so
a first `./gradlew clean dll` saw the source path before
buildNative produced OpenZenLoader.exe and ended the task as
NO-SOURCE. Doing the copy in doLast defers source resolution
to after buildNative, which is when the EXE actually exists.
- Bail with a clear GradleException if the EXE is still missing
after buildNative instead of silently leaving build/dist
empty, and log the packaged size on success.
native/loader/src/MainWindow.cpp
- Loosen the in-game window filter to: title startsWith
"Minecraft" OR window class equals "GLFW30" (case-insensitive).
GLFW30 is the LWJGL3 window class Minecraft uses before the
title is set, so this catches the early-startup window too.
Drop the "Launcher" blacklist - launchers are no longer
matched by either branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loader UI
- Replace the Win32 ListView loader window with a Qt6 Widgets
one. main.cpp + new MainWindow.h/cpp: QTableWidget showing
PID / Window Title / Window Class, QTimer auto-refresh every
1 s, double-click a row to inject. Title filter is built in
(startsWith "Minecraft" and not containing "Launcher"), so
HMCL / MultiMC / generic Java apps never show up.
- Dark Fusion theme + inline QSS so the single EXE looks the
same on any Windows version.
- native/vcpkg.json declares qtbase[widgets] and build.gradle's
new findVcpkg() detects VCPKG_ROOT / C:/vcpkg / D:/vcpkg /
~/vcpkg / D:/vcpkg-<version> and passes
-DCMAKE_TOOLCHAIN_FILE + -DVCPKG_TARGET_TRIPLET=
x64-windows-static so Qt is fully statically linked. Final
OpenZenLoader.exe stays single-file (~30 MB) with no Qt6*.dll
or vcruntime/msvcp DLL imports beyond Windows system DLLs.
- Drop the old ui.cpp and the now-unused run_ui() entry on
loader.h.
Patch transformer
- asm.patchify.loader.PatchTransformer.wrapInvoke now tries a
strict owner+name+desc match first, falling back to the
historical (owner || name) matcher only if strict turns up
nothing. The old loose matcher counted every Mth.*(FFF)F call
against a wrap aimed at Mth.lerp(FFF)F, so a same-method
sibling wrap that deleted its own site shifted later slice
indices and made onRenderPitchLerp miss.
LivingEntityRendererPatch
- With the strict matcher in place, onRenderPitchLerp's slice
drops from (4,4) to (1,1) - there is exactly one
Mth.lerp(FFF)F call site in LivingEntityRenderer.render
(pitch lerp), as verified via javap on the runtime srg jar.
onRenderHeadYawLerp keeps slice (2,2) (2nd of three
Mth.rotLerp calls = head yaw). No more "no call site of
m_14179_" warning at boot.
Scaffold
- Rename the BooleanSetting field from advancedBlockSearch to
sneak so the Java name matches the user-facing label ("Sneak").
README
- Add the vcpkg / Qt prerequisites and update the injector
usage section to describe the auto-refreshing UI + double-
click flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Loader no longer touches disk: drop the LoadLibraryW path that
extracted OpenZen.dll to %TEMP%\OpenZenLoader and replace it with
an in-process PE loader that maps the embedded DLL straight into
the target Java process.
native/loader (new manual_map.cpp/.h):
- VirtualAllocEx in the remote process for SizeOfImage,
locally apply relocations + IAT patching (kernel32 etc. are
KnownDLLs so local GetProcAddress addresses are valid in the
remote), WriteProcessMemory the finished image once,
VirtualProtectEx to match section characteristics, then run a
minimal x64 trampoline shellcode that calls DllMain with
DLL_PROCESS_ATTACH using the proper ABI (shadow space,
16-byte stack alignment).
- embedded_dll.cpp now hands back a pointer into the .rsrc
section instead of writing the DLL out. injector.cpp shrinks
to a 14-line shim around inject_in_memory.
native (top-level CMakeLists.txt):
- Force /MT (CMAKE_MSVC_RUNTIME_LIBRARY = MultiThreaded) for
both the DLL and the loader EXE. Without this the manual
mapper's local-address import resolution trips over
vcruntime140 / ucrtbase, which are not KnownDLLs and may have
different bases in the target process - leading to NULL
derefs inside msvcp140 after injection. With /MT the only
import surface left is KERNEL32.
native/dll (jar_extract.cpp):
- Replace FindResource/LoadResource/SizeofResource with a
hand-rolled PE resource directory walker. FindResource paths
through LdrFindResource_U, which depends on the PEB.Ldr table
that manual-mapped modules are not part of, so the standard
API returns NULL for the embedded zen.jar after injection.
native/loader UI:
- Drop the now-pointless DLL Path edit + Browse button.
- Add per-process "Window Class" column harvested via
GetClassNameW alongside the existing window title.
- Add a Minecraft Only checkbox (default on): show only rows
whose window title startsWith "Minecraft" and does not
contain "Launcher", filtering HMCL / MultiMC / generic Java
apps out of the picker.
build.gradle:
- Add cleanNative (Delete) hooked into clean so wiping the
project also removes native/build/ and the staged
native/zen.jar (CMake would otherwise hold onto the previous
configure).
gradle.properties:
- Fill in the real mod metadata (id=hey, name=OpenZen,
authors=Shirona1337, group=io.github.openzen, description).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OpenZenLoader.exe now ships with a requireAdministrator manifest
instead of asInvoker. The Windows loader prompts for UAC consent
once at launch, which:
- guarantees OpenProcess / VirtualAllocEx / CreateRemoteThread
succeed against javaw.exe when the Minecraft launcher (HMCL,
MultiMC) was itself started elevated,
- avoids the silent injection failure observed on locked-down
standard user accounts.
The EXE also picks up the UAC shield overlay on its file icon.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ship a self-contained OpenZenLoader.exe (OpenZen.dll embedded as
RCDATA) that injects into a running Minecraft 1.20.1 Forge
javaw.exe and brings up the Zen client without -javaagent or a
mods/ entry. The existing Forge mod path stays intact.
Native (native/):
- DLL: JNI_GetCreatedJavaVMs + JVMTI late-attach via the JDK's
instrument.dll Agent_OnAttach to obtain a real Instrumentation;
URLClassLoader on the game loader loads a single bridge class.
- Loader EXE: Win32 listview of javaw.exe/java.exe processes
with window titles, CreateRemoteThread+LoadLibraryW injection,
embedded DLL extracted to %TEMP%\OpenZenLoader at runtime.
Java:
- shit.zen.dll.GameLoaderBridge re-defines every jar class onto
the Forge GameClassLoader (fixed-point retry for super-class
deps) and extracts non-class resources to
%TEMP%\openzen-resources-<pid>, exposed via the
openzen.resources system property.
- shit.zen.dll.DllBootstrap only runs Bootstrap.init +
registerPatches + installPatchesAndRetransform; ZenClient
construction stays on MinecraftPatch.onTick lazy-init.
- shit.zen.asm.Bootstrap parses mapping.srg, detects SRG vs
mojmap runtime via Minecraft.tick reflection, exposes
remapMethod/remapField used at 5 PatchTransformer match
points and from ReflectionUtil.
- ReflectionUtil.resolveField walks the superclass chain trying
SRG then mojmap on each level so e.g. activeEffects on
LocalPlayer resolves on LivingEntity.
- shit.zen.utils.misc.Assets unifies resource lookup with a
fallback to openzen.resources; fonts, cloud assets and WebUI
static files route through it.
- PatchAgent.installPatchesAndRetransform is now idempotent so
DllBootstrap and ZenClient.init can both call it safely.
Build:
- ./gradlew jar forge mod jar (unchanged)
- ./gradlew dll single-file Loader EXE in build/dist
- CMake auto-located via vswhere when VS 2022 ships it.
- README documents both paths and required toolchain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>