fix: 修复在sodium(或sodium based)的视觉优化模组加载时xray不工作

This commit is contained in:
Kajiki
2026-06-19 22:45:56 +08:00
parent 55fe4699f2
commit 21508a6f34
6 changed files with 179 additions and 8 deletions
@@ -8,5 +8,13 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
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 "";
}
@@ -72,6 +72,29 @@ public final class PatchAgent {
for (Class<?> patch : PatchRegistry.getPatches()) {
asm.patchify.annotation.Patch ann = patch.getAnnotation(asm.patchify.annotation.Patch.class);
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;
try {
target = ann.value();
@@ -42,7 +42,12 @@ public final class PatchClassFileTransformer implements ClassFileTransformer {
for (Class<?> patchClass : PatchRegistry.getPatches()) {
Patch patch = patchClass.getAnnotation(Patch.class);
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);
}
}
@@ -72,20 +72,42 @@ public final class PatchTransformer {
if (patchAnnotation == null) {
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<>();
// Collect handlers whose desc is empty (name-only wildcards) separately
List<Method> nameOnlyHandlers = new ArrayList<>();
for (Method handler : patchClass.getDeclaredMethods()) {
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget);
collectHandler(patchClass, patchTargetOwner, handler, handlersByTarget, nameOnlyHandlers);
}
LOGGER.info("Loading patch {} -> {} ({} handler(s))",
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<String> nameOnlyMatched = new HashSet<>();
for (MethodNode method : target.methods) {
MethodKey key = new MethodKey(method.name, method.desc);
List<Method> handlers = handlersByTarget.get(key);
if (handlers == null && !nameOnlyHandlers.isEmpty()) {
// Try name-only match for handlers registered with empty desc
for (Method candidate : nameOnlyHandlers) {
Inject inject = candidate.getAnnotation(Inject.class);
Overwrite overwrite = candidate.getAnnotation(Overwrite.class);
String targetName = inject != null ? inject.method()
: overwrite != null ? overwrite.method() : null;
if (targetName != null && targetName.equals(method.name)) {
if (handlers == null) handlers = new ArrayList<>();
handlers.add(candidate);
nameOnlyMatched.add(method.name);
}
}
}
if (handlers == null) continue;
matched.add(key);
for (Method handler : handlers) {
@@ -121,11 +143,34 @@ public final class PatchTransformer {
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,
Method handler,
Map<MethodKey, List<Method>> handlersByTarget) {
Map<MethodKey, List<Method>> handlersByTarget,
List<Method> nameOnlyHandlers) {
if (!(handler.isAnnotationPresent(Inject.class)
|| handler.isAnnotationPresent(Overwrite.class)
|| handler.isAnnotationPresent(Transform.class)
@@ -176,7 +221,13 @@ public final class PatchTransformer {
// production Forge environment the live class only has SRG names, so
// remap before matching against ClassNode.methods.
name = Bootstrap.remapMethod(patchTargetOwner, name, desc);
handlersByTarget.computeIfAbsent(new MethodKey(name, desc), k -> new ArrayList<>()).add(handler);
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);
}
}
private static void validateInjectSignature(Class<?> patchClass, Method handler, Inject inject) {
@@ -700,7 +751,9 @@ public final class PatchTransformer {
private static String targetClassName(Method handler) {
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,
+10
View File
@@ -12,6 +12,8 @@ import lombok.Getter;
import lombok.Setter;
import net.minecraft.client.Minecraft;
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.EventTarget;
import shit.zen.event.impl.TickEvent;
@@ -22,6 +24,7 @@ import shit.zen.manager.HudManager;
import shit.zen.manager.LagManager;
import shit.zen.manager.ModuleManager;
import shit.zen.manager.TargetManager;
import shit.zen.patch.BlockOcclusionCachePatch;
import shit.zen.patch.BlockPatch;
import shit.zen.patch.ChatScreenPatch;
import shit.zen.patch.ClientLevelPatch;
@@ -209,6 +212,13 @@ public class ZenClient extends ClientBase {
PatchRegistry.register(ItemPatch.class);
PatchRegistry.register(PlayerTabOverlayPatch.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() {
@@ -0,0 +1,72 @@
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. Registration in
* {@link shit.zen.ZenClient#registerPatches()} is guarded by a
* {@link Class#forName(String)} check so the patch is only loaded when Embeddium is
* present. 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 parameters are declared as {@link Object} rather than their actual types
* because Embeddium may be compiled with either Yarn or Mojmap mappings — using
* {@code Object} avoids a {@code VerifyError} when the handler's descriptor
* doesn't match the runtime descriptor exactly. 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();
}
}