diff --git a/src/main/java/asm/patchify/loader/PatchTransformer.java b/src/main/java/asm/patchify/loader/PatchTransformer.java index 8d052b3..f6d4c80 100644 --- a/src/main/java/asm/patchify/loader/PatchTransformer.java +++ b/src/main/java/asm/patchify/loader/PatchTransformer.java @@ -95,13 +95,12 @@ public final class PatchTransformer { MethodKey key = new MethodKey(method.name, method.desc); List handlers = handlersByTarget.get(key); if (handlers == null && !nameOnlyHandlers.isEmpty()) { - // Try name-only match for handlers registered with empty desc + // 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) { - 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)) { + String targetName = getHandlerTargetMethodName(candidate); + if (targetName.equals(method.name)) { if (handlers == null) handlers = new ArrayList<>(); handlers.add(candidate); nameOnlyMatched.add(method.name); @@ -267,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. + * + *

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.

+ */ + private static String headHandlerMismatch(MethodNode method, Method handler) { + List 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) { + 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); String handlerOwner = Type.getInternalName(handler.getDeclaringClass()); String handlerName = handler.getName(); diff --git a/src/main/java/shit/zen/patch/BlockOcclusionCachePatch.java b/src/main/java/shit/zen/patch/BlockOcclusionCachePatch.java index 330909e..a1eefd9 100644 --- a/src/main/java/shit/zen/patch/BlockOcclusionCachePatch.java +++ b/src/main/java/shit/zen/patch/BlockOcclusionCachePatch.java @@ -22,12 +22,14 @@ import shit.zen.modules.impl.render.XRay; * *

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.

+ * available at compile time. The patch is registered unconditionally in + * {@link shit.zen.ZenClient#registerPatches()} (we must not 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.

*/ @Patch(className = "me.jellysquid.mods.sodium.client.render.chunk.compile.pipeline.BlockOcclusionCache") public class BlockOcclusionCachePatch { @@ -38,13 +40,17 @@ public class BlockOcclusionCachePatch { * faces visible = rendered through walls) and non-target blocks return * {@code false} (no faces visible = completely transparent). * - *

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).

+ *

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 type mismatches: the + * parameter count 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).

* * @param self the {@code BlockOcclusionCache} instance (unused) * @param selfState the {@code BlockState} of the block being rendered