FabricMC/Mixin

How to access the previous method after redirecting?

aleck099 opened this issue · 1 comments

I tried to redirect the getTeleportTarget method in entity.moveToWorld

@Mixin(Entity.class)

@Redirect(method = "moveToWorld",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getTeleportTarget(Lnet/minecraft/server/world/ServerWorld;)Lnet/minecraft/world/TeleportTarget;"))
private TeleportTarget invokeTeleportTarget(Entity entity, ServerWorld destination) {

And I need to cancel the redirection in some occasions, so I return calling the previous getTeleportTarget()

if (some_occasion)
    return modified_value;
else
    return ((EntityInvoker) entity).getTeleportTarget(destination);

Notice that getTeleportTarget is a protected method, so I need an @Invoker to access it.

@Mixin(Entity.class)
public interface EntityInvoker {
    @Invoker("getTeleportTarget")
    public TeleportTarget getTeleportTarget(ServerWorld destination);
}

Then weird thing happened.
It crashed with StackOverFlowError, showing thousands of getTeleportTarget calls in the stack frame.
What surprised me:

  1. Setting a breakpoint at getTeleportTarget or inside it makes no sense
  2. Setting a breakpoint at my redirector succeeds, but when I steps into the else call, it continues running. We know that "step into" should always stop your program at next line of code, and it should never let the program run itself.
  3. When it starts to produce thousands of getTeleportTarget calls, all breakpoints including those in getTeleportTarget, invokeTeleportTarget and moveToWorld don't work.

From those details above, I can say that the duplicated getTeleportTarget call is not a real method call existing in .class files. It may be a mixin-hacked method, so that the debugger is unable to set breakpoints on it.
@Redirect only redirects method calls in moveToWorld, it shouldn't redirect other calls. So getTeleportTarget in invokeTeleportTarget should call the raw method without redirecting, at least it should call a normal method, not a hacked one.
I guess the previous getTeleportTarget is not itself anymore. Its bytecode has been corrupted.

Notice that getTeleportTarget is a protected method, so I need an @invoker to access it.

You can simply @Shadow that method in your mixin, and cast entity to your mixin, no need for an invoker.

It crashed with StackOverFlowError

That's because your invoker method is called the same thing as the target method. It needs to be called something else, typically callGetTeleportTarget or invokeGetTeleportTarget.
To make your accessor mixin work, Mixin has to make Entity implement EntityInvoker, and it then has to implement your interface's method. It will implement the method as a bridge to the original method, so when you call your invoker method, mixin will internally invoke the inaccessible target method. However, of course, in java only one method can exist with a given name, argument types and return type, and as such, your invoker method replaces the method it's trying to call. It then tries to invoke itself, leading to the stackoverflow.