feat: new ValueProvider improvements and source generator.

This commit is contained in:
redglow
2026-07-20 11:06:35 +02:00
parent 036e4af431
commit c540b99eb6
12 changed files with 79 additions and 37 deletions
+10 -2
View File
@@ -2,6 +2,7 @@ using Godot;
using GodotHostTest.AetherBind; using GodotHostTest.AetherBind;
using GodotHostTest.Game.Target; using GodotHostTest.Game.Target;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using OwofGames.AetherBind;
using OwofGames.GodotLume; using OwofGames.GodotLume;
using ServiceSource = GodotHostTest.AetherBind.ServiceSource; using ServiceSource = GodotHostTest.AetherBind.ServiceSource;
@@ -13,7 +14,14 @@ public partial class LevelServiceSource : ServiceSource
protected override void OnScopeCreated(IProvider serviceProvider) protected override void OnScopeCreated(IProvider serviceProvider)
{ {
serviceProvider.Get<IValueSetter<ITargetCollector>>().Set(_targetSpawner); OnScopeCreatedWithProvider(serviceProvider);
serviceProvider.Get<IValueSetter<ITargetSpawner>>().Set(_targetSpawner); }
[WithProvider]
private void OnScopeCreated(IValueSetter<ITargetCollector> targetCollector,
IValueSetter<ITargetSpawner> targetSpawner)
{
targetCollector.Set(_targetSpawner);
targetSpawner.Set(_targetSpawner);
} }
} }
+2 -4
View File
@@ -1,5 +1,4 @@
using Godot; using Godot;
using GodotHostTest.AetherBind;
using GodotHostTest.Helpers; using GodotHostTest.Helpers;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -8,16 +7,15 @@ using R3;
namespace GodotHostTest.Game.Projectile; namespace GodotHostTest.Game.Projectile;
[Injected]
public partial class Projectile : Node2D public partial class Projectile : Node2D
{ {
[Inject] private ILogger<Projectile> _logger = null!; [Inject] private ILogger<Projectile> _logger = null!;
[Inject] private IValueGetter<ITargetSpawner> _targetSpawner = null!; [Inject] private ITargetSpawner _targetSpawner = null!;
// Called when the node enters the scene tree for the first time. // Called when the node enters the scene tree for the first time.
public override void _Ready() public override void _Ready()
{ {
_targetSpawner.Value.TargetSpawned _targetSpawner.TargetSpawned
.Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this); .Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this);
} }
} }
+7 -8
View File
@@ -1,13 +1,10 @@
using System;
using Godot; using Godot;
using GodotHostTest.AetherBind; using GodotHostTest.AetherBind;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using OwofGames.AetherBind; using OwofGames.AetherBind;
using OwofGames.GodotLume;
namespace GodotHostTest.Game.Root; namespace GodotHostTest.Game.Root;
[Injected]
public partial class Root : Node2D public partial class Root : Node2D
{ {
[Inject] private ICurrentLevel _currentLevel = null!; [Inject] private ICurrentLevel _currentLevel = null!;
@@ -17,11 +14,13 @@ public partial class Root : Node2D
public override void _Ready() public override void _Ready()
{ {
var levelData = _levelDataProvider.GetLevelData(_currentLevel.LevelNumber); AddChild(_sceneInstantiator.Instantiate(_levelScene, OnInstantiateWithProvider));
AddChild(_sceneInstantiator.Instantiate(_levelScene,
OnInstantiate(levelValueSetter => levelValueSetter.Set(levelData))));
} }
[ServiceMethod] [WithProvider]
private partial Action<IProvider> OnInstantiate(Action<IValueSetter<ILevel>> a); private void OnInstantiate(IValueSetter<ILevel> levelValueSetter)
{
var levelData = _levelDataProvider.GetLevelData(_currentLevel.LevelNumber);
levelValueSetter.Set(levelData);
}
} }
+2 -4
View File
@@ -1,5 +1,4 @@
using Godot; using Godot;
using GodotHostTest.AetherBind;
using GodotHostTest.Helpers; using GodotHostTest.Helpers;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using OwofGames.AetherBind; using OwofGames.AetherBind;
@@ -7,20 +6,19 @@ using R3;
namespace GodotHostTest.Game.Score; namespace GodotHostTest.Game.Score;
[Injected]
public partial class Score : Control, IScore public partial class Score : Control, IScore
{ {
[Export] private AnimationPlayer _animationPlayer = null!; [Export] private AnimationPlayer _animationPlayer = null!;
[Export] private Label _label = null!; [Export] private Label _label = null!;
private int _score; private int _score;
[Inject] private IValueGetter<ITargetSpawner> _targetSpawner = null!; [Inject] private ITargetSpawner _targetSpawner = null!;
int IScore.Score => _score; int IScore.Score => _score;
public override void _Ready() public override void _Ready()
{ {
_targetSpawner.Value.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); _targetSpawner.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this);
base._Ready(); base._Ready();
} }
+2 -4
View File
@@ -1,12 +1,10 @@
using Godot; using Godot;
using GodotHostTest.AetherBind;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using OwofGames.AetherBind; using OwofGames.AetherBind;
using R3; using R3;
namespace GodotHostTest.Game.Target; namespace GodotHostTest.Game.Target;
[Injected]
public partial class Target : Node2D, ITarget public partial class Target : Node2D, ITarget
{ {
private readonly Subject<Unit> _hit = new(); private readonly Subject<Unit> _hit = new();
@@ -17,7 +15,7 @@ public partial class Target : Node2D, ITarget
[Export] public double TimeOutDurationInSeconds; [Export] public double TimeOutDurationInSeconds;
[Export] public required Timer TimeOutTimer; [Export] public required Timer TimeOutTimer;
[Inject] private IValueGetter<ITargetCollector> _targetCollector = null!; [Inject] private ITargetCollector _targetCollector = null!;
public Observable<Unit> TimedOut => _timedOut.AsObservable(); public Observable<Unit> TimedOut => _timedOut.AsObservable();
@@ -32,7 +30,7 @@ public partial class Target : Node2D, ITarget
public override void _Ready() public override void _Ready()
{ {
_targetCollector.Value.NewTarget(this); _targetCollector.NewTarget(this);
} }
public override void _ExitTree() public override void _ExitTree()
+4 -3
View File
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using Godot; using Godot;
using GodotHostTest.Helpers; using GodotHostTest.Helpers;
using GodotHostTest.Interfaces; using GodotHostTest.Interfaces;
using OwofGames.AetherBind;
using R3; using R3;
namespace GodotHostTest.Game.Target; namespace GodotHostTest.Game.Target;
@@ -11,10 +12,10 @@ public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner
private readonly List<ITarget> _availableTargets = []; private readonly List<ITarget> _availableTargets = [];
private readonly Subject<ITarget> _targetSpawned = new(); private readonly Subject<ITarget> _targetSpawned = new();
[Export] public double TimeBetweenSpawns;
[Export] public required Timer Timer; [Export] public required Timer Timer;
[Inject] private ILevel _level = null!;
public void NewTarget(ITarget target) public void NewTarget(ITarget target)
{ {
_availableTargets.Add(target); _availableTargets.Add(target);
@@ -26,7 +27,7 @@ public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner
public override void _Ready() public override void _Ready()
{ {
Spawn(); Spawn();
Timer.Start(TimeBetweenSpawns); Timer.Start(_level.TimeBetweenSpawns.TotalSeconds);
} }
private void MakeTargetAvailable(ITarget target) private void MakeTargetAvailable(ITarget target)
-1
View File
@@ -4,7 +4,6 @@ using OwofGames.AetherBind;
namespace GodotHostTest.Game.Turret; namespace GodotHostTest.Game.Turret;
[Injected]
public partial class Turret : Sprite2D public partial class Turret : Sprite2D
{ {
[Export] private PackedScene _projectilePackedScene = null!; [Export] private PackedScene _projectilePackedScene = null!;
+1 -2
View File
@@ -74,8 +74,8 @@ _data = {
[node name="LevelScope" type="Node" parent="." unique_id=1267156771 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")] [node name="LevelScope" type="Node" parent="." unique_id=1267156771 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")]
script = ExtResource("1_5saw1") script = ExtResource("1_5saw1")
_targetSpawner = NodePath("../Spawner") _targetSpawner = NodePath("../Spawner")
InjectedNodes = [NodePath("../Target"), NodePath("../Target2"), NodePath("../Target3"), NodePath("../Target4"), NodePath("../Target5"), NodePath("../Target6"), NodePath("../Score"), NodePath("../Turret"), NodePath("../Spawner")]
_serviceScope = "GodotHostTest.Game.Level.LevelScope, godot-host-test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" _serviceScope = "GodotHostTest.Game.Level.LevelScope, godot-host-test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
InjectedNodes = [NodePath("../Target"), NodePath("../Target2"), NodePath("../Target3"), NodePath("../Target4"), NodePath("../Target5"), NodePath("../Target6"), NodePath("../Score"), NodePath("../Turret")]
[node name="Target" parent="." unique_id=2101558052 instance=ExtResource("1_y7rhs")] [node name="Target" parent="." unique_id=2101558052 instance=ExtResource("1_y7rhs")]
position = Vector2(198, 143) position = Vector2(198, 143)
@@ -103,7 +103,6 @@ TimeOutDurationInSeconds = 2.0
[node name="Spawner" type="Node2D" parent="." unique_id=251404460 node_paths=PackedStringArray("Timer")] [node name="Spawner" type="Node2D" parent="." unique_id=251404460 node_paths=PackedStringArray("Timer")]
script = ExtResource("2_5saw1") script = ExtResource("2_5saw1")
TimeBetweenSpawns = 1.5
Timer = NodePath("Timer") Timer = NodePath("Timer")
[node name="Timer" type="Timer" parent="Spawner" unique_id=1066078427] [node name="Timer" type="Timer" parent="Spawner" unique_id=1066078427]
+1 -1
View File
@@ -5,7 +5,7 @@
[sub_resource type="Resource" id="Resource_bbjqm"] [sub_resource type="Resource" id="Resource_bbjqm"]
script = ExtResource("1_bkfnq") script = ExtResource("1_bkfnq")
TimeBetweenSpawns = 2.0 TimeBetweenSpawns = 0.5
metadata/_custom_type_script = "uid://ywe7vkywrdvk" metadata/_custom_type_script = "uid://ywe7vkywrdvk"
[resource] [resource]
+8 -1
View File
@@ -5,18 +5,25 @@ namespace GodotHostTest.AetherBind;
public static class BuilderExtensions public static class BuilderExtensions
{ {
public static Builder AddValueProvider<TValueType, TSourceScope, TDestinationScope>(this Builder builder) public static Builder AddValueProvider<TValueType, TSourceScope, TDestinationScope>(this Builder builder)
where TValueType : class
where TSourceScope : RootScope where TSourceScope : RootScope
where TDestinationScope : TSourceScope where TDestinationScope : TSourceScope
{ {
var valueProvider = new ValueProvider<TValueType>(); var valueProvider = new ValueProvider<TValueType>();
return builder return builder
// add the setter to the source scope
.AddScoped<IValueSetter<TValueType>, IValueSetter<TValueType>, TSourceScope>(Builder.NoDependencies, .AddScoped<IValueSetter<TValueType>, IValueSetter<TValueType>, TSourceScope>(Builder.NoDependencies,
_ => valueProvider.Setter) _ => valueProvider.Setter)
// add the getter to the destination scope
.AddScoped<IValueGetter<TValueType>, IValueGetter<TValueType>, TDestinationScope>(Builder.NoDependencies, .AddScoped<IValueGetter<TValueType>, IValueGetter<TValueType>, TDestinationScope>(Builder.NoDependencies,
_ => valueProvider.Getter); _ => valueProvider.Getter)
// add a factory to automatically extract TValueType from the IValueGetter<TValueType>
.AddScoped<TValueType, TValueType, TDestinationScope>(_ => [typeof(IValueGetter<TValueType>)],
provider => provider.Get<IValueGetter<TValueType>>().Value);
} }
public static Builder AddValueProvider<TValueType, TScope>(this Builder builder) public static Builder AddValueProvider<TValueType, TScope>(this Builder builder)
where TValueType : class
where TScope : RootScope where TScope : RootScope
{ {
return builder.AddValueProvider<TValueType, TScope, TScope>(); return builder.AddValueProvider<TValueType, TScope, TScope>();
+28 -1
View File
@@ -15,4 +15,31 @@ TOC:
- deep dive into why it's all done at composition root - deep dive into why it's all done at composition root
- using extension methods to split the code as necessary - using extension methods to split the code as necessary
- sending data into a scope - sending data into a scope
- initializing data in the scope - initializing data in the scope
NOTES
field with \[Inject\] or method with \[Inject\] automatically receive injection IF they are between the injected objects
of a ServiceSource
a method with \[WithProvider\] generates a companion method for unwrapping provider. E.g.:
```csharp
[WithProvider]
private void OnReady(IService1 service1, IService2 service2)
{
// ...
}
```
will generate a companion method similar to:
```csharp
private void OnReadyWithProvider(IProvider provider)
{
OnReady(
provider.Get<IService1>(),
provider.Get<IService2>()
);
}
```
+14 -6
View File
@@ -3,13 +3,14 @@ using System;
namespace GodotHostTest.AetherBind; namespace GodotHostTest.AetherBind;
/// <summary> /// <summary>
/// An object that allows communication through scopes, by providing a couple getter/setter that can be injected in /// An object that allows communication through scopes (or inside the scope itself) by providing a couple getter/setter that can be injected in different scopes (or the same scope).
/// different scopes.
/// </summary> /// </summary>
/// <typeparam name="T">The type that's been passed through scopes.</typeparam> /// <typeparam name="T">The type that's been passed through scopes.</typeparam>
/// <seealso cref="BuilderExtensions.AddValueProvider" /> /// <seealso cref="BuilderExtensions.AddValueProvider{TValueType,TScope}" />
/// <seealso cref="BuilderExtensions.AddValueProvider{TValueType,TSourceScope,TDestinationScope}" />
public class ValueProvider<T> public class ValueProvider<T>
{ {
private IValueGetter<T>? _getter;
private bool _isSet; private bool _isSet;
private T? _value; private T? _value;
@@ -20,9 +21,16 @@ public class ValueProvider<T>
public IValueSetter<T> Setter { get; } public IValueSetter<T> Setter { get; }
public IValueGetter<T> Getter => !_isSet public IValueGetter<T> Getter
? throw new InvalidOperationException($"Value of type {typeof(T)} has not been set yet.") {
: new GetterImplementation(_value!); get
{
_getter ??= !_isSet
? throw new InvalidOperationException($"Value of type {typeof(T)} has not been set yet.")
: new GetterImplementation(_value!);
return _getter;
}
}
private class SetterImplementation(ValueProvider<T> valueProvider) : IValueSetter<T> private class SetterImplementation(ValueProvider<T> valueProvider) : IValueSetter<T>
{ {