diff --git a/Packages/com.chark.game-management/CHANGELOG.md b/Packages/com.chark.game-management/CHANGELOG.md index 8baa2e5..8992688 100644 --- a/Packages/com.chark.game-management/CHANGELOG.md +++ b/Packages/com.chark.game-management/CHANGELOG.md @@ -25,6 +25,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed - `GameStorage.GetValueAsync` (now `GameStorage.ReadValueAsync`) not switching back to main thread when no value is found. +- Message bus not handling abstract and interface listeners. +- Message bus iteration breaking when listener would remove itself during `Raise` call. ## [v0.0.2](https://github.com/chark/game-management/compare/v0.0.1...v0.0.2) - 2023-10-06 diff --git a/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs b/Packages/com.chark.game-management/Runtime/Assets/DefaultResourceLoader.cs similarity index 96% rename from Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs rename to Packages/com.chark.game-management/Runtime/Assets/DefaultResourceLoader.cs index d9521ea..1e6f9e9 100644 --- a/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs +++ b/Packages/com.chark.game-management/Runtime/Assets/DefaultResourceLoader.cs @@ -10,11 +10,11 @@ namespace CHARK.GameManagement.Assets { - internal sealed class ResourceLoader : IResourceLoader + internal sealed class DefaultResourceLoader : IResourceLoader { private readonly ISerializer serializer; - public ResourceLoader(ISerializer serializer) + public DefaultResourceLoader(ISerializer serializer) { this.serializer = serializer; } diff --git a/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs.meta b/Packages/com.chark.game-management/Runtime/Assets/DefaultResourceLoader.cs.meta similarity index 100% rename from Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs.meta rename to Packages/com.chark.game-management/Runtime/Assets/DefaultResourceLoader.cs.meta diff --git a/Packages/com.chark.game-management/Runtime/Entities/EntityManager.cs b/Packages/com.chark.game-management/Runtime/Entities/DefaultEntityManager.cs similarity index 96% rename from Packages/com.chark.game-management/Runtime/Entities/EntityManager.cs rename to Packages/com.chark.game-management/Runtime/Entities/DefaultEntityManager.cs index 083068a..7b6ce8b 100644 --- a/Packages/com.chark.game-management/Runtime/Entities/EntityManager.cs +++ b/Packages/com.chark.game-management/Runtime/Entities/DefaultEntityManager.cs @@ -5,14 +5,14 @@ namespace CHARK.GameManagement.Entities { - internal sealed class EntityManager : IEntityManager + internal sealed class DefaultEntityManager : IEntityManager { private readonly List entities = new(); private readonly IGameManagerSettingsProfile profile; public IReadOnlyList Entities => entities; - public EntityManager(IGameManagerSettingsProfile profile) + public DefaultEntityManager(IGameManagerSettingsProfile profile) { this.profile = profile; } diff --git a/Packages/com.chark.game-management/Runtime/Entities/EntityManager.cs.meta b/Packages/com.chark.game-management/Runtime/Entities/DefaultEntityManager.cs.meta similarity index 100% rename from Packages/com.chark.game-management/Runtime/Entities/EntityManager.cs.meta rename to Packages/com.chark.game-management/Runtime/Entities/DefaultEntityManager.cs.meta diff --git a/Packages/com.chark.game-management/Runtime/GameManager.External.cs b/Packages/com.chark.game-management/Runtime/GameManager.External.cs index 2ed1b4b..b434824 100644 --- a/Packages/com.chark.game-management/Runtime/GameManager.External.cs +++ b/Packages/com.chark.game-management/Runtime/GameManager.External.cs @@ -241,8 +241,7 @@ public static void Publish(TMessage message) where TMessage : IMessage } /// - public static void AddListener(Action listener) - where TMessage : IMessage + public static void AddListener(OnMessageReceived listener) where TMessage : IMessage { var gameManager = GetGameManager(); var messageBus = gameManager.messageBus; @@ -251,8 +250,7 @@ public static void AddListener(Action listener) } /// - public static void RemoveListener(Action listener) - where TMessage : IMessage + public static void RemoveListener(OnMessageReceived listener) where TMessage : IMessage { var gameManager = GetGameManager(); var messageBus = gameManager.messageBus; diff --git a/Packages/com.chark.game-management/Runtime/GameManager.cs b/Packages/com.chark.game-management/Runtime/GameManager.cs index 1d17b6f..399ad23 100644 --- a/Packages/com.chark.game-management/Runtime/GameManager.cs +++ b/Packages/com.chark.game-management/Runtime/GameManager.cs @@ -159,7 +159,7 @@ protected virtual ISerializer CreateSerializer() /// protected virtual IStorage CreateRuntimeStorage() { - return new FileStorage( + return new DefaultFileStorage( serializer: serializer, profile: Settings.ActiveProfile, persistentDataPath: Application.persistentDataPath, @@ -172,7 +172,7 @@ protected virtual IStorage CreateRuntimeStorage() /// protected virtual IResourceLoader CreateResourceLoader() { - return new ResourceLoader(serializer); + return new DefaultResourceLoader(serializer); } /// @@ -181,7 +181,7 @@ protected virtual IResourceLoader CreateResourceLoader() protected virtual IEntityManager CreateEntityManager() { var profile = Settings.ActiveProfile; - return new EntityManager(profile); + return new DefaultEntityManager(profile); } /// @@ -189,7 +189,7 @@ protected virtual IEntityManager CreateEntityManager() /// protected virtual IMessageBus CreateMessageBus() { - return new MessageBus(); + return new DefaultMessageBus(); } private void InitializeGameManager() diff --git a/Packages/com.chark.game-management/Runtime/Messaging/DefaultMessageBus.cs b/Packages/com.chark.game-management/Runtime/Messaging/DefaultMessageBus.cs new file mode 100644 index 0000000..1959dea --- /dev/null +++ b/Packages/com.chark.game-management/Runtime/Messaging/DefaultMessageBus.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using CHARK.GameManagement.Utilities; + +namespace CHARK.GameManagement.Messaging +{ + internal sealed class DefaultMessageBus : IMessageBus + { + private readonly IDictionary interfacesByListenerTypeCache = + new Dictionary(); + + private readonly IDictionary listenersByType = + new Dictionary(); + + public int CachedTypeCount => interfacesByListenerTypeCache.Count; + + public int MessageListenerCount => listenersByType.Count; + + public int TotalListenerCount + { + get + { + var totalListenerCount = 0; + foreach (var messageListener in listenersByType.Values) + { + totalListenerCount += messageListener.ListenerCount; + } + + return totalListenerCount; + } + } + + public void Publish(TMessage message) where TMessage : IMessage + { +#if UNITY_EDITOR + if (message == null) + { + Logging.LogError($"Message of type {typeof(TMessage)} cannot be null", GetType()); + return; + } +#endif + + if (TryGetListener(out var messageListener) == false) + { +#if UNITY_EDITOR + Logging.LogWarning($"Could not find a listener for {typeof(TMessage)}", GetType()); +#endif + return; + } + + messageListener.Raise(message); + } + + public void AddListener(OnMessageReceived listener) where TMessage : IMessage + { +#if UNITY_EDITOR + if (listener == null) + { + Logging.LogError($"Listener of type {typeof(TMessage)} cannot be null", GetType()); + return; + } +#endif + + var listenerType = typeof(TMessage); + if (TryGetListener(out var messageListener)) + { + messageListener.AddListener(listener); + return; + } + + var newMessageListener = new MessageListener(); + newMessageListener.AddListener(listener); + + listenersByType[listenerType] = newMessageListener; + } + + public void RemoveListener(OnMessageReceived listener) where TMessage : IMessage + { +#if UNITY_EDITOR + if (listener == null) + { + Logging.LogError($"Listener of type {typeof(TMessage)} cannot be null", GetType()); + return; + } +#endif + + var listenerType = typeof(TMessage); + if (TryGetListener(out var messageListener) == false) + { +#if UNITY_EDITOR + Logging.LogWarning($"Could not find a listener for {typeof(TMessage)}", GetType()); +#endif + return; + } + + messageListener.RemoveListener(listener); + + if (messageListener.ListenerCount == 0) + { + interfacesByListenerTypeCache.Remove(listenerType); + listenersByType.Remove(listenerType); + } + } + + private bool TryGetListener(out MessageListener listener) where TMessage : IMessage + { + var listenerType = typeof(TMessage); + + // Simple base type listener, the most common case + { + if (listenersByType.TryGetValue(listenerType, out listener)) + { + return true; + } + } + + // Look up base types (abstract classes and such) + { + var listenerBaseType = listenerType.BaseType; + while (listenerBaseType != null && typeof(IMessage).IsAssignableFrom(listenerBaseType)) + { + if (listenersByType.TryGetValue(listenerBaseType, out listener)) + { + return true; + } + + listenerBaseType = listenerBaseType.BaseType; + } + } + + // Look up interfaces + { + // ReSharper disable once InlineOutVariableDeclaration + Type[] interfaceTypes; + + if (interfacesByListenerTypeCache.TryGetValue(listenerType, out interfaceTypes) == false) + { + interfaceTypes = listenerType.GetInterfaces(); + interfacesByListenerTypeCache[listenerType] = interfaceTypes; + } + + // ReSharper disable once ForCanBeConvertedToForeach + for (var index = 0; index < interfaceTypes.Length; index++) + { + var interfaceType = interfaceTypes[index]; + if (typeof(IMessage).IsAssignableFrom(interfaceType) == false) + { + continue; + } + + if (listenersByType.TryGetValue(interfaceType, out listener)) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/Packages/com.chark.game-management/Runtime/Messaging/MessageBus.cs.meta b/Packages/com.chark.game-management/Runtime/Messaging/DefaultMessageBus.cs.meta similarity index 100% rename from Packages/com.chark.game-management/Runtime/Messaging/MessageBus.cs.meta rename to Packages/com.chark.game-management/Runtime/Messaging/DefaultMessageBus.cs.meta diff --git a/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs b/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs new file mode 100644 index 0000000..dc0e934 --- /dev/null +++ b/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs @@ -0,0 +1,4 @@ +namespace CHARK.GameManagement.Messaging +{ + public delegate void OnMessageReceived(TMessage message) where TMessage : IMessage; +} diff --git a/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs.meta b/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs.meta new file mode 100644 index 0000000..d3a2398 --- /dev/null +++ b/Packages/com.chark.game-management/Runtime/Messaging/Delegates.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6d69e40ebcb40ea8ba194bdf377b051 +timeCreated: 1725624946 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs b/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs index 2439333..5105ed4 100644 --- a/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs +++ b/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs @@ -1,6 +1,4 @@ -using System; - -namespace CHARK.GameManagement.Messaging +namespace CHARK.GameManagement.Messaging { /// /// Core application event messaging system. @@ -17,11 +15,11 @@ public interface IMessageBus /// Add a new which listens for incoming messages of type /// . /// - public void AddListener(Action listener) where TMessage : IMessage; + public void AddListener(OnMessageReceived listener) where TMessage : IMessage; /// /// Remove existing of type . /// - public void RemoveListener(Action listener) where TMessage : IMessage; + public void RemoveListener(OnMessageReceived listener) where TMessage : IMessage; } } diff --git a/Packages/com.chark.game-management/Runtime/Messaging/MessageBus.cs b/Packages/com.chark.game-management/Runtime/Messaging/MessageBus.cs deleted file mode 100644 index f37bde4..0000000 --- a/Packages/com.chark.game-management/Runtime/Messaging/MessageBus.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using CHARK.GameManagement.Utilities; -using UnityEngine; - -namespace CHARK.GameManagement.Messaging -{ - internal sealed class MessageBus : IMessageBus - { - private readonly IDictionary listenersByType = - new Dictionary(); - - public void Publish(TMessage message) where TMessage : IMessage - { -#if UNITY_EDITOR - if (message == null) - { - Logging.LogError($"Message of type {typeof(TMessage)} cannot be null", GetType()); - return; - } -#endif - - var listenerType = message.GetType(); - if (listenersByType.TryGetValue(listenerType, out var messageListener) == false) - { - return; - } - - var typedMessageListener = (MessageListener) messageListener; - typedMessageListener.Raise(message); - } - - public void AddListener(Action listener) where TMessage : IMessage - { -#if UNITY_EDITOR - if (listener == null) - { - Logging.LogError($"Listener of type {typeof(TMessage)} cannot be null", GetType()); - return; - } -#endif - - var listenerType = typeof(TMessage); - if (listenersByType.TryGetValue(listenerType, out var messageListener)) - { - var existingMessageListener = (MessageListener) messageListener; - existingMessageListener.AddListener(listener); - return; - } - - var newMessageListener = new MessageListener(); - newMessageListener.AddListener(listener); - - listenersByType[listenerType] = newMessageListener; - } - - public void RemoveListener(Action listener) where TMessage : IMessage - { -#if UNITY_EDITOR - if (listener == null) - { - Logging.LogError($"Listener of type {typeof(TMessage)} cannot be null", GetType()); - return; - } -#endif - - var listenerType = typeof(TMessage); - if (listenersByType.TryGetValue(listenerType, out var messageListener) == false) - { - return; - } - - var existingMessageListener = (MessageListener) messageListener; - existingMessageListener.RemoveListener(listener); - - if (existingMessageListener.ListenerCount == 0) - { - listenersByType.Remove(listenerType); - } - } - } -} diff --git a/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs b/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs index 931365c..2c806ee 100644 --- a/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs +++ b/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs @@ -4,21 +4,24 @@ namespace CHARK.GameManagement.Messaging { - internal sealed class MessageListener : MessageListener where TMessage : IMessage + internal sealed class MessageListener { - private readonly IList> listeners = new List>(); + private readonly IDictionary> wrapperListenersByInstance = + new Dictionary>(); - public int ListenerCount => listeners.Count; + private readonly IList> wrapperListeners = + new List>(); - public void Raise(TMessage message) + public int ListenerCount => wrapperListeners.Count; + + public void Raise(IMessage message) { - // ReSharper disable once ForCanBeConvertedToForeach - for (var index = 0; index < listeners.Count; index++) + for (var index = wrapperListeners.Count - 1; index >= 0; index--) { - var listener = listeners[index]; + var wrapperListener = wrapperListeners[index]; try { - listener.Invoke(message); + wrapperListener.Invoke(message); } catch (Exception exception) { @@ -27,18 +30,31 @@ public void Raise(TMessage message) } } - public void AddListener(Action listener) + public void AddListener(OnMessageReceived listener) where TMessage : IMessage { - listeners.Add(listener); + if (wrapperListenersByInstance.TryGetValue(listener, out var wrapperListener)) + { + wrapperListeners.Add(wrapperListener); + return; + } + + var newWrapperListener = new Action( + message => { listener.Invoke((TMessage)message); } + ); + + wrapperListenersByInstance[listener] = newWrapperListener; + wrapperListeners.Add(newWrapperListener); } - public void RemoveListener(Action listener) + public void RemoveListener(OnMessageReceived listener) where TMessage : IMessage { - listeners.Remove(listener); - } - } + if (wrapperListenersByInstance.TryGetValue(listener, out var wrapperListener) == false) + { + return; + } - internal abstract class MessageListener - { + wrapperListenersByInstance.Remove(wrapperListener); + wrapperListeners.Remove(wrapperListener); + } } } diff --git a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/DefaultFileStorage.cs similarity index 97% rename from Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs rename to Packages/com.chark.game-management/Runtime/Storage/DefaultFileStorage.cs index 7b3df06..f9460e2 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/DefaultFileStorage.cs @@ -5,12 +5,12 @@ namespace CHARK.GameManagement.Storage { - internal sealed class FileStorage : Storage + internal sealed class DefaultFileStorage : Storage { private readonly string persistentDataPath; private readonly IGameManagerSettingsProfile profile; - public FileStorage( + public DefaultFileStorage( ISerializer serializer, IGameManagerSettingsProfile profile, string persistentDataPath, diff --git a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/DefaultFileStorage.cs.meta similarity index 100% rename from Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta rename to Packages/com.chark.game-management/Runtime/Storage/DefaultFileStorage.cs.meta diff --git a/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs b/Packages/com.chark.game-management/Tests/Editor/DefaultFileStorageTest.cs similarity index 93% rename from Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs rename to Packages/com.chark.game-management/Tests/Editor/DefaultFileStorageTest.cs index 9d1d1fd..dd9513e 100644 --- a/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs +++ b/Packages/com.chark.game-management/Tests/Editor/DefaultFileStorageTest.cs @@ -6,11 +6,11 @@ namespace CHARK.GameManagement.Tests.Editor { // ReSharper disable once UnusedType.Global - internal sealed class FileStorageTest : StorageTest + internal sealed class DefaultFileStorageTest : StorageTest { protected override IStorage CreateStorage() { - return new FileStorage( + return new DefaultFileStorage( serializer: DefaultSerializer.Instance, profile: GameManagerTestProfile.Instance, persistentDataPath: Application.persistentDataPath, diff --git a/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/DefaultFileStorageTest.cs.meta similarity index 100% rename from Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs.meta rename to Packages/com.chark.game-management/Tests/Editor/DefaultFileStorageTest.cs.meta diff --git a/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs b/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs new file mode 100644 index 0000000..17d62b1 --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs @@ -0,0 +1,184 @@ +using System; +using CHARK.GameManagement.Messaging; +using NUnit.Framework; + +namespace CHARK.GameManagement.Tests.Editor +{ + internal sealed class DefaultMessageBusTest + { + private DefaultMessageBus messageBus; + + [SetUp] + public void SetUp() + { + messageBus = new DefaultMessageBus(); + } + + [Test] + public void ShouldRegisterAndUnregisterLocalListener() + { + // When + messageBus.AddListener(OnSimpleTestMessage); + messageBus.RemoveListener(OnSimpleTestMessage); + + // Then + Assert.AreEqual(0, messageBus.MessageListenerCount); + Assert.AreEqual(0, messageBus.TotalListenerCount); + Assert.AreEqual(0, messageBus.CachedTypeCount); + + return; + + // Given + void OnSimpleTestMessage(SimpleTestMessage message) + { + } + } + + [Test] + public void ShouldRegisterAndUnregisterDelegateListener() + { + // Given + OnMessageReceived onSimpleTestOnMessage = _ => + { + }; + + // When + messageBus.AddListener(onSimpleTestOnMessage); + messageBus.RemoveListener(onSimpleTestOnMessage); + + // Then + Assert.AreEqual(0, messageBus.MessageListenerCount); + Assert.AreEqual(0, messageBus.TotalListenerCount); + Assert.AreEqual(0, messageBus.CachedTypeCount); + } + + [Test] + public void ShouldRegisterSimpleListenerAndPublishSimpleMessage() + { + // Given + var expectedMessage = new SimpleTestMessage(); + + // When + SimpleTestMessage actualMessage = default; + messageBus.AddListener( + message => { actualMessage = message; } + ); + + messageBus.Publish(expectedMessage); + + // Then + Assert.AreEqual(expectedMessage, actualMessage); + } + + [Test] + public void ShouldRegisterAbstractListenerAndPublishConcreteMessage() + { + // Given + var expectedMessage = new ChildTestMessage(); + + // When + BaseTestMessage actualMessage = default; + messageBus.AddListener( + publishedMessage => { actualMessage = publishedMessage; } + ); + + messageBus.Publish(expectedMessage); + + // Then + Assert.IsNotNull(actualMessage); + Assert.IsAssignableFrom(actualMessage); + Assert.AreEqual(expectedMessage, actualMessage); + } + + [Test] + public void ShouldRegisterInterfaceListenerAndPublishConcreteMessage() + { + // Given + var expectedMessage = new ChildTestMessage(); + + // When + ITestMessage actualMessage = default; + messageBus.AddListener( + publishedMessage => { actualMessage = publishedMessage; } + ); + + messageBus.Publish(expectedMessage); + + // Then + Assert.IsNotNull(actualMessage); + Assert.IsAssignableFrom(actualMessage); + Assert.AreEqual(expectedMessage, actualMessage); + } + + [Test] + public void ShouldReceiveMessagesWhenRemovingListenersDuringProcessing() + { + // Given + var listener01 = new SimpleMessageTestController(messageBus); + listener01.Initialize(); + listener01.OnSimpleMessageReceived += () => listener01.Dispose(); + + var listener02 = new SimpleMessageTestController(messageBus); + listener02.Initialize(); + listener02.OnSimpleMessageReceived += () => listener02.Dispose(); + + var listener03 = new SimpleMessageTestController(messageBus); + listener03.Initialize(); + listener03.OnSimpleMessageReceived += () => listener03.Dispose(); + + // When + messageBus.Publish(new SimpleTestMessage()); + + // Then + Assert.AreEqual(1, listener01.ReceivedMessageCount); + Assert.AreEqual(1, listener02.ReceivedMessageCount); + Assert.AreEqual(1, listener03.ReceivedMessageCount); + } + + private class SimpleMessageTestController + { + private readonly IMessageBus messageBus; + + public int ReceivedMessageCount { get; private set; } + + public event Action OnSimpleMessageReceived; + + public SimpleMessageTestController(IMessageBus messageBus) + { + this.messageBus = messageBus; + } + + public void Initialize() + { + messageBus.AddListener(OnSimpleMessage); + } + + public void Dispose() + { + messageBus.RemoveListener(OnSimpleMessage); + } + + private void OnSimpleMessage(SimpleTestMessage message) + { + ReceivedMessageCount++; + OnSimpleMessageReceived?.Invoke(); + } + } + + private sealed class SimpleTestMessage : IMessage + { + } + + private sealed class ChildTestMessage : BaseTestMessage + { + } + + private abstract class BaseTestMessage : ITestMessage + { + } + + private interface ITestMessage : IMessage + { + } + } +} diff --git a/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs.meta new file mode 100644 index 0000000..42ff7de --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/DefaultMessageBusTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e91caff16b50440dbd09f8e99826e001 +timeCreated: 1713461962 \ No newline at end of file