com.wallstop-studios.dxmessaging 2.0.0-rc27.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/Docs/Comparisons.md +194 -66
  2. package/Docs/GettingStarted.md +84 -17
  3. package/Docs/Overview.md +114 -20
  4. package/Docs/Patterns.md +41 -2
  5. package/Docs/Performance.md +9 -9
  6. package/Docs/VisualGuide.md +75 -10
  7. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  8. package/Editor/Analyzers/Microsoft.CodeAnalysis.CSharp.dll.meta +3 -3
  9. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll +0 -0
  10. package/Editor/Analyzers/Microsoft.CodeAnalysis.dll.meta +2 -2
  11. package/Editor/Analyzers/System.Collections.Immutable.dll +0 -0
  12. package/Editor/Analyzers/System.Reflection.Metadata.dll +0 -0
  13. package/Editor/Analyzers/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  14. package/README.md +262 -63
  15. package/Runtime/Core/MessageBus/MessageBus.cs +13 -0
  16. package/Runtime/Core/MessageHandler.cs +27 -33
  17. package/Runtime/Core/MessageRegistrationToken.cs +12 -21
  18. package/Runtime/Unity/MessageAwareComponent.cs +6 -0
  19. package/Runtime/Unity/MessagingComponent.cs +24 -0
  20. package/Samples~/Mini Combat/README.md +81 -21
  21. package/Samples~/Mini Combat/Walkthrough.md +23 -1
  22. package/Samples~/UI Buttons + Inspector/README.md +55 -12
  23. package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs +89 -0
  24. package/Tests/Runtime/Core/MessagingComponentLifecycleTests.cs.meta +11 -0
  25. package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs +86 -0
  26. package/Tests/Runtime/Core/ReflexiveMessageWarningTests.cs.meta +11 -0
  27. package/Tests/Runtime/Core/SourceGeneratorNestedTests.cs +1 -1
  28. package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs +116 -0
  29. package/Tests/Runtime/Core/UntargetedPrefreezeTests.cs.meta +11 -0
  30. package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs +14 -0
  31. package/Tests/Runtime/Scripts/Components/ReflexiveReceiverComponent.cs.meta +11 -0
  32. package/package.json +1 -1
@@ -1782,6 +1782,7 @@ namespace DxMessaging.Core
1782
1782
  public long version;
1783
1783
  public long lastSeenVersion = -1;
1784
1784
  public long lastSeenEmissionId;
1785
+ internal int prefreezeInvocationCount;
1785
1786
  }
1786
1787
 
1787
1788
  /// <summary>
@@ -1895,39 +1896,16 @@ namespace DxMessaging.Core
1895
1896
  /// <param name="priority">Priority at which to run the handlers.</param>
1896
1897
  public void HandleUntargeted(ref T message, int priority, long emissionId)
1897
1898
  {
1898
- // Pre-freeze untargeted post-processors for this emission/priority
1899
- if (
1900
- _untargetedPostProcessingFastHandlers != null
1901
- && _untargetedPostProcessingFastHandlers.TryGetValue(priority, out var upf)
1902
- )
1903
- {
1904
- _ = GetOrAddNewHandlerStack(upf, emissionId);
1905
- }
1906
- if (
1907
- _untargetedPostProcessingHandlers != null
1908
- && _untargetedPostProcessingHandlers.TryGetValue(priority, out var up)
1909
- )
1910
- {
1911
- _ = GetOrAddNewHandlerStack(up, emissionId);
1912
- }
1913
- // Pre-freeze post-processors for this emission/priority
1914
- if (
1915
- _untargetedPostProcessingFastHandlers != null
1916
- && _untargetedPostProcessingFastHandlers.TryGetValue(
1917
- priority,
1918
- out var fastCache
1919
- )
1920
- )
1921
- {
1922
- _ = GetOrAddNewHandlerStack(fastCache, emissionId);
1923
- }
1924
- if (
1925
- _untargetedPostProcessingHandlers != null
1926
- && _untargetedPostProcessingHandlers.TryGetValue(priority, out var cache)
1927
- )
1928
- {
1929
- _ = GetOrAddNewHandlerStack(cache, emissionId);
1930
- }
1899
+ PrefreezeHandlersForEmission(
1900
+ _untargetedPostProcessingFastHandlers,
1901
+ priority,
1902
+ emissionId
1903
+ );
1904
+ PrefreezeHandlersForEmission(
1905
+ _untargetedPostProcessingHandlers,
1906
+ priority,
1907
+ emissionId
1908
+ );
1931
1909
 
1932
1910
  RunFastHandlers(_untargetedFastHandlers, ref message, priority, emissionId);
1933
1911
  RunHandlers(_untargetedHandlers, ref message, priority, emissionId);
@@ -3535,6 +3513,22 @@ namespace DxMessaging.Core
3535
3513
  return actionCache.cache;
3536
3514
  }
