ruipin / fvtt-lib-wrapper

Library for Foundry VTT which provides module developers with a simple way to modify core Foundry VTT code, while reducing the likelihood of conflict with other modules.
GNU Lesser General Public License v3.0
35 stars 15 forks source link

Question: Help with a possible async issue #55

Closed caewok closed 2 years ago

caewok commented 3 years ago

Confirm you have read the above I am the libRuler developer. I am working on a fork of Drag Ruler that uses libRuler, and I am having an issue that may be related to libWrapper and async, but I am not sure. This is bit involved for a discord question and is rather libWrapper-specific, so I thought I would try here instead.

Setup Foundry 0.8.9 libWrapper 1.10.5.0 Drag Ruler fork: https://github.com/caewok/foundryvtt-drag-ruler/tree/caewok-libruler libRuler: https://github.com/caewok/fvtt-lib-ruler

Describe the question When dragging one or more tokens, I am seeing instances where the token moves places it should not. Sometimes, the token ends up at a different location than expected (usually seen with groups, where the group starts in a tight square and ends up spread out). Sometimes, the token drifts and then jumps back to a position. This, to me, suggests a problem with how async is being used to update token positions.

I reviewed issue #7 and have tried various versions of the following code, but this problem still persists. So I was hoping that you might have some insight into what might be going wrong here.

Code flow I think what should be happening is:

Additional context Sorry, this will be somewhat involved in order to explain relevant the code steps...

libRuler overrides Ruler.prototype.moveToken:

libWrapper.register(MODULE_ID, 'Ruler.prototype.moveToken', libRulerMoveToken, 'OVERRIDE');

The key part of the override for this issue, I think, is where the libRuler version iterates through the ruler segments (lines between ruler waypoints), and calls a new method, animateToken:

for ( let [i, r] of rays.entries() ) {
      // Break the movement if the game is paused
      if ( !wasPaused && game.paused ) break;

      // Break the movement if Token is no longer located at the prior destination (some other change override this)
      if ( priorDest && ((token.data.x !== priorDest.x) || (token.data.y !== priorDest.y)) ) break;

      priorDest = await this.animateToken(token, r, dx, dy, i + 1); // increment by 1 b/c first segment is 1.
    }   

libRuler adds to the new method to the Ruler class :

Object.defineProperty(Ruler.prototype, "animateToken", {
  value: libRulerAnimateToken,
  writable: true,
  configurable: true
});

export async function libRulerAnimateToken(token, ray, dx, dy, segment_num) {
  log(`Animating token for segment_num ${segment_num}`);

  // Adjust the ray based on token size
  const dest = canvas.grid.getTopLeft(ray.B.x, ray.B.y);
  const path = new Ray({x: token.data.x, y: token.data.y}, {x: dest[0] + dx, y: dest[1] + dy});

  // Commit the movement and update the final resolved destination coordinates
  const priorDest = duplicate(path.B); // resolve issue #3; get prior dest before update.
  await token.document.update(path.B);
  path.B.x = token.data.x;
  path.B.y = token.data.y;

  // Update the path which may have changed during the update, and animate it
  await token.animateMovement(path);

  return priorDest;
}

Now, Drag Ruler wraps moveToken and animateToken:

libWrapper.register(settingsKey, "Ruler.prototype.moveToken", dragRulerMoveToken, "MIXED");
libWrapper.register(settingsKey, "Ruler.prototype.animateToken", dragRulerAnimateToken, "WRAPPER");

Drag Ruler wraps moveToken because until the dragged token is let go, no actual movement should happen. The relevant portion is:

async function dragRulerMoveToken(wrapped) {
    if(!this.isDragRuler || this._state === Ruler.STATES.MOVING) {
        const p1 = wrapped();
        const res = await p1;
        return res;
    }
      ...

Drag Ruler wraps animateToken so it can handle adjustments for dragging multiple tokens. The relevant portion is:

async function dragRulerAnimateToken(wrapped, token, ray, dx, dy, segment_num) { 
...
  const promises = [];
  for(const entity of selectedEntities) {
    //const top_left = canvas.grid.getTopLeft(entity.center.x, entity.center.y);
    const offset = { x: entity.center.x - token.center.x, y: entity.center.y - token.center.y };
    const offsetRay = new Ray(ray.A, { x: ray.B.x + offset.x, y: ray.B.y + offset.y });

    // don't await here b/c this makes the tokens all move one-by-one
    promises.push(wrapped(entity, offsetRay, dx, dy, segment_num)); // usually works, but can occassionally fail to keep multiple tokens in original arrangement
    //promises.push(wrapped(entity, ray, dx + offset.x, dy + offset.y, segment_num)); // Fails to keep tokens in original arrangement much of the time
  }

  // use Promise.all to await to avoid confusing move errors if moving repeatedly very quickly
  promises.push(wrapped(token, ray, dx, dy, segment_num));
  const res = await Promise.allSettled(promises);

  // need to return the path from the movement for the token
  return res[res.length -1].value;  // Promise.allSettled returns an array of [{status: , value: }]; Promise.all returns an array
}
ruipin commented 3 years ago

I doubt this is directly related to libWrapper - all libWrapper does is detect a Promise, and if it is one it then delays the cleanup with a then call. Essentially, all Promise-related code is delegated to the callee/caller.

Every single unit/integration test in the suite will run both with async and not-async code paths (and randomised settlement delays), so this code should be well exercised.

The relevant libWrapper code can be found in https://github.com/ruipin/fvtt-lib-wrapper/blob/master/src/lib/wrapper.js#L480-L496

My immediate suspicion is the fact that Drag Ruler seems to be calling wrapped multiple times inside its animateToken wrapper, and then waiting for the Promises to settle in parallel:

If it is indeed the browser breaking the Promise resolution order for whatever reason, you might need to detect an on-going animation, and automatically settle/cancel the previous Promises (or ensure your new Promises are a then of the previous ones).

See for example how Token-Animation-Tools does the animation cancellation: https://github.com/ruipin/fvtt-token-animation-tools/blob/master/token-animation-tools.js#L168-L193

That said, I would suggest a few things that could allow us to remove libWrapper from the equation:

ruipin commented 2 years ago

Closing since I believe this has been answered, and there have been no further replies in over two months. Feel free to re-open if you wish to continue the discussion.