From c540b99eb60bc1fdbe3cd9e6fa309cb377b270f1 Mon Sep 17 00:00:00 2001 From: redglow Date: Mon, 20 Jul 2026 11:06:35 +0200 Subject: [PATCH] feat: new ValueProvider improvements and source generator. --- Game/Level/LevelServiceSource.cs | 12 +++++++++-- Game/Projectile/Projectile.cs | 6 ++---- Game/Root/Root.cs | 15 +++++++------ Game/Score/Score.cs | 6 ++---- Game/Target/Target.cs | 6 ++---- Game/Target/TargetSpawner.cs | 7 ++++--- Game/Turret/Turret.cs | 1 - Game/level.tscn | 3 +-- Game/levels_data.tres | 2 +- addons/AetherBind/BuilderExtensions.cs | 9 +++++++- addons/AetherBind/README.md | 29 +++++++++++++++++++++++++- addons/AetherBind/ValueProvider.cs | 20 ++++++++++++------ 12 files changed, 79 insertions(+), 37 deletions(-) diff --git a/Game/Level/LevelServiceSource.cs b/Game/Level/LevelServiceSource.cs index c1192c7..07028b2 100644 --- a/Game/Level/LevelServiceSource.cs +++ b/Game/Level/LevelServiceSource.cs @@ -2,6 +2,7 @@ using Godot; using GodotHostTest.AetherBind; using GodotHostTest.Game.Target; using GodotHostTest.Interfaces; +using OwofGames.AetherBind; using OwofGames.GodotLume; using ServiceSource = GodotHostTest.AetherBind.ServiceSource; @@ -13,7 +14,14 @@ public partial class LevelServiceSource : ServiceSource protected override void OnScopeCreated(IProvider serviceProvider) { - serviceProvider.Get>().Set(_targetSpawner); - serviceProvider.Get>().Set(_targetSpawner); + OnScopeCreatedWithProvider(serviceProvider); + } + + [WithProvider] + private void OnScopeCreated(IValueSetter targetCollector, + IValueSetter targetSpawner) + { + targetCollector.Set(_targetSpawner); + targetSpawner.Set(_targetSpawner); } } \ No newline at end of file diff --git a/Game/Projectile/Projectile.cs b/Game/Projectile/Projectile.cs index 95f7dfe..716108d 100644 --- a/Game/Projectile/Projectile.cs +++ b/Game/Projectile/Projectile.cs @@ -1,5 +1,4 @@ using Godot; -using GodotHostTest.AetherBind; using GodotHostTest.Helpers; using GodotHostTest.Interfaces; using Microsoft.Extensions.Logging; @@ -8,16 +7,15 @@ using R3; namespace GodotHostTest.Game.Projectile; -[Injected] public partial class Projectile : Node2D { [Inject] private ILogger _logger = null!; - [Inject] private IValueGetter _targetSpawner = null!; + [Inject] private ITargetSpawner _targetSpawner = null!; // Called when the node enters the scene tree for the first time. public override void _Ready() { - _targetSpawner.Value.TargetSpawned + _targetSpawner.TargetSpawned .Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this); } } \ No newline at end of file diff --git a/Game/Root/Root.cs b/Game/Root/Root.cs index 8da9031..9562a0f 100644 --- a/Game/Root/Root.cs +++ b/Game/Root/Root.cs @@ -1,13 +1,10 @@ -using System; using Godot; using GodotHostTest.AetherBind; using GodotHostTest.Interfaces; using OwofGames.AetherBind; -using OwofGames.GodotLume; namespace GodotHostTest.Game.Root; -[Injected] public partial class Root : Node2D { [Inject] private ICurrentLevel _currentLevel = null!; @@ -17,11 +14,13 @@ public partial class Root : Node2D public override void _Ready() { - var levelData = _levelDataProvider.GetLevelData(_currentLevel.LevelNumber); - AddChild(_sceneInstantiator.Instantiate(_levelScene, - OnInstantiate(levelValueSetter => levelValueSetter.Set(levelData)))); + AddChild(_sceneInstantiator.Instantiate(_levelScene, OnInstantiateWithProvider)); } - [ServiceMethod] - private partial Action OnInstantiate(Action> a); + [WithProvider] + private void OnInstantiate(IValueSetter levelValueSetter) + { + var levelData = _levelDataProvider.GetLevelData(_currentLevel.LevelNumber); + levelValueSetter.Set(levelData); + } } \ No newline at end of file diff --git a/Game/Score/Score.cs b/Game/Score/Score.cs index f1f7ced..86e0cc6 100644 --- a/Game/Score/Score.cs +++ b/Game/Score/Score.cs @@ -1,5 +1,4 @@ using Godot; -using GodotHostTest.AetherBind; using GodotHostTest.Helpers; using GodotHostTest.Interfaces; using OwofGames.AetherBind; @@ -7,20 +6,19 @@ using R3; namespace GodotHostTest.Game.Score; -[Injected] public partial class Score : Control, IScore { [Export] private AnimationPlayer _animationPlayer = null!; [Export] private Label _label = null!; private int _score; - [Inject] private IValueGetter _targetSpawner = null!; + [Inject] private ITargetSpawner _targetSpawner = null!; int IScore.Score => _score; public override void _Ready() { - _targetSpawner.Value.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); + _targetSpawner.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); base._Ready(); } diff --git a/Game/Target/Target.cs b/Game/Target/Target.cs index e24064a..e1428ab 100644 --- a/Game/Target/Target.cs +++ b/Game/Target/Target.cs @@ -1,12 +1,10 @@ using Godot; -using GodotHostTest.AetherBind; using GodotHostTest.Interfaces; using OwofGames.AetherBind; using R3; namespace GodotHostTest.Game.Target; -[Injected] public partial class Target : Node2D, ITarget { private readonly Subject _hit = new(); @@ -17,7 +15,7 @@ public partial class Target : Node2D, ITarget [Export] public double TimeOutDurationInSeconds; [Export] public required Timer TimeOutTimer; - [Inject] private IValueGetter _targetCollector = null!; + [Inject] private ITargetCollector _targetCollector = null!; public Observable TimedOut => _timedOut.AsObservable(); @@ -32,7 +30,7 @@ public partial class Target : Node2D, ITarget public override void _Ready() { - _targetCollector.Value.NewTarget(this); + _targetCollector.NewTarget(this); } public override void _ExitTree() diff --git a/Game/Target/TargetSpawner.cs b/Game/Target/TargetSpawner.cs index 43f5252..6179cde 100644 --- a/Game/Target/TargetSpawner.cs +++ b/Game/Target/TargetSpawner.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Godot; using GodotHostTest.Helpers; using GodotHostTest.Interfaces; +using OwofGames.AetherBind; using R3; namespace GodotHostTest.Game.Target; @@ -11,10 +12,10 @@ public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner private readonly List _availableTargets = []; private readonly Subject _targetSpawned = new(); - [Export] public double TimeBetweenSpawns; - [Export] public required Timer Timer; + [Inject] private ILevel _level = null!; + public void NewTarget(ITarget target) { _availableTargets.Add(target); @@ -26,7 +27,7 @@ public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner public override void _Ready() { Spawn(); - Timer.Start(TimeBetweenSpawns); + Timer.Start(_level.TimeBetweenSpawns.TotalSeconds); } private void MakeTargetAvailable(ITarget target) diff --git a/Game/Turret/Turret.cs b/Game/Turret/Turret.cs index b290b69..28ba29e 100644 --- a/Game/Turret/Turret.cs +++ b/Game/Turret/Turret.cs @@ -4,7 +4,6 @@ using OwofGames.AetherBind; namespace GodotHostTest.Game.Turret; -[Injected] public partial class Turret : Sprite2D { [Export] private PackedScene _projectilePackedScene = null!; diff --git a/Game/level.tscn b/Game/level.tscn index 0f1a040..7e9f14f 100644 --- a/Game/level.tscn +++ b/Game/level.tscn @@ -74,8 +74,8 @@ _data = { [node name="LevelScope" type="Node" parent="." unique_id=1267156771 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")] script = ExtResource("1_5saw1") _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" -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")] position = Vector2(198, 143) @@ -103,7 +103,6 @@ TimeOutDurationInSeconds = 2.0 [node name="Spawner" type="Node2D" parent="." unique_id=251404460 node_paths=PackedStringArray("Timer")] script = ExtResource("2_5saw1") -TimeBetweenSpawns = 1.5 Timer = NodePath("Timer") [node name="Timer" type="Timer" parent="Spawner" unique_id=1066078427] diff --git a/Game/levels_data.tres b/Game/levels_data.tres index fb20401..6167fc4 100644 --- a/Game/levels_data.tres +++ b/Game/levels_data.tres @@ -5,7 +5,7 @@ [sub_resource type="Resource" id="Resource_bbjqm"] script = ExtResource("1_bkfnq") -TimeBetweenSpawns = 2.0 +TimeBetweenSpawns = 0.5 metadata/_custom_type_script = "uid://ywe7vkywrdvk" [resource] diff --git a/addons/AetherBind/BuilderExtensions.cs b/addons/AetherBind/BuilderExtensions.cs index aa9499a..1012243 100644 --- a/addons/AetherBind/BuilderExtensions.cs +++ b/addons/AetherBind/BuilderExtensions.cs @@ -5,18 +5,25 @@ namespace GodotHostTest.AetherBind; public static class BuilderExtensions { public static Builder AddValueProvider(this Builder builder) + where TValueType : class where TSourceScope : RootScope where TDestinationScope : TSourceScope { var valueProvider = new ValueProvider(); return builder + // add the setter to the source scope .AddScoped, IValueSetter, TSourceScope>(Builder.NoDependencies, _ => valueProvider.Setter) + // add the getter to the destination scope .AddScoped, IValueGetter, TDestinationScope>(Builder.NoDependencies, - _ => valueProvider.Getter); + _ => valueProvider.Getter) + // add a factory to automatically extract TValueType from the IValueGetter + .AddScoped(_ => [typeof(IValueGetter)], + provider => provider.Get>().Value); } public static Builder AddValueProvider(this Builder builder) + where TValueType : class where TScope : RootScope { return builder.AddValueProvider(); diff --git a/addons/AetherBind/README.md b/addons/AetherBind/README.md index b629d3d..fd63114 100644 --- a/addons/AetherBind/README.md +++ b/addons/AetherBind/README.md @@ -15,4 +15,31 @@ TOC: - 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 \ No newline at end of file + - 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(), + provider.Get() + ); +} +``` \ No newline at end of file diff --git a/addons/AetherBind/ValueProvider.cs b/addons/AetherBind/ValueProvider.cs index bc2e238..86db727 100644 --- a/addons/AetherBind/ValueProvider.cs +++ b/addons/AetherBind/ValueProvider.cs @@ -3,13 +3,14 @@ using System; namespace GodotHostTest.AetherBind; /// -/// An object that allows communication through scopes, by providing a couple getter/setter that can be injected in -/// different scopes. +/// 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). /// /// The type that's been passed through scopes. -/// +/// +/// public class ValueProvider { + private IValueGetter? _getter; private bool _isSet; private T? _value; @@ -20,9 +21,16 @@ public class ValueProvider public IValueSetter Setter { get; } - public IValueGetter Getter => !_isSet - ? throw new InvalidOperationException($"Value of type {typeof(T)} has not been set yet.") - : new GetterImplementation(_value!); + public IValueGetter Getter + { + 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 valueProvider) : IValueSetter {