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