From f1e846b6359133b9f6c54c0678fba14fc76de92d Mon Sep 17 00:00:00 2001 From: Joanna May Date: Fri, 22 Sep 2023 22:33:21 -0500 Subject: [PATCH] docs: readme --- .vscode/launch.json | 4 +- Chickensoft.AutoInject.Tests/src/Dependent.cs | 4 ++ .../test/src/ResolutionTest.cs | 12 +++++ .../test/src/subjects/Dependents.cs | 9 ++++ README.md | 49 ++++++++++++++++++- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4432b31..93260f0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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. @@ -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. diff --git a/Chickensoft.AutoInject.Tests/src/Dependent.cs b/Chickensoft.AutoInject.Tests/src/Dependent.cs index 3470f2d..0d2ed6e 100644 --- a/Chickensoft.AutoInject.Tests/src/Dependent.cs +++ b/Chickensoft.AutoInject.Tests/src/Dependent.cs @@ -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 { diff --git a/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs b/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs index 1a008c1..8e41bcf 100644 --- a/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs +++ b/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs @@ -195,6 +195,18 @@ public void AccessingDependencyBeforeProvidedEvenIfCreatedThrows() { Should.Throw(() => 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; } diff --git a/Chickensoft.AutoInject.Tests/test/src/subjects/Dependents.cs b/Chickensoft.AutoInject.Tests/test/src/subjects/Dependents.cs index 5c37d2c..d087ea7 100644 --- a/Chickensoft.AutoInject.Tests/test/src/subjects/Dependents.cs +++ b/Chickensoft.AutoInject.Tests/test/src/subjects/Dependents.cs @@ -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; +} diff --git a/README.md b/README.md index 4aa8f37..b8e925c 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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(() => "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 —