From 338e253fee74af02fc633c34ae50b57afd44dc99 Mon Sep 17 00:00:00 2001 From: Yuri Sokolov <7556847+migus88@users.noreply.github.com> Date: Sat, 30 May 2026 17:44:45 +0300 Subject: [PATCH 1/2] Include events on the generated Singleton interface Events were silently dropped by the formatter and by the auto-include candidate check, so a public event on a [Singleton] class never appeared on its generated interface. Format IEventSymbol as `event Type Name;`, accept events in auto-mode, widen [SingletonInclude] / [SingletonIgnore] to AttributeTargets.Event, and add a demo CountChanged event with a test that subscribes through the interface. --- README.md | 6 ++--- .../Helpers/SymbolFormatter.cs | 12 +++++++++ .../Singleton/SingletonGenerator.cs | 1 + .../Demo/Singletons/Code/GameManager.cs | 4 +++ .../Demo/Singletons/Tests/GameManagerTests.cs | 25 ++++++++++++++++++ .../Runtime/Plugins/EngineRoom.Generators.dll | Bin 40448 -> 40448 bytes .../Attributes/SingletonIgnoreAttribute.cs | 8 +++--- .../Attributes/SingletonIncludeAttribute.cs | 4 +-- 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 44f91a0..b9fd184 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,13 @@ public partial class DataStoreManager : MonoBehaviour, IDataStoreManager   -When the generator synthesises the interface, it has to decide which members of your class show up on it. By default that's every public method and property. `[SingletonInclude]` and `[SingletonIgnore]` give you fine-grained control. They are mutually exclusive *per class* — using `[SingletonInclude]` anywhere on the class flips the generator into **explicit mode**. +When the generator synthesises the interface, it has to decide which members of your class show up on it. By default that's every public method, property, and event. `[SingletonInclude]` and `[SingletonIgnore]` give you fine-grained control. They are mutually exclusive *per class* — using `[SingletonInclude]` anywhere on the class flips the generator into **explicit mode**. > **Note** — Neither attribute applies when you supply your own interface via `[Singleton(typeof(IFoo))]`; in that case the interface contract is whatever you typed. **Auto mode** *(default — no `[SingletonInclude]` on the class)* -Every public instance method and property is on the interface. Use `[SingletonIgnore]` to hide individual members: +Every public instance method, property, and event is on the interface. Use `[SingletonIgnore]` to hide individual members: ```csharp [Singleton] @@ -255,7 +255,7 @@ public partial class SoundManager : MonoBehaviour **Constraints** -- Both attributes target methods and properties only. +- Both attributes target methods, properties, and events only. - `[SingletonInclude]` members must be **public** and **non-static** — the analyzer raises `ER0xxx` diagnostics otherwise. diff --git a/src/generators-src/EngineRoom.Generators/Helpers/SymbolFormatter.cs b/src/generators-src/EngineRoom.Generators/Helpers/SymbolFormatter.cs index 1b7aa67..cc0ddf8 100644 --- a/src/generators-src/EngineRoom.Generators/Helpers/SymbolFormatter.cs +++ b/src/generators-src/EngineRoom.Generators/Helpers/SymbolFormatter.cs @@ -34,6 +34,7 @@ public static string FormatAsInterfaceMember(ISymbol symbol) { IMethodSymbol method => FormatMethod(method), IPropertySymbol property => FormatProperty(property), + IEventSymbol @event => FormatEvent(@event), _ => string.Empty, }; } @@ -81,6 +82,17 @@ private static string FormatProperty(IPropertySymbol property) return builder.ToString(); } + private static string FormatEvent(IEventSymbol @event) + { + var builder = new StringBuilder(); + builder.Append("event "); + builder.Append(@event.Type.ToDisplayString(FullyQualifiedType)); + builder.Append(' '); + builder.Append(@event.Name); + builder.Append(';'); + return builder.ToString(); + } + private static string FormatParameter(IParameterSymbol parameter) { var builder = new StringBuilder(); diff --git a/src/generators-src/EngineRoom.Generators/Singleton/SingletonGenerator.cs b/src/generators-src/EngineRoom.Generators/Singleton/SingletonGenerator.cs index 336c13f..66ed6b9 100644 --- a/src/generators-src/EngineRoom.Generators/Singleton/SingletonGenerator.cs +++ b/src/generators-src/EngineRoom.Generators/Singleton/SingletonGenerator.cs @@ -186,6 +186,7 @@ private static bool IsAutoIncludeCandidate(INamedTypeSymbol classSymbol, ISymbol { IMethodSymbol method => method.MethodKind == MethodKind.Ordinary, IPropertySymbol property => !property.IsIndexer, + IEventSymbol => true, _ => false, }; } diff --git a/src/generators-unity/Assets/Demo/Singletons/Code/GameManager.cs b/src/generators-unity/Assets/Demo/Singletons/Code/GameManager.cs index cc82feb..049b236 100644 --- a/src/generators-unity/Assets/Demo/Singletons/Code/GameManager.cs +++ b/src/generators-unity/Assets/Demo/Singletons/Code/GameManager.cs @@ -1,3 +1,4 @@ +using System; using EngineRoom.Runtime.Singleton; using UnityEngine; @@ -6,6 +7,8 @@ namespace EngineRoom.Demo.Singletons [Singleton] public partial class GameManager : MonoBehaviour { + public event Action CountChanged; + public int Count => _count; [Dependency] private ISoundManager _soundManager; @@ -26,6 +29,7 @@ public void RegisterTap() _dataStoreManager.SetScore(_count); _soundManager.PlayTap(); _uiManager.SetCount(_count); + CountChanged?.Invoke(_count); } } } diff --git a/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs b/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs index 8ceeb21..5ffaf11 100644 --- a/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs +++ b/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs @@ -28,5 +28,30 @@ public void RegisterTap_IncrementsCount_AndNotifiesSoundAndUi() Object.DestroyImmediate(((GameManager)game).gameObject); } } + + [Test] + public void RegisterTap_RaisesCountChangedEventOnInterface() + { + MockDataStoreManager.Install(); + MockSoundManager.Install(); + MockUIManager.Install(); + + IGameManager game = GameManager.Create(); + + try + { + var observed = -1; + game.CountChanged += value => observed = value; + + game.RegisterTap(); + game.RegisterTap(); + + Assert.AreEqual(2, observed); + } + finally + { + Object.DestroyImmediate(((GameManager)game).gameObject); + } + } } } diff --git a/src/generators-unity/Packages/games.engine-room.generators/Runtime/Plugins/EngineRoom.Generators.dll b/src/generators-unity/Packages/games.engine-room.generators/Runtime/Plugins/EngineRoom.Generators.dll index e66297c27f8dcc59dcbeb5a3e607229b4e5de747..6460a01ae6c74f62f1e41c2b5047aeb0407a3197 100644 GIT binary patch delta 11965 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 F{{euuHPiqA delta 11691 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 C /// On a -decorated class that has no member - /// tagged with , marks a public method or - /// property to be excluded from the generated singleton interface. Has no - /// effect when the class is in explicit mode (i.e. uses + /// tagged with , marks a public method, + /// property, or event to be excluded from the generated singleton interface. + /// Has no effect when the class is in explicit mode (i.e. uses /// elsewhere). /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)] public sealed class SingletonIgnoreAttribute : Attribute { } diff --git a/src/generators-unity/Packages/games.engine-room.generators/Runtime/Singleton/Attributes/SingletonIncludeAttribute.cs b/src/generators-unity/Packages/games.engine-room.generators/Runtime/Singleton/Attributes/SingletonIncludeAttribute.cs index 2b1c1d9..54f6f9b 100644 --- a/src/generators-unity/Packages/games.engine-room.generators/Runtime/Singleton/Attributes/SingletonIncludeAttribute.cs +++ b/src/generators-unity/Packages/games.engine-room.generators/Runtime/Singleton/Attributes/SingletonIncludeAttribute.cs @@ -3,10 +3,10 @@ namespace EngineRoom.Runtime.Singleton { /// - /// Marks a method or property on a -decorated class + /// Marks a method, property, or event on a -decorated class /// to be projected onto the generated singleton interface. /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)] public sealed class SingletonIncludeAttribute : Attribute { } From 19fba2063b978cc61065d59d454db8a5ae07fb7d Mon Sep 17 00:00:00 2001 From: Yuri Sokolov <7556847+migus88@users.noreply.github.com> Date: Sat, 30 May 2026 18:02:36 +0300 Subject: [PATCH 2/2] Convert demo Singleton tests to PlayMode The tests exercise [Dependency] resolution and the singleton lifecycle, which only run in PlayMode. Drop the EditMode-only test runner reference and Editor platform restriction so the [UnityTest] coroutines that yield a frame for Start() can run. --- .../EngineRoom.Demo.Singletons.Tests.asmdef | 7 ++----- .../Demo/Singletons/Tests/GameManagerTests.cs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/generators-unity/Assets/Demo/Singletons/Tests/EngineRoom.Demo.Singletons.Tests.asmdef b/src/generators-unity/Assets/Demo/Singletons/Tests/EngineRoom.Demo.Singletons.Tests.asmdef index cfc1f70..5c1ecd8 100644 --- a/src/generators-unity/Assets/Demo/Singletons/Tests/EngineRoom.Demo.Singletons.Tests.asmdef +++ b/src/generators-unity/Assets/Demo/Singletons/Tests/EngineRoom.Demo.Singletons.Tests.asmdef @@ -4,12 +4,9 @@ "references": [ "EngineRoom.Demo.Singletons", "EngineRoom.Generators.Runtime", - "UnityEngine.TestRunner", - "UnityEditor.TestRunner" - ], - "includePlatforms": [ - "Editor" + "UnityEngine.TestRunner" ], + "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, diff --git a/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs b/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs index 5ffaf11..cfc943c 100644 --- a/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs +++ b/src/generators-unity/Assets/Demo/Singletons/Tests/GameManagerTests.cs @@ -1,12 +1,14 @@ +using System.Collections; using NUnit.Framework; using UnityEngine; +using UnityEngine.TestTools; namespace EngineRoom.Demo.Singletons.Tests { public class GameManagerTests { - [Test] - public void RegisterTap_IncrementsCount_AndNotifiesSoundAndUi() + [UnityTest] + public IEnumerator RegisterTap_IncrementsCount_AndNotifiesSoundAndUi() { var store = MockDataStoreManager.Install(); var sound = MockSoundManager.Install(); @@ -14,6 +16,8 @@ public void RegisterTap_IncrementsCount_AndNotifiesSoundAndUi() var game = GameManager.Create(); + yield return null; + try { game.RegisterTap(); @@ -29,14 +33,16 @@ public void RegisterTap_IncrementsCount_AndNotifiesSoundAndUi() } } - [Test] - public void RegisterTap_RaisesCountChangedEventOnInterface() + [UnityTest] + public IEnumerator RegisterTap_RaisesCountChangedEventOnInterface() { MockDataStoreManager.Install(); MockSoundManager.Install(); MockUIManager.Install(); - IGameManager game = GameManager.Create(); + var game = GameManager.Create(); + + yield return null; try {