fix: 修复在sodium(或sodium based)的视觉优化模组加载时xray不工作
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user