feat: aetherbind, first source generator
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static Builder AddValueProvider<TValueType, TScope>(this Builder builder)
|
||||
where TScope : RootScope
|
||||
{
|
||||
return builder.AddValueProvider<TValueType, TScope, TScope>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://o6qak8rcn6bw
|
||||
@@ -0,0 +1,109 @@
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.AetherBind.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// Taken from https://github.com/godotengine/godot-proposals/issues/12294
|
||||
/// </summary>
|
||||
public static class GodotScriptPathCache
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, string> TypeToPath = new();
|
||||
private static readonly ConcurrentDictionary<string, Type> PathToType = new();
|
||||
private static readonly Type GodotObject = typeof(GodotObject);
|
||||
private static readonly Type ScriptPathAttribute = typeof(ScriptPathAttribute);
|
||||
|
||||
private static AssemblyLoadEventHandler? _assemblyLoadEventHandler;
|
||||
|
||||
private static void InitializeIfNeeded()
|
||||
{
|
||||
if (_assemblyLoadEventHandler != null) return;
|
||||
Initialize();
|
||||
AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly())!
|
||||
.Unloading += _ => { Deinitialize(); };
|
||||
}
|
||||
|
||||
private static void Initialize()
|
||||
{
|
||||
GD.Print($"Initializing {nameof(GodotScriptPathCache)}");
|
||||
TypeToPath.Clear();
|
||||
PathToType.Clear();
|
||||
_assemblyLoadEventHandler = (sender, args) => CacheScriptsInAssembly(args.LoadedAssembly);
|
||||
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoadEventHandler;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
CacheScriptsInAssembly(assembly);
|
||||
return;
|
||||
|
||||
void CacheScriptsInAssembly(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes()
|
||||
.Where(t => t.IsClass)
|
||||
.Where(t => t.IsSubclassOf(GodotObject)))
|
||||
// get script path attribute
|
||||
if (type.GetCustomAttributes(ScriptPathAttribute, false).FirstOrDefault() is ScriptPathAttribute
|
||||
scriptPath)
|
||||
{
|
||||
TypeToPath[type] = scriptPath.Path;
|
||||
PathToType[scriptPath.Path] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Deinitialize()
|
||||
{
|
||||
GD.Print($"Deinitializing {nameof(GodotScriptPathCache)}");
|
||||
AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoadEventHandler;
|
||||
_assemblyLoadEventHandler = null;
|
||||
TypeToPath.Clear();
|
||||
PathToType.Clear();
|
||||
}
|
||||
|
||||
public static bool TryGetScriptPath(Type type, [MaybeNullWhen(false)] out string path)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return TypeToPath.TryGetValue(type, out path);
|
||||
}
|
||||
|
||||
public static Script GetScriptFromType(Type type)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return TryGetScriptPath(type, out string path)
|
||||
? GD.Load<Script>(path)
|
||||
: throw new InvalidOperationException("Script path not found in cache.");
|
||||
}
|
||||
|
||||
public static Script GetScriptFromType<T>()
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return GetScriptFromType(typeof(T));
|
||||
}
|
||||
|
||||
public static bool TryGetTypeFromPath(string path, [MaybeNullWhen(false)] out Type type)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return PathToType.TryGetValue(path, out type);
|
||||
}
|
||||
|
||||
public static Type? GetCSharpScriptType(this GodotObject node)
|
||||
{
|
||||
if (node.GetScript().AsGodotObject() is CSharpScript script &&
|
||||
TryGetTypeFromPath(script.ResourcePath, out var type))
|
||||
return type;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsServiceSource(this GodotObject node)
|
||||
{
|
||||
return node.GetScript().AsGodotObject() is CSharpScript script &&
|
||||
TryGetTypeFromPath(script.ResourcePath, out var type) &&
|
||||
type.IsAssignableTo(typeof(ServiceSource));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://brjpccpd5ta6x
|
||||
@@ -0,0 +1,176 @@
|
||||
#if TOOLS
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Godot;
|
||||
using OwofGames.AetherBind;
|
||||
using Array = Godot.Collections.Array;
|
||||
|
||||
namespace GodotHostTest.AetherBind.Editor;
|
||||
|
||||
[Tool]
|
||||
public partial class Plugin : EditorPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of debounce before updating the injections
|
||||
/// </summary>
|
||||
private const double DebounceDelay = 0.1;
|
||||
|
||||
private const BindingFlags InjectBindingFlags =
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
private ScopeInspectorPlugin? _plugin;
|
||||
private SceneTreeTimer? _sceneTreeTimer;
|
||||
|
||||
private bool _treeChangesRegistered;
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
GD.Print("Initializing Godot DI");
|
||||
|
||||
// add the inspector plugin to provide a better interface for the "scope" property
|
||||
_plugin = new ScopeInspectorPlugin();
|
||||
AddInspectorPlugin(_plugin);
|
||||
|
||||
// update injected nodes when the scene changes
|
||||
SceneChanged += OnSceneChanged;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
GD.Print("Deinitializing Godot DI");
|
||||
|
||||
SceneChanged -= OnSceneChanged;
|
||||
|
||||
UnregisterTreeChanges();
|
||||
|
||||
RemoveInspectorPlugin(_plugin);
|
||||
_plugin?.Dispose();
|
||||
_plugin = null;
|
||||
}
|
||||
|
||||
private void OnTreeChanged()
|
||||
{
|
||||
var root = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||
OnSceneChanged(root);
|
||||
}
|
||||
|
||||
private void OnSceneChanged(Node? sceneRoot)
|
||||
{
|
||||
// register to tree changes, in case the registration wasn't possible before
|
||||
RegisterTreeChanges();
|
||||
|
||||
// don't do anything if there's no scene root
|
||||
if (sceneRoot == null) return;
|
||||
|
||||
// apply debounce
|
||||
if (_sceneTreeTimer != null)
|
||||
{
|
||||
_sceneTreeTimer.SetTimeLeft(DebounceDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
_sceneTreeTimer = GetTree().CreateTimer(DebounceDelay);
|
||||
_sceneTreeTimer.Timeout += () => ActualOnSceneChanged(sceneRoot);
|
||||
}
|
||||
|
||||
private void ActualOnSceneChanged(Node sceneRoot)
|
||||
{
|
||||
// mark debounce as completed
|
||||
_sceneTreeTimer = null;
|
||||
|
||||
|
||||
// check that the instance is still valid after debouncing (could have been disposed)
|
||||
if (!IsInstanceValid(sceneRoot)) return;
|
||||
|
||||
var sceneNodes = sceneRoot.FindChildren("*").Where(node => node.Owner == sceneRoot).Append(sceneRoot).ToList();
|
||||
GD.Print($"Updating injection for root {sceneRoot.Name}");
|
||||
|
||||
// look for the service source
|
||||
var serviceSource = sceneNodes.Where(child => child.IsServiceSource())
|
||||
.Single(out var failureReason);
|
||||
|
||||
switch (failureReason)
|
||||
{
|
||||
case EnumerableExtensions.SingleFailureReason.LessThanOne:
|
||||
return;
|
||||
case EnumerableExtensions.SingleFailureReason.MoreThanOne:
|
||||
GD.PushWarning("Found more than one service source owned by the scene.");
|
||||
return;
|
||||
default:
|
||||
{
|
||||
if (!IsInstanceValid(serviceSource)) return;
|
||||
// find all nodes that need injection
|
||||
var nodesNeedingInjection = sceneNodes.Where(node =>
|
||||
{
|
||||
var type = node.GetCSharpScriptType();
|
||||
return type != null && (
|
||||
type.GetFields(InjectBindingFlags).Any(field =>
|
||||
field.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0) ||
|
||||
type.GetMethods(InjectBindingFlags).Any(field =>
|
||||
field.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0)
|
||||
);
|
||||
}).ToList();
|
||||
|
||||
// add all nodes needed to the scope
|
||||
var injectedNodes = serviceSource.Get(ServiceSource.PropertyName.InjectedNodes).AsGodotArray();
|
||||
Array? newArray = null;
|
||||
foreach (var node in nodesNeedingInjection
|
||||
.Where(node => !injectedNodes.Contains(node)))
|
||||
{
|
||||
newArray ??= new Array(injectedNodes);
|
||||
newArray.Add(node);
|
||||
GD.Print($"Added node {node.Name} to scope {serviceSource.GetPath()}.");
|
||||
}
|
||||
|
||||
// remove all nodes that are no longer needing injection
|
||||
foreach (var node in injectedNodes
|
||||
.Select(n => (Node?)n.AsGodotObject())
|
||||
.Where(node => !nodesNeedingInjection.Contains(node)))
|
||||
{
|
||||
if (node == null) continue;
|
||||
newArray ??= new Array(injectedNodes);
|
||||
newArray.Remove(node);
|
||||
GD.Print($"Removed node {node.Name} to scope {serviceSource.GetPath()}.");
|
||||
}
|
||||
|
||||
// apply changes to the scene if necessary
|
||||
if (newArray != null)
|
||||
{
|
||||
GD.Print("Apply changes.");
|
||||
var undoRedo = GetUndoRedo();
|
||||
undoRedo.CreateAction("Update injected nodes");
|
||||
undoRedo.AddDoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes, newArray);
|
||||
undoRedo.AddUndoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes,
|
||||
new Array(injectedNodes));
|
||||
undoRedo.CommitAction();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach to the TreeChanged event of the SceneTree if it's not already registered and if there's a scene tree to
|
||||
/// attach to from the currently edited scene.
|
||||
/// </summary>
|
||||
private void RegisterTreeChanges()
|
||||
{
|
||||
if (_treeChangesRegistered) return;
|
||||
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||
if (sceneRoot == null) return;
|
||||
var tree = sceneRoot.GetTree();
|
||||
tree.TreeChanged += OnTreeChanged;
|
||||
_treeChangesRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterTreeChanges()
|
||||
{
|
||||
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||
if (!_treeChangesRegistered || sceneRoot == null) return;
|
||||
var tree = sceneRoot.GetTree();
|
||||
tree.TreeChanged -= OnTreeChanged;
|
||||
SceneChanged -= OnSceneChanged;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtvb8nb6d2j4m
|
||||
@@ -0,0 +1,30 @@
|
||||
#if TOOLS
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.AetherBind.Editor;
|
||||
|
||||
// it's inside ServiceSource so that it can safely access nameof(_serviceScope)
|
||||
public partial class ScopeInspectorPlugin : EditorInspectorPlugin
|
||||
{
|
||||
public override bool _CanHandle(GodotObject @object)
|
||||
{
|
||||
var result = @object.IsServiceSource();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool _ParseProperty(GodotObject @object, Variant.Type type, string name, PropertyHint hintType,
|
||||
string hintString,
|
||||
PropertyUsageFlags usageFlags, bool wide)
|
||||
{
|
||||
// We handle properties of type integer.
|
||||
if (name != nameof(ServiceSource.PropertyName._serviceScope)) return false;
|
||||
// Create an instance of the custom property editor and register
|
||||
// it to a specific property path.
|
||||
AddPropertyEditor(name, new ScopePickerEditor());
|
||||
// Inform the editor to remove the default property editor for
|
||||
// this property type.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cl7d2h5juc2gh
|
||||
@@ -0,0 +1,97 @@
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.AetherBind.Editor;
|
||||
|
||||
public partial class ScopePickerEditor : EditorProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently chosen type.
|
||||
/// </summary>
|
||||
private string _currentType;
|
||||
|
||||
/// <summary>
|
||||
/// All the known fully qualified names for scopes.
|
||||
/// </summary>
|
||||
private string[] _fullyQualifiedNames;
|
||||
|
||||
/// <summary>
|
||||
/// The "combo box" that picks a choice from the scopes.
|
||||
/// </summary>
|
||||
private OptionButton _optionButton = new();
|
||||
|
||||
/// <summary>
|
||||
/// A guard against internal changes while the property is being updated.
|
||||
/// </summary>
|
||||
private bool _updating;
|
||||
|
||||
public ScopePickerEditor()
|
||||
{
|
||||
// save all the known fully qualified names
|
||||
_fullyQualifiedNames = ScopeTypesAndNames.GetQualifiedNames().ToArray();
|
||||
|
||||
// fill the options button
|
||||
for (var i = 0; i < _fullyQualifiedNames.Length; i++)
|
||||
{
|
||||
// root scope is always the first, and root scope = not scoped
|
||||
_optionButton.AddItem(
|
||||
i == 0
|
||||
? "not a scope boundary"
|
||||
: ScopeTypesAndNames.GetSimpleNameFromQualifiedName(_fullyQualifiedNames[i]), i);
|
||||
}
|
||||
|
||||
// add the control as a direct child of EditorProperty node.
|
||||
AddChild(_optionButton);
|
||||
|
||||
// make sure the control is able to retain the focus.
|
||||
AddFocusable(_optionButton);
|
||||
|
||||
// initialize the starting value
|
||||
_currentType = _fullyQualifiedNames[0];
|
||||
|
||||
// update the property when the button value changes
|
||||
_optionButton.ItemSelected += OnOptionButtonItemSelected;
|
||||
}
|
||||
|
||||
public override void _UpdateProperty()
|
||||
{
|
||||
// get the new value and immediately return if it's already the selected one.
|
||||
var newValue = GetEditedObject().Get(GetEditedProperty()).AsString();
|
||||
if (newValue == _currentType) return;
|
||||
|
||||
_updating = true;
|
||||
try
|
||||
{
|
||||
var nameIndex = Array.IndexOf(_fullyQualifiedNames, newValue);
|
||||
if (nameIndex < 0)
|
||||
{
|
||||
GD.PushWarning($"Could not find name {newValue} in the list of scope names, resetting to root scope");
|
||||
_currentType = _fullyQualifiedNames[0];
|
||||
EmitChanged(GetEditedProperty(), _currentType);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentType = newValue;
|
||||
UpdateControl();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOptionButtonItemSelected(long index)
|
||||
{
|
||||
_currentType = _fullyQualifiedNames[index];
|
||||
UpdateControl();
|
||||
EmitChanged(GetEditedProperty(), _currentType);
|
||||
}
|
||||
|
||||
private void UpdateControl()
|
||||
{
|
||||
_optionButton.Selected = Array.IndexOf(_fullyQualifiedNames, _currentType);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cy4jjk14my305
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public enum SingleFailureReason
|
||||
{
|
||||
LessThanOne,
|
||||
MoreThanOne
|
||||
}
|
||||
|
||||
public static T? Single<T>(this IEnumerable<T> enumerable, out SingleFailureReason? failureReason)
|
||||
where T : class
|
||||
{
|
||||
using var enumerator = enumerable.GetEnumerator();
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
failureReason = SingleFailureReason.LessThanOne;
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = enumerator.Current;
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
failureReason = SingleFailureReason.MoreThanOne;
|
||||
return null;
|
||||
}
|
||||
|
||||
failureReason = null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ca5k0qqats56p
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
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 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, Delegate? onScopeCreated = null);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bv7aut87rpm5u
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
public interface IValueGetter<out T>
|
||||
{
|
||||
public T Value { get; }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cti8rs1qvkwgs
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
public interface IValueSetter<in T>
|
||||
{
|
||||
public void Set(T value);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ci0sgiavxhc37
|
||||
@@ -0,0 +1,18 @@
|
||||
TOC:
|
||||
|
||||
- what GodotDI offers (in short):
|
||||
- logging
|
||||
- configuration
|
||||
- possibility to attach extra DI-based services like telemetry)
|
||||
- minimal description of what problems DI tackles
|
||||
- minimal example with logging, configuration, decoupling of components
|
||||
- how to install
|
||||
- creation of the root service source
|
||||
- example
|
||||
- scopes
|
||||
- why
|
||||
- definition
|
||||
- deep dive into why it's all done at composition root
|
||||
- using extension methods to split the code as necessary
|
||||
- sending data into a scope
|
||||
- initializing data in the scope
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OwofGames.GodotHost;
|
||||
using OwofGames.GodotLume;
|
||||
using OwofGames.GodotLume.Microsoft.DependencyInjection;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
/// <summary>
|
||||
/// The base class for a root scope. Implement this class and its abstract methods to kick off the DI system.
|
||||
/// </summary>
|
||||
public abstract partial class RootServiceSource : ServiceSource
|
||||
{
|
||||
private Host? _host;
|
||||
|
||||
private ILogger<RootServiceSource>? _logger;
|
||||
|
||||
private Host Host
|
||||
{
|
||||
get => _host ??
|
||||
throw new InvalidOperationException(
|
||||
"Some method of RootScope has been invoked before it entered the tree");
|
||||
set => _host = value;
|
||||
}
|
||||
|
||||
private ILogger<RootServiceSource> Logger => _logger ??= Host.GetLogger<RootServiceSource>();
|
||||
|
||||
public override void _EnterTree()
|
||||
{
|
||||
// create the host
|
||||
Host = new HostBuilder()
|
||||
.SetServiceCollectionBuilder(InnerConfigure)
|
||||
.SetServiceProviderFactory(new LumeServiceProviderFactory(), Configure)
|
||||
.Build();
|
||||
|
||||
// resolve nodes
|
||||
ResolveInjectedNodes(Host.ServiceProvider.GetRequiredService<IProvider>(), null);
|
||||
}
|
||||
|
||||
private void InnerConfigure(IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddTransient<ISceneInstantiator, SceneInstantiator>();
|
||||
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 virtual void ConfigureServices(IServiceCollection serviceCollection)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://fr0c6wwras8c
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
internal class SceneInstantiator(IProvider provider) : ISceneInstantiator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null)
|
||||
{
|
||||
var node = packedScene.Instantiate();
|
||||
var scope = node.GetChildren().OfType<ServiceSource>().SingleOrDefault();
|
||||
if (scope == null)
|
||||
provider.Get<ILogger<SceneInstantiator>>().LogDebug(
|
||||
"Instantiated the scene {PackedSceneName} through ISceneInstantiator.Instantiate, but no scope found.",
|
||||
packedScene.GetName());
|
||||
else
|
||||
scope.ResolveInjectedNodes(provider, onScopeCreated);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public T Instantiate<T>(PackedScene packedScene, Delegate? onScopeCreated = null) where T : Node
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://uiy5un3c0oee
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
public static class SceneInstantiatorExtensions
|
||||
{
|
||||
/// <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="sceneInstantiator">The scene instantiator to use.</param>
|
||||
/// <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>
|
||||
private static T Instantiate<T>(this ISceneInstantiator sceneInstantiator, PackedScene packedScene,
|
||||
Delegate? onScopeCreated = null) where T : Node
|
||||
{
|
||||
return (T)sceneInstantiator.Instantiate(packedScene, onScopeCreated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://drmkxc3gmkneu
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Godot;
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
public static class ScopeTypesAndNames
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, string> TypeToQualifiedName = new();
|
||||
private static readonly ConcurrentDictionary<string, Type> QualifiedNameToType = new();
|
||||
private static readonly ConcurrentDictionary<string, string> QualifiedNameToSimpleName = new();
|
||||
private static readonly Type RootScopeType = typeof(RootScope);
|
||||
|
||||
private static AssemblyLoadEventHandler? _assemblyLoadEventHandler;
|
||||
|
||||
private static void InitializeIfNeeded()
|
||||
{
|
||||
if (_assemblyLoadEventHandler != null) return;
|
||||
Initialize();
|
||||
AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly())!.Unloading += OnUnloading;
|
||||
}
|
||||
|
||||
private static void OnUnloading(AssemblyLoadContext _)
|
||||
{
|
||||
Deinitialize();
|
||||
}
|
||||
|
||||
private static void Initialize()
|
||||
{
|
||||
GD.Print($"Initializing {nameof(ScopeTypesAndNames)}");
|
||||
TypeToQualifiedName.Clear();
|
||||
QualifiedNameToType.Clear();
|
||||
QualifiedNameToSimpleName.Clear();
|
||||
_assemblyLoadEventHandler = (sender, args) => CacheScriptsInAssembly(args.LoadedAssembly);
|
||||
AppDomain.CurrentDomain.AssemblyLoad += _assemblyLoadEventHandler;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
CacheScriptsInAssembly(assembly);
|
||||
return;
|
||||
|
||||
void CacheScriptsInAssembly(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes()
|
||||
.Where(t => t.IsClass)
|
||||
.Where(t => t == RootScopeType || t.IsSubclassOf(RootScopeType)))
|
||||
{
|
||||
if (type.AssemblyQualifiedName == null)
|
||||
{
|
||||
GD.PrintErr("Assembly qualified name of {type} is null");
|
||||
continue;
|
||||
}
|
||||
|
||||
TypeToQualifiedName[type] = type.AssemblyQualifiedName;
|
||||
QualifiedNameToType[type.AssemblyQualifiedName] = type;
|
||||
QualifiedNameToSimpleName[type.AssemblyQualifiedName] = type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Deinitialize()
|
||||
{
|
||||
GD.Print($"Deinitializing {nameof(ScopeTypesAndNames)}");
|
||||
AppDomain.CurrentDomain.AssemblyLoad -= _assemblyLoadEventHandler;
|
||||
_assemblyLoadEventHandler = null;
|
||||
TypeToQualifiedName.Clear();
|
||||
QualifiedNameToType.Clear();
|
||||
QualifiedNameToSimpleName.Clear();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetQualifiedNames()
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return from p in TypeToQualifiedName
|
||||
orderby p.Key == RootScopeType ? 0 : 1
|
||||
select p.Value;
|
||||
}
|
||||
|
||||
public static Type GetType(string qualifiedName)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return QualifiedNameToType[qualifiedName];
|
||||
}
|
||||
|
||||
public static string GetSimpleNameFromQualifiedName(string qualifiedName)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
return QualifiedNameToSimpleName[qualifiedName];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgxakmyv77lcu
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OwofGames.AetherBind;
|
||||
using OwofGames.GodotLume;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
[Icon("res://addons/AetherBind/ServiceSource.svg")]
|
||||
public partial class ServiceSource : Node
|
||||
{
|
||||
private static readonly string RootScopeQualifiedName = typeof(RootScope).AssemblyQualifiedName!;
|
||||
[Export] protected Node?[] InjectedNodes = [];
|
||||
[Export] private string _serviceScope = RootScopeQualifiedName;
|
||||
|
||||
internal void ResolveInjectedNodes(IProvider provider, Delegate? onScopeCreated)
|
||||
{
|
||||
var logger = provider.Get<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);
|
||||
provider = provider.GetScopedProvider(serviceScope);
|
||||
// invoke onScopeCreated, if present
|
||||
// TODO: replace with a source code generation by turning this method private 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 => provider.Get(parameterInfo.ParameterType))
|
||||
.ToArray();
|
||||
onScopeCreated.DynamicInvoke(parameters);
|
||||
}
|
||||
|
||||
OnScopeCreated(provider);
|
||||
}
|
||||
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
|
||||
foreach (var node in InjectedNodes)
|
||||
{
|
||||
if (node == null) continue;
|
||||
logger.LogTrace("Injecting {Node}.", node.Name);
|
||||
|
||||
if (node is IInjectable injectable)
|
||||
{
|
||||
injectable.Inject(provider);
|
||||
}
|
||||
|
||||
// var nodeType = node.GetType();
|
||||
// foreach (var field in nodeType
|
||||
// .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
// {
|
||||
// if (field.GetCustomAttribute<InjectAttribute>() == null) continue;
|
||||
// var fieldType = field.FieldType;
|
||||
// var service = provider.Get(fieldType);
|
||||
// field.SetValue(node, service);
|
||||
// }
|
||||
//
|
||||
// foreach (var method in nodeType.GetMethods(BindingFlags.Instance | BindingFlags.Public |
|
||||
// BindingFlags.NonPublic))
|
||||
// {
|
||||
// if (method.GetCustomAttribute<InjectAttribute>() == null) continue;
|
||||
// var parameters = method.GetParameters();
|
||||
// var values = new object?[parameters.Length];
|
||||
// var i = 0;
|
||||
// foreach (var parameter in parameters)
|
||||
// {
|
||||
// var parameterType = parameter.ParameterType;
|
||||
// var service = provider.Get(parameterType);
|
||||
// values[i++] = service;
|
||||
// }
|
||||
//
|
||||
// method.Invoke(node, values);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: also add source generator to mark methods in derived classes with [OnScopeCreated] and perform parameter injection
|
||||
protected virtual void OnScopeCreated(IProvider serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://qjonl8stia46
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#e0e0e0" d="M15 14a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-1h14zM3 12H1V4h2zM12 4v2l-6 6H4v-2l6-6zM15 12h-2V4h2zM14 1a1 1 0 0 1 1 1v1H1V2a1 1 0 0 1 1-1z"/></svg>
|
||||
|
After Width: | Height: | Size: 259 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ce5hi46doaiv8"
|
||||
path="res://.godot/imported/ServiceSource.svg-22939ab8c3bebd4d414fabd470fcc1cf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/AetherBind/ServiceSource.svg"
|
||||
dest_files=["res://.godot/imported/ServiceSource.svg-22939ab8c3bebd4d414fabd470fcc1cf.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace GodotHostTest.AetherBind;
|
||||
|
||||
/// <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 Value => value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsubhm0j58vwo
|
||||
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="AetherBind"
|
||||
description=""
|
||||
author="owof games"
|
||||
version="0.1"
|
||||
script="Editor/Plugin.cs"
|
||||
Reference in New Issue
Block a user