Skip to content

Commit

Permalink
docs: readme
Browse files Browse the repository at this point in the history
  • Loading branch information
jolexxa committed Sep 23, 2023
1 parent 89a4720 commit f1e846b
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${env:GODOT4}",
"program": "${env:GODOT}",
"args": [
"--headless",
// These command line flags are used by GoDotTest to run tests.
Expand All @@ -25,7 +25,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${env:GODOT4}",
"program": "${env:GODOT}",
"args": [
"--headless",
// These command line flags are used by GoDotTest to run tests.
Expand Down
4 changes: 4 additions & 0 deletions Chickensoft.AutoInject.Tests/src/Dependent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ void onProviderInitialized(IProvider provider) {
// Inform dependent that dependencies have been resolved.
resolve();
}

// We *could* check to see if a provider for every dependency was found
// and throw an exception if any were missing, but this would break support
// for fallback values.
}

public class DefaultProvider : IProvider {
Expand Down
12 changes: 12 additions & 0 deletions Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ public void AccessingDependencyBeforeProvidedEvenIfCreatedThrows() {
Should.Throw<ProviderNotInitializedException>(() => dependent.MyDependency);
}

[Test]
public void DependentWithNoDependenciesHasOnResolvedCalled() {
var provider = new StringProvider();
var dependent = new NoDependenciesDependent();

provider.AddChild(dependent);

dependent._Notification((int)Node.NotificationReady);

dependent.OnResolvedCalled.ShouldBeTrue();
}

public class BadProvider : IProvider {
public ProviderState ProviderState { get; }

Expand Down
9 changes: 9 additions & 0 deletions Chickensoft.AutoInject.Tests/test/src/subjects/Dependents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,12 @@ public void OnResolved() {
StringResolvedValue = StringDependency;
}
}

[SuperNode(typeof(Dependent))]
public partial class NoDependenciesDependent : Node {
public override partial void _Notification(int what);

public bool OnResolvedCalled { get; private set; }

public void OnResolved() => OnResolvedCalled = true;
}
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ If you have a node script which is both a `Dependent` and a `Provider`, you can

The general rule of thumb for any `Provider` node is as follows: **call `Provide` as soon as you possibly can: either from `_Ready/OnReady` or from `OnResolved`.** If all providers in your project follow this rule, dependency provision will complete before processing occurs for nodes that are already in the tree. Dependent nodes added later will begin the dependency resolution process once the node receives the `Node.NotificationReady` notification.

## ⚠️ Advice
## 🙏 Tips

### Simple Dependency Trees
### Keep Dependency Trees Simple

For best results, keep dependency trees simple and free from asynchronous initialization. If you try to get too fancy, you can introduce dependency resolution deadlock. Avoiding complex dependency hierarchies can often be done with a little extra experimentation as you design your game.

Expand Down Expand Up @@ -146,6 +146,51 @@ public partial class MyDependent : Node {
}
```

### Fallback Values

You can provide fallback values to use when a provider can't be found. This can make it easier to run a scene by itself from the editor without having to worry about setting up production dependencies. Naturally, the fallback value will only be used if a provider can't be found for that type above the dependent node.

```csharp
[Dependency]
public string MyDependency => DependOn<string>(() => "fallback_value");
```

## How AutoInject Works

AutoInject uses a simple, specific algorithm to resolve dependencies.

- When the Dependent PowerUp is added to a SuperNode, the SuperNodes generator will copy the code from the Dependent PowerUp into the node it was applied to.
- A node script with the Dependent PowerUp observes its lifecycle. When it notices the `Node.NotificationReady` signal, it will begin the dependency resolution process without you having to write any code in your node script.
- The dependency process works as follows:
- All properties of the node script are inspected using SuperNode's static reflection table generation. This allows the script to introspect itself without having to resort to C#'s runtime reflection calls. Properties with the `[Dependency]` attribute are collected into the set of required dependencies.
- All required dependencies are added to the remaining dependencies set.
- The dependent node begins searching its ancestors, beginning with itself, then its parent, and so on up the tree.
- If the current search node implements `IProvide` for any of the remaining dependencies, the individual resolution process begins.
- The dependency stores the provider in a dictionary property on your node script which was copied over from the Dependent PowerUp.
- The dependency is added to the set of found dependencies.
- If the provider search node has not already provided its dependencies, the dependent subscribes to the `OnInitialized` event of the provider.
- Pending dependency provider callbacks track a counter for the dependent node that also remove that provider's dependency from the remaining dependencies set and initiate the OnResolved process if nothing is left.
- Subscribing to an event on the provider node and tracking whether or not the provider is initialized is made possible by SuperNodes, which copies the code from the Provider PowerUp into the provider's node script.
- After checking all the remaining dependencies, the set of found dependencies are removed from the remaining dependencies set and the found dependencies set is cleared for the next search node.
- If all the dependencies are found, the dependent initiates the OnResolved process and finishes the search.
- Otherwise, the search node's parent becomes the next parent to search.
- Search concludes when providers for each dependency are found, or the top of the scene tree is reached.

There are some natural consequences to this algorithm, such as `OnResolved` not being invoked on a dependent until all providers have provided a value. This is intentional — providers are expected to synchronously initialize their provided values after `_Ready` has been invoked on them.

AutoInject primarily exists to to locate providers from dependents and subscribe to the providers just long enough for their own `_Ready` method to be invoked — waiting longer than that to call `Provide` from a provider can introduce dependency resolution deadlock or other undesirable circumstances that are indicative of anti-patterns.

By calling `Provide()` from `_Ready` in provider nodes, you ensure that the order of execution unfolds as follows, synchronously:

1. Dependent node `_Ready` (descendant of the provider, deepest nodes ready-up first).
2. Provider node `_Ready` (which calls `Provide`).
3. Dependent `OnResolved`
4. Frame 1 `_Process`
5. Frame 2 `_Process`
6. Etc.

By following the `Provide()` on `_Ready` convention, you guarantee all dependent nodes receive an `OnResolved` callback before the first process invocation occurs, guaranteeing that nodes are setup before frame processing begins ✨.

---

🐣 Package generated from a 🐤 Chickensoft Template — <https://chickensoft.games>
Expand Down

0 comments on commit f1e846b

Please sign in to comment.