Skip to content

Commit e2c3c97

Browse files
author
Joanna May
authored
Merge pull request #113 from chickensoft-games/fix/qol
fix: allow configuring whether `OnEnter` callbacks should run when restoring a state
2 parents b4db68b + 1329701 commit e2c3c97

File tree

5 files changed

+156
-9
lines changed

5 files changed

+156
-9
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace Chickensoft.LogicBlocks.Tests.Fixtures;
2+
3+
using Chickensoft.Introspection;
4+
5+
[Meta, Id("serializable_logic_block_with_on_enter")]
6+
[LogicBlock(typeof(State), Diagram = false)]
7+
public partial class SerializableLogicBlockWithOnEnter :
8+
LogicBlock<SerializableLogicBlockWithOnEnter.State> {
9+
public override Transition GetInitialState() => To<StateA>();
10+
11+
public SerializableLogicBlockWithOnEnter() {
12+
Set(new Data());
13+
}
14+
15+
public record Data {
16+
public bool AutomaticallyLeaveAOnEnter { get; set; }
17+
}
18+
19+
public static class Input {
20+
public readonly record struct GoToA;
21+
public readonly record struct GoToB;
22+
}
23+
24+
public static class Output {
25+
public readonly record struct StateEntered;
26+
public readonly record struct StateAEntered;
27+
public readonly record struct StateBEntered;
28+
}
29+
30+
[Meta]
31+
public abstract partial record State : StateLogic<State>,
32+
IGet<Input.GoToA>, IGet<Input.GoToB> {
33+
public State() {
34+
this.OnEnter(() => Output(new Output.StateEntered()));
35+
}
36+
37+
public Transition On(in Input.GoToA input) => To<StateA>();
38+
public Transition On(in Input.GoToB input) => To<StateB>();
39+
}
40+
41+
[Meta, Id("serializable_logic_block_with_on_enter_state_a")]
42+
public partial record StateA : State {
43+
public StateA() {
44+
this.OnEnter(() => {
45+
Output(new Output.StateAEntered());
46+
if (Get<Data>().AutomaticallyLeaveAOnEnter) {
47+
Input(new Input.GoToB());
48+
}
49+
});
50+
}
51+
}
52+
53+
[Meta, Id("serializable_logic_block_with_on_enter_state_b")]
54+
public partial record StateB : State {
55+
public StateB() {
56+
this.OnEnter(() => Output(new Output.StateBEntered()));
57+
}
58+
}
59+
}

Chickensoft.LogicBlocks.Tests/test/src/InternalStateTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void InteractsWithUnderlyingContext() {
3434

3535
[Fact]
3636
public void EqualsAnythingElse() {
37-
var state = new InternalState();
37+
var state = new InternalState(new FakeLogicBlock.ContextAdapter());
3838
state.Equals(new object()).ShouldBeTrue();
3939
}
4040
}

Chickensoft.LogicBlocks.Tests/test/src/LogicBlockTest.cs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,9 @@ public void DoesNothingOnUnhandledInput() {
192192
[Fact]
193193
public void CallsEnterAndExitOnStatesInProperOrder() {
194194
var logic = new TestMachine();
195-
using var listener = Listen(logic);
196-
var context = new TestMachine.DefaultContext(logic);
197195

196+
using var listener = Listen(logic);
198197
var outputs = new List<object>();
199-
200198
void onOutput(object output) => outputs.Add(output);
201199

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

522+
[Fact]
523+
public void EqualToItself() {
524+
var logic = new FakeLogicBlock();
525+
526+
logic.Equals(logic).ShouldBeTrue();
527+
}
528+
524529
[Fact]
525530
public void NotEqualToNonLogicBlock() {
526531
var logic = new FakeLogicBlock();
@@ -629,5 +634,66 @@ public void RestoreFromCopiesStateAndBlackboard() {
629634
other.Value.ShouldBeOfType<FakeLogicBlock.State.StateB>();
630635
other.Get<string>().ShouldBe(data);
631636
}
637+
638+
[Fact]
639+
public void RestoreFromCallsOnEnter() {
640+
var logic = new SerializableLogicBlockWithOnEnter();
641+
logic.Input(new SerializableLogicBlockWithOnEnter.Input.GoToA());
642+
643+
var other = new SerializableLogicBlockWithOnEnter();
644+
645+
using var listener = Listen(other);
646+
var outputs = new List<object>();
647+
void onOutput(object output) => outputs.Add(output);
648+
649+
listener.OnOutput += onOutput;
650+
651+
other.RestoreFrom(logic, shouldCallOnEnter: true);
652+
653+
other
654+
.Get<SerializableLogicBlockWithOnEnter.Data>()
655+
.AutomaticallyLeaveAOnEnter = true;
656+
657+
other.Start();
658+
659+
outputs.ShouldBe(new object[] {
660+
new SerializableLogicBlockWithOnEnter.Output.StateEntered(),
661+
new SerializableLogicBlockWithOnEnter.Output.StateAEntered(),
662+
new SerializableLogicBlockWithOnEnter.Output.StateBEntered()
663+
});
664+
665+
other
666+
.Value
667+
.ShouldBeOfType<SerializableLogicBlockWithOnEnter.StateB>();
668+
}
669+
670+
[Fact]
671+
public void RestoreFromDoesNotCallOnEnter() {
672+
var logic = new SerializableLogicBlockWithOnEnter();
673+
logic.Input(new SerializableLogicBlockWithOnEnter.Input.GoToA());
674+
675+
var other = new SerializableLogicBlockWithOnEnter();
676+
677+
using var listener = Listen(other);
678+
var outputs = new List<object>();
679+
void onOutput(object output) => outputs.Add(output);
680+
681+
listener.OnOutput += onOutput;
682+
683+
other.RestoreFrom(logic, shouldCallOnEnter: false);
684+
685+
other
686+
.Get<SerializableLogicBlockWithOnEnter.Data>()
687+
.AutomaticallyLeaveAOnEnter = true;
688+
689+
other.Start();
690+
691+
outputs.ShouldBeEmpty();
692+
693+
// Should be in StateA, since OnEnter was not called.
694+
other
695+
.Value
696+
.ShouldBeOfType<SerializableLogicBlockWithOnEnter.StateA>();
697+
}
632698
}
633699
}

Chickensoft.LogicBlocks/src/InternalState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Chickensoft.LogicBlocks;
77
/// Internal state stored in each logic block state. This is used to store
88
/// entrance and exit callbacks without tripping up equality checking.
99
/// </summary>
10-
internal readonly struct InternalState {
10+
internal class InternalState {
1111
/// <summary>
1212
/// Callbacks to be invoked when the state is entered.
1313
/// </summary>

Chickensoft.LogicBlocks/src/LogicBlock.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ public interface ILogicBlock<TState> :
104104
/// Restores the logic block from a deserialized logic block.
105105
/// </summary>
106106
/// <param name="logic">Other logic block.</param>
107-
void RestoreFrom(ILogicBlock<TState> logic);
107+
/// <param name="shouldCallOnEnter">Whether or not to call OnEnter callbacks
108+
/// when entering the restored state.</param>
109+
void RestoreFrom(ILogicBlock<TState> logic, bool shouldCallOnEnter = true);
108110

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

185+
// Sometimes, it is preferable not to call OnEnter callbacks when starting
186+
// a logic block, such as when restoring from a saved / serialized logic
187+
// block.
188+
private bool _shouldCallOnEnter = true;
189+
183190
/// <summary>
184191
/// <para>Creates a new LogicBlock.</para>
185192
/// <para>
@@ -395,6 +402,8 @@ internal TState ProcessInputs<TInputType>(
395402

396403
_isProcessing--;
397404

405+
_shouldCallOnEnter = true;
406+
398407
return _value!;
399408
}
400409

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

434443
if (state is not null) {
435444
state.Attach(Context);
436-
state.Enter(previous);
445+
446+
if (_shouldCallOnEnter) {
447+
state.Enter(previous);
448+
}
449+
437450
if (stateIsDifferent) {
438451
AnnounceState(state);
439452
}
@@ -503,6 +516,8 @@ private TState Flush() {
503516
/// <param name="obj">Other logic block.</param>
504517
/// <returns>True if</returns>
505518
public override bool Equals(object? obj) {
519+
if (ReferenceEquals(this, obj)) { return true; }
520+
506521
if (obj is not LogicBlockBase logic) { return false; }
507522

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

547562
/// <inheritdoc />
548-
public void RestoreFrom(ILogicBlock<TState> logic) {
563+
public void RestoreFrom(
564+
ILogicBlock<TState> logic, bool shouldCallOnEnter = true
565+
) {
566+
_shouldCallOnEnter = shouldCallOnEnter;
567+
549568
if ((logic.ValueAsObject ?? logic.RestoredState) is not TState state) {
550-
throw new LogicBlockException($"Cannot restore from logic block {logic}.");
569+
throw new LogicBlockException(
570+
$"Cannot restore from an uninitialized logic block ({logic}). Please " +
571+
"make sure you've called Start() on it first."
572+
);
551573
}
552574

553575
Stop();

0 commit comments

Comments
 (0)