diff --git a/Game/Root.cs b/Game/Root.cs new file mode 100644 index 0000000..18127b4 --- /dev/null +++ b/Game/Root.cs @@ -0,0 +1,15 @@ +using Godot; +using GodotHostTest.GodotDI; + +namespace GodotHostTest.Game; + +public partial class Root : Node2D +{ + [Inject] private readonly ISceneInstantiator _sceneInstantiator = null!; + [Export] private PackedScene _levelScene = null!; + + public override void _Ready() + { + AddChild(_sceneInstantiator.Instantiate(_levelScene)); + } +} \ No newline at end of file diff --git a/Game/Root.cs.uid b/Game/Root.cs.uid new file mode 100644 index 0000000..0410f5f --- /dev/null +++ b/Game/Root.cs.uid @@ -0,0 +1 @@ +uid://bvjvojer872c8 diff --git a/Game/level.tscn b/Game/level.tscn new file mode 100644 index 0000000..ac8cb9c --- /dev/null +++ b/Game/level.tscn @@ -0,0 +1,144 @@ +[gd_scene format=3 uid="uid://xfd48ep5qi1k"] + +[ext_resource type="Script" uid="uid://dk578xnlxjwrr" path="res://Game/LevelServiceSource.cs" id="1_5saw1"] +[ext_resource type="PackedScene" uid="uid://3v8kq1rfwhmr" path="res://Game/target.tscn" id="1_y7rhs"] +[ext_resource type="Script" uid="uid://cxfrtglku2gfa" path="res://Game/TargetSpawner.cs" id="2_5saw1"] +[ext_resource type="Script" uid="uid://bgrjeadxhgmvi" path="res://Game/Score.cs" id="3_spj1u"] +[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="4_esqs7"] +[ext_resource type="Script" uid="uid://ce7gnp1lcrrdv" path="res://Game/Turret.cs" id="5_himod"] +[ext_resource type="PackedScene" uid="uid://brbufibn7hi3o" path="res://Game/projectile.tscn" id="6_sin03"] + +[sub_resource type="Animation" id="Animation_bbjqm"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Level/Score:offset_transform_position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(0, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Level/Score:offset_transform_rotation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} + +[sub_resource type="Animation" id="Animation_4ty2a"] +resource_name = "wiggle" +length = 0.3 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Level/Score:offset_transform_position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.049999997, 0.099999994, 0.15, 0.2, 0.24999999), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 0, +"values": [Vector2(0, 0), Vector2(10, -10), Vector2(-10, -10), Vector2(10, 10), Vector2(-10, 10), Vector2(0, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Level/Score:offset_transform_rotation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.049999997, 0.099999994, 0.15, 0.2, 0.24999999), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 0, +"values": [0.0, 0.08726646, -0.08726646, 0.08726646, -0.08726646, 0.0] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_rckqj"] +_data = { +&"RESET": SubResource("Animation_bbjqm"), +&"wiggle": SubResource("Animation_4ty2a") +} + +[node name="Level" type="Node2D" unique_id=1331632726] + +[node name="RootScope" type="Node" parent="." unique_id=1267156771 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")] +script = ExtResource("1_5saw1") +_targetSpawner = NodePath("../Spawner") +_serviceScope = "GodotHostTest.Game.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) +TimeOutDurationInSeconds = 2.0 + +[node name="Target2" parent="." unique_id=1009901052 instance=ExtResource("1_y7rhs")] +position = Vector2(669, 64) +TimeOutDurationInSeconds = 2.0 + +[node name="Target3" parent="." unique_id=1433382188 instance=ExtResource("1_y7rhs")] +position = Vector2(1060, 263) +TimeOutDurationInSeconds = 2.0 + +[node name="Target4" parent="." unique_id=1553596281 instance=ExtResource("1_y7rhs")] +position = Vector2(961, 620) +TimeOutDurationInSeconds = 2.0 + +[node name="Target5" parent="." unique_id=46923989 instance=ExtResource("1_y7rhs")] +position = Vector2(469, 591) +TimeOutDurationInSeconds = 2.0 + +[node name="Target6" parent="." unique_id=2092179937 instance=ExtResource("1_y7rhs")] +position = Vector2(53, 417) +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] + +[node name="Score" type="ColorRect" parent="." unique_id=1267746809 node_paths=PackedStringArray("_animationPlayer", "_label")] +offset_right = 200.0 +offset_bottom = 40.0 +offset_transform_enabled = true +color = Color(1, 1, 1, 0.2784314) +script = ExtResource("3_spj1u") +_animationPlayer = NodePath("../AnimationPlayer") +_label = NodePath("Label") + +[node name="Label" type="Label" parent="Score" unique_id=558063655] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 10.0 +offset_right = -10.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "Score: 0" +vertical_alignment = 1 + +[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1215951318] +root_node = NodePath("../..") +libraries/ = SubResource("AnimationLibrary_rckqj") + +[node name="Turret" type="Sprite2D" parent="." unique_id=152356196] +modulate = Color(1.92523e-07, 0.88004553, 0.8802661, 1) +position = Vector2(539, 307) +texture = ExtResource("4_esqs7") +script = ExtResource("5_himod") +_projectilePackedScene = ExtResource("6_sin03") + +[connection signal="timeout" from="Spawner/Timer" to="Spawner" method="OnTimer"] diff --git a/Interfaces/ILevel.cs.uid b/Interfaces/ILevel.cs.uid new file mode 100644 index 0000000..d71b971 --- /dev/null +++ b/Interfaces/ILevel.cs.uid @@ -0,0 +1 @@ +uid://bahq074tnev8g diff --git a/addons/GodotDI/Editor/Plugin.cs b/addons/GodotDI/Editor/Plugin.cs new file mode 100644 index 0000000..9a7a084 --- /dev/null +++ b/addons/GodotDI/Editor/Plugin.cs @@ -0,0 +1,170 @@ +#if TOOLS +using System.Linq; +using System.Reflection; +using Godot; +using Godot.Collections; + +namespace GodotHostTest.GodotDI.Editor; + +[Tool] +public partial class Plugin : EditorPlugin +{ + /// + /// Amount of debounce before updating the injections + /// + 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 = + EditorInterface.Singleton.GetEditedSceneRoot().GetTree().CreateTimer(DebounceDelay); + _sceneTreeTimer.Timeout += () => ActualOnSceneChanged(sceneRoot); + } + + private void ActualOnSceneChanged(Node sceneRoot) + { + // mark debounce as completed + _sceneTreeTimer = null; + + var sceneNodes = sceneRoot.FindChildren("*"); + 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: + { + // find all nodes that need injection + var nodesNeedingInjection = sceneNodes.Append(sceneRoot).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 injection."); + } + + // 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} from injection."); + } + + // 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, injectedNodes); + undoRedo.CommitAction(); + } + + break; + } + } + } + + /// + /// 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. + /// + 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 \ No newline at end of file diff --git a/addons/GodotDI/Editor/ScopeInspectorPlugin.cs b/addons/GodotDI/Editor/ScopeInspectorPlugin.cs new file mode 100644 index 0000000..da37aa8 --- /dev/null +++ b/addons/GodotDI/Editor/ScopeInspectorPlugin.cs @@ -0,0 +1,30 @@ +#if TOOLS +using Godot; + +namespace GodotHostTest.GodotDI.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 \ No newline at end of file diff --git a/addons/GodotDI/EnumerableExtensions.cs.uid b/addons/GodotDI/EnumerableExtensions.cs.uid new file mode 100644 index 0000000..29137aa --- /dev/null +++ b/addons/GodotDI/EnumerableExtensions.cs.uid @@ -0,0 +1 @@ +uid://ca5k0qqats56p diff --git a/addons/GodotDI/GodotDI.cs b/addons/GodotDI/GodotDI.cs deleted file mode 100644 index cf20750..0000000 --- a/addons/GodotDI/GodotDI.cs +++ /dev/null @@ -1,26 +0,0 @@ -#if TOOLS -using Godot; - -namespace GodotHostTest.GodotDI; - -[Tool] -public partial class GodotDI : EditorPlugin -{ - private ServiceSource.ScopeInspectorPlugin? _plugin; - - public override void _EnterTree() - { - GD.Print("Initializing Godot DI"); - _plugin = new ServiceSource.ScopeInspectorPlugin(); - AddInspectorPlugin(_plugin); - } - - public override void _ExitTree() - { - GD.Print("Deinitializing Godot DI"); - RemoveInspectorPlugin(_plugin); - _plugin?.Dispose(); - _plugin = null; - } -} -#endif \ No newline at end of file diff --git a/addons/GodotDI/ScopeInspectorPlugin.cs b/addons/GodotDI/ScopeInspectorPlugin.cs deleted file mode 100644 index d8a6b37..0000000 --- a/addons/GodotDI/ScopeInspectorPlugin.cs +++ /dev/null @@ -1,36 +0,0 @@ -#if TOOLS -using Godot; -using GodotHostTest.addons.godot_di; - -namespace GodotHostTest.GodotDI; - -public partial class ServiceSource -{ - // 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.GetScript().AsGodotObject() is CSharpScript script && - GodotScriptPathCache.TryGetTypeFromPath(script.ResourcePath, out var type) && - type.IsAssignableTo(typeof(ServiceSource)); - 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(_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 \ No newline at end of file