3537
3515
 
3516
+ private static void PrefreezeHandlersForEmission<THandler>(
3517
+ Dictionary<int, HandlerActionCache<THandler>> handlers,
3518
+ int priority,
3519
+ long emissionId
3520
+ )
3521
+ {
3522
+ if (
3523
+ handlers != null
3524
+ && handlers.TryGetValue(priority, out HandlerActionCache<THandler> cache)
3525
+ )
3526
+ {
3527
+ cache.prefreezeInvocationCount++;
3528
+ _ = GetOrAddNewHandlerStack(cache, emissionId);
3529
+ }
3530
+ }
3531
+
3538
3532
  private static Action AddHandler<TU>(
3539
3533
  InstanceId context,
3540
3534
  ref Dictionary<
@@ -59,6 +59,7 @@ namespace DxMessaging.Core
59
59
 
60
60
  private readonly Dictionary<MessageRegistrationHandle, Action> _registrations = new();
61
61
  private readonly Dictionary<MessageRegistrationHandle, Action> _deregistrations = new();
62
+ private readonly List<Action> _deregistrationQueue = new();
62
63
  internal readonly Dictionary<
63
64
  MessageRegistrationHandle,
64
65
  MessageRegistrationMetadata
@@ -1878,13 +1879,11 @@ namespace DxMessaging.Core
1878
1879
 
1879
1880
  if (_deregistrations is { Count: > 0 })
1880
1881
  {
1881
- Dictionary<
1882
- MessageRegistrationHandle,
1883
- Action
1884
- >.ValueCollection.Enumerator enumerator = _deregistrations.Values.GetEnumerator();
1885
- while (enumerator.MoveNext())
1882
+ _deregistrationQueue.Clear();
1883
+ _deregistrationQueue.AddRange(_deregistrations.Values);
1884
+ foreach (Action deregistration in _deregistrationQueue)
1886
1885
  {
1887
- enumerator.Current();
1886
+ deregistration?.Invoke();
1888
1887
  }
1889
1888
  }
1890
1889
 
@@ -1905,15 +1904,13 @@ namespace DxMessaging.Core
1905
1904
  /// </example>
1906
1905
  public void UnregisterAll()
1907
1906
  {
1908
- if (_enabled && _deregistrations is { Count: > 0 })
1907
+ if (_deregistrations is { Count: > 0 })
1909
1908
  {
1910
- Dictionary<
1911
- MessageRegistrationHandle,
1912
- Action
1913
- >.ValueCollection.Enumerator enumerator = _deregistrations.Values.GetEnumerator();
1914
- while (enumerator.MoveNext())
1909
+ _deregistrationQueue.Clear();
1910
+ _deregistrationQueue.AddRange(_deregistrations.Values);
1911
+ foreach (Action deregistration in _deregistrationQueue)
1915
1912
  {
1916
- enumerator.Current();
1913
+ deregistration?.Invoke();
1917
1914
  }
1918
1915
  }
1919
1916
 
@@ -1934,16 +1931,10 @@ namespace DxMessaging.Core
1934
1931
  /// </example>
1935
1932
  public void RemoveRegistration(MessageRegistrationHandle handle)
1936
1933
  {
1937
- if (
1938
- _deregistrations != null
1939
- && _deregistrations.TryGetValue(handle, out Action deregistrationAction)
1940
- )
1934
+ if (_deregistrations?.Remove(handle, out Action deregistrationAction) == true)
1941
1935
  {
1942
- deregistrationAction();
1943
- _ = _deregistrations.Remove(handle);
1936
+ deregistrationAction?.Invoke();
1944
1937
  }
1945
-
1946
- _ = _registrations?.Remove(handle);
1947
1938
  }
1948
1939
 
1949
1940
  /// <summary>
@@ -124,8 +124,14 @@ namespace DxMessaging.Unity
124
124
  /// </summary>
125
125
  protected virtual void OnDestroy()
126
126
  {
127
+ if (_messagingComponent != null)
128
+ {
129
+ _messagingComponent.Release(this);
130
+ }
131
+
127
132
  _messageRegistrationToken?.Disable();
128
133
  _messageRegistrationToken = null;
134
+ _messagingComponent = null;
129
135
  }
130
136
 
131
137
  /// <summary>
@@ -73,6 +73,30 @@ namespace DxMessaging.Unity
73
73
  return createdToken;
74
74
  }
75
75
 
76
+ /// <summary>
77
+ /// Releases the registration token previously created for <paramref name="listener"/>.
78
+ /// </summary>
79
+ /// <param name="listener">Listener whose token should be released.</param>
80
+ /// <remarks>
81
+ /// Invokes <see cref="MessageRegistrationToken.Disable"/> and removes the listener from the internal cache.
82
+ /// Safe to call multiple times.
83
+ /// </remarks>
84
+ public bool Release(MonoBehaviour listener)
85
+ {
86
+ if (listener is null)
87
+ {
88
+ return false;
89
+ }
90
+
91
+ if (_registeredListeners.Remove(listener, out MessageRegistrationToken token))
92
+ {
93
+ token?.Disable();
94
+ return true;
95
+ }
96
+
97
+ return false;
98
+ }
99
+
76
100
  /// <summary>
77
101
  /// Ensures the underlying <see cref="Core.MessageHandler"/> exists.
78
102
  /// </summary>
@@ -1,16 +1,79 @@
1
1
  # Mini Combat Sample
2
2
 
3
- > **What You'll Learn**: This sample demonstrates a simple combat system using DxMessaging to show how components communicate through messages. Perfect for beginners learning the messaging system!
3
+ > **Perfect for:** First-time users who want to see messaging in action with a working combat example
4
4
 
5
- ## Overview
5
+ ## What You'll Learn in 10 Minutes
6
6
 
7
- This mini-sample showcases a basic combat loop using DxMessaging with Unity-friendly APIs. You'll see how different game objects (Player, Enemy, UI) communicate without direct references to each other.
7
+ **Stop reading documentation and start seeing it work!** This sample shows:
8
8
 
9
- ### Key Concepts Demonstrated:
9
+ 1. How Player heals without UI knowing about Player (**zero coupling**)
10
+ 2. How Enemy broadcasts damage without knowing who cares (**observer pattern**)
11
+ 3. How settings changes update everything automatically (**global events**)
10
12
 
11
- - **Untargeted Messages**: Global messages anyone can listen to (like game settings)
12
- - **Targeted Messages**: Messages sent to a specific component (like healing a player)
13
- - **Broadcast Messages**: Messages announced to all listeners (like damage events)
13
+ **No prior messaging experience needed** - this sample walks you through everything.
14
+
15
+ ---
16
+
17
+ ## Why This Sample Matters
18
+
19
+ ### Before DxMessaging:
20
+
21
+ ```csharp
22
+ public class Player {
23
+ [SerializeField] private UI ui; // Coupling!
24
+ [SerializeField] private AudioManager audio; // More coupling!
25
+
26
+ void Heal(int amount) {
27
+ health += amount;
28
+ ui.UpdateHealth(health); // Manual call
29
+ audio.PlayHealSound(); // Manual call
30
+ }
31
+ }
32
+ ```
33
+
34
+ #### Every new system = another SerializeField + another manual call.
35
+
36
+ ##### With DxMessaging:
37
+
38
+ ```csharp
39
+ public class Player : MessageAwareComponent {
40
+ void Heal(int amount) {
41
+ health += amount;
42
+ new PlayerHealed(amount).EmitBroadcast(this);
43
+ // Done! UI, audio, analytics all react automatically.
44
+ }
45
+ }
46
+ ```
47
+
48
+ ###### Zero coupling. Zero manual wiring. That's the power of messaging.
49
+
50
+ ---
51
+
52
+ ## Key Concepts (3 Message Types)
53
+
54
+ ### 1. Untargeted - "Everyone, listen up!"
55
+
56
+ **Example:** Game settings changed
57
+
58
+ - Like a stadium announcement - everyone hears it
59
+ - No specific target
60
+ - Perfect for: settings, pause, scene transitions
61
+
62
+ ### 2. Targeted - "Hey YOU, do this!"
63
+
64
+ **Example:** Heal this specific player
65
+
66
+ - Like mailing a letter to one address
67
+ - Only the target receives it
68
+ - Perfect for: commands, direct actions
69
+
70
+ ### 3. Broadcast - "I did something!"
71
+
72
+ **Example:** Enemy took damage
73
+
74
+ - Like ringing a bell - anyone nearby can hear
75
+ - Source announces, observers react
76
+ - Perfect for: events, notifications, analytics
14
77
 
15
78
  ---
16
79
 
@@ -28,25 +91,22 @@ Here's what each script does:
28
91
 
29
92
  ---
30
93
 
31
- ## Quick Start Guide
94
+ ## Quick Start Guide (2 Minutes to Working Demo)
32
95
 
33
- ### Method 1: Import from Package Manager (Recommended)
96
+ ### Import & Run (Fastest Way)
34
97
 
35
- 1. **Open Package Manager**:
36
- - Window → Package Manager
98
+ #### Want to see it work immediately?
37
99
 
38
- 1. **Find DxMessaging**:
39
- - Look for "DxMessaging" in the package list
40
- - Click on it to select
100
+ 1. **Open Package Manager**: Window → Package Manager
101
+ 2. **Find DxMessaging** in the package list
102
+ 3. **Scroll to Samples** section → Find "Mini Combat" → Click **Import**
103
+ 4. **Navigate to** Assets/Samples/DxMessaging/.../Mini Combat/
104
+ 5. **Open the scene**
105
+ 6. **Press Play** 🎮
41
106
 
42
- 1. **Import the Sample**:
43
- - In the Package Manager details view, scroll down to find the "Samples" section
44
- - Find "Mini Combat" and click **Import**
45
- - The sample files will be imported into your Assets/Samples folder
107
+ **That's it!** Watch the Console logs as messages flow.
46
108
 
47
- 1. **Open the Scene**:
48
- - Navigate to Assets/Samples/DxMessaging/.../Mini Combat/
49
- - Open the sample scene or create a new scene and follow Step 1 below
109
+ **Pro tip:** Enable diagnostics in the Inspector (MessagingComponent → Enable Diagnostics) to see message traffic in real-time.
50
110
 
51
111
  ### Method 2: Manual Setup in Your Scene
52
112
 
@@ -1,6 +1,28 @@
1
1
  # Mini Combat Walkthrough
2
2
 
3
- > **Deep Dive**: This walkthrough explains the implementation details and shows how all the sample scripts work together at runtime. Great for understanding the "why" behind the code!
3
+ > **Deep Dive**: Ready to understand HOW it works? This walkthrough explains every line of code and shows the complete message flow.
4
+
5
+ ## Who This Is For
6
+
7
+ - ✅ **You've imported and run the sample** - now you want to understand it
8
+ - ✅ **You want to build your own** - need to see the patterns in action
9
+ - ✅ **You're debugging something** - need to understand the flow
10
+
11
+ **Haven't run the sample yet?** Go back to [the sample README](README.md) and press Play first. Come back when you've seen it work.
12
+
13
+ ---
14
+
15
+ ## What You'll Understand By The End
16
+
17
+ After reading this walkthrough, you'll know:
18
+
19
+ 1. **Why each message type was chosen** - the reasoning behind Untargeted vs Targeted vs Broadcast
20
+ 2. **How the code flows** - step-by-step from Boot.cs through every script
21
+ 3. **Common patterns** - Observer, Broadcaster, Orchestrator, and more
22
+ 4. **Debugging strategies** - how to find and fix issues
23
+ 5. **How to extend it** - add your own messages and handlers
24
+
25
+ **Estimated time:** 20-30 minutes for thorough understanding
4
26
 
5
27
  ---
6
28
 
@@ -1,21 +1,64 @@
1
- # UI Buttons + Inspector (Sample)
1
+ # UI Buttons + Inspector Sample
2
2
 
3
- Make a Unity UI Button send a message with a single OnClick hook — no plumbing, no scene-wide singletons, and easy to understand even if you’re new to event systems.
3
+ > **Perfect for:** Seeing DxMessaging work with Unity UI in 60 seconds
4
4
 
5
- ## What Youll Learn
5
+ ## What You'll Learn (No Code Required!)
6
6
 
7
- - How to wire a UI Button to emit a DxMessaging message from the Inspector.
8
- - How to observe messages in the Console with a tiny listener component.
9
- - How to turn on diagnostics to see message traffic while you click.
7
+ **Stop writing button click handlers!** This sample shows you how to:
10
8
 
11
- ## Import The Sample (60 seconds)
9
+ 1. **Wire UI Buttons to messages** - directly from the Inspector (drag & drop)
10
+ 2. **See messages flow in real-time** - watch the Console as you click
11
+ 3. **Enable diagnostics** - see every message with timestamps and payloads
12
12
 
13
- 1. Open `Window > Package Manager` in Unity.
14
- 1. Select `com.wallstop-studios.dxmessaging`.
15
- 1. In Samples, import “UI Buttons + Inspector”.
16
- 1. Open the sample scene (created under your project’s `Assets/Samples/...`).
13
+ **Why this matters:** You can add new systems (analytics, audio, achievements) that react to button clicks WITHOUT touching existing code.
17
14
 
18
- That’s it — the scene includes everything you need to click and see messages.
15
+ ---
16
+
17
+ ## The Power Move
18
+
19
+ ### Before DxMessaging:
20
+
21
+ ```csharp
22
+ public class PlayButton : MonoBehaviour {
23
+ [SerializeField] private GameManager gameManager; // Coupling
24
+ [SerializeField] private AudioManager audio; // Coupling
25
+ [SerializeField] private Analytics analytics; // Coupling
26
+
27
+ public void OnClick() {
28
+ gameManager.StartGame(); // Manual call
29
+ audio.PlayClickSound(); // Manual call
30
+ analytics.LogButtonClick(); // Manual call
31
+ }
32
+ }
33
+ ```
34
+
35
+ #### Every new system = update PlayButton script. Exhausting.
36
+
37
+ ##### With DxMessaging:
38
+
39
+ ```csharp
40
+ public class PlayButton : MonoBehaviour {
41
+ public void OnClick() {
42
+ new ButtonClicked("Play").Emit();
43
+ // Done! Everything reacts automatically.
44
+ }
45
+ }
46
+ ```
47
+
48
+ ###### GameManager, Audio, and Analytics listen independently. Zero coupling.
49
+
50
+ ## Import & Run (30 Seconds)
51
+
52
+ ### Want to see it immediately?
53
+
54
+ 1. **Window → Package Manager**
55
+ 2. **Find DxMessaging** → Scroll to **Samples**
56
+ 3. **"UI Buttons + Inspector"** → Click **Import**
57
+ 4. **Navigate to** Assets/Samples/.../UI Buttons + Inspector/
58
+ 5. **Open the scene** → **Press Play** 🎮
59
+ 6. **Click the buttons** → Watch Console logs
60
+
61
+ **Done!** You're seeing DxMessaging in action.
19
62
 
20
63
  ## Click-To-Message: The Quick Path
21
64
 
@@ -0,0 +1,89 @@
1
+ namespace DxMessaging.Tests.Runtime.Core
2
+ {
3
+ using System.Collections;
4
+ using DxMessaging.Core;
5
+ using DxMessaging.Tests.Runtime.Scripts.Components;
6
+ using DxMessaging.Unity;
7
+ using NUnit.Framework;
8
+ using UnityEngine;
9
+ using UnityEngine.TestTools;
10
+
11
+ public sealed class MessagingComponentLifecycleTests : MessagingTestBase
12
+ {
13
+ [UnityTest]
14
+ public IEnumerator ReleasesListenerOnDestroy()
15
+ {
16
+ GameObject go = new(
17
+ "Lifecycle",
18
+ typeof(MessagingComponent),
19
+ typeof(SimpleMessageAwareComponent)
20
+ );
21
+ _spawned.Add(go);
22
+
23
+ MessagingComponent messaging = go.GetComponent<MessagingComponent>();
24
+ SimpleMessageAwareComponent listener = go.GetComponent<SimpleMessageAwareComponent>();
25
+
26
+ yield return null;
27
+
28
+ Assert.AreEqual(
29
+ 1,
30
+ messaging._registeredListeners.Count,
31
+ "Expected initial listener registration."
32
+ );
33
+
34
+ Object.Destroy(listener);
35
+ yield return null;
36
+
37
+ Assert.AreEqual(
38
+ 0,
39
+ messaging._registeredListeners.Count,
40
+ "Listener dictionary should be cleared after destroy."
41
+ );
42
+
43
+ SimpleMessageAwareComponent replacement =
44
+ go.AddComponent<SimpleMessageAwareComponent>();
45
+ yield return null;
46
+
47
+ Assert.AreEqual(
48
+ 1,
49
+ messaging._registeredListeners.Count,
50
+ "Replacement listener should be tracked."
51
+ );
52
+ Assert.IsTrue(messaging._registeredListeners.ContainsKey(replacement));
53
+ }
54
+
55
+ [UnityTest]
56
+ public IEnumerator ManualReleaseRemovesListenerAndDisablesToken()
57
+ {
58
+ GameObject go = new(
59
+ "ManualRelease",
60
+ typeof(MessagingComponent),
61
+ typeof(ManualListenerComponent)
62
+ );
63
+ _spawned.Add(go);
64
+
65
+ MessagingComponent messaging = go.GetComponent<MessagingComponent>();
66
+ ManualListenerComponent listener = go.GetComponent<ManualListenerComponent>();
67
+
68
+ MessageRegistrationToken token = listener.RequestToken(messaging);
69
+ Assert.AreEqual(
70
+ 1,
71
+ messaging._registeredListeners.Count,
72
+ "Token request should register listener."
73
+ );
74
+
75
+ token.Enable();
76
+ Assert.IsTrue(token.Enabled, "Token should enable successfully.");
77
+
78
+ messaging.Release(listener);
79
+ yield return null;
80
+
81
+ Assert.AreEqual(
82
+ 0,
83
+ messaging._registeredListeners.Count,
84
+ "Manual release should remove listener."
85
+ );
86
+ Assert.IsFalse(token.Enabled, "Released token should be disabled.");
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 23c1f6cf389e77147b1fa5edfeb1652f
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,86 @@
1
+ namespace DxMessaging.Tests.Runtime.Core
2
+ {
3
+ using System;
4
+ using System.Collections;
5
+ using System.Collections.Generic;
6
+ using System.Linq;
7
+ using DxMessaging.Core;
8
+ using DxMessaging.Core.MessageBus;
9
+ using DxMessaging.Core.Messages;
10
+ using DxMessaging.Tests.Runtime.Scripts.Components;
11
+ using NUnit.Framework;
12
+ using UnityEngine;
13
+ using UnityEngine.TestTools;
14
+
15
+ public sealed class ReflexiveMessageWarningTests : MessagingTestBase
16
+ {
17
+ [UnityTest]
18
+ public IEnumerator LogsWarningOncePerBus()
19
+ {
20
+ List<(LogLevel level, string message)> logs = new();
21
+ var previousLogFunction = MessagingDebug.LogFunction;
22
+ try
23
+ {
24
+ MessagingDebug.LogFunction = (level, message) => logs.Add((level, message));
25
+ MessagingDebug.enabled = true;
26
+ logs.Clear();
27
+
28
+ GameObject go = new("ReflexiveReceiver", typeof(ReflexiveReceiverComponent));
29
+ _spawned.Add(go);
30
+ ReflexiveReceiverComponent receiver = go.GetComponent<ReflexiveReceiverComponent>();
31
+
32
+ MessageBus bus = new();
33
+ ReflexiveMessage message = new("OnReflexive", ReflexiveSendMode.Flat);
34
+
35
+ int warningsBefore = CountWarnings(logs);
36
+ InstanceId target = receiver;
37
+ bus.TargetedBroadcast(ref target, ref message);
38
+ Assert.AreEqual(1, receiver.InvocationCount);
39
+ int warningsAfter = CountWarnings(logs);
40
+ Assert.Greater(
41
+ warningsAfter,
42
+ warningsBefore,
43
+ "First reflexive dispatch should log a warning."
44
+ );
45
+ StringAssert.Contains("ReflexiveMessage", logs[^1].message);
46
+
47
+ warningsBefore = warningsAfter;
48
+ target = receiver;
49
+ bus.TargetedBroadcast(ref target, ref message);
50
+ Assert.AreEqual(2, receiver.InvocationCount);
51
+ warningsAfter = CountWarnings(logs);
52
+ Assert.AreEqual(
53
+ warningsBefore,
54
+ warningsAfter,
55
+ "Second dispatch on the same bus should not emit additional warnings."
56
+ );
57
+
58
+ MessageBus secondBus = new();
59
+ warningsBefore = warningsAfter;
60
+ target = receiver;
61
+ secondBus.TargetedBroadcast(ref target, ref message);
62
+ Assert.AreEqual(3, receiver.InvocationCount);
63
+ warningsAfter = CountWarnings(logs);
64
+ Assert.AreEqual(
65
+ warningsBefore + 1,
66
+ warningsAfter,
67
+ "A new bus should emit its own warning."
68
+ );
69
+ }
70
+ finally
71
+ {
72
+ MessagingDebug.LogFunction = previousLogFunction;
73
+ }
74
+
75
+ yield break;
76
+ }
77
+
78
+ private static int CountWarnings(List<(LogLevel level, string message)> logs)
79
+ {
80
+ return logs.Count(entry =>
81
+ entry.level == LogLevel.Warn
82
+ && entry.message.IndexOf("ReflexiveMessage dispatch", StringComparison.Ordinal) >= 0
83
+ );
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 9682cda4e2ef15a48869a68d1810cebe
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -39,7 +39,7 @@ namespace DxMessaging.Tests.Runtime.Core
39
39
  public readonly string c;
40
40
 
41
41
  [DxOptionalParameter(Expression = "null")]
42
- public readonly string? d;
42
+ public readonly string d;
43
43
  }
44
44
  }
45
45