Skip to content

Redirects

Lukas22041 edited this page Aug 30, 2026 · 5 revisions

Redirects are a type of patch that targets a specific call within a method, not the method itself. They allow intercepting a call and either replacing it outright or modifying the outcome. There are four types of redirects as of now:

  • @RedirectMethodCall: Allows intercepting a method call, like Global.getSector().getPlayerFleet().
  • @RedirectConstructorCall: Allows intercepting a constructor call, like new Vector2f.
  • @RedirectFieldRead: Allows intercepting a field read, like float xPos = location.x;
  • @RedirectFieldWrite: Allows intercepting a field write, like location.x = 100f;

Redirects are generally more useful for very targeted changes in specific locations.

Example Redirect

To get an example going, let's say we want to modify the addAll call in reportPlayerDumpedCargo in CoreUI. The original code looks something like this:

// From CoreUI.java

@Override
public void reportPlayerDumpedCargo(CargoAPI cargo) {
    super.reportPlayerDumpedCargo(cargo);

    CampaignFleetAPI playerFleet = Global.getSector().getPlayerFleet();

    CustomCampaignEntityAPI pods = Misc.addCargoPods(playerFleet.getContainingLocation(), playerFleet.getLocation());
    pods.getCargo().addAll(cargo);

    CargoPodsResponse script = new CargoPodsResponse(pods);
    pods.getContainingLocation().addScript(script);
    ListenerUtil.reportPlayerLeftCargoPods(pods);
}

Since addAll is a method call, we will use @RedirectMethodCall. We target the class itself the same way as we would with hooks.

@Patch(target = @ClassMatch(type = CoreScript.class))
public class ExampleRedirect {

    @RedirectMethodCall(
            target = @MethodMatch(methodName = "reportPlayerDumpedCargo"),
            call = @MethodMatch(methodName = "addAll"),
            owner = @ClassMatch(type = CargoAPI.class))
    public static void redirectReportPlayerDumpedCargo(MethodCallContext context) {
        
    }
}

One thing that sticks out is the number of arguments in our @RedirectMethodCall annotation. This is because redirects have two things they need to target.

  1. The host method that contains the call (i.e. reportPlayerDumpedCargo)
  2. The call itself (i.e. addAll)

For method redirects, you use the call argument with a @MethodMatch matcher. The name of this argument and the type you provide differ for each redirect type. You can find the exact details on each redirect's page.

You may also notice the owner argument in the example. This is a secondary matcher that you can use to further narrow the target down if needed. It matches by what type the call is performed on. In our case, addAll is called on an object of type CargoAPI.

Now we can write our redirect. For the example case, we are simply going to intercept the call and make the player dumping their cargo add an AI core to the dumped cargo.

@Patch(target = @ClassMatch(type = CoreScript.class))
public class ExampleRedirect {

    @RedirectMethodCall(
            target = @MethodMatch(methodName = "reportPlayerDumpedCargo"),
            call = @MethodMatch(methodName = "addAll"),
            owner = @ClassMatch(type = CargoAPI.class))
    public static void redirectReportPlayerDumpedCargo(MethodCallContext context) {
        //Retrieve the "cargo" argument to the addAll call
        CargoAPI dumpedCargo = (CargoAPI) context.getCallArg(0);

        //Modify the argument
        dumpedCargo.addCommodity(Commodities.ALPHA_CORE, 1);

        //We need to call the next layer, or otherwise the addAll call will never perform its original effect
        context.call();
    }
}

There are two things of importance here. The first is to note the getCallArg method. It is not to be confused with getArg. Redirects have access to getArg the same way that hooks do, which provides access to arguments from the host method.

getCallArg is specific to method redirects. It allows retrieving arguments passed into the intercepted call.

The second thing of note is the context.call() method. We will go into detail in the next section.

Layered Order

Similar to Hooks, redirects also have an order attribute in their annotation. However, they work with a layered model instead. A redirect with a lower order value is a higher layer, whereas the highest order is at the bottom of the layered model. At the very bottom is the original call that you are intercepting.

This model ensures compatibility between multiple patches targeting the same call and enables you to place your code before or after the original call. This is especially useful for listeners.

To make this model work, each redirect type has a method that continues to the next layer and will return its result. For @RedirectMethodCall that is:

  • Object context.call(): Call the next layer with the original call arguments supplied
  • Object context.call(Object[] args): Call the next layer with your own selection of arguments

Importantly, if you do not use .call, the next layer is never called. This allows you to cause a call to be entirely skipped, but also means that you have to make sure to call it in any case where the original effect of the call is still required.

The other important method in this equation is context.setResult(). If you are redirecting a method that has a return type, so one that is not void, you will need to have your layer set the result. If your layer does not set a result, it will return null, which may break things where it's not expected. In most cases where you do not need to modify the return value yourself, you can apply a pattern like the following:

Object originalReturnValue = context.call();
context.setResult(originalReturnValue);

To visualize this model, you can take a look at the following sequence diagram.

sequenceDiagram
    participant Host as Hosting method
    participant Outer as Patch A (order 0)
    participant Inner as Patch B (order 100)
    participant Original as Original call

    Host->>+Outer: Intercepted call
    Outer->>+Inner: context.call()
    Inner->>+Original: context.call()
    Original-->>-Inner: Original result
    Note over Inner: Modify result<br/>context.setResult(...)
    Inner-->>-Outer: Modified result
    Note over Outer: Modify result<br/>context.setResult(...)
    Outer-->>-Host: Final result
Loading

Each context.call goes a layer deeper, eventually reaching the original call. context.call will then bring the return value back up from the layer it called. Each layer can then decide if it wants to do something with that returned value or potentially even replace it entirely with context.setResult. If a layer does not call context.setResult, it returns null.

Next up, we can look at an example where Patch A does not perform context.call and skips Patch B and the original call because of it. This can make sense in some cases. You should just be aware that it may conflict with other patches. You may want to adjust Patch A's order accordingly.

sequenceDiagram
    participant Host as Hosting method
    participant Outer as Patch A (order 0)
    participant Inner as Patch B (order 100)
    participant Original as Original call

    Host->>+Outer: Intercepted call
    Note over Outer: context.call() is not used<br/>context.setResult(...)
    Note over Inner,Original: Never called
    Outer-->>-Host: Patch A result
Loading

Context

Each redirect type has a specific context that has to be used for its patch handler:

  • @RedirectMethodCall > MethodCallContext
  • @RedirectConstructorCall > ConstructorCallContext
  • @RedirectFieldWrite > FieldWriteContext
  • @RedirectFieldRead > FieldReadContext

Clone this wiki locally