Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions Forge.Tests/Helpers/StatescriptTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,292 @@ private static AbilityData CreateAbilityData(string name, Func<IAbilityBehavior>
}
}

/// <summary>
/// A state node that stays active and counts what it was called with, so a test can tell the frame update and the
/// fixed update apart by which counter moved.
/// </summary>
internal sealed class TrackingStateNode : StateNode<StateNodeContext>
{
public int ActivateCount { get; private set; }

public int DeactivateCount { get; private set; }

public int UpdateCount { get; private set; }

public int FixedUpdateCount { get; private set; }

public double LastUpdateDelta { get; private set; }

public double LastFixedUpdateDelta { get; private set; }

public ulong LastSeenUpdateStamp { get; private set; }

protected override void OnActivate(GraphContext graphContext)
{
ActivateCount++;
}

protected override void OnDeactivate(GraphContext graphContext)
{
DeactivateCount++;
}

protected override void OnUpdate(double deltaTime, GraphContext graphContext)
{
UpdateCount++;
LastUpdateDelta = deltaTime;
LastSeenUpdateStamp = graphContext.UpdateStamp;
}

protected override void OnFixedUpdate(double deltaTime, GraphContext graphContext)
{
FixedUpdateCount++;
LastFixedUpdateDelta = deltaTime;
LastSeenUpdateStamp = graphContext.UpdateStamp;
}
}

/// <summary>
/// An ability behavior that overrides only the frame hook, so the fixed hook falls through to the interface default.
/// </summary>
internal sealed class FrameOnlyBehavior : IAbilityBehavior
{
public int UpdateCount { get; private set; }

public void OnStarted(AbilityBehaviorContext context)
{
}

public void OnEnded(AbilityBehaviorContext context)
{
}

public void OnUpdate(double deltaTime)
{
UpdateCount++;
}
}

/// <summary>
/// A state node that runs an arbitrary callback from its own update, so a test can re-enter the processor that is
/// walking it.
/// </summary>
/// <param name="onUpdate">What to run from the frame update.</param>
internal sealed class CallbackStateNode(Action onUpdate) : StateNode<StateNodeContext>
{
private readonly Action _onUpdate = onUpdate;

public int UpdateCount { get; private set; }

protected override void OnActivate(GraphContext graphContext)
{
}

protected override void OnDeactivate(GraphContext graphContext)
{
}

protected override void OnUpdate(double deltaTime, GraphContext graphContext)
{
UpdateCount++;
_onUpdate();
}
}

/// <summary>
/// An ability behavior that drives another update of its own entity from inside one, which is the misuse the
/// re-entrancy guard exists to catch.
/// </summary>
/// <param name="entity">The entity whose update is re-entered.</param>
internal sealed class ReenteringBehavior(TestEntity entity) : IAbilityBehavior
{
private readonly TestEntity _entity = entity;

private bool _reentered;

public int UpdateCount { get; private set; }

public void OnStarted(AbilityBehaviorContext context)
{
}

public void OnEnded(AbilityBehaviorContext context)
{
}

public void OnUpdate(double deltaTime)
{
UpdateCount++;

if (_reentered)
{
return;
}

_reentered = true;
_entity.Abilities.UpdateAbilities(deltaTime);
}
}

/// <summary>
/// An ability behavior that ends its own instance from inside an update, which is what a graph completing on that
/// rail does. Ending removes the behavior from the dictionary the dispatch loop is walking.
/// </summary>
/// <param name="endOnFixedUpdate">Whether to end from the fixed hook rather than the frame hook.</param>
internal sealed class SelfEndingBehavior(bool endOnFixedUpdate) : IAbilityBehavior
{
private readonly bool _endOnFixedUpdate = endOnFixedUpdate;

private AbilityBehaviorContext? _context;

public void OnStarted(AbilityBehaviorContext context)
{
_context = context;
}

public void OnEnded(AbilityBehaviorContext context)
{
}

public void OnUpdate(double deltaTime)
{
if (!_endOnFixedUpdate)
{
_context?.InstanceHandle.End();
}
}

public void OnFixedUpdate(double deltaTime)
{
if (_endOnFixedUpdate)
{
_context?.InstanceHandle.End();
}
}
}

/// <summary>
/// An ability behavior that clears its own ability from inside an update, removing the handle from the granted set the
/// entity-level dispatch loop is walking.
/// </summary>
/// <param name="entity">The entity whose ability is cleared.</param>
/// <param name="clearOnFixedUpdate">Whether to clear from the fixed hook rather than the frame hook.</param>
internal sealed class SelfClearingBehavior(TestEntity entity, bool clearOnFixedUpdate) : IAbilityBehavior
{
private readonly TestEntity _entity = entity;
private readonly bool _clearOnFixedUpdate = clearOnFixedUpdate;

private AbilityBehaviorContext? _context;

public void OnStarted(AbilityBehaviorContext context)
{
_context = context;
}

public void OnEnded(AbilityBehaviorContext context)
{
}

public void OnUpdate(double deltaTime)
{
if (!_clearOnFixedUpdate)
{
Clear();
}
}

public void OnFixedUpdate(double deltaTime)
{
if (_clearOnFixedUpdate)
{
Clear();
}
}

private void Clear()
{
if (_context is not null)
{
_entity.Abilities.ClearAbility(_context.AbilityHandle);
}
}
}

/// <summary>
/// An ability behavior that grants a second ability from inside an update, adding to the granted set the entity-level
/// dispatch loop is walking. Adding invalidates an enumerator where removing no longer does.
/// </summary>
/// <param name="entity">The entity the new ability is granted to.</param>
/// <param name="granted">The ability to grant.</param>
/// <param name="grantOnFixedUpdate">Whether to grant from the fixed hook rather than the frame hook.</param>
internal sealed class GrantingBehavior(TestEntity entity, AbilityData granted, bool grantOnFixedUpdate)
: IAbilityBehavior
{
private readonly TestEntity _entity = entity;
private readonly AbilityData _granted = granted;
private readonly bool _grantOnFixedUpdate = grantOnFixedUpdate;

private bool _done;

public void OnStarted(AbilityBehaviorContext context)
{
}

public void OnEnded(AbilityBehaviorContext context)
{
}

public void OnUpdate(double deltaTime)
{
if (!_grantOnFixedUpdate)
{
Grant();
}
}

public void OnFixedUpdate(double deltaTime)
{
if (_grantOnFixedUpdate)
{
Grant();
}
}

private void Grant()
{
if (_done)
{
return;
}

_done = true;
_entity.Abilities.GrantAbilityPermanently(_granted, 1, LevelComparison.Higher, null);
}
}

/// <summary>
/// A state node that deactivates another one from inside its own fixed update, for the case where the set of active
/// nodes changes while it is being walked.
/// </summary>
/// <param name="target">The state node to deactivate from the fixed update.</param>
internal sealed class DeactivateOnFixedUpdateNode(TrackingStateNode target) : StateNode<StateNodeContext>
{
private readonly TrackingStateNode _target = target;

protected override void OnActivate(GraphContext graphContext)
{
}

protected override void OnDeactivate(GraphContext graphContext)
{
}

protected override void OnFixedUpdate(double deltaTime, GraphContext graphContext)
{
_target.InputPorts[AbortPort].ReceiveMessage(graphContext);
}
}

internal sealed class TrackingActionNode(string? name = null, List<string>? executionLog = null) : ActionNode
{
private readonly string? _name = name;
Expand Down
Loading
Loading