Skip to content

Commit 38d4174

Browse files
author
Joanna May
authored
Merge pull request #29 from chickensoft-games/feat/restore
feat: allow states to be restored before starting
2 parents 9c35eab + 40591d6 commit 38d4174

22 files changed

+193
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ obj/
1010
.generated/
1111
.vs/
1212
.DS_Store
13+
*.DotSettings.user

.idea/.idea.Chickensoft.LogicBlocks/.idea/.gitignore

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.Chickensoft.LogicBlocks/.idea/.name

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.Chickensoft.LogicBlocks/.idea/encodings.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.Chickensoft.LogicBlocks/.idea/indexLayout.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.Chickensoft.LogicBlocks/.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.vscode/settings.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"[csharp]": {
33
"editor.codeActionsOnSave": {
4-
"source.addMissingImports": true,
5-
"source.fixAll": true,
6-
"source.organizeImports": true
4+
"source.addMissingImports": "explicit",
5+
"source.fixAll": "explicit",
6+
"source.organizeImports": "explicit"
77
},
88
"editor.formatOnPaste": true,
99
"editor.formatOnSave": true,

Chickensoft.LogicBlocks.Example/Chickensoft.LogicBlocks.Example.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net6.0</TargetFramework>
5+
<TargetFramework>net7.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<LangVersion>preview</LangVersion>
88
<Nullable>enable</Nullable>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated_005Cchickensoft.logicblocks.generator_005Cchickensoft.logicblocks.generator.logicblocksgenerator/@EntryIndexedValue">True</s:Boolean>
4+
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated_005Cchickensoft.logicblocks.generator/@EntryIndexedValue">True</s:Boolean>
6+
7+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated/@EntryIndexedValue">True</s:Boolean>
8+
</wpf:ResourceDictionary>

Chickensoft.LogicBlocks.Generator.Tests/Chickensoft.LogicBlocks.Generator.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net7.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_cases/@EntryIndexedValue">True</s:Boolean>
4+
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_cases_005Cpartial_split_across_files/@EntryIndexedValue">True</s:Boolean>
6+
7+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated_005Cchickensoft.logicblocks.generator_005Cchickensoft.logicblocks.generator.logicblocksgenerator/@EntryIndexedValue">True</s:Boolean>
8+
9+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated/@EntryIndexedValue">True</s:Boolean>
10+
11+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=.generated_005Cchickensoft.logicblocks.generator/@EntryIndexedValue">True</s:Boolean>
12+
</wpf:ResourceDictionary>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=isexternalinit/@EntryIndexedValue">True</s:Boolean>
4+
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nupkg/@EntryIndexedValue">True</s:Boolean>
6+
7+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src/@EntryIndexedValue">True</s:Boolean>
8+
9+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Ccommon_005Cutils/@EntryIndexedValue">True</s:Boolean>
10+
11+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Ccommon/@EntryIndexedValue">True</s:Boolean>
12+
13+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Ccommon_005Cmodels/@EntryIndexedValue">True</s:Boolean>
14+
15+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nupkg_005Cnetstandard2.0/@EntryIndexedValue">True</s:Boolean>
16+
17+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Ccommon_005Cservices/@EntryIndexedValue">True</s:Boolean>
18+
</wpf:ResourceDictionary>

Chickensoft.LogicBlocks.Tests/Chickensoft.LogicBlocks.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net7.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=badges/@EntryIndexedValue">True</s:Boolean>
4+
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=coverage/@EntryIndexedValue">True</s:Boolean>
6+
7+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test/@EntryIndexedValue">True</s:Boolean>
8+
9+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=coverage_005Creport/@EntryIndexedValue">True</s:Boolean>
10+
11+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_005Csrc/@EntryIndexedValue">True</s:Boolean>
12+
13+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_005Cfixtures/@EntryIndexedValue">True</s:Boolean>
14+
15+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_005Ctest_utils/@EntryIndexedValue">True</s:Boolean>
16+
17+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_005Csrc_005Cutilities/@EntryIndexedValue">True</s:Boolean>
18+
19+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=test_005Csrc_005Cexamples/@EntryIndexedValue">True</s:Boolean>
20+
</wpf:ResourceDictionary>

Chickensoft.LogicBlocks.Tests/test/fixtures/MyLogicBlock.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public interface IMyLogicBlock : ILogicBlock<MyLogicBlock.IState> { }
66

77
[StateMachine]
88
public partial class MyLogicBlock : LogicBlock<MyLogicBlock.IState>, IMyLogicBlock {
9-
public override State GetInitialState() => new State.SomeState();
9+
public override IState GetInitialState() => new State.SomeState();
1010

1111
public static class Input {
1212
public readonly record struct SomeInput;

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,33 @@ public void CreatesFakeContext() {
409409

410410
Should.Throw<InvalidOperationException>(() => context.Get<string>());
411411
}
412+
413+
[Fact]
414+
public void Restores() {
415+
var block = new FakeLogicBlock();
416+
417+
var state = new FakeLogicBlock.State.StateB("a", "b");
418+
block.Restore(state);
419+
420+
block.Value.ShouldBe(state);
421+
}
422+
423+
[Fact]
424+
public void RestoreThrowsIfAlreadyStarted() {
425+
var block = new FakeLogicBlock();
426+
block.Start();
427+
428+
var state = new FakeLogicBlock.State.StateB("a", "b");
429+
Should.Throw<InvalidOperationException>(() => block.Restore(state));
430+
}
431+
432+
[Fact]
433+
public void RestoreThrowsIfAlreadyRestored() {
434+
var block = new FakeLogicBlock();
435+
block.Restore(new FakeLogicBlock.State.StateB("a", "b"));
436+
437+
var state = new FakeLogicBlock.State.StateC("c");
438+
Should.Throw<InvalidOperationException>(() => block.Restore(state));
439+
block.Value.ShouldBeOfType<FakeLogicBlock.State.StateB>();
440+
}
412441
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
3+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=isexternalinit/@EntryIndexedValue">True</s:Boolean>
4+
5+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nupkg/@EntryIndexedValue">True</s:Boolean>
6+
7+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src/@EntryIndexedValue">True</s:Boolean>
8+
9+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=src_005Cutilities/@EntryIndexedValue">True</s:Boolean>
10+
11+
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=nupkg_005Cnetstandard2.1/@EntryIndexedValue">True</s:Boolean>
12+
</wpf:ResourceDictionary>

Chickensoft.LogicBlocks/src/Logic.Binding.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,6 @@ private readonly List<Func<dynamic, TState, dynamic, dynamic, bool>>
312312
// Callbacks for this state type registered with .Call()
313313
private readonly List<Action<dynamic, TState?>> _callbacks = new();
314314

315-
internal WhenBinding() { }
316-
317315
/// <summary>
318316
/// Determines if this binding should run for a given state.
319317
/// </summary>

Chickensoft.LogicBlocks/src/Logic.cs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Chickensoft.LogicBlocks;
1414
/// <typeparam name="THandler">Input handler type.</typeparam>
1515
/// <typeparam name="TInputReturn">Input method return type.</typeparam>
1616
/// <typeparam name="TUpdate">Update callback type.</typeparam>
17-
public partial interface ILogic<TState, THandler, TInputReturn, TUpdate>
17+
public interface ILogic<TState, THandler, TInputReturn, TUpdate>
1818
where TState : Logic<TState, THandler, TInputReturn, TUpdate>.ILogicState {
1919
/// <summary>
2020
/// Logic block execution context.
@@ -64,6 +64,16 @@ public partial interface ILogic<TState, THandler, TInputReturn, TUpdate>
6464
/// </summary>
6565
/// <returns>Logic block binding.</returns>
6666
Logic<TState, THandler, TInputReturn, TUpdate>.IBinding Bind();
67+
68+
/// <summary>
69+
/// Restores the logic block from a state. This method can only be called
70+
/// before the logic block has been started. The state provided to this method
71+
/// takes precedence over <see cref="GetInitialState"/>, ensuring that the
72+
/// logic block's first state will be the one provided here.
73+
/// </summary>
74+
/// <param name="state">State to use as the logic block's initial state.
75+
/// </param>
76+
void Restore(TState state);
6777
}
6878

6979
/// <summary>
@@ -115,20 +125,6 @@ public UpdateCallback(TUpdate callback, Func<dynamic?, bool> isType) {
115125
}
116126
}
117127

118-
/// <summary>
119-
/// Signature of a callback action which can be invoked when a transition
120-
/// occurs from one state to another.
121-
/// </summary>
122-
/// <param name="stateA">Starting (previous) state.</param>
123-
/// <param name="stateB">Ending (current) state.</param>
124-
/// <typeparam name="TStateTypeA">Type of the starting (previous) state.
125-
/// </typeparam>
126-
/// <typeparam name="TStateTypeB">Type of the ending (current) state.
127-
/// </typeparam>
128-
public delegate void Transition<TStateTypeA, TStateTypeB>(
129-
TStateTypeA stateA, TStateTypeB stateB
130-
) where TStateTypeA : TState where TStateTypeB : TState;
131-
132128
/// <inheritdoc />
133129
public event Action<object>? OnInput;
134130
/// <inheritdoc />
@@ -150,6 +146,7 @@ public delegate void Transition<TStateTypeA, TStateTypeB>(
150146
public abstract bool IsProcessing { get; }
151147

152148
internal TState? _value;
149+
private TState? _restoredState;
153150

154151
private readonly Queue<PendingInput> _inputs = new();
155152
private readonly Dictionary<Type, dynamic> _blackboard = new();
@@ -170,6 +167,26 @@ internal Logic() {
170167
/// <inheritdoc />
171168
public virtual IBinding Bind() => new Binding(this);
172169

170+
/// <inheritdoc />
171+
public void Restore(TState state) {
172+
if (_restoredState is not null) {
173+
throw new InvalidOperationException(
174+
$"Logic block was already restored. Note that a logic block cannot " +
175+
"be restored more than once."
176+
);
177+
}
178+
179+
if (_value is not null) {
180+
throw new InvalidOperationException(
181+
$"Attempted to restore a logic block that was already started. Note " +
182+
"that a logic block cannot be restored after its first state is " +
183+
"created."
184+
);
185+
}
186+
187+
_restoredState = state;
188+
}
189+
173190
/// <inheritdoc />
174191
public abstract TState GetInitialState();
175192

@@ -287,6 +304,19 @@ internal void FinalizeStateChange(TState state) =>
287304
internal void AnnounceInput(object input) =>
288305
OnInput?.Invoke(input);
289306

307+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
308+
internal TState GetStartState() {
309+
if (_restoredState is not { } state) {
310+
// No state to restore from, so let the developer's override of
311+
// GetInitialState determine the start state.
312+
return GetInitialState();
313+
}
314+
315+
// Clear restored state and return it as the first state.
316+
_restoredState = default;
317+
return state;
318+
}
319+
290320
/// <inheritdoc />
291321
public TData Get<TData>() where TData : notnull {
292322
var type = typeof(TData);
@@ -307,12 +337,11 @@ public TData Get<TData>() where TData : notnull {
307337
/// has already been added.</exception>
308338
protected void Set<TData>(TData data) where TData : notnull {
309339
var type = typeof(TData);
310-
if (_blackboard.ContainsKey(type)) {
340+
if (!_blackboard.TryAdd(type, data)) {
311341
throw new ArgumentException(
312342
$"Data of type {type} already exists in the blackboard."
313343
);
314344
}
315-
_blackboard.Add(type, data);
316345
}
317346

318347
/// <summary>

Chickensoft.LogicBlocks/src/LogicBlock.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,12 @@ public abstract partial class LogicBlock<TState> :
5858
/// <summary>Creates a new logic block.</summary>
5959
protected LogicBlock() { }
6060

61-
/// <inheritdoc />
62-
public abstract override TState GetInitialState();
63-
6461
/// <inheritdoc />
6562
public override TState Value => _value ?? AttachState();
6663

6764
private TState AttachState() {
6865
_isProcessing = true;
69-
_value = GetInitialState();
66+
_value = GetStartState();
7067
_value.Attach(Context);
7168
_isProcessing = false;
7269
return Process();
@@ -124,10 +121,10 @@ internal override TState Process() {
124121

125122
/// <inheritdoc />
126123
public void Start() =>
127-
Value.Enter(previous: null, onError: (e) => AddError(e));
124+
Value.Enter(previous: null, onError: AddError);
128125

129126
/// <inheritdoc />
130-
public void Stop() => Value.Exit(next: null, onError: (e) => AddError(e));
127+
public void Stop() => Value.Exit(next: null, onError: AddError);
131128

132129
internal override Func<object, TState> GetInputHandler<TInputType>()
133130
=> (input) => {

Chickensoft.LogicBlocks/src/LogicBlockAsync.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,11 @@ protected LogicBlockAsync() {
7070
_processTask.SetResult(default!);
7171
}
7272

73-
/// <inheritdoc />
74-
public abstract override TState GetInitialState();
75-
7673
/// <inheritdoc />
7774
public override TState Value => _value ?? AttachState();
7875

7976
private TState AttachState() {
80-
_value = GetInitialState();
77+
_value = GetStartState();
8178
_value.Attach(Context);
8279

8380
// If inputs were added to the logic block during the attach callbacks,
@@ -159,10 +156,10 @@ private async Task<TState> ProcessInputs() {
159156

160157
/// <inheritdoc />
161158
public Task Start() =>
162-
Value.Enter(previous: null, onError: (e) => AddError(e));
159+
Value.Enter(previous: null, onError: AddError);
163160

164161
/// <inheritdoc />
165-
public Task Stop() => Value.Exit(next: null, onError: (e) => AddError(e));
162+
public Task Stop() => Value.Exit(next: null, onError: AddError);
166163

167164
internal override Func<object, Task<TState>> GetInputHandler<TInputType>()
168165
=> (input) => {

0 commit comments

Comments
 (0)