Skip to content

Commit

Permalink
Merge pull request #14 from chickensoft-games/fix/determinism
Browse files Browse the repository at this point in the history
fix: deterministic lifecycle
  • Loading branch information
jolexxa authored Jul 6, 2024
2 parents f38bda6 + f3fad28 commit 372cdd5
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public interface IAutoConnect : IMixin<IAutoConnect>, IFakeNodeTreeEnabled {
void IMixin<IAutoConnect>.Handler() {
var what = MixinState.Get<NotificationState>().Notification;

if (what == Node.NotificationSceneInstantiated) {
if (what == Node.NotificationEnterTree) {
AutoConnector.ConnectNodes(Types.Graph.GetProperties(GetType()), this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,28 @@ IEnumerable<PropertyMetadata> properties
GetDependenciesToResolve(properties)
);

var node = ((Node)dependent).GetParent();
var self = (Node)dependent;
var node = self.GetParent();
var foundDependencies = new HashSet<PropertyMetadata>();
var providersInitializing = 0;

void resolve() {
if (self.IsNodeReady()) {
// Godot node is already ready.
if (!dependent.IsTesting) {
dependent.Setup();
}
dependent.OnResolved();
return;
}

// Godot node is not ready yet, so we will wait for OnReady before
// calling Setup() and OnResolved().

if (!dependent.IsTesting) {
dependent.Setup();
state.PleaseCallSetup = true;
}
dependent.OnResolved();
state.PleaseCallOnResolved = true;
}

void onProviderInitialized(IBaseProvider provider) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public class DependentState {
/// </summary>
public bool ShouldResolveDependencies { get; set; } = true;

/// <summary>Set internally when Setup() should be called.</summary>
public bool PleaseCallSetup { get; set; }
/// <summary>Set internally when OnResolved() should be called.</summary>
public bool PleaseCallOnResolved { get; set; }

/// <summary>
/// Dictionary of providers we are listening to that are still initializing
/// their provided values. We use this in the rare event that we have to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ namespace Chickensoft.AutoInject;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using System.Globalization;
using System;
using System.Runtime.CompilerServices;


/// <summary>
/// Dependent mixin. Apply this to an introspective node to automatically
/// resolve dependencies marked with the [Dependency] attribute.
/// </summary>
[Mixin]
public interface IDependent : IMixin<IDependent>, IAutoInit {
public interface IDependent : IMixin<IDependent>, IAutoInit, IReadyAware {
DependentState DependentState {
get {
AddStateIfNeeded();
Expand All @@ -36,6 +38,22 @@ void Setup() { }
/// </summary>
void OnResolved() { }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IReadyAware.OnBeforeReady() { }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IReadyAware.OnAfterReady() {
if (DependentState.PleaseCallSetup) {
Setup();
DependentState.PleaseCallSetup = false;
}
if (DependentState.PleaseCallOnResolved) {
OnResolved();
DependentState.PleaseCallOnResolved = false;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddStateIfNeeded() {
if (MixinState.Has<DependentState>()) { return; }

Expand Down
6 changes: 6 additions & 0 deletions Chickensoft.AutoInject.Tests/src/auto_on/IAutoOn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ public static void InvokeNotificationMethods(object? obj, int what) {
autoNode.OnChildOrderChanged();
break;
case (int)Node.NotificationReady:
if (node is IReadyAware readyAware) {
readyAware.OnBeforeReady();
autoNode.OnReady();
readyAware.OnAfterReady();
break;
}
autoNode.OnReady();
break;
case (int)Node.NotificationEditorPostSave:
Expand Down
12 changes: 12 additions & 0 deletions Chickensoft.AutoInject.Tests/src/misc/IReadyAware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Chickensoft.AutoInject;

/// <summary>
/// Types that want to be informed of ready can implement this interface.
/// </summary>
public interface IReadyAware {
/// <summary>Called right before the node is ready.</summary>
void OnBeforeReady();

/// <summary>Called right after the node is readied.</summary>
void OnAfterReady();
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,29 @@ public void ThrowsIfFakedChildNodeIsWrongType() {
scene.FakeNodeTree(new() { ["Node3D"] = new Mock<INode3D>().Object });

Should.Throw<InvalidOperationException>(
() => scene._Notification((int)Node.NotificationSceneInstantiated)
() => scene._Notification((int)Node.NotificationEnterTree)
);
}

[Test]
public void ThrowsIfNoNode() {
var scene = new AutoConnectInvalidCastTestScene();
Should.Throw<InvalidOperationException>(
() => scene._Notification((int)Node.NotificationEnterTree)
);
}

[Test]
public void ThrowsIfTypeIsWrong() {
var scene = new AutoConnectInvalidCastTestScene();

var node = new Control {
Name = "Node3D"
};
scene.AddChild(node);

Should.Throw<InvalidOperationException>(
() => scene._Notification((int)Node.NotificationEnterTree)
);
}
}
20 changes: 11 additions & 9 deletions Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace Chickensoft.AutoInject.Tests;

using System.Threading.Tasks;
using Chickensoft.AutoInject.Tests.Subjects;
using Chickensoft.GoDotTest;
using Chickensoft.GodotTestDriver;
using Godot;
using Shouldly;

Expand Down Expand Up @@ -59,33 +61,34 @@ public void ResolvesDependencyWhenProviderIsAlreadyInitialized() {
}

[Test]
public void ResolvesDependencyAfterProviderIsResolved() {
public async Task ResolvesDependencyAfterProviderIsResolved() {
var value = "Hello, world!";
var obj = new StringProvider() { Value = value };
var provider = obj as IBaseProvider;
var dependent = new StringDependent();

var fixture = new Fixture(TestScene.GetTree());
obj.AddChild(dependent);

((IProvide<string>)provider).Value().ShouldBe(value);
await fixture.AddToRoot(obj);

dependent._Notification((int)Node.NotificationReady);
((IProvide<string>)provider).Value().ShouldBe(value);

obj._Notification((int)Node.NotificationReady);
provider.ProviderState.IsInitialized.ShouldBeTrue();
obj.OnProvidedCalled.ShouldBeTrue();

dependent.OnResolvedCalled.ShouldBeTrue();
dependent.ResolvedValue.ShouldBe(value);
((IDependent)dependent).DependentState.Pending.ShouldBeEmpty();

await fixture.Cleanup();

obj.RemoveChild(dependent);
dependent.QueueFree();
obj.QueueFree();
}

[Test]
public void FindsDependenciesAcrossAncestors() {
public async Task FindsDependenciesAcrossAncestors() {
var value = "Hello, world!";

var objA = new StringProvider() { Value = value };
Expand All @@ -94,17 +97,16 @@ public void FindsDependenciesAcrossAncestors() {
var providerB = objB as IBaseProvider;
var depObj = new StringDependent();
var dependent = depObj as IDependent;
var fixture = new Fixture(TestScene.GetTree());

objA.AddChild(objB);
objA.AddChild(depObj);

depObj._Notification((int)Node.NotificationReady);
await fixture.AddToRoot(objA);

objA._Notification((int)Node.NotificationReady);
providerA.ProviderState.IsInitialized.ShouldBeTrue();
objA.OnProvidedCalled.ShouldBeTrue();

objB._Notification((int)Node.NotificationReady);
providerB.ProviderState.IsInitialized.ShouldBeTrue();
objB.OnProvidedCalled.ShouldBeTrue();

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,24 @@ var myNode = new MyNode() {

For example tests, please see the [Game Demo] project.

## 🌱 Enhanced Lifecycle

AutoInject enhances the typical Godot node lifecycle by adding additional hooks that allow you to handle dependencies and initialization in a more controlled manner (primarily for making testing easier).

This is the lifecycle of a dependent node in the game environment:

```text
Initialize() -> OnReady() -> Setup() -> OnResolved()
```

Note that this lifecycle is preserved regardless of how the node is added to the scene tree.

And this is the lifecycle of a dependent node in a test environment:

```text
OnReady() -> OnResolved()
```

## 🔋 IAutoOn

The `IAutoOn` mixin allows node scripts to implement .NET-style handler methods for Godot notifications, prefixed with `On`.
Expand Down

0 comments on commit 372cdd5

Please sign in to comment.