feat: more plugin work and intra-scope communication
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.GodotDI;
|
||||
|
||||
public static class BuilderExtensions
|
||||
{
|
||||
public static Builder AddValueProvider<TValueType, TSourceScope, TDestinationScope>(this Builder builder)
|
||||
where TSourceScope : RootScope
|
||||
where TDestinationScope : TSourceScope
|
||||
{
|
||||
var valueProvider = new ValueProvider<TValueType>();
|
||||
return builder
|
||||
.AddScoped<IValueSetter<TValueType>, IValueSetter<TValueType>, TSourceScope>(Builder.NoDependencies,
|
||||
_ => valueProvider.Setter)
|
||||
.AddScoped<IValueGetter<TValueType>, IValueGetter<TValueType>, TDestinationScope>(Builder.NoDependencies,
|
||||
_ => valueProvider.Getter);
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ public partial class Plugin : EditorPlugin
|
||||
// mark debounce as completed
|
||||
_sceneTreeTimer = null;
|
||||
|
||||
|
||||
// check that the instance is still valid after debouncing (could have been disposed)
|
||||
if (!IsInstanceValid(sceneRoot)) return;
|
||||
|
||||
@@ -96,6 +97,7 @@ public partial class Plugin : EditorPlugin
|
||||
return;
|
||||
default:
|
||||
{
|
||||
if (!IsInstanceValid(serviceSource)) return;
|
||||
// find all nodes that need injection
|
||||
var nodesNeedingInjection = sceneNodes.Where(node =>
|
||||
{
|
||||
@@ -109,7 +111,7 @@ public partial class Plugin : EditorPlugin
|
||||
}).ToList();
|
||||
|
||||
// add all nodes needed to the scope
|
||||
var injectedNodes = serviceSource!.Get(ServiceSource.PropertyName.InjectedNodes).AsGodotArray();
|
||||
var injectedNodes = serviceSource.Get(ServiceSource.PropertyName.InjectedNodes).AsGodotArray();
|
||||
Array? newArray = null;
|
||||
foreach (var node in nodesNeedingInjection
|
||||
.Where(node => !injectedNodes.Contains(node)))
|
||||
@@ -137,7 +139,8 @@ public partial class Plugin : EditorPlugin
|
||||
var undoRedo = GetUndoRedo();
|
||||
undoRedo.CreateAction("Update injected nodes");
|
||||
undoRedo.AddDoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes, newArray);
|
||||
undoRedo.AddUndoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes, injectedNodes);
|
||||
undoRedo.AddUndoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes,
|
||||
new Array(injectedNodes));
|
||||
undoRedo.CommitAction();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.GodotDI;
|
||||
@@ -5,16 +6,18 @@ namespace GodotHostTest.GodotDI;
|
||||
public interface ISceneInstantiator
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiate a packed scene, just like <see cref="PackedScene.Instantiate"/>, but also looks for a scope between the top level children and triggers the dependency injection mechanism when found.
|
||||
/// Instantiate a packed scene, just like <see cref="PackedScene.Instantiate"/>, but also looks for a scope between the top level children of the instantiated scene and triggers the dependency injection mechanism when found.
|
||||
/// </summary>
|
||||
/// <param name="packedScene">The packed scene to instantiate.</param>
|
||||
/// <param name="onScopeCreated">An optional callback invoked once the scope has been created, but before instantiating the scene.</param>
|
||||
/// <returns>The created node, with its dependencies satisfied.</returns>
|
||||
Node Instantiate(PackedScene packedScene);
|
||||
Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null);
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a packed scene, just like <see cref="PackedScene.Instantiate{T}"/>, but also looks for a scope between the top level children and triggers the dependency injection mechanism when found.
|
||||
/// </summary>
|
||||
/// <param name="packedScene">The packed scene to instantiate.</param>
|
||||
/// <param name="onScopeCreated">An optional callback invoked once the scope has been created, but before instantiating the scene.</param>
|
||||
/// <returns>The created node, with its dependencies satisfied.</returns>
|
||||
T Instantiate<T>(PackedScene packedScene) where T : Node;
|
||||
T Instantiate<T>(PackedScene packedScene, Delegate? onScopeCreated = null) where T : Node;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace GodotHostTest.GodotDI;
|
||||
|
||||
public interface IValueGetter<out T>
|
||||
{
|
||||
public T Get();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace GodotHostTest.GodotDI;
|
||||
|
||||
public interface IValueSetter<in T>
|
||||
{
|
||||
public void Set(T value);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Godot;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OwofGames.GodotHost;
|
||||
using OwofGames.GodotLume;
|
||||
using OwofGames.GodotLume.Microsoft.DependencyInjection;
|
||||
|
||||
namespace GodotHostTest.GodotDI;
|
||||
@@ -15,6 +16,8 @@ public abstract partial class RootServiceSource : ServiceSource, ISceneInstantia
|
||||
{
|
||||
private Host? _host;
|
||||
|
||||
private ILogger<RootServiceSource>? _logger;
|
||||
|
||||
private Host Host
|
||||
{
|
||||
get => _host ??
|
||||
@@ -23,57 +26,59 @@ public abstract partial class RootServiceSource : ServiceSource, ISceneInstantia
|
||||
set => _host = value;
|
||||
}
|
||||
|
||||
private ILogger<RootServiceSource>? _logger;
|
||||
|
||||
private ILogger<RootServiceSource> Logger => _logger ??= Host.GetLogger<RootServiceSource>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null)
|
||||
{
|
||||
var node = packedScene.Instantiate();
|
||||
var scope = node.GetChildren().OfType<ServiceSource>().SingleOrDefault();
|
||||
if (scope == null)
|
||||
Logger.LogDebug(
|
||||
"Instantiated the scene {PackedSceneName} through ISceneInstantiator.Instantiate, but no scope found.",
|
||||
packedScene.GetName());
|
||||
else
|
||||
scope.ResolveInjectedNodes(Host.ServiceProvider, onScopeCreated);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Instantiate<T>(PackedScene packedScene, Delegate? onScopeCreated = null)
|
||||
where T : Node
|
||||
{
|
||||
return (T)Instantiate(packedScene, onScopeCreated);
|
||||
}
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
// create the host
|
||||
Host = new HostBuilder()
|
||||
.SetServiceCollectionBuilder(InnerConfigure)
|
||||
.SetServiceProviderFactory(new LumeServiceProviderFactory())
|
||||
.SetServiceProviderFactory(new LumeServiceProviderFactory(), Configure)
|
||||
.Build();
|
||||
|
||||
// resolve nodes
|
||||
ResolveInjectedNodes(Host.ServiceProvider);
|
||||
ResolveInjectedNodes(Host.ServiceProvider, null);
|
||||
}
|
||||
|
||||
private void InnerConfigure(IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<ISceneInstantiator>(this);
|
||||
Configure(serviceCollection);
|
||||
ConfigureServices(serviceCollection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the service collection by adding the registrations your game needs.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to enrich with your services.</param>
|
||||
protected abstract void Configure(Builder builder);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the service collection by adding the registrations your game needs.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">The service collection to enrich.</param>
|
||||
protected abstract void Configure(IServiceCollection serviceCollection);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Node Instantiate(PackedScene packedScene)
|
||||
protected virtual void ConfigureServices(IServiceCollection serviceCollection)
|
||||
{
|
||||
var node = packedScene.Instantiate();
|
||||
var scope = node.GetChildren().OfType<ServiceSource>().SingleOrDefault();
|
||||
if (scope == null)
|
||||
{
|
||||
Logger.LogDebug(
|
||||
"Instantiated the scene {PackedSceneName} through ISceneInstantiator.Instantiate, but no scope found.",
|
||||
packedScene.GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
scope.ResolveInjectedNodes(Host.ServiceProvider);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T Instantiate<T>(PackedScene packedScene)
|
||||
where T : Node
|
||||
{
|
||||
return (T)Instantiate(packedScene);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Godot;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.GodotDI;
|
||||
@@ -13,18 +15,33 @@ public partial class ServiceSource : Node
|
||||
[Export] private string _serviceScope = RootScopeQualifiedName;
|
||||
[Export] protected Node?[] InjectedNodes = [];
|
||||
|
||||
internal void ResolveInjectedNodes(IServiceProvider serviceProvider)
|
||||
internal void ResolveInjectedNodes(IServiceProvider serviceProvider, Delegate? onScopeCreated)
|
||||
{
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<ServiceSource>>();
|
||||
|
||||
// if this service source must also act as a scope, create the scope and use its service provider
|
||||
if (_serviceScope != RootScopeQualifiedName)
|
||||
{
|
||||
// create the scoped provider
|
||||
var serviceScope = ScopeTypesAndNames.GetType(_serviceScope);
|
||||
var scopedProvider = serviceProvider
|
||||
.GetRequiredService<IProvider>()
|
||||
.GetScopedProvider(serviceScope);
|
||||
// convert it also to an IServiceProvider
|
||||
var serviceProviderCreator = serviceProvider.GetRequiredService<Func<IProvider, IServiceProvider>>();
|
||||
var scopedProvider = serviceProvider.GetRequiredService<IProvider>()
|
||||
.GetScopedProvider(ScopeTypesAndNames.GetType(_serviceScope));
|
||||
serviceProvider = serviceProviderCreator(scopedProvider);
|
||||
// serviceProvider = serviceProvider
|
||||
// .CreateScope()
|
||||
// .ServiceProvider;
|
||||
// invoke onScopeCreated, if present
|
||||
// TODO: replace with a source code generation by turning this method private partial with Action<IProvider> argument, and every invocation site calls the necessary provider.Get<...> to build the argument list
|
||||
if (onScopeCreated != null)
|
||||
{
|
||||
var parameters = onScopeCreated.Method.GetParameters()
|
||||
.Select(parameterInfo => scopedProvider.Get(parameterInfo.ParameterType));
|
||||
onScopeCreated.DynamicInvoke(parameters);
|
||||
}
|
||||
}
|
||||
else if (onScopeCreated != null)
|
||||
{
|
||||
logger.LogWarning("onScopeCreated passed during instantiation, but no scoped provider was created.");
|
||||
}
|
||||
|
||||
// resolve the injected nodes against the chosen service provider
|
||||
@@ -32,6 +49,7 @@ public partial class ServiceSource : Node
|
||||
foreach (var node in InjectedNodes)
|
||||
{
|
||||
if (node == null) continue;
|
||||
logger.LogTrace("Injecting {Node}.", node.Name);
|
||||
var nodeType = node.GetType();
|
||||
foreach (var field in nodeType
|
||||
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace GodotHostTest.GodotDI;
|
||||
|
||||
/// <summary>
|
||||
/// An object that allows communication through scopes, by providing a couple getter/setter that can be injected in
|
||||
/// different scopes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that's been passed through scopes.</typeparam>
|
||||
/// <seealso cref="BuilderExtensions.AddValueProvider" />
|
||||
public class ValueProvider<T>
|
||||
{
|
||||
private bool _isSet;
|
||||
private T? _value;
|
||||
|
||||
public ValueProvider()
|
||||
{
|
||||
Setter = new SetterImplementation(this);
|
||||
}
|
||||
|
||||
public IValueSetter<T> Setter { get; }
|
||||
|
||||
public IValueGetter<T> Getter => !_isSet
|
||||
? throw new InvalidOperationException($"Value of type {typeof(T)} has not been set yet.")
|
||||
: new GetterImplementation(_value!);
|
||||
|
||||
private class SetterImplementation(ValueProvider<T> valueProvider) : IValueSetter<T>
|
||||
{
|
||||
public void Set(T value)
|
||||
{
|
||||
if (valueProvider._isSet)
|
||||
throw new InvalidOperationException($"A value (of type {typeof(T)}) cannot be set more than once.");
|
||||
|
||||
valueProvider._value = value;
|
||||
valueProvider._isSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
private class GetterImplementation(T value) : IValueGetter<T>
|
||||
{
|
||||
public T Get()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user