Skip to content

Commit

Permalink
Merge pull request #113 from chickensoft-games/fix/qol
Browse files Browse the repository at this point in the history
fix: allow configuring whether `OnEnter` callbacks should run when restoring a state
  • Loading branch information
jolexxa authored Nov 26, 2024
2 parents b4db68b + 1329701 commit e2c3c97
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace Chickensoft.LogicBlocks.Tests.Fixtures;

using Chickensoft.Introspection;

[Meta, Id("serializable_logic_block_with_on_enter")]
[LogicBlock(typeof(State), Diagram = false)]
public partial class SerializableLogicBlockWithOnEnter :
LogicBlock<SerializableLogicBlockWithOnEnter.State> {
public override Transition GetInitialState() => To<StateA>();

public SerializableLogicBlockWithOnEnter() {
Set(new Data());
}

public record Data {
public bool AutomaticallyLeaveAOnEnter { get; set; }
}

public static class Input {
public readonly record struct GoToA;
public readonly record struct GoToB;
}

public static class Output {
public readonly record struct StateEntered;
public readonly record struct StateAEntered;
public readonly record struct StateBEntered;
}

[Meta]
public abstract partial record State : StateLogic<State>,
IGet<Input.GoToA>, IGet<Input.GoToB> {
public State() {
this.OnEnter(() => Output(new Output.StateEntered()));
}

public Transition On(in Input.GoToA input) => To<StateA>();
public Transition On(in Input.GoToB input) => To<StateB>();
}

[Meta, Id("serializable_logic_block_with_on_enter_state_a")]
public partial record StateA : State {
public StateA() {
this.OnEnter(() => {
Output(new Output.StateAEntered());
if (Get<Data>().AutomaticallyLeaveAOnEnter) {
Input(new Input.GoToB());
}
});
}
}

[Meta, Id("serializable_logic_block_with_on_enter_state_b")]
public partial record StateB : State {
public StateB() {
this.OnEnter(() => Output(new Output.StateBEntered()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void InteractsWithUnderlyingContext() {

[Fact]
public void EqualsAnythingElse() {
var state = new InternalState();
var state = new InternalState(new FakeLogicBlock.ContextAdapter());
state.Equals(new object()).ShouldBeTrue();
}
}
72 changes: 69 additions & 3 deletions Chickensoft.LogicBlocks.Tests/test/src/LogicBlockTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,9 @@ public void DoesNothingOnUnhandledInput() {
[Fact]
public void CallsEnterAndExitOnStatesInProperOrder() {
var logic = new TestMachine();
using var listener = Listen(logic);
var context = new TestMachine.DefaultContext(logic);

using var listener = Listen(logic);
var outputs = new List<object>();

void onOutput(object output) => outputs.Add(output);

listener.OnOutput += onOutput;
Expand Down Expand Up @@ -521,6 +519,13 @@ public void RestoreStateThrowsIfStateAlreadyExists() {
public class LogicBlockEquality {
private sealed record TestValue(int Value);

[Fact]
public void EqualToItself() {
var logic = new FakeLogicBlock();

logic.Equals(logic).ShouldBeTrue();
}

[Fact]
public void NotEqualToNonLogicBlock() {
var logic = new FakeLogicBlock();
Expand Down Expand Up @@ -629,5 +634,66 @@ public void RestoreFromCopiesStateAndBlackboard() {
other.Value.ShouldBeOfType<FakeLogicBlock.State.StateB>();
other.Get<string>().ShouldBe(data);
}

[Fact]
public void RestoreFromCallsOnEnter() {
var logic = new SerializableLogicBlockWithOnEnter();
logic.Input(new SerializableLogicBlockWithOnEnter.Input.GoToA());

var other = new SerializableLogicBlockWithOnEnter();

using var listener = Listen(other);
var outputs = new List<object>();
void onOutput(object output) => outputs.Add(output);

listener.OnOutput += onOutput;

other.RestoreFrom(logic, shouldCallOnEnter: true);

other
.Get<SerializableLogicBlockWithOnEnter.Data>()
.AutomaticallyLeaveAOnEnter = true;

other.Start();

outputs.ShouldBe(new object[] {
new SerializableLogicBlockWithOnEnter.Output.StateEntered(),
new SerializableLogicBlockWithOnEnter.Output.StateAEntered(),
new SerializableLogicBlockWithOnEnter.Output.StateBEntered()
});

other
.Value
.ShouldBeOfType<SerializableLogicBlockWithOnEnter.StateB>();
}

[Fact]
public void RestoreFromDoesNotCallOnEnter() {
var logic = new SerializableLogicBlockWithOnEnter();
logic.Input(new SerializableLogicBlockWithOnEnter.Input.GoToA());

var other = new SerializableLogicBlockWithOnEnter();

using var listener = Listen(other);
var outputs = new List<object>();
void onOutput(object output) => outputs.Add(output);

listener.OnOutput += onOutput;

other.RestoreFrom(logic, shouldCallOnEnter: false);

other
.Get<SerializableLogicBlockWithOnEnter.Data>()
.AutomaticallyLeaveAOnEnter = true;

other.Start();

outputs.ShouldBeEmpty();

// Should be in StateA, since OnEnter was not called.
other
.Value
.ShouldBeOfType<SerializableLogicBlockWithOnEnter.StateA>();
}
}
}
2 changes: 1 addition & 1 deletion Chickensoft.LogicBlocks/src/InternalState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Chickensoft.LogicBlocks;
/// Internal state stored in each logic block state. This is used to store
/// entrance and exit callbacks without tripping up equality checking.
/// </summary>
internal readonly struct InternalState {
internal class InternalState {
/// <summary>
/// Callbacks to be invoked when the state is entered.
/// </summary>
Expand Down
30 changes: 26 additions & 4 deletions Chickensoft.LogicBlocks/src/LogicBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ public interface ILogicBlock<TState> :
/// Restores the logic block from a deserialized logic block.
/// </summary>
/// <param name="logic">Other logic block.</param>
void RestoreFrom(ILogicBlock<TState> logic);
/// <param name="shouldCallOnEnter">Whether or not to call OnEnter callbacks
/// when entering the restored state.</param>
void RestoreFrom(ILogicBlock<TState> logic, bool shouldCallOnEnter = true);

/// <summary>
/// Adds a binding to the logic block. This is used internally by the standard
Expand Down Expand Up @@ -180,6 +182,11 @@ public override void RestoreState(object state) {
private readonly BoxlessQueue _inputs;
private readonly HashSet<ILogicBlockBinding<TState>> _bindings = new();

// Sometimes, it is preferable not to call OnEnter callbacks when starting
// a logic block, such as when restoring from a saved / serialized logic
// block.
private bool _shouldCallOnEnter = true;

/// <summary>
/// <para>Creates a new LogicBlock.</para>
/// <para>
Expand Down Expand Up @@ -395,6 +402,8 @@ internal TState ProcessInputs<TInputType>(

_isProcessing--;

_shouldCallOnEnter = true;

return _value!;
}

Expand Down Expand Up @@ -433,7 +442,11 @@ private void ChangeState(TState? state) {

if (state is not null) {
state.Attach(Context);
state.Enter(previous);

if (_shouldCallOnEnter) {
state.Enter(previous);
}

if (stateIsDifferent) {
AnnounceState(state);
}
Expand Down Expand Up @@ -503,6 +516,8 @@ private TState Flush() {
/// <param name="obj">Other logic block.</param>
/// <returns>True if</returns>
public override bool Equals(object? obj) {
if (ReferenceEquals(this, obj)) { return true; }

if (obj is not LogicBlockBase logic) { return false; }

if (GetType() != logic.GetType()) {
Expand Down Expand Up @@ -545,9 +560,16 @@ public override bool Equals(object? obj) {
public override int GetHashCode() => base.GetHashCode();

/// <inheritdoc />
public void RestoreFrom(ILogicBlock<TState> logic) {
public void RestoreFrom(
ILogicBlock<TState> logic, bool shouldCallOnEnter = true
) {
_shouldCallOnEnter = shouldCallOnEnter;

if ((logic.ValueAsObject ?? logic.RestoredState) is not TState state) {
throw new LogicBlockException($"Cannot restore from logic block {logic}.");
throw new LogicBlockException(
$"Cannot restore from an uninitialized logic block ({logic}). Please " +
"make sure you've called Start() on it first."
);
}

Stop();
Expand Down

0 comments on commit e2c3c97

Please sign in to comment.