_parseStepActionGraph(sdkStepInterface) {
    // get root action for step
    const sdkStep = sdkStepInterface.getSdkStep();
    const rootAction = sdkStep.getAction();
    const rootActionIndex = rootAction.getIndex();

    // check if the first sequence of this step should show all together
    let stepFirstSequenceShowsAllAtOnce = false;
    if (rootAction instanceof SDK.ApplyCardToBoardAction && !rootAction.getIsImplicit()) {
      const card = rootAction.getCard();
      if (card instanceof SDK.Spell) {
        // spells should always sequence as one
        stepFirstSequenceShowsAllAtOnce = true;
      }
    }

    // flatten depth first actions into mix of breadth first and depth first
    let currentActionsFlattening = [rootAction];
    let currentActionsToFlatten = [];
    let nonModifierActionInterfaces = [];
    let modifierActionInterfaces = [];
    while (currentActionsFlattening.length > 0) {
      // shift current action from the list
      var currentAction = currentActionsFlattening.shift();
      const currentActionIndex = currentAction.getIndex();

      // only process actions we've not yet processed
      // skip actions with no sub actions when they are targeting cards in deck
      var sdkActionInterface = this._actionInterfacesByIndex[currentActionIndex];
      if (sdkActionInterface == null && (currentActionIndex === rootActionIndex || !this._getIsActionSkippable(currentAction))) {
        // create action interface for action and map to step
        sdkActionInterface = this._actionInterfacesByIndex[currentActionIndex] = new SdkActionInterface(currentAction);
        sdkActionInterface.setSdkStepInterface(sdkStepInterface);

        // get sub actions, but not resolve sub actions
        // we want to make sure that all actions get shown
        // and there are cases where resolve sub actions are tied to a previous step (ex: followups)
        const subActions = currentAction.getSubActions().slice(0);

        // convert and cache all modifier apply/remove/trigger to their own actions
        this._createModifierActionInterfacesForActionInterface(sdkActionInterface);

        // add current action
        if (currentAction.getCreatedByTriggeringModifier() || currentAction instanceof SDK.ApplyModifierAction || currentAction instanceof SDK.RemoveModifierAction) {
          // action is a modifier action when created by a triggering modifier and not forced to be in the first sequence of actions
          modifierActionInterfaces.push(sdkActionInterface);
        } else {
          // action is created by another action not related to modifiers
          nonModifierActionInterfaces.push(sdkActionInterface);
        }

        // add current action triggered modifier actions
        if (sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces.length > 0) {
          modifierActionInterfaces = modifierActionInterfaces.concat(sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces);
        }

        // cache resolve sub actions and filter
        const resolveSubActions = currentAction.getResolveSubActions();
        if (currentAction instanceof SDK.StopBufferingEventsAction) {
          sdkActionInterface.cachedResolveSubActions = _.union(subActions, _.filter(resolveSubActions, (resolveSubAction) => !this._getIsActionSkippable(resolveSubAction)));
        } else {
          sdkActionInterface.cachedResolveSubActions = [];
          for (var i = 0, il = resolveSubActions.length; i < il; i++) {
            const resolveSubAction = resolveSubActions[i];
            if (!this._getIsActionSkippable(resolveSubAction)) {
              var parentAction = resolveSubAction.getParentAction();
              // don't allow sub actions that belong to an end followup action
              if (!(parentAction instanceof SDK.StopBufferingEventsAction)) {
                sdkActionInterface.cachedResolveSubActions.push(resolveSubAction);
              }
            }
          }
        }

        // find all depth first actions
        for (var i = subActions.length - 1; i >= 0; i--) {
          const subAction = subActions[i];
          if (subAction.getIsDepthFirst()) {
            // add to depth first actions
            currentActionsFlattening.unshift(subAction);

            // remove from sub actions
            subActions.splice(i, 1);
          }
        }

        // add any remaining sub actions for breadth first replay
        if (subActions.length > 0) {
          currentActionsToFlatten = currentActionsToFlatten.concat(subActions);
        }
      }

      if (currentActionsFlattening.length === 0) {
        currentActionsFlattening = currentActionsToFlatten;
        currentActionsToFlatten = [];
      }
    }

    // gather all first sequence actions
    // an action is in the first sequence if it is either the root or one of the root's sub actions
    let firstSequenceActionInterfaces = [];
    for (var i = 0, il = nonModifierActionInterfaces.length; i < il; i++) {
      var currentActionInterface = nonModifierActionInterfaces[i];
      currentActionInterface.isFirstSequence = true;
      firstSequenceActionInterfaces.push(currentActionInterface);
    }

    // set root as root of first sequence
    const rootActionInterface = this._actionInterfacesByIndex[rootActionIndex];
    rootActionInterface.isFirstSequenceRoot = true;
    rootActionInterface.isSequenceRoot = true;
    rootActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(rootAction);
    rootActionInterface.isSequenceSequencedAsOne = rootActionInterface.isSequencedAsOne;

    // force some flattened actions to be the root of a new sequence
    // unless the first sequence is showing all at once
    if (stepFirstSequenceShowsAllAtOnce) {
      for (var i = 0, il = nonModifierActionInterfaces.length; i < il; i++) {
        var currentActionInterface = nonModifierActionInterfaces[i];
        if (currentActionInterface.isFirstSequence) {
          currentActionInterface.isFirstSequenceAndSequencedAsOne = true;
        }
      }
    }

    // setup sequence roots and rearrange modifier actions
    for (var i = 0, il = modifierActionInterfaces.length; i < il; i++) {
      var sdkActionInterface = modifierActionInterfaces[i];
      var currentAction = sdkActionInterface.getSdkAction();
      var parentAction = currentAction.getParentAction();
      var resolveParentAction = parentAction instanceof SDK.StopBufferingEventsAction ? parentAction : currentAction.getResolveParentAction();
      if (resolveParentAction != null) {
        var resolveParentActionInterface = this._actionInterfacesByIndex[resolveParentAction.getIndex()];
        if (resolveParentActionInterface != null
          && resolveParentActionInterface.rearrangedActionInterfaces == null && !resolveParentActionInterface.rearranging
          && ((resolveParentActionInterface.cachedResolveTriggeredModifierActionInterfaces && resolveParentActionInterface.cachedResolveTriggeredModifierActionInterfaces.length > 0)
            || currentAction instanceof SDK.ApplyModifierAction || currentAction instanceof SDK.RemoveModifierAction)) {
          // when the parent action triggers modifiers, get all of its actions depth first
          var rearrangedActionInterfaces = this._getActionInterfacesDepthFirstForTriggeringSequence(resolveParentActionInterface);

          // make parent action a new sequence root
          var sequenceRootSdkActionInterface;
          if (resolveParentAction.getTriggeringModifier() instanceof SDK.ModifierStrikeback) {
            sequenceRootSdkActionInterface = rearrangedActionInterfaces.shift();
          } else {
            sequenceRootSdkActionInterface = resolveParentActionInterface;
          }
          const sequenceRootAction = sequenceRootSdkActionInterface.getSdkAction();
          sequenceRootSdkActionInterface.rearrangedActionInterfaces = rearrangedActionInterfaces;
          sequenceRootSdkActionInterface.isSequenceRoot = true;
          sequenceRootSdkActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(sequenceRootAction);
          sequenceRootSdkActionInterface.isSequenceSequencedAsOne = true;

          if (resolveParentActionInterface.isFirstSequenceAndSequencedAsOne
            && this.getIsActionRemoval(resolveParentAction)) {
            // parent action is part of first sequence as one and it is a removal action
            // remove parent action and any rearranged actions from flattened actions and first sequence actions
            // and move everything to the end of the first sequence
            nonModifierActionInterfaces = _.difference(nonModifierActionInterfaces, rearrangedActionInterfaces);
            nonModifierActionInterfaces = _.without(nonModifierActionInterfaces, resolveParentActionInterface);
            firstSequenceActionInterfaces = _.difference(firstSequenceActionInterfaces, rearrangedActionInterfaces);
            firstSequenceActionInterfaces = _.without(firstSequenceActionInterfaces, resolveParentActionInterface);
            nonModifierActionInterfaces.push(sequenceRootSdkActionInterface);
            firstSequenceActionInterfaces.push(sequenceRootSdkActionInterface);
          }

          // check that sequence root is valid
          if (sequenceRootSdkActionInterface != null && !sequenceRootSdkActionInterface.isFirstSequenceRoot) {
            let isInvalidSequenceRoot = false;
            if (sequenceRootAction.getTriggeringModifierIndex() != null) {
              // actions created by a triggering modifier are not valid sequence roots
              isInvalidSequenceRoot = true;
            } else if (sequenceRootAction instanceof SDK.ApplyCardToBoardAction && sequenceRootAction.getIsValidApplication()) {
              // apply card to board actions that only trigger first blood are invalid sequence roots
              let hasValidTriggeringModifier = false;
              for (var j = 0, jl = rearrangedActionInterfaces.length; j < jl; j++) {
                const rearrangedActionInterface = rearrangedActionInterfaces[j];
                const rearrangedAction = rearrangedActionInterface.getSdkAction();
                if (rearrangedAction instanceof ModifierTriggeredAction && !(rearrangedAction.getModifier() instanceof SDK.ModifierFirstBlood)) {
                  hasValidTriggeringModifier = true;
                  break;
                }
              }
              isInvalidSequenceRoot = !hasValidTriggeringModifier;
            }

            if (isInvalidSequenceRoot) {
              sequenceRootSdkActionInterface.isSequenceRoot = sequenceRootSdkActionInterface.isSequencedAsOne = sequenceRootSdkActionInterface.isSequenceSequencedAsOne = false;
            }
          }
        } else if (!sdkActionInterface.rearranging) {
          // this modifier action has not yet been rearranged
          // if parent is rearranging, get root rearranged action
          if (resolveParentActionInterface.rearranging) {
            resolveParentActionInterface = this._getRootDepthFirstRearrangedActionInterface(resolveParentActionInterface);
          }

          // add actions to the flattened actions (it must not exist there yet)
          if (resolveParentActionInterface.isFirstSequenceAndSequencedAsOne && sdkActionInterface.isSequenceRoot) {
            // parent is a part of the first sequence, add this new sequence to the end of first sequence
            nonModifierActionInterfaces.splice(_.indexOf(nonModifierActionInterfaces, firstSequenceActionInterfaces[firstSequenceActionInterfaces.length - 1]) + 1, 0, sdkActionInterface);
            firstSequenceActionInterfaces.push(sdkActionInterface);
          } else {
            const index = _.indexOf(nonModifierActionInterfaces, resolveParentActionInterface);
            if (index !== -1) {
              nonModifierActionInterfaces.splice(index + 1, 0, sdkActionInterface);
            } else {
              nonModifierActionInterfaces.push(sdkActionInterface);
            }
          }
        }
      }
    }

    // add all sequences
    // order is reversed so the smaller sequences (more accurate) can be added first
    let actionInterfaceSequence = [];
    var addSequencedActions = function (actionInterfaces, rearranged) {
      for (let i = actionInterfaces.length - 1; i >= 0; i--) {
        const sdkActionInterface = actionInterfaces[i];
        const { rearrangedActionInterfaces } = sdkActionInterface;
        if ((!sdkActionInterface.rearranging || rearranged || (rearrangedActionInterfaces != null && rearrangedActionInterfaces.length > 0)) && !_.contains(actionInterfaceSequence, sdkActionInterface)) {
          if (rearrangedActionInterfaces != null) {
            addSequencedActions(rearrangedActionInterfaces, true);
          }
          actionInterfaceSequence.unshift(sdkActionInterface);
        }
      }
    };
    addSequencedActions(nonModifierActionInterfaces);

    // handle triggering modifier actions
    for (var i = actionInterfaceSequence.length - 1; i >= 0; i--) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var currentAction = sdkActionInterface.getSdkAction();
      if (currentAction instanceof ModifierTriggeredAction) {
        const modifier = currentAction.getModifier();
        if (modifier instanceof SDK.Modifier) {
          var resolveParentAction = currentAction.getResolveParentAction();
          var resolveParentActionInterface = this._actionInterfacesByIndex[resolveParentAction.getIndex()];
          let nextActionAfterTriggeringGroupIsSequenceRoot = false;

          // some triggering modifiers must always be followed by a new sequence
          if (resolveParentAction instanceof SDK.AttackAction
            || resolveParentAction instanceof SDK.DamageAsAttackAction
            || (resolveParentActionInterface === rootActionInterface && resolveParentActionInterface.isFirstSequenceAndSequencedAsOne)) {
            nextActionAfterTriggeringGroupIsSequenceRoot = true;
          }

          // some triggering modifiers must always be the root of a new sequence
          if ((resolveParentAction instanceof SDK.AttackAction || resolveParentAction instanceof SDK.DamageAsAttackAction)
            && !(modifier instanceof SDK.ModifierFrenzy || modifier instanceof SDK.ModifierBlastAttack)) {
            sdkActionInterface.isSequenceRoot = true;
            sdkActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(currentAction);
            sdkActionInterface.isSequenceSequencedAsOne = true;
          }

          // in some cases the the next action following trigger actions should be a sequence root
          if (nextActionAfterTriggeringGroupIsSequenceRoot) {
            // find next action that is not a modifier action or has a triggering modifier index
            for (var j = i, jl = actionInterfaceSequence.length; j < jl; j++) {
              var nextActionInterface = actionInterfaceSequence[j];
              var nextAction = nextActionInterface.getSdkAction();
              let nextActionRootAction = nextAction;
              let nextActionResolveParentAction = nextAction.getResolveParentAction();
              while (nextActionResolveParentAction != null && nextActionResolveParentAction != resolveParentAction) {
                nextActionRootAction = nextActionResolveParentAction;
                nextActionResolveParentAction = nextActionResolveParentAction.getResolveParentAction();
              }
              if (!(nextActionRootAction instanceof ModifierAction) && nextActionRootAction.getTriggeringModifierIndex() == null) {
                nextActionInterface.isSequenceRoot = true;
                nextActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(nextAction);
                nextActionInterface.isSequenceSequencedAsOne = true;
                break;
              }
            }
          }
        }
      }
    }

    // ensure certain actions directly follow their parent action
    for (var i = 0, il = actionInterfaceSequence.length; i < il; i++) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var currentAction = sdkActionInterface.getSdkAction();

      if (this._getActionAlwaysFollowsParent(currentAction)) {
        var resolveParentAction = currentAction.getResolveParentAction();
        if (resolveParentAction != null) {
          var resolveParentActionInterface = this._actionInterfacesByIndex[resolveParentAction.getIndex()];
          const indexOfResolveParentAction = _.indexOf(actionInterfaceSequence, resolveParentActionInterface);
          if (i > indexOfResolveParentAction + 1) {
            actionInterfaceSequence.splice(i, 1);
            actionInterfaceSequence.splice(indexOfResolveParentAction + 1, 0, sdkActionInterface);
          }

          // don't allow this action to be a sequence root
          sdkActionInterface.isSequenceRoot = sdkActionInterface.isSequencedAsOne = sdkActionInterface.isSequenceSequencedAsOne = false;
          sdkActionInterface.isFirstSequence = sdkActionInterface.isFirstSequenceAndSequencedAsOne = false;

          // resolve parent action should always be a sequence root
          if (!resolveParentActionInterface.isSequenceRoot) {
            resolveParentActionInterface.isSequenceRoot = true;
            resolveParentActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(resolveParentAction);
            resolveParentActionInterface.isSequenceSequencedAsOne = true;
          }
        }
      }
    }

    // check that all sequences are followed correctly by a sequence root
    // do this backwards so we aren't causing new sequence root checks
    for (var i = actionInterfaceSequence.length - 1; i >= 0; i--) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var { rearrangedActionInterfaces } = sdkActionInterface;

      // check following sequence root
      if (sdkActionInterface.isSequenceRoot && rearrangedActionInterfaces != null && rearrangedActionInterfaces.length > 0) {
        const nextSequenceRootIndex = i + rearrangedActionInterfaces.length + 1;
        const nextSequenceRootActionInterface = actionInterfaceSequence[nextSequenceRootIndex];
        if (nextSequenceRootActionInterface && !nextSequenceRootActionInterface.isSequenceRoot) {
          nextSequenceRootActionInterface.isSequenceRoot = true;
          nextSequenceRootActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(nextSequenceRootActionInterface.getSdkAction());
          nextSequenceRootActionInterface.isSequenceSequencedAsOne = true;
        }
      }
    }

    // when this step ends the game, cache all actions that end the game
    const isGameEndingStep = sdkStep === SDK.GameSession.getInstance().getGameEndingStep();
    if (isGameEndingStep) {
      sdkStepInterface.actionsEndingGame = [];
    }

    // ensure certain actions are always sequence roots
    // this is done before sequence sorting to ensure correct ordering
    const followupTriggerActionMap = {};
    for (var i = 0, il = actionInterfaceSequence.length; i < il; i++) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var currentAction = sdkActionInterface.getSdkAction();
      var sequenceRootSdkActionInterface = null;
      var resolveParentAction = null;
      if (this._getActionIsAlwaysSequenceRoot(currentAction)) {
        sequenceRootSdkActionInterface = sdkActionInterface;

        // both triggering modifier action and this action should be sequence roots
        const { rearrangingParentActionInterface } = sdkActionInterface;
        const rearrangingParentAction = rearrangingParentActionInterface && rearrangingParentActionInterface.getSdkAction();
        if (rearrangingParentAction instanceof ModifierTriggeredAction && !rearrangingParentActionInterface.isSequenceRoot) {
          rearrangingParentActionInterface.isSequenceRoot = true;
          rearrangingParentActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(rearrangingParentAction);
          rearrangingParentActionInterface.isSequenceSequencedAsOne = true;
        }

        // when revealing a hidden card, the action following this one should be a sequence root
        if (currentAction instanceof SDK.RevealHiddenCardAction && actionInterfaceSequence.length > i + 1) {
          var nextActionInterface = actionInterfaceSequence[i + 1];
          var nextAction = nextActionInterface.getSdkAction();
          nextActionInterface.isSequenceRoot = true;
          nextActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(nextAction);
          nextActionInterface.isSequenceSequencedAsOne = true;
        }
      } else if (currentAction.getParentAction() instanceof SDK.StopBufferingEventsAction) {
        const resolveParentActionIndex = currentAction.getResolveParentActionIndex();
        if (followupTriggerActionMap[resolveParentActionIndex] == null) {
          followupTriggerActionMap[resolveParentActionIndex] = true;
          sequenceRootSdkActionInterface = sdkActionInterface;
        }
      }

      // search for actions that end the game
      // these actions should be cached and forced into sequence roots
      if (isGameEndingStep) {
        if (currentAction instanceof SDK.RemoveAction) {
          const target = currentAction.getTarget();
          // cache actions that are killing current or previous generals
          // in the case that generals were swapped during this step, visually we need to show them all dying
          if (target instanceof SDK.Entity && (target.getIsGeneral() || target.getWasGeneral())) {
            sdkStepInterface.actionsEndingGame.unshift(currentAction);
            sequenceRootSdkActionInterface = sdkActionInterface;
          }
        }
      }

      if (sequenceRootSdkActionInterface != null && !sequenceRootSdkActionInterface.isSequenceRoot) {
        sequenceRootSdkActionInterface.isSequenceRoot = true;
        sequenceRootSdkActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(sequenceRootSdkActionInterface.getSdkAction());
        sequenceRootSdkActionInterface.isSequenceSequencedAsOne = true;
      }
    }

    // cross-link all sequences
    let forcedSortIndex = null;
    let sortedActionInterfaces = [];
    let currentSequenceRootSdkActionInterface;
    let currentSequenceActionInterfaces;
    for (var i = 0, il = actionInterfaceSequence.length; i < il; i++) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var currentAction = sdkActionInterface.getSdkAction();
      if (sdkActionInterface.isSequenceRoot) {
        sortedActionInterfaces.push(sdkActionInterface);
        currentSequenceRootSdkActionInterface = sdkActionInterface;
        currentSequenceActionInterfaces = sdkActionInterface.sequenceActionInterfaces = [];

        // force sort index of sequence roots to be right before parent action when overwatch triggers
        // unless overwatch was triggered by a played card
        if (currentAction instanceof ModifierTriggeredAction
            && currentAction.getModifier() instanceof SDK.ModifierOverwatch
            && !(currentAction.getResolveParentAction() instanceof SDK.ApplyCardToBoardAction)) {
          sdkActionInterface.forcedSortIndex = forcedSortIndex = currentAction.getParentActionIndex() - 0.1;
        } else if (forcedSortIndex != null) {
          sdkActionInterface.forcedSortIndex = forcedSortIndex;
        }
      } else {
        // reset forced sort index when done buffering events
        if (currentAction instanceof SDK.StopBufferingEventsAction) {
          forcedSortIndex = null;
        }

        if (currentAction instanceof ModifierAction
          || currentAction instanceof SDK.ApplyModifierAction
          || currentAction instanceof SDK.RemoveModifierAction) {
          let parentSdkActionInterface = sdkActionInterface;
          var parentAction = currentAction;
          while (parentAction instanceof ModifierAction
          || parentAction instanceof SDK.ApplyModifierAction
          || parentAction instanceof SDK.RemoveModifierAction) {
            const parentOfParentAction = parentAction.getParentAction();
            const resolveParentOfParentAction = parentOfParentAction instanceof SDK.StopBufferingEventsAction ? parentOfParentAction : parentAction.getResolveParentAction();
            parentSdkActionInterface = this._actionInterfacesByIndex[resolveParentOfParentAction.getIndex()];
            parentAction = parentSdkActionInterface && parentSdkActionInterface.getSdkAction();
          }
          if (parentSdkActionInterface != null && parentSdkActionInterface != sdkActionInterface) {
            sdkActionInterface.sequenceRootSdkActionInterface = parentSdkActionInterface;
            if (parentSdkActionInterface.sequenceActionInterfaces == null) {
              parentSdkActionInterface.sequenceActionInterfaces = [];
            }
            parentSdkActionInterface.sequenceActionInterfaces.push(sdkActionInterface);
          } else {
            sdkActionInterface.sequenceRootSdkActionInterface = currentSequenceRootSdkActionInterface;
            currentSequenceActionInterfaces.push(sdkActionInterface);
          }
        } else {
          currentSequenceRootSdkActionInterface.sequenceActionInterfacesAreOnlyForModifiers = false;
          sdkActionInterface.sequenceRootSdkActionInterface = currentSequenceRootSdkActionInterface;
          currentSequenceActionInterfaces.push(sdkActionInterface);
        }
      }
    }

    // sort all sequence roots by index
    sortedActionInterfaces = _.sortBy(sortedActionInterfaces, (sdkActionInterface) => {
      const { forcedSortIndex } = sdkActionInterface;
      if (forcedSortIndex != null) {
        return forcedSortIndex;
      }
      const currentAction = sdkActionInterface.getSdkAction();
      if (currentAction instanceof ModifierAction) {
        // use parent action index plus 0.5 as the minimum possible for the next action would be plus 1.0
        // and we want all modifier pseudo-actions to directly follow their parent actions
        return currentAction.getParentActionIndex() + 0.1;
      }
      return currentAction.getIndex();
    });

    // reset actionInterfaceSequence
    actionInterfaceSequence = [];

    // add all sorted sequences
    var addSortedSequence = function (sdkActionInterface) {
      actionInterfaceSequence.push(sdkActionInterface);
      const { sequenceActionInterfaces } = sdkActionInterface;
      if (sequenceActionInterfaces != null && sequenceActionInterfaces.length > 0) {
        for (let i = 0, il = sequenceActionInterfaces.length; i < il; i++) {
          addSortedSequence(sequenceActionInterfaces[i]);
        }
      }
    };
    for (var i = 0, il = sortedActionInterfaces.length; i < il; i++) {
      addSortedSequence(sortedActionInterfaces[i]);
    }

    // complete flattening
    for (var i = actionInterfaceSequence.length - 1; i >= 0; i--) {
      var sdkActionInterface = actionInterfaceSequence[i];
      var currentAction = sdkActionInterface.getSdkAction();

      // add all modifiers that were deactivated/activated just after the action they were removed/added by
      let afterActionModifierActionInterfaces = [];
      if (sdkActionInterface.cachedResolveDeactivatedModifierActionInterfaces != null && sdkActionInterface.cachedResolveDeactivatedModifierActionInterfaces.length > 0) {
        afterActionModifierActionInterfaces = afterActionModifierActionInterfaces.concat(sdkActionInterface.cachedResolveDeactivatedModifierActionInterfaces);
      }
      if (sdkActionInterface.cachedResolveActivatedModifierActionInterfaces != null && sdkActionInterface.cachedResolveActivatedModifierActionInterfaces.length > 0) {
        afterActionModifierActionInterfaces = afterActionModifierActionInterfaces.concat(sdkActionInterface.cachedResolveActivatedModifierActionInterfaces);
      }
      if (afterActionModifierActionInterfaces.length > 0) {
        Array.prototype.splice.apply(actionInterfaceSequence, [i + 1, 0].concat(afterActionModifierActionInterfaces));
      }

      // add all modifiers that changed an action just before the action they changed
      const triggeredModifierActionInterfacesForChanges = sdkActionInterface.cachedTriggeredModifierActionInterfacesForChanges;
      if (triggeredModifierActionInterfacesForChanges && triggeredModifierActionInterfacesForChanges.length > 0) {
        Array.prototype.splice.apply(actionInterfaceSequence, [i, 0].concat(triggeredModifierActionInterfacesForChanges));

        // setup sequencing
        const firstChangeActionInterface = triggeredModifierActionInterfacesForChanges[0];
        firstChangeActionInterface.isSequenceRoot = firstChangeActionInterface.isSequencedAsOne = firstChangeActionInterface.isSequenceSequencedAsOne = sdkActionInterface.isSequenceRoot;
      }

      // ensure some actions are never sequence roots
      // unless action caused modifiers to trigger
      // this is done after all sequence sorting to ensure correct ordering
      if (sdkActionInterface.isSequenceRoot
        && (!this._getActionIsAlwaysSequenceRoot(currentAction)
          || this._getActionIsExceptionToSequenceRoot(currentAction))
        && (sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces == null
          || sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces.length === 0
          || sdkActionInterface.sequenceActionInterfaces == null
          || sdkActionInterface.sequenceActionInterfaces.length === 0
          || sdkActionInterface.sequenceActionInterfacesAreOnlyForModifiers)) {
        var resolveParentAction = currentAction.getResolveParentAction();
        if (resolveParentAction != null) {
          // when current action has more than 1 sibling of the same class
          // and current action is not the first
          var { siblingActions } = sdkActionInterface;
          if (siblingActions == null) {
            // calculate sibling actions
            siblingActions = [currentAction];
            var currentActionType = currentAction.getType();
            const findSiblingsFromActions = function (actions) {
              for (let j = 0, jl = actions.length; j < jl; j++) {
                const action = actions[j];
                if (action.getType() === currentActionType && !_.contains(siblingActions, action)) {
                  // add matching action as sibling
                  siblingActions.push(action);
                }

                // look through sub actions for sibling actions
                const subActions = action.getResolveSubActions();
                for (let k = 0, kl = subActions.length; k < kl; k++) {
                  const subAction = subActions[k];
                  if (subAction.getType() === currentActionType && !_.contains(siblingActions, subAction)) {
                    siblingActions.push(subAction);
                  }
                }
              }
            };
            findSiblingsFromActions(currentAction.getResolveSiblingActions());
            findSiblingsFromActions(resolveParentAction.getResolveSiblingActions());

            // sort siblings by index
            siblingActions = _.sortBy(siblingActions, 'index');

            // cache results with each sibling action
            for (var j = 0, jl = siblingActions.length; j < jl; j++) {
              const siblingAction = siblingActions[j];
              const siblingActionInterface = this._actionInterfacesByIndex[siblingAction.getIndex()];
              // sibling action may not have action interface if it is a skippable action
              if (siblingActionInterface != null) {
                siblingActionInterface.siblingActions = siblingActions;
              }
            }
          }

          // if this action has more than 1 sibling
          if (siblingActions.length > 1) {
            const indexInSiblings = _.indexOf(siblingActions, currentAction);
            if (indexInSiblings === -1) {
              // don't allow this action to be a root if not in sibling actions
              sdkActionInterface.isSequenceRoot = sdkActionInterface.isSequencedAsOne = sdkActionInterface.isSequenceSequencedAsOne = false;
            } else if (siblingActions[0] === currentAction) {
              // don't allow this action to be a root if it is first and did not cause modifiers to trigger, unless it was caused by an attack
              if ((sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces == null
                || sdkActionInterface.cachedResolveTriggeredModifierActionInterfaces.length === 0)
                && !(resolveParentAction instanceof SDK.AttackAction || resolveParentAction instanceof SDK.DamageAsAttackAction)) {
                sdkActionInterface.isSequenceRoot = sdkActionInterface.isSequencedAsOne = sdkActionInterface.isSequenceSequencedAsOne = false;
              }
            } else {
              // don't allow this action to be a root unless previous caused modifiers to trigger
              const previousSiblingAction = siblingActions[indexInSiblings - 1];
              const previousSiblingActionInterface = this._actionInterfacesByIndex[previousSiblingAction.getIndex()];
              if (previousSiblingActionInterface != null
                && (previousSiblingActionInterface.cachedResolveTriggeredModifierActionInterfaces == null
                  || previousSiblingActionInterface.cachedResolveTriggeredModifierActionInterfaces.length === 0
                  || previousSiblingActionInterface.sequenceActionInterfaces == null
                  || previousSiblingActionInterface.sequenceActionInterfaces.length === 0
                  || previousSiblingActionInterface.sequenceActionInterfacesAreOnlyForModifiers)) {
                sdkActionInterface.isSequenceRoot = sdkActionInterface.isSequencedAsOne = sdkActionInterface.isSequenceSequencedAsOne = false;
              }
            }
          }
        }
      }
    }

    // ensure if first action is play card from hand that next action is root of new sequence
    const firstActionInterface = actionInterfaceSequence[0];
    const firstAction = firstActionInterface.getSdkAction();
    if (firstAction instanceof SDK.PlayCardFromHandAction || firstAction instanceof SDK.PlaySignatureCardAction) {
      const secondActionInterface = actionInterfaceSequence[1];
      if (secondActionInterface != null && !secondActionInterface.isSequenceRoot) {
        secondActionInterface.isSequenceRoot = true;
        secondActionInterface.isSequencedAsOne = this._getIsActionSequencedAsOne(secondActionInterface.getSdkAction());
        secondActionInterface.isSequenceSequencedAsOne = true;
      }
    }

    sdkStepInterface.actionInterfaceSequence = actionInterfaceSequence;
  }
Converts a step's action graph into a sequence by generating pseudo-actions for modifiers and rearranging for visual clarity.
NOTE: the step's action sequence can be found at "sdkStepInterface.actionInterfaceSequence"
@param {SdkStepInterface} sdkStepInterface
@private