diff --git a/Example/Assertions.cs b/Example/Assertions.cs new file mode 100644 index 0000000..8f2476d --- /dev/null +++ b/Example/Assertions.cs @@ -0,0 +1,22 @@ +using System; +using Godot; + +namespace Decembrist.Example +{ + public static class Assertions + { + public static void AssertTrue(bool expression, string test) + { + if (expression) + { + GD.Print($"PASSED:{test}"); + } + else + { + var message = $"FAILED:{test}"; + GD.PrintErr(message); + throw new Exception(message); + } + } + } +} \ No newline at end of file diff --git a/Example/DiTestNode.cs b/Example/DiTestNode.cs new file mode 100644 index 0000000..f4989d2 --- /dev/null +++ b/Example/DiTestNode.cs @@ -0,0 +1,59 @@ +using System; +using Godot; +using Decembrist.Di; +using Decembrist.Example.Service; +using Decembrist.Utils; +using static Decembrist.Example.Assertions; + +namespace Decembrist.Example +{ + public class DiTestNode : Node2D + { + [Inject] private IService _service1; + + [Inject] private IService _service2; + + [Inject] private PrototypeService1 _prototypeService1; + + [Inject] private PrototypeService1 _prototypeService2; + + [Inject] private InstanceService _instanceService; + + [SettingsValue(DecembristSettings.EventBusEnabled)] + private bool _eventBusEnabled; + + public override void _Ready() + { + // init dependencies + var watch = new System.Diagnostics.Stopwatch(); + watch.Start(); + this.InjectAll(); + watch.Stop(); + GD.Print($"Injection time Time: {watch.ElapsedMilliseconds} ms"); + GD.Print("Service assertions................................................"); + AssertTrue(_service1 != null, "singleton service exists"); + AssertTrue(this.Resolve() != null, "singleton service exists"); + AssertTrue(_instanceService == InstanceService.Instance, "service is singleton"); + AssertTrue(this.Resolve() == _service1, "service is singleton"); + AssertTrue(_service1 == _service2, "service is singleton"); + AssertTrue(this.Resolve() != null, "prototype service exists"); + AssertTrue(this.Resolve() != _prototypeService1, "service is prototype"); + AssertTrue(_prototypeService1 != _prototypeService2, "service is prototype"); + AssertTrue(_prototypeService1.PrototypeService != _prototypeService2.PrototypeService, + "service.PrototypeService is prototype"); + AssertTrue(_eventBusEnabled, "settings value check"); + GD.Print("Service test......................................................"); + ServiceEcho(); + } + + private void ServiceEcho() + { + GD.Print($"singleton1 says {_service1.GetString()}"); + GD.Print($"singleton2 says {_service2.GetString()}"); + GD.Print($"resolved singleton says {this.Resolve().GetString()}"); + GD.Print($"prototype1 says {_prototypeService1.GetString()}"); + GD.Print($"prototype2 says {_prototypeService2.GetString()}"); + GD.Print($"resolved prototype says {this.Resolve().GetString()}"); + } + } +} \ No newline at end of file diff --git a/Example/EventBusTest/Consumer.cs b/Example/EventBusTest/Consumer.cs new file mode 100644 index 0000000..f9f7b84 --- /dev/null +++ b/Example/EventBusTest/Consumer.cs @@ -0,0 +1,49 @@ +using Decembrist.Events; +using Godot; + +namespace Decembrist.Example.EventBusTest +{ + public class Consumer : Node2D + { + public const string ConsumerAddress1 = "consumer-address1"; + public const string ConsumerAddress2 = "consumer-address2"; + public const string TestError = "test error"; + public const int TestErrorCode = 2; + + private EventBusSubscription _subscription1; + private EventBusSubscription _subscription2; + + public override void _Ready() + { + var messageCount1 = 0; + _subscription1 = this.Consumer(ConsumerAddress1, message => + { + messageCount1++; + HandleMessage(_subscription1, message, messageCount1); + }); + var messageCount2 = 0; + _subscription2 = this.Consumer(ConsumerAddress2, message => + { + messageCount2++; + HandleMessage(_subscription2, message, messageCount2); + }); + } + + private void HandleMessage( + EventBusSubscription eventBusSubscription, + ReplyEventMessage message, + int messageCount) + { + Assertions.AssertTrue(!message.IsError(), "not error message"); + if (messageCount > 5) + { + message.ErrorReply("test error", 2); + eventBusSubscription.Stop(); + } + else + { + message.Reply(message.Content + 1); + } + } + } +} diff --git a/Example/EventBusTest/Producer.cs b/Example/EventBusTest/Producer.cs new file mode 100644 index 0000000..69054a6 --- /dev/null +++ b/Example/EventBusTest/Producer.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Decembrist.Events; +using Godot; +using static Decembrist.Example.Assertions; + +namespace Decembrist.Example.EventBusTest +{ + [Tool] + public class Producer : Node2D + { + private readonly List _responses = new(); + + private string _errorMessage; + + private int? _errorCode; + + [Export] private string _messageAddress; + + public override async void _Ready() + { + await Task.Delay(1000); + GD.Print($"EventBus test started for {_messageAddress}...................."); + const int messageCount = 6; + ProduceMessages(messageCount); + await Task.Delay(1000); + AssertTrue(_responses.Count == 5, "all messages consumed"); + for (var i = 1; i < messageCount - 1; i++) + { + AssertTrue(_responses[i - 1] == i + 1, "message response ok"); + } + + AssertTrue(_errorMessage == Consumer.TestError, "event bus error message ok"); + AssertTrue(_errorCode == Consumer.TestErrorCode, "event bus error code ok"); + GD.Print("EventBus test stopped..........................................."); + } + + private async void ProduceMessages(int messageCount) + { + for (var i = 1; i <= messageCount; i++) + { + try + { + var response = await this.SendMessageAsync(_messageAddress, i); + _responses.Add(response); + } + catch (SendEventException ex) + { + _errorMessage = ex.Message; + _errorCode = ex.GetCode(); + } + } + } + } +} \ No newline at end of file diff --git a/Example/TestScene.cs b/Example/TestScene.cs deleted file mode 100644 index b04e9ea..0000000 --- a/Example/TestScene.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using Godot; -using Decembrist.Di; -using Decembrist.Example.Service; -using Decembrist.Utils; - -namespace Decembrist.Example -{ - public class TestScene : Node2D - { - [Inject] private IService service1; - - [Inject] private IService service2; - - [Inject] private PrototypeService1 prototypeService1; - - [Inject] private PrototypeService1 prototypeService2; - - [Inject] private InstanceService instanceService; - - public override void _Ready() - { - // init dependencies - var watch = new System.Diagnostics.Stopwatch(); - watch.Start(); - this.InjectAll(); - watch.Stop(); - GD.Print($"Injection time Time: {watch.ElapsedMilliseconds} ms"); - GD.Print("Service assertions................................................"); - AssertTrue(service1 != null, "singleton service exists"); - AssertTrue(this.Resolve() != null, "singleton service exists"); - AssertTrue(instanceService == InstanceService.Instance, "service is singleton"); - AssertTrue(this.Resolve() == service1, "service is singleton"); - AssertTrue(service1 == service2, "service is singleton"); - AssertTrue(this.Resolve() != null, "prototype service exists"); - AssertTrue(this.Resolve() != prototypeService1, "service is prototype"); - AssertTrue(prototypeService1 != prototypeService2, "service is prototype"); - AssertTrue(prototypeService1.PrototypeService != prototypeService2.PrototypeService, - "service.PrototypeService is prototype"); - GD.Print("Service test......................................................"); - ServiceEcho(); - } - - private void ServiceEcho() - { - GD.Print($"singleton1 says {service1.GetString()}"); - GD.Print($"singleton2 says {service2.GetString()}"); - GD.Print($"resolved singleton says {this.Resolve().GetString()}"); - GD.Print($"prototype1 says {prototypeService1.GetString()}"); - GD.Print($"prototype2 says {prototypeService2.GetString()}"); - GD.Print($"resolved prototype says {this.Resolve().GetString()}"); - } - - private static void AssertTrue(bool expression, string test) - { - if (expression) - { - GD.Print($"PASSED:{test}"); - } - else - { - var message = $"FAILED:{test}"; - GD.PrintErr(message); - throw new Exception(message); - } - } - } -} \ No newline at end of file diff --git a/Example/TestScene.tscn b/Example/TestScene.tscn index cb38d22..657177a 100644 --- a/Example/TestScene.tscn +++ b/Example/TestScene.tscn @@ -1,6 +1,23 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=4 format=2] -[ext_resource path="res://Example/TestScene.cs" type="Script" id=1] +[ext_resource path="res://Example/DiTestNode.cs" type="Script" id=1] +[ext_resource path="res://Example/EventBusTest/Producer.cs" type="Script" id=2] +[ext_resource path="res://Example/EventBusTest/Consumer.cs" type="Script" id=3] [node name="Node2D" type="Node2D"] + +[node name="DiTestNode" type="Node2D" parent="."] script = ExtResource( 1 ) + +[node name="EventBusTestNode" type="Node2D" parent="."] + +[node name="Producer1" type="Node2D" parent="EventBusTestNode"] +script = ExtResource( 2 ) +_messageAddress = "consumer-address1" + +[node name="Producer2" type="Node2D" parent="EventBusTestNode"] +script = ExtResource( 2 ) +_messageAddress = "consumer-address2" + +[node name="Consumer" type="Node2D" parent="EventBusTestNode"] +script = ExtResource( 3 ) diff --git a/README.md b/README.md index 5b0453d..85bc8cd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ **Godot Decembrist Plugin** _Utils for godot development_ * (DI) Dependency Injection for Godot -* Util classes for _Buttons_, _Controls_, _Vectors_ etc \ No newline at end of file +* Event driven development +* Util classes for _Buttons_, _Controls_, _Vectors_ etc + +[Documentation](https://github.com/decembrist-revolt/godot-decembrist-plugin/wiki) \ No newline at end of file diff --git a/addons/decembrist_plugin/Autoload/AutoloadUtils.cs b/addons/decembrist_plugin/Autoload/AutoloadUtils.cs new file mode 100644 index 0000000..79d19df --- /dev/null +++ b/addons/decembrist_plugin/Autoload/AutoloadUtils.cs @@ -0,0 +1,22 @@ +using Godot; + +namespace Decembrist.Autoload +{ + public static class AutoloadUtils + { + /// + /// Get DiService autoload instance + /// + /// + /// DiService singleton instance + public static DiService GetDiService(this Node node) => + node.GetNode("/root/DecembristAutoload").DiService; + + /// + /// Get event bus singleton instance + /// + /// EventBus instance + public static EventBus GetEventBus(this Node node) => + node.GetNode("/root/DecembristAutoload").EventBus; + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Autoload/DecembristAutoload.cs b/addons/decembrist_plugin/Autoload/DecembristAutoload.cs new file mode 100644 index 0000000..748ffa9 --- /dev/null +++ b/addons/decembrist_plugin/Autoload/DecembristAutoload.cs @@ -0,0 +1,40 @@ +using System; +using Decembrist.Di; +using Godot; + +namespace Decembrist.Autoload +{ + public class DecembristAutoload: Node + { + [SettingsValue(DecembristSettings.EventBusEnabled)] + private bool _eventBusEnabled; + + public readonly DiService DiService; + private EventBus _eventBus; + + public EventBus EventBus + { + get + { + if (!_eventBusEnabled) + { + throw new Exception("Event bus disabled"); + } + return _eventBus; + } + } + + public DecembristAutoload() + { + DiService = new DiService(); + AddChild(DiService); + } + + public override void _Ready() + { + this.InjectAll(); + _eventBus = DiService.Resolve(); + AddChild(_eventBus); + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Autoload/DiService.cs b/addons/decembrist_plugin/Autoload/DiService.cs index a79408f..0d4e626 100644 --- a/addons/decembrist_plugin/Autoload/DiService.cs +++ b/addons/decembrist_plugin/Autoload/DiService.cs @@ -8,18 +8,14 @@ namespace Decembrist.Autoload { - public class DiService : Node2D + public class DiService : Node { - public DiContainer Container; - - public override void _Ready() - { - } + public readonly DiContainer Container; public DiService() { var builder = new ContainerBuilder(); - builder.RegisterInstance(new ConfigService()); + builder.Register(); builder = InstantiateConfig()?.ConfigDi(builder) ?? builder; Container = builder.Build(); } @@ -35,28 +31,58 @@ public void InjectAll(object instance) var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); foreach (var field in fields) { - if (field.GetCustomAttribute(typeof(Inject)) != null) - { - var serviceType = field.FieldType; - var service = Container.ResolveOrNull(serviceType); - if (service != null) - { - field.SetValue(instance, service); - } - else - { - GD.PrintErr($"No such type registered in container: {serviceType} for class {type}"); - } - } + HandleInject(instance, field, type); + HandleSettingsValue(instance, field, type); + } + } + + /// + /// Handle [Inject] attribute + /// + private void HandleInject(object instance, FieldInfo field, Type? type) + { + if (field.GetCustomAttribute(typeof(InjectAttribute)) == null) return; + + var serviceType = field.FieldType; + var service = Container.ResolveOrNull(serviceType); + if (service != null) + { + field.SetValue(instance, service); + } + else + { + throw new UnsatisfiedDependencyException($"class {type}. Dependency type => {serviceType}"); + } + } + + /// + /// Handle [SettingsValue] attribute + /// + private void HandleSettingsValue(object instance, FieldInfo field, Type type) + { + var attribute = field.GetCustomAttribute(typeof(SettingsValueAttribute)); + if (attribute is not SettingsValueAttribute settingsAttribute) return; + + var settingsName = settingsAttribute.Name; + var settingsValue = ProjectSettings.GetSetting(settingsName); + var fieldType = field.FieldType; + if (settingsValue != null && fieldType.IsInstanceOfType(settingsValue)) + { + field.SetValue(instance, settingsValue); + } + else + { + throw new UnsatisfiedDependencyException($"class {type}. Settings value => {settingsName} not found"); } } private IDecembristConfiguration? InstantiateConfig() { - if (!(ProjectSettings.GetSetting("decembrist/config_class") is string configClass)) + if (ProjectSettings.GetSetting(DecembristSettings.ConfigClass) is not string configClass) { return null; } + var configType = Type.GetType(configClass); if (configType == null) { diff --git a/addons/decembrist_plugin/Autoload/EventBus.cs b/addons/decembrist_plugin/Autoload/EventBus.cs new file mode 100644 index 0000000..8f53001 --- /dev/null +++ b/addons/decembrist_plugin/Autoload/EventBus.cs @@ -0,0 +1,131 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Decembrist.Di; +using Decembrist.Events; +using Decembrist.Utils.Callback; +using Decembrist.Utils.Task; +using Godot; +using Object = Godot.Object; + +namespace Decembrist.Autoload +{ + public class EventBus : Node + { + public const string NodeName = "EventBus"; + + [Signal] + private delegate void MessageSignal(string source, string messageId); + + [Signal] + internal delegate void ReplySignal(string replyMessageId); + + private ConcurrentDictionary _messages = new(); + private ConcurrentDictionary _replies = new(); + private ConcurrentDictionary _replyCallbacks = new(); + + public EventBus() + { + Name = NodeName; + } + + public override void _Ready() + { + this.InjectAll(); + } + + /// + /// Add reply event to list + /// + /// reply event + /// + internal void AddReplyMessage(EventMessage eventMessage) + { + _replies[eventMessage.MessageId] = eventMessage; + } + + /// + /// Send message through event bus for every registered consumer + /// + /// Message address + /// Message content + /// Consumer response handler + /// Message content type + /// Response message type + public void Send(string to, TRequest? message, + Action> replyHandler) + { + var eventMessage = new ReplyEventMessage(this, message); + var messageId = eventMessage.MessageId; + var callback = this.Subscribe(nameof(ReplySignal), (string replyMessageId) => + { + if (messageId != replyMessageId) return; + + _replies.TryRemove(messageId, out var reply); + if (reply is EventMessage response) + { + replyHandler(response); + this.Unsubscribe(nameof(ReplySignal), _replyCallbacks[messageId]); + } + }); + _replyCallbacks[messageId] = callback; + _messages[messageId] = eventMessage; + EmitSignal(nameof(MessageSignal), to, messageId); + } + + /// + /// Async version for + /// + /// Message address + /// Message content + /// Message content type + /// Response message type + /// Consumer response task + public Task Send(string to, TRequest? message = default) + { + return Promises.Of((resolve, reject) => + { + Send(to, message, (responseMessage) => + { + if (responseMessage.IsError()) + { + reject(new SendEventException(responseMessage.Error, responseMessage.ErrorCode)); + } + else + { + resolve(responseMessage.Content); + } + }); + }); + } + + /// + /// Subscribe on messages from "" address + /// + /// Message address + /// Message handler + /// Message content type + /// Response message type + /// Subscription + public EventBusSubscription Consumer( + string from, + Action> messageHandler) + { + const string signal = nameof(MessageSignal); + var callback = this.Subscribe(signal, (string source, string messageId) => + { + if (source != from) return; + + _messages.TryRemove(messageId, out var message); + if (message is ReplyEventMessage eventMessage) + { + messageHandler(eventMessage); + } + } + ); + + return new EventBusSubscription(this, callback, signal); + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/DecembristPlugin.cs b/addons/decembrist_plugin/DecembristPlugin.cs index 1b5789e..47f595e 100644 --- a/addons/decembrist_plugin/DecembristPlugin.cs +++ b/addons/decembrist_plugin/DecembristPlugin.cs @@ -6,13 +6,14 @@ public class DecembristPlugin : EditorPlugin { public override void EnablePlugin() { - AddAutoloadSingleton("DI", "res://addons/decembrist_plugin/Autoload/DiService.cs"); - CheckSetting("decembrist/config_class", "DecembristConfiguration"); + AddAutoloadSingleton("DecembristAutoload", "res://addons/decembrist_plugin/Autoload/DecembristAutoload.cs"); + CheckSetting(DecembristSettings.ConfigClass, "DecembristConfiguration"); + CheckSetting(DecembristSettings.EventBusEnabled, true); } public override void DisablePlugin() { - RemoveAutoloadSingleton("DI"); + RemoveAutoloadSingleton("DecembristAutoload"); } private void CheckSetting(string name, object @default) diff --git a/addons/decembrist_plugin/DecembristSettings.cs b/addons/decembrist_plugin/DecembristSettings.cs new file mode 100644 index 0000000..e02084f --- /dev/null +++ b/addons/decembrist_plugin/DecembristSettings.cs @@ -0,0 +1,5 @@ +public static class DecembristSettings +{ + public const string ConfigClass = "decembrist_plugin/commons/config_class"; + public const string EventBusEnabled = "decembrist_plugin/commons/event_bus_enabled"; +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Di/Attributes.cs b/addons/decembrist_plugin/Di/Attributes.cs new file mode 100644 index 0000000..f713aa2 --- /dev/null +++ b/addons/decembrist_plugin/Di/Attributes.cs @@ -0,0 +1,29 @@ +using System; + +namespace Decembrist.Di +{ + /// + /// Inject dependency instance for field (by field type) + /// Use to inject + /// + [AttributeUsage(AttributeTargets.Field)] + public class InjectAttribute: Attribute + { + + } + + /// + /// Inject project settings for value type field + /// Use to inject + /// + [AttributeUsage(AttributeTargets.Field)] + public class SettingsValueAttribute: Attribute + { + public readonly string Name; + + public SettingsValueAttribute(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Di/Container.cs b/addons/decembrist_plugin/Di/Container.cs index eb10fda..90b9322 100644 --- a/addons/decembrist_plugin/Di/Container.cs +++ b/addons/decembrist_plugin/Di/Container.cs @@ -111,7 +111,7 @@ private void ThrowUnsatisfiedDependenciesException(Dictionary var typesArr = typeMap.Values .Select(type => type.ToString()) .ToArray(); - throw new Exception($"Unsatisfied dependencies for [{string.Join(", ", typesArr)}]"); + throw new UnsatisfiedDependencyException($"[{string.Join(", ", typesArr)}]"); } /// diff --git a/addons/decembrist_plugin/Di/DependencyUtils.cs b/addons/decembrist_plugin/Di/DependencyUtils.cs index 394d032..e4dcb20 100644 --- a/addons/decembrist_plugin/Di/DependencyUtils.cs +++ b/addons/decembrist_plugin/Di/DependencyUtils.cs @@ -1,6 +1,7 @@ using Decembrist.Autoload; using Godot; +#nullable enable namespace Decembrist.Di { public static class DependencyUtils @@ -11,32 +12,12 @@ public static class DependencyUtils /// some node /// Dependency type /// dependency instance - public static T Resolve(this Node node) where T : class - { - var service = node.GetNode("/root/DI"); - return service.ResolveOrNull(); - } - + public static T? Resolve(this Node node) where T : class => node.GetDiService().ResolveOrNull(); + /// /// Inject all fields of node /// /// some node - public static void InjectAll(this Node node) - { - var service = node.GetNode("/root/DI"); - service.InjectAll(node); - } - - // public static void UpdateConfig(this Node node, IConfig config) - // { - // var configService = node.Resolve(); - // configService.Update(config); - // } - // - // public static T LoadConfig(this Node node) where T : class, IConfig - // { - // var configService = node.Resolve(); - // return (T) configService.Get(); - // } + public static void InjectAll(this Node node) => node.GetDiService().InjectAll(node); } } \ No newline at end of file diff --git a/addons/decembrist_plugin/Di/Inject.cs b/addons/decembrist_plugin/Di/Inject.cs deleted file mode 100644 index daa812e..0000000 --- a/addons/decembrist_plugin/Di/Inject.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Decembrist.Di -{ - /// - /// Inject dependency instance for field (by field type) - /// Use to inject - /// - [AttributeUsage(AttributeTargets.Field)] - public class Inject: Attribute - { - - } -} \ No newline at end of file diff --git a/addons/decembrist_plugin/Di/UnsatisfiedDependencyException.cs b/addons/decembrist_plugin/Di/UnsatisfiedDependencyException.cs new file mode 100644 index 0000000..97ecb45 --- /dev/null +++ b/addons/decembrist_plugin/Di/UnsatisfiedDependencyException.cs @@ -0,0 +1,13 @@ +#nullable enable +using System; + +namespace Decembrist.Di +{ + public class UnsatisfiedDependencyException : Exception + { + public UnsatisfiedDependencyException( + string? emptyDependency) : base($"Unsatisfied dependencies for {emptyDependency}") + { + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Events/EventBusSubscription.cs b/addons/decembrist_plugin/Events/EventBusSubscription.cs new file mode 100644 index 0000000..1d7b457 --- /dev/null +++ b/addons/decembrist_plugin/Events/EventBusSubscription.cs @@ -0,0 +1,19 @@ +using System; +using Decembrist.Autoload; +using Decembrist.Utils.Callback; + +namespace Decembrist.Events +{ + public class EventBusSubscription + { + /// + /// Unsubscribe this consumer + /// + public readonly Action Stop; + + public EventBusSubscription(EventBus eventBus, AbstractCallback callback, string signal) + { + Stop = () => eventBus.Unsubscribe(signal, callback); + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Events/EventBusUtils.cs b/addons/decembrist_plugin/Events/EventBusUtils.cs new file mode 100644 index 0000000..ea40236 --- /dev/null +++ b/addons/decembrist_plugin/Events/EventBusUtils.cs @@ -0,0 +1,59 @@ +#nullable enable +using System; +using System.Threading.Tasks; +using Decembrist.Autoload; +using Godot; + +namespace Decembrist.Events +{ + public static class EventBusUtils + { + /// + /// + /// See + /// + public static void SendMessage( + this Node node, + string to, + TRequest? message, + Action> replyHandler) => node.GetEventBus().Send(to, message, replyHandler); + + /// + /// + /// + public static void SendMessage( + this Node node, + string to, + object? message, + Action> replyHandler + ) => node.SendMessage(to, message, replyHandler); + + /// + /// + /// With null message + /// + public static void SendMessage( + this Node node, + string to, + Action> replyHandler) => node.SendMessage(to, null, replyHandler); + + /// + /// + /// See + /// + public static Task SendMessageAsync( + this Node node, + string to, + TRequest? message = default) => node.GetEventBus().Send(to, message); + + /// + /// + /// See + /// + public static EventBusSubscription Consumer( + this Node node, + string from, + Action> messageHandler) => node.GetEventBus().Consumer(from, messageHandler); + + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Events/EventMessage.cs b/addons/decembrist_plugin/Events/EventMessage.cs new file mode 100644 index 0000000..4b201b6 --- /dev/null +++ b/addons/decembrist_plugin/Events/EventMessage.cs @@ -0,0 +1,88 @@ +using System; +using Decembrist.Autoload; +using Decembrist.Utils; +using Decembrist.Utils.Callback; +using Godot; +using Object = Godot.Object; + +#nullable enable +namespace Decembrist.Events +{ + public class EventMessage : Object + { + /// + /// Event error + /// + public readonly string? Error; + + /// + /// User defined error code + /// + public readonly int? ErrorCode; + + /// + /// Event message content + /// + public readonly T? Content; + + /// + /// Uniq message identifier + /// + public readonly string MessageId; + + protected internal EventMessage(T? content, string? error = null, int? errorCode = null) + : this(Uuid.Get(), content, error, errorCode) + { + } + + protected internal EventMessage(string messageId, T? content, string? error = null, int? errorCode = null) + { + Error = error; + ErrorCode = errorCode; + Content = content; + MessageId = messageId; + } + + /// False if error found + public bool IsError() => Error != null; + } + + public class ReplyEventMessage : EventMessage + { + private readonly EventBus _eventBus; + + private bool _replied = false; + + internal ReplyEventMessage(EventBus eventBus, TRequest? content) : base(content) + { + _eventBus = eventBus; + } + + /// + /// Reply sender + /// + /// Response content + /// If already replied + public void Reply(TResponse content) + { + if (_replied) throw new MultipleReplyException(); + _eventBus.AddReplyMessage(new EventMessage(MessageId, content)); + _eventBus.EmitSignal(nameof(EventBus.ReplySignal), MessageId); + _replied = true; + } + + /// + /// Reply sender with error + /// + /// Error text + /// Error code + /// If already replied + public void ErrorReply(string error, int? code = null) + { + if (_replied) throw new MultipleReplyException(); + _eventBus.AddReplyMessage(new EventMessage(MessageId, default, error, code)); + _eventBus.EmitSignal(nameof(EventBus.ReplySignal), MessageId); + _replied = true; + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Events/MultipleReplyException.cs b/addons/decembrist_plugin/Events/MultipleReplyException.cs new file mode 100644 index 0000000..c7321dd --- /dev/null +++ b/addons/decembrist_plugin/Events/MultipleReplyException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Decembrist.Events +{ + public class MultipleReplyException : Exception + { + public MultipleReplyException() : base("Multiple reply exception") + { + } + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Events/SendEventException.cs b/addons/decembrist_plugin/Events/SendEventException.cs new file mode 100644 index 0000000..11b29e9 --- /dev/null +++ b/addons/decembrist_plugin/Events/SendEventException.cs @@ -0,0 +1,23 @@ +#nullable enable +using System; + +namespace Decembrist.Events +{ + public class SendEventException : Exception + { + private readonly int? _code; + + public SendEventException(string? message, int? code) : base(message) + { + _code = code; + } + + /// + /// Get user defined error code + /// + /// + public int? GetCode() => _code; + + public static SendEventException WithMessage(string? message, int? code) => new(message, code); + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/Utils/Task/Promise.cs b/addons/decembrist_plugin/Utils/Task/Promise.cs index f410763..1ec6907 100644 --- a/addons/decembrist_plugin/Utils/Task/Promise.cs +++ b/addons/decembrist_plugin/Utils/Task/Promise.cs @@ -1,16 +1,17 @@ -using System; +#nullable enable +using System; using System.Threading.Tasks; namespace Decembrist.Utils.Task { public class Promise { - private readonly TaskCompletionSource _promise = new TaskCompletionSource(); - private readonly Action, Action> _block; + private readonly TaskCompletionSource _promise = new(); + private readonly Action, Action> _block; private bool _started; - public Promise(Action, Action> block) + public Promise(Action, Action> block) { _block = block; } diff --git a/addons/decembrist_plugin/Utils/Task/Promises.cs b/addons/decembrist_plugin/Utils/Task/Promises.cs index 1a60cad..24a7c66 100644 --- a/addons/decembrist_plugin/Utils/Task/Promises.cs +++ b/addons/decembrist_plugin/Utils/Task/Promises.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Threading.Tasks; using Godot; using VoidTask = System.Threading.Tasks.Task; @@ -7,8 +8,8 @@ namespace Decembrist.Utils.Task { public static class Promises { - public static Task Of(Action, Action> block) => new Promise(block).Start(); + public static Task Of(Action, Action> block) => new Promise(block).Start(); - public static VoidTask Of(Action, Action> block) => new Promise(block).Start(); + public static VoidTask Of(Action, Action> block) => new Promise(block).Start(); } } \ No newline at end of file diff --git a/addons/decembrist_plugin/Utils/Uuid.cs b/addons/decembrist_plugin/Utils/Uuid.cs new file mode 100644 index 0000000..5ff9089 --- /dev/null +++ b/addons/decembrist_plugin/Utils/Uuid.cs @@ -0,0 +1,7 @@ +namespace Decembrist.Utils +{ + public static class Uuid + { + public static string Get() => System.Guid.NewGuid().ToString(); + } +} \ No newline at end of file diff --git a/addons/decembrist_plugin/plugin.cfg b/addons/decembrist_plugin/plugin.cfg index 7dc7896..598da57 100644 --- a/addons/decembrist_plugin/plugin.cfg +++ b/addons/decembrist_plugin/plugin.cfg @@ -3,5 +3,5 @@ name="Decembrist Plugin" description="Utils for godot development" author="decembrist.org" -version="0.1.2-beta" +version="0.2-beta" script="DecembristPlugin.cs" diff --git a/project.godot b/project.godot index b32fc85..4c95df2 100644 --- a/project.godot +++ b/project.godot @@ -31,12 +31,17 @@ config/icon="res://icon.png" [autoload] -DI="*res://addons/decembrist_plugin/Autoload/DiService.cs" +DecembristAutoload="*res://addons/decembrist_plugin/Autoload/DecembristAutoload.cs" [decembrist] config_class="Decembrist.Example.DecembristConfiguration" +[decembrist_plugin] + +commons/config_class="Decembrist.Example.DecembristConfiguration" +commons/event_bus_enabled=true + [editor_plugins] enabled=PoolStringArray( "res://addons/decembrist_plugin/plugin.cfg" )