ruipin/fvtt-lib-wrapper

Question re wrapping order

Closed this issue · 2 comments

Confirm you have read the above
I am developing a Foundry module that needs to override a method wrapped by another module, and I am getting an unexpected result.

Describe the question
The Wall Height module wraps ClockwiseSweepPolygon.prototype._testWallInclusion.

libWrapper.register(MODULE_ID, "ClockwiseSweepPolygon.prototype._testWallInclusion", testWallInclusion, "WRAPPER", { perf_mode: "FAST" });

In the Wall Height implementation, it calls the wrapper, then depending on the result, runs some tests and returns a new value:

 function testWallInclusion(wrapped, ...args) {
    if (!wrapped(...args)) return false;
    // ... run tests 
    // ... return true or false 
  } 

I thought I could just override ClockwiseSweepPolygon.prototype._testWallInclusion to use my own test, but that is not working because the override is getting called before Wall Height's wrapper. My override looks like this:

libWrapper.register(MODULE_ID, "ClockwiseSweepPolygon.prototype._testWallInclusion", _testWallInclusionClockwisePolygonSweep, libWrapper.OVERRIDE, {perf_mode: libWrapper.PERF_FAST});

When adding some logging, I see that the order looks like:

  1. Wall Height testWallInclusion function is called.
  2. In Wall Height's testWallInclusion, it calls the wrapped method.
  3. Apparently as a result, my OVERRIDE method is called.
  4. Control then returns to Wall Height's testWallInclusion and thus it changes the result of my OVERRIDE.

From the libWrapper descriptions, I would have expected my OVERRIDE to be called last or otherwise prevent further modification of the return value of ClockwiseSweepPolygon.prototype._testWallInclusion. While I see that my OVERRIDE is technically called last in the chain, this is a very problematic result because my OVERRIDE is basically being ignored.

Is there something I can do here to get the OVERRIDE to result in ClockwiseSweepPolygon.prototype._testWallInclusion returning my value, and not the value from the Wall Height wrapper?

There seems to be a slight misunderstanding about what is meant by "called last".

OVERRIDE functions are indeed guaranteed to be called last in the chain. However, there are two parts of the chain, let's call them "pre-call" (before any return) and post-call (after any return). This is due to how Javascript functions work, the returns execute in reverse order from the function calls. Being called last (i.e. executing the "precall" first) directly and unavoidably implies returning (executing the "postcall") first.

For instance, let's assume we have a method called Foo.prototype.bar, and three modules m1 (WRAPPER), m2 (MIXED), and m3 (MIXED).

Let's define |-> as "calls wrapped(...args)", and |<- as "function returns".

The call chain will be:


user code calls `Foo.prototype.bar()`
----------- call start (precall)
|-> m1
    |-> m2
        |-> m3
----------- original method call
            |-> Foo.prototype.bar
            |<-
----------- postcall
            m3
        |<-
        m2
    |<-
    m1
|<-
----------- call complete
user code

Note that this is an unavoidable artifact of how Javascript works. The only way you can change the return flow is by throwing an exception.

  • WRAPPER functions are guaranteed to be the first called during the precall, and last called during the postcall. In other words, WRAPPER functions are the furthest away from the original method.

The API also prohibits them from returning before calling wrapped(), so all WRAPPER functions are guaranteed to see both a precall and postcall (excluding exceptions being thrown).

  • MIXED functions will be called after any WRAPPER functions during the precall, but will execute their postcall first. In other words, MIXED functions are the closest ones to the original method.

Because any MIXED function can initiate the postcall by returning without calling wrapped, there is no guarantee MIXED functions will ever be called.

For instance, if m2 returned without calling wrapped, we'd have the following call chain (note the absence of m3 and the original method):

user code calls `Foo.prototype.bar()`
----------- call start (precall)
|-> m1
    |-> m2
    |<-
----------- postcall
    m1
|<-
----------- call complete
user code
  • OVERRIDE functions will take the place of the original method. In other words, they never call the original method, and are called last during the precall, and called first during the postcall.

For instance, back to the first example call chain, but if m3 was actually OVERRIDE (rather than MIXED), we'd have the following call chain (note the absence of the original method):

user code calls `Foo.prototype.bar()`
----------- call start (precall)
|-> m1
    |-> m2
----------- OVERRIDE call
        |-> m3
        |<-
----------- postcall
        m2
    |<-
    m1
|<-
----------- call complete
user code

Now that this is explained, let's discuss this exact case.

The module Wall Height registers a WRAPPER function, and your module an OVERRIDE. As such, we have the following call chain:

user code calls `ClockwiseSweepPolygon.prototype._testWallInclusion`
----------- call start (precall)
|-> Wall Height
    |-> Your Module
    |<-
----------- postcall
    Wall Height
|<-
----------- call complete
user code

By registering an OVERRIDE, you've guaranteed you are able to replace the original method call, by being called last, but as a direct consequence you're also the "lowest in the rung" when it comes to deciding what the final result will be. This is by design, in order to avoid having modules "skipped over" when someone wishes to modify the original function, but also a consequence of how Javascript works.

There is a question now, why do you need to override Wall Height's test result? I am not sure exactly what your use case is, but you are overriding the Foundry test, and then Wall Height is adding extra information to it. This behaviour ensures that no module's behaviour is "skipped over", exactly as designed. The WRAPPER function from Wall Height is simply "enhancing" the function, i.e. adding extra functionality.

That said, in terms of solutions if you really wanted to completely override everything, including the Wall Height behaviour, the only way to ensure it works reliably is to work together with the developer of Wall Height and add a way to have it skip over registering said WRAPPER (or otherwise modifying the return result), for example through an API/setting, or when it detects your module being active, or whatever. You could also modify the calling function (i.e. the "user code" in the above diagrams) instead.


Hopefully this helps, let me know if you have further questions.

Yeah, that makes sense and was sort of what I was afraid of.

It is sort of a unique case. Wall Height is not just adding extra functionality, it is actually changing the result (so more of mixed than a wrap). Foundry's LOS sweep can ignore irrelevant walls. Wall Height is removing walls that it thinks are irrelevant because they are lower than the vision source. I will ultimately remove the walls from the sweep just like Wall Height does, but first I need to know which walls will be removed, because I do something with them later. Re-running the wall filter is one option, but not particularly performant.

For now, I will probably override a higher-level function to get what I need, and revisit this later with the Wall Height author. (If I ever manage to get this module to a releasable state! )

Thanks for the very thorough explanation!