diff --git a/Packages/com.chark.game-management/CHANGELOG.md b/Packages/com.chark.game-management/CHANGELOG.md index 8a8f071..8baa2e5 100644 --- a/Packages/com.chark.game-management/CHANGELOG.md +++ b/Packages/com.chark.game-management/CHANGELOG.md @@ -9,19 +9,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added -- `ISerializer` with `GameManager.TryDeserializeValue` and `GameManager.TrySerializeValue` methods, this can be used to serialize and deserialize data using internal `GameManager` serialization utilities. -- `GameManager.DeleteRuntimeValueAsync` which can be used to delete data asynchronously. -- `GameManager.GetResourceAsync` which can be used to retrieve resources from StreamingAssets directory. +- `GameManager.TryDeserializeValue`, `GameManager.TrySerializeValue`, `GameManager.TryDeserializeStream` and `GameManager.TrySerializeStream` methods, these can be used to serialize and deserialize data using internal `GameManager` serialization utilities. You can customize this via `ISerializer` interface (see `CreateSerializer` method on `GameManager`). +- `GameManager.DeleteDataAsync` which can be used to delete data asynchronously. +- `GameManager.ReadResourceAsync` and `GameManager.ReadResourceStreamAsync` methods which can be used to retrieve resources from StreamingAssets directory. +- `GameManager.ReadDataStream` and `GameManager.ReadDataStreamAsync` methods which can be used to read a `Stream` from a file on disk. +- `GameManager.SaveDataStream` and `GameManager.SaveDataStreamAsync` methods which can be used to persist a `Stream` to disk. ### Changed -- Renamed `IResourceLoader` methods to use `Get*` prefix instead of `Load*` so its more consistent with other methods. +- Renamed some `IResourceLoader` methods to use `Get*` prefix instead of `Load*` so its more consistent with other methods. Methods which read from _StreamingAssets_ directory will use `Read*` prefix. - Renamed `IGameStorage` to `IStorage`. -- Cancellation tokens can now be used in async methods. +- Cancellation tokens can now be used in all async methods. +- Renamed `IStorage` methods to use `Read*` and `Save*` prefixes to emphasise that these methods interact with data on dist. ### Fixed -- `GameStorage.GetValueAsync` not switching back to main thread when no value is found. +- `GameStorage.GetValueAsync` (now `GameStorage.ReadValueAsync`) not switching back to main thread when no value is found. ## [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/IResourceLoader.cs b/Packages/com.chark.game-management/Runtime/Assets/IResourceLoader.cs index 7e74274..93df01e 100644 --- a/Packages/com.chark.game-management/Runtime/Assets/IResourceLoader.cs +++ b/Packages/com.chark.game-management/Runtime/Assets/IResourceLoader.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using UnityEngine; @@ -8,24 +9,46 @@ namespace CHARK.GameManagement.Assets public interface IResourceLoader { /// - /// Enumerable of resources retrieved from given . + /// Resources of type retrieved from given . /// + /// + /// is relative to Resources directory. + /// public IEnumerable GetResources(string path = null) where TResource : Object; /// - /// true if is retrieved from given + /// true if resource of type is retrieved from given /// or false otherwise. /// + /// + /// is relative to Resources directory. + /// public bool TryGetResource(string path, out TResource resource) where TResource : Object; /// - /// Asset loaded asynchronously from given . - ///

- /// Note, is relative to StreamingAssets directory. + /// Resource of type retrieved from given . /// - public Task GetResourceAsync( + /// + /// is relative to StreamingAssets directory. + /// + /// + /// if could not be retrieved. + /// + public Task ReadResourceAsync( + string path, + CancellationToken cancellationToken = default + ); + + /// + /// Resource retrieved from given . If something + /// fails, an empty stream is be returned. + /// + /// + /// is relative to StreamingAssets directory. + /// + public Task ReadResourceStreamAsync( string path, CancellationToken cancellationToken = default ); diff --git a/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs b/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs index c099220..d9521ea 100644 --- a/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs +++ b/Packages/com.chark.game-management/Runtime/Assets/ResourceLoader.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using CHARK.GameManagement.Serialization; +using CHARK.GameManagement.Utilities; using UnityEngine; using Object = UnityEngine.Object; @@ -46,11 +48,16 @@ public bool TryGetResource(string path, out TResource resource) where return false; } - public async Task GetResourceAsync( + public async Task ReadResourceAsync( string path, CancellationToken cancellationToken ) { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException($"{path} cannot be null or empty"); + } + var actualPath = Path.Combine(Application.streamingAssetsPath, path); #if UNITY_ANDROID @@ -73,7 +80,51 @@ await Cysharp.Threading.Tasks.UnityAsyncExtensions.ToUniTask( return value; } - return default; + throw new Exception($"Could not retrieve resource from path: {actualPath}"); + } + +#pragma warning disable CS1998 + public async Task ReadResourceStreamAsync( +#pragma warning restore CS1998 + string path, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(path)) + { + return Stream.Null; + } + + var actualPath = Path.Combine(Application.streamingAssetsPath, path); + + try + { +#if UNITY_ANDROID + var request = UnityEngine.Networking.UnityWebRequest.Get(actualPath); + var operation = request.SendWebRequest(); + + await Cysharp.Threading.Tasks.UnityAsyncExtensions.ToUniTask( + operation, + cancellationToken: cancellationToken + ); + + var handler = request.downloadHandler; + var data = handler.data; + if (data == null || data.Length == 0) + { + return Stream.Null; + } + + return new MemoryStream(data); +#else + return File.OpenRead(actualPath); +#endif + } + catch (Exception exception) + { + Logging.LogException(exception, GetType()); + return Stream.Null; + } } } } diff --git a/Packages/com.chark.game-management/Runtime/Entities/IEntityManager.cs b/Packages/com.chark.game-management/Runtime/Entities/IEntityManager.cs index 0d1a233..3ae882e 100644 --- a/Packages/com.chark.game-management/Runtime/Entities/IEntityManager.cs +++ b/Packages/com.chark.game-management/Runtime/Entities/IEntityManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace CHARK.GameManagement.Entities { @@ -10,7 +11,7 @@ public interface IEntityManager public IReadOnlyList Entities { get; } /// - /// Add a to the Entity Manager. + /// Add a of type to the Entity Manager. /// /// /// true if given was added or false otherwise. @@ -18,7 +19,7 @@ public interface IEntityManager public bool AddEntity(TEntity entity) where TEntity : class; /// - /// Remove a from the Entity Manager. + /// Remove an if type from the Entity Manager. /// /// /// true if given was removed or false otherwise. @@ -26,7 +27,8 @@ public interface IEntityManager public bool RemoveEntity(TEntity entity) where TEntity : class; /// - /// true if is retrieved or false otherwise. + /// true if of type is retrieved or + /// false otherwise. /// public bool TryGetEntity(out TEntity entity); @@ -36,9 +38,11 @@ public interface IEntityManager public IEnumerable GetEntities(); /// - /// Entity of type or throws if entity - /// is not added to the manager. + /// Entity of type . /// + /// + /// if system of type is not found. + /// public TEntity GetEntity(); } } diff --git a/Packages/com.chark.game-management/Runtime/GameManager.External.cs b/Packages/com.chark.game-management/Runtime/GameManager.External.cs index b4af54f..2ed1b4b 100644 --- a/Packages/com.chark.game-management/Runtime/GameManager.External.cs +++ b/Packages/com.chark.game-management/Runtime/GameManager.External.cs @@ -1,7 +1,12 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Threading; using System.Threading.Tasks; +using CHARK.GameManagement.Assets; using CHARK.GameManagement.Messaging; +using CHARK.GameManagement.Serialization; +using CHARK.GameManagement.Storage; using CHARK.GameManagement.Systems; using Object = UnityEngine.Object; @@ -9,11 +14,9 @@ namespace CHARK.GameManagement { public abstract partial class GameManager { - /// - /// Resources of type from at - /// given . - /// - public static IEnumerable GetResources(string path = null) where TResource : Object + /// + public static IEnumerable GetResources(string path = null) + where TResource : Object { var gameManager = GetGameManager(); var resourceLoader = gameManager.resourceLoader; @@ -21,11 +24,9 @@ public static IEnumerable GetResources(string path = null) return resourceLoader.GetResources(path); } - /// - /// true if resource of type is retrieved from - /// at given or false otherwise. - /// - public static bool TryGetResource(string path, out TResource resource) where TResource : Object + /// + public static bool TryGetResource(string path, out TResource resource) + where TResource : Object { var gameManager = GetGameManager(); var resourceLoader = gameManager.resourceLoader; @@ -33,115 +34,162 @@ public static bool TryGetResource(string path, out TResource resource return resourceLoader.TryGetResource(path, out resource); } - /// - /// Task containing a resource of type retrieved from - /// at given or default if - /// value could not be retrieved. - /// - public static Task GetResourceAsync(string path) + /// + public static Task ReadResourceAsync( + string path, + CancellationToken cancellationToken = default + ) { var gameManager = GetGameManager(); var resourceLoader = gameManager.resourceLoader; - return resourceLoader.GetResourceAsync(path); + return resourceLoader.ReadResourceAsync(path, cancellationToken); } - /// - /// Value retrieved from at given - /// asynchronously or default if no value is could be retrieved. - /// - public static Task GetRuntimeValueAsync(string path) + /// + public static Task ReadResourceStreamAsync( + string path, + CancellationToken cancellationToken = default + ) + { + var gameManager = GetGameManager(); + var resourceLoader = gameManager.resourceLoader; + + return resourceLoader.ReadResourceStreamAsync(path, cancellationToken); + } + + /// + public static bool TryReadData(string path, out TData data) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - return runtimeStorage.GetValueAsync(path); + return runtimeStorage.TryReadData(path, out data); } - /// - /// true if is retrieved by given - /// from . - /// - public static bool TryGetRuntimeValue(string path, out TValue value) + /// + public static Task ReadDataAsync( + string path, + CancellationToken cancellationToken = default + ) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - return runtimeStorage.TryGetValue(path, out value); + return runtimeStorage.ReadDataAsync(path, cancellationToken); } - /// - /// Persist a by given asynchronously to - /// . - /// - public static Task SetRuntimeValueAsync(string path, TValue value) + /// + public static Stream ReadDataStream(string path) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - return runtimeStorage.SetValueAsync(path, value); + return runtimeStorage.ReadDataStream(path); } - /// - /// Persist a by given to - /// . - /// - public static void SetRuntimeValue(string path, TValue value) + /// + public static Task ReadDataStreamAsync( + string path, + CancellationToken cancellationToken = default + ) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - runtimeStorage.SetValue(path, value); + return runtimeStorage.ReadDataStreamAsync(path, cancellationToken); } - /// - /// Delete persisted value at given - /// asynchronously from . - /// - public static Task DeleteRuntimeValueAsync(string path) + /// + public static void SaveData(string path, TData data) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - return runtimeStorage.DeleteValueAsync(path); + runtimeStorage.SaveData(path, data); } - /// - /// Delete persisted value at given - /// from . - /// - public static void DeleteRuntimeValue(string path) + /// + public static void SaveDataStream(string path, Stream stream) { var gameManager = GetGameManager(); var runtimeStorage = gameManager.runtimeStorage; - runtimeStorage.DeleteValue(path); + runtimeStorage.SaveDataStream(path, stream); } - /// - /// true if is retrieved by given - /// from . - /// - public static bool TryGetEditorValue(string path, out TValue value) + /// + public static Task SaveDataAsync( + string path, + TData data, + CancellationToken cancellationToken = default + ) + { + var gameManager = GetGameManager(); + var runtimeStorage = gameManager.runtimeStorage; + + return runtimeStorage.SaveDataAsync(path, data, cancellationToken); + } + + /// + public static Task SaveDataStreamAsync( + string path, + Stream stream, + CancellationToken cancellationToken = default + ) { - return EditorStorage.TryGetValue(path, out value); + var gameManager = GetGameManager(); + var runtimeStorage = gameManager.runtimeStorage; + + return runtimeStorage.SaveDataStreamAsync(path, stream, cancellationToken); } - /// - /// Persist a by given to - /// . - /// - public static void SetEditorValue(string path, TValue value) + /// + public static void DeleteData(string path) { - EditorStorage.SetValue(path, value); + var gameManager = GetGameManager(); + var runtimeStorage = gameManager.runtimeStorage; + + runtimeStorage.DeleteData(path); } - /// - /// Delete persisted value at given . - /// - public static void DeleteEditorValue(string path) + /// + public static Task DeleteDataAsync( + string path, + CancellationToken cancellationToken = default + ) { - EditorStorage.DeleteValue(path); + var gameManager = GetGameManager(); + var runtimeStorage = gameManager.runtimeStorage; + + return runtimeStorage.DeleteDataAsync(path, cancellationToken); + } + + /// + /// + /// This method should only be used in Editor, it will not function in builds. + /// + public static bool TryReadEditorData(string path, out TData data) + { + return EditorStorage.TryReadData(path, out data); + } + + /// + /// + /// This method should only be used in Editor, it will not function in builds. + /// + public static void SaveEditorData(string path, TData data) + { + EditorStorage.SaveData(path, data); + } + + /// + /// + /// This method should only be used in Editor, it will not function in builds. + /// + public static void DeleteEditorData(string path) + { + EditorStorage.DeleteData(path); } /// @@ -157,7 +205,8 @@ public static bool TryGetSystem(out TSystem system) where TSystem : ISy } /// - /// Enumerable of systems of type from . + /// Enumerable of systems of type retrieved from + /// . /// public static IEnumerable GetSystems() where TSystem : ISystem { @@ -169,8 +218,11 @@ public static IEnumerable GetSystems() where TSystem : ISystem } /// - /// Systems of type from . + /// System of type retrieved from . /// + /// + /// if system of type is not found. + /// public static TSystem GetSystem() where TSystem : ISystem { var gameManager = GetGameManager(); @@ -179,9 +231,7 @@ public static TSystem GetSystem() where TSystem : ISystem return entityManager.GetEntity(); } - /// - /// Publish a message to the . - /// + /// public static void Publish(TMessage message) where TMessage : IMessage { var gameManager = GetGameManager(); @@ -190,9 +240,7 @@ public static void Publish(TMessage message) where TMessage : IMessage messageBus.Publish(message); } - /// - /// Add a listener to the . - /// + /// public static void AddListener(Action listener) where TMessage : IMessage { @@ -202,9 +250,7 @@ public static void AddListener(Action listener) messageBus.AddListener(listener); } - /// - /// Remove a listener from the . - /// + /// public static void RemoveListener(Action listener) where TMessage : IMessage { @@ -214,30 +260,22 @@ public static void RemoveListener(Action listener) messageBus.RemoveListener(listener); } - /// - /// true if is deserialized to - /// using successfully or - /// false otherwise. - /// - public static bool TryDeserializeValue(string serializedValue, out TValue deserializedValue) + /// + public static bool TryDeserializeValue(string value, out TValue deserializedValue) { var gameManager = GetGameManager(); var serializer = gameManager.serializer; - return serializer.TryDeserializeValue(serializedValue, out deserializedValue); + return serializer.TryDeserializeValue(value, out deserializedValue); } - /// - /// true if is serialized to - /// using successfully or - /// false otherwise. - /// - public static bool TrySerializeValue(TValue deserializedValue, out string serializedValue) + /// + public static bool TrySerializeValue(TValue value, out string serializedValue) { var gameManager = GetGameManager(); var serializer = gameManager.serializer; - return serializer.TrySerializeValue(deserializedValue, out serializedValue); + return serializer.TrySerializeValue(value, out serializedValue); } } } diff --git a/Packages/com.chark.game-management/Runtime/GameManager.cs b/Packages/com.chark.game-management/Runtime/GameManager.cs index 3b50cca..1d17b6f 100644 --- a/Packages/com.chark.game-management/Runtime/GameManager.cs +++ b/Packages/com.chark.game-management/Runtime/GameManager.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using CHARK.GameManagement.Assets; using CHARK.GameManagement.Entities; using CHARK.GameManagement.Messaging; @@ -20,7 +21,7 @@ public abstract partial class GameManager : MonoBehaviour #if UNITY_EDITOR new EditorPrefsStorage(DefaultSerializer.Instance, $"{nameof(GameManager)}."); #else - DefaultStorage.Instance; + NullStorage.Instance; #endif private static GameManagerSettings Settings => GameManagerSettings.Instance; @@ -158,11 +159,11 @@ protected virtual ISerializer CreateSerializer() /// protected virtual IStorage CreateRuntimeStorage() { - var keyPrefix = $"{GetGameManagerName()}."; return new FileStorage( serializer: serializer, + profile: Settings.ActiveProfile, persistentDataPath: Application.persistentDataPath, - pathPrefix: keyPrefix + pathPrefix: "Data" + Path.DirectorySeparatorChar ); } diff --git a/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs b/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs index ec37e91..2439333 100644 --- a/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs +++ b/Packages/com.chark.game-management/Runtime/Messaging/IMessageBus.cs @@ -8,18 +8,19 @@ namespace CHARK.GameManagement.Messaging public interface IMessageBus { /// - /// Publish a and invoke listeners which are listening for + /// Publish a and invoke all listeners which are listening for /// messages of type . /// public void Publish(TMessage message) where TMessage : IMessage; /// - /// Add a new for messages of type . + /// Add a new which listens for incoming messages of type + /// . /// public void AddListener(Action listener) where TMessage : IMessage; /// - /// Remove existing from messages of type . + /// Remove existing of type . /// public void RemoveListener(Action listener) where TMessage : IMessage; } diff --git a/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs b/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs index 460bb91..931365c 100644 --- a/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs +++ b/Packages/com.chark.game-management/Runtime/Messaging/MessageListener.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using UnityEngine; +using CHARK.GameManagement.Utilities; namespace CHARK.GameManagement.Messaging { @@ -22,7 +22,7 @@ public void Raise(TMessage message) } catch (Exception exception) { - Debug.LogException(exception); + Logging.LogException(exception, GetType()); } } } diff --git a/Packages/com.chark.game-management/Runtime/Serialization/DefaultSerializer.cs b/Packages/com.chark.game-management/Runtime/Serialization/DefaultSerializer.cs index c502ed4..1f9cf72 100644 --- a/Packages/com.chark.game-management/Runtime/Serialization/DefaultSerializer.cs +++ b/Packages/com.chark.game-management/Runtime/Serialization/DefaultSerializer.cs @@ -1,8 +1,10 @@ using System; using System.ComponentModel; +using System.IO; +using System.Text; +using CHARK.GameManagement.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using UnityEngine; namespace CHARK.GameManagement.Serialization { @@ -10,16 +12,18 @@ internal class DefaultSerializer : ISerializer { internal static DefaultSerializer Instance { get; } = new(); - private readonly JsonSerializerSettings serializerSettings; + private readonly JsonSerializer jsonSerializer = new() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + }; private DefaultSerializer() { - serializerSettings = CreateDefaultSerializerSettings(); } - public bool TryDeserializeValue(string serializedValue, out TValue deserializedValue) + public bool TryDeserializeValue(string value, out TValue deserializedValue) { - if (TryDeserializeConverterValue(serializedValue, out TValue converterValue)) + if (TryDeserializeConverterValue(value, out TValue converterValue)) { deserializedValue = converterValue; return true; @@ -29,7 +33,7 @@ public bool TryDeserializeValue(string serializedValue, out TValue deser try { - var jsonValue = JsonConvert.DeserializeObject(serializedValue); + var jsonValue = JsonConvert.DeserializeObject(value); if (jsonValue == null) { return false; @@ -40,42 +44,104 @@ public bool TryDeserializeValue(string serializedValue, out TValue deser } catch (Exception exception) { - Debug.LogException(exception); + Logging.LogException(exception, GetType()); return false; } } - public bool TrySerializeValue(TValue deserializedValue, out string serializedJson) + public bool TryDeserializeStream(Stream stream, out TValue deserializedValue) { - serializedJson = default; + if (TryDeserializeConverterValue(stream, out TValue converterValue)) + { + deserializedValue = converterValue; + return true; + } + + deserializedValue = default; + + try + { + using var streamReader = new StreamReader(stream); + using var jsonReader = new JsonTextReader(streamReader); + + var jsonValue = jsonSerializer.Deserialize(jsonReader); + if (jsonValue == null) + { + return false; + } + + deserializedValue = jsonValue; + return true; + } + catch (Exception exception) + { + Logging.LogException(exception, GetType()); + return false; + } + } + + public bool TrySerializeValue(TValue value, out string serializedValue) + { + serializedValue = default; var valueType = typeof(TValue); if (valueType.IsPrimitive || valueType == typeof(string)) { - var valueString = deserializedValue.ToString(); - serializedJson = valueString; + var valueString = value.ToString(); + serializedValue = valueString; return true; } try { - var valueJson = JsonConvert.SerializeObject(deserializedValue, serializerSettings); + using var stringWriter = new StringWriter(); + jsonSerializer.Serialize(stringWriter, value); + + var valueJson = stringWriter.ToString(); if (string.IsNullOrWhiteSpace(valueJson)) { return false; } - serializedJson = valueJson; + serializedValue = valueJson; return true; } catch (Exception exception) { - Debug.LogException(exception); - throw; + Logging.LogException(exception, GetType()); + return false; } } - private static bool TryDeserializeConverterValue(string json, out TValue deserializedValue) + public bool TrySerializeStream(Stream stream, out string serializedValue) + { + serializedValue = default; + + try + { + using var memoryStream = new MemoryStream(); + using var streamWriter = new StreamWriter(memoryStream); + using var jsonWriter = new JsonTextWriter(streamWriter); + + jsonSerializer.Serialize(jsonWriter, stream); + + var valueJson = Encoding.UTF8.GetString(memoryStream.ToArray()); + if (string.IsNullOrWhiteSpace(valueJson)) + { + return false; + } + + serializedValue = valueJson; + return true; + } + catch (Exception exception) + { + Logging.LogException(exception, GetType()); + return false; + } + } + + private static bool TryDeserializeConverterValue(Stream stream, out TValue deserializedValue) { deserializedValue = default; @@ -88,6 +154,9 @@ private static bool TryDeserializeConverterValue(string json, out TValue try { var converter = TypeDescriptor.GetConverter(valueType); + var streamReader = new StreamReader(stream); + var json = streamReader.ReadToEnd(); + var converterValue = (TValue)converter.ConvertFrom(json); if (converterValue == null) { @@ -99,17 +168,38 @@ private static bool TryDeserializeConverterValue(string json, out TValue } catch (Exception exception) { - Debug.LogException(exception); + Logging.LogException(exception, typeof(DefaultSerializer)); return false; } } - private static JsonSerializerSettings CreateDefaultSerializerSettings() + private static bool TryDeserializeConverterValue(string json, out TValue deserializedValue) { - return new JsonSerializerSettings + deserializedValue = default; + + var valueType = typeof(TValue); + if (valueType.IsPrimitive == false && valueType != typeof(string)) + { + return false; + } + + try + { + var converter = TypeDescriptor.GetConverter(valueType); + var converterValue = (TValue)converter.ConvertFrom(json); + if (converterValue == null) + { + return false; + } + + deserializedValue = converterValue; + return true; + } + catch (Exception exception) { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - }; + Logging.LogException(exception, typeof(DefaultSerializer)); + return false; + } } } } diff --git a/Packages/com.chark.game-management/Runtime/Serialization/ISerializer.cs b/Packages/com.chark.game-management/Runtime/Serialization/ISerializer.cs index 6a9c811..13b77aa 100644 --- a/Packages/com.chark.game-management/Runtime/Serialization/ISerializer.cs +++ b/Packages/com.chark.game-management/Runtime/Serialization/ISerializer.cs @@ -1,17 +1,31 @@ -namespace CHARK.GameManagement.Serialization +using System.IO; + +namespace CHARK.GameManagement.Serialization { public interface ISerializer { /// - /// true if is retrieved from - /// or false otherwise. + /// true if is deserialized to + /// or false otherwise. /// - public bool TryDeserializeValue(string serializedValue, out TValue deserializedValue); + public bool TryDeserializeValue(string value, out TValue deserializedValue); /// - /// true if is retrieved from + /// true if is deserialized to /// or false otherwise. /// - public bool TrySerializeValue(TValue deserializedValue, out string serializedValue); + public bool TryDeserializeStream(Stream stream, out TValue deserializedValue); + + /// + /// true if is serialized to + /// or false otherwise. + /// + public bool TrySerializeValue(TValue value, out string serializedValue); + + /// + /// true if is serialized to + /// or false otherwise. + /// + public bool TrySerializeStream(Stream stream, out string serializedValue); } } diff --git a/Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs deleted file mode 100644 index 2d312d6..0000000 --- a/Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CHARK.GameManagement.Serialization; - -namespace CHARK.GameManagement.Storage -{ - /// - /// Game storage which does nothing (placeholder). - /// - internal sealed class DefaultStorage : Storage - { - public static DefaultStorage Instance { get; } = new(); - - private DefaultStorage() : base(DefaultSerializer.Instance, default) - { - } - - public override bool TryGetValue(string path, out TValue value) - { - value = default; - return false; - } - - public override void SetValue(string path, TValue value) - { - } - - protected override string GetString(string path) - { - return default; - } - - protected override void SetString(string path, string value) - { - } - - protected override void Delete(string path) - { - } - } -} diff --git a/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs index 4a8b003..bed07e0 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs @@ -1,4 +1,6 @@ -using CHARK.GameManagement.Serialization; +using System.IO; +using System.Text; +using CHARK.GameManagement.Serialization; namespace CHARK.GameManagement.Storage { @@ -7,22 +9,39 @@ namespace CHARK.GameManagement.Storage /// internal sealed class EditorPrefsStorage : Storage { - public EditorPrefsStorage(ISerializer serializer, string keyPrefix = "") : base(serializer, keyPrefix) + public EditorPrefsStorage(ISerializer serializer, string pathPrefix = "") : base(serializer, pathPrefix) { } - protected override string GetString(string path) + protected override Stream ReadStream(string path) { #if UNITY_EDITOR - return UnityEditor.EditorPrefs.GetString(path); + var editorString = UnityEditor.EditorPrefs.GetString(path); + if (string.IsNullOrWhiteSpace(editorString)) + { + return Stream.Null; + } + + var bytes = Encoding.UTF8.GetBytes(editorString); + return new MemoryStream(bytes); #else - return default; + return Stream.Null; +#endif + } + + protected override void SaveString(string path, string value) + { +#if UNITY_EDITOR + UnityEditor.EditorPrefs.SetString(path, value); #endif } - protected override void SetString(string path, string value) + protected override void SaveStream(string path, Stream stream) { #if UNITY_EDITOR + using var streamReader = new StreamReader(stream); + var value = streamReader.ReadToEnd(); + UnityEditor.EditorPrefs.SetString(path, value); #endif } diff --git a/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs.meta index ee940dd..e09cda0 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/EditorPrefsStorage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 3795b11e96151c5438282149a64c9e21 +guid: 3df6529c17164ab68ceb29ac9dfb116a timeCreated: 1669925611 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs index 7474b3a..7b3df06 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs @@ -1,34 +1,82 @@ using System.IO; using CHARK.GameManagement.Serialization; +using CHARK.GameManagement.Settings; +using CHARK.GameManagement.Utilities; namespace CHARK.GameManagement.Storage { internal sealed class FileStorage : Storage { private readonly string persistentDataPath; + private readonly IGameManagerSettingsProfile profile; - public FileStorage(ISerializer serializer, string persistentDataPath, string pathPrefix = "") : base(serializer, pathPrefix) + public FileStorage( + ISerializer serializer, + IGameManagerSettingsProfile profile, + string persistentDataPath, + string pathPrefix = "" + ) : base(serializer, pathPrefix) { this.persistentDataPath = persistentDataPath; + this.profile = profile; } - protected override string GetString(string path) + protected override Stream ReadStream(string path) { var actualPath = GetFilePath(path); if (File.Exists(actualPath) == false) { - return default; + if (profile.IsVerboseLogging) + { + Logging.LogWarning($"File does not exist: {actualPath}", GetType()); + } + + return Stream.Null; } - return File.ReadAllText(actualPath); + return File.OpenRead(actualPath); } - protected override void SetString(string path, string value) + protected override void SaveString(string path, string value) { var actualPath = GetFilePath(path); + var directory = Path.GetDirectoryName(actualPath); + + if (string.IsNullOrWhiteSpace(directory) == false) + { + Directory.CreateDirectory(directory); + } + + if (profile.IsVerboseLogging) + { + Logging.LogDebug($"Saving string to file: {actualPath}", GetType()); + } + File.WriteAllText(actualPath, value); } + protected override void SaveStream(string path, Stream stream) + { + var actualPath = GetFilePath(path); + var directory = Path.GetDirectoryName(actualPath); + + if (string.IsNullOrWhiteSpace(directory) == false) + { + Directory.CreateDirectory(directory); + } + + if (profile.IsVerboseLogging) + { + Logging.LogDebug( + $"Saving stream (length={stream.Length}, position={stream.Position}) to file: {actualPath}", + GetType() + ); + } + + using var fileStream = new FileStream(actualPath, FileMode.Create); + stream.CopyTo(fileStream); + } + protected override void Delete(string path) { var actualPath = GetFilePath(path); @@ -37,15 +85,22 @@ protected override void Delete(string path) return; } + if (profile.IsVerboseLogging) + { + Logging.LogDebug($"Deleting file: {actualPath}", GetType()); + } + File.Delete(actualPath); } private string GetFilePath(string path) { - var fileName = $"{path}.json"; - var actualPath = Path.Combine(persistentDataPath, fileName); + if (string.IsNullOrWhiteSpace(persistentDataPath)) + { + return path; + } - return actualPath; + return Path.Combine(persistentDataPath, path); } } } diff --git a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta index f2d41e0..59920a6 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/FileStorage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 153e72cf9cea8144e8bffc11797d7e46 +guid: adee4469de0f4d7cb145558833da4d23 timeCreated: 1670271717 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs index 06b9778..c5b38af 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace CHARK.GameManagement.Storage @@ -9,44 +10,81 @@ namespace CHARK.GameManagement.Storage public interface IStorage { /// - /// Value from persistent game storage asynchronously or default if no value is - /// could be retrieved. + /// true if persisted of type is + /// retrieved from given or false otherwise. /// - public Task GetValueAsync( + public bool TryReadData(string path, out TData data); + + /// + /// Persisted data retrieved from given + /// or an empty stream if no data is could be retrieved. + /// + public Stream ReadDataStream(string path); + + /// + /// Persisted retrieved from given + /// asynchronously or default if no data is could be retrieved. + /// + /// + /// if could not be retrieved. + /// + public Task ReadDataAsync( string path, CancellationToken cancellationToken = default ); - /// - /// Get a value from persistent game storage. - /// - public bool TryGetValue(string path, out TValue value); - /// - /// Save a value to persistent game storage asynchronously. + /// Persisted data retrieved from given + /// asynchronously or an empty stream if no data is could be retrieved. /// - public Task SetValueAsync( + public Task ReadDataStreamAsync( string path, - TValue value, CancellationToken cancellationToken = default ); /// - /// Save a value to persistent game storage. + /// Persist of type to to given + /// . + /// + public void SaveData(string path, TData data); + + /// + /// Persist to . + /// asynchronously. /// - public void SetValue(string path, TValue value); + public void SaveDataStream(string path, Stream stream); /// - /// Delete value at given asynchronously. + /// Persist of type to . + /// asynchronously. /// - public Task DeleteValueAsync( + public Task SaveDataAsync( string path, + TData data, CancellationToken cancellationToken = default ); /// - /// Delete value at given . + /// Persist of data to given . + /// asynchronously. /// - public void DeleteValue(string path); + public Task SaveDataStreamAsync( + string path, + Stream stream, + CancellationToken cancellationToken = default + ); + + /// + /// Delete persisted data from given . + /// + public void DeleteData(string path); + + /// + /// Delete persisted data from given asynchronously. + /// + public Task DeleteDataAsync( + string path, + CancellationToken cancellationToken = default + ); } } diff --git a/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs.meta index f197056..55c341e 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/IStorage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 63cf4c2156065ce498a83c99e0e73f55 +guid: 375fb5d7cc45456e81be7de326b35c08 timeCreated: 1669919582 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs new file mode 100644 index 0000000..f90011b --- /dev/null +++ b/Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs @@ -0,0 +1,44 @@ +using System.IO; +using CHARK.GameManagement.Serialization; + +namespace CHARK.GameManagement.Storage +{ + /// + /// Storage which does nothing, used as a fall-back when regular storage cannot be created. + /// + internal sealed class NullStorage : Storage + { + public static NullStorage Instance { get; } = new(); + + private NullStorage() : base(DefaultSerializer.Instance, default) + { + } + + public override bool TryReadData(string path, out TValue data) + { + data = default; + return false; + } + + public override void SaveData(string path, TValue data) + { + } + + protected override Stream ReadStream(string path) + { + return Stream.Null; + } + + protected override void SaveString(string path, string value) + { + } + + protected override void SaveStream(string path, Stream stream) + { + } + + protected override void Delete(string path) + { + } + } +} diff --git a/Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs.meta similarity index 54% rename from Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs.meta rename to Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs.meta index 2e763e5..e11b18d 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/DefaultStorage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/NullStorage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: ce724dba3e44b704380f4b972c5d202d +guid: 31ce34677dfc433fa18370eef1d451c2 timeCreated: 1669928210 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs b/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs index 376516b..9fefdaa 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs @@ -1,4 +1,6 @@ -using CHARK.GameManagement.Serialization; +using System.IO; +using System.Text; +using CHARK.GameManagement.Serialization; using UnityEngine; namespace CHARK.GameManagement.Storage @@ -12,16 +14,32 @@ public PlayerPrefsStorage(ISerializer serializer, string pathPrefix = "") : base { } - protected override string GetString(string path) + protected override Stream ReadStream(string path) { - return PlayerPrefs.GetString(path); + var prefsString = PlayerPrefs.GetString(path); + if (string.IsNullOrWhiteSpace(prefsString)) + { + return Stream.Null; + } + + var prefsBytes = Encoding.UTF8.GetBytes(prefsString); + + return new MemoryStream(prefsBytes); } - protected override void SetString(string path, string value) + protected override void SaveString(string path, string value) { PlayerPrefs.SetString(path, value); } + protected override void SaveStream(string path, Stream stream) + { + using var streamReader = new StreamReader(stream); + var value = streamReader.ReadToEnd(); + + PlayerPrefs.SetString(path, value); + } + protected override void Delete(string path) { PlayerPrefs.DeleteKey(path); diff --git a/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs.meta index 18941be..709d90f 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/PlayerPrefsStorage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: ca7aedef44f0f4245aaa77fc97f64c43 +guid: 3bd652a013914f44959f0d3180367bd2 timeCreated: 1669925549 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Storage/Storage.cs b/Packages/com.chark.game-management/Runtime/Storage/Storage.cs index 4af8e1d..6162a36 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/Storage.cs +++ b/Packages/com.chark.game-management/Runtime/Storage/Storage.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System; +using System.IO; +using System.Threading; using System.Threading.Tasks; using CHARK.GameManagement.Serialization; using Cysharp.Threading.Tasks; @@ -16,16 +18,50 @@ protected Storage(ISerializer serializer, string pathPrefix) this.pathPrefix = pathPrefix; } - public async Task GetValueAsync( + public virtual bool TryReadData(string path, out TValue data) + { + data = default; + + if (TryGetFullPath(path, out var fullPath) == false) + { + return false; + } + + using var stream = ReadStream(fullPath); + if (stream.Length == 0) + { + return false; + } + + if (serializer.TryDeserializeStream(stream, out TValue deserializedValue) == false) + { + return false; + } + + data = deserializedValue; + return true; + } + + public Stream ReadDataStream(string path) + { + if (TryGetFullPath(path, out var fullPath) == false) + { + return Stream.Null; + } + + return ReadStream(fullPath); + } + + public async Task ReadDataAsync( string path, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ) { await UniTask.SwitchToThreadPool(); try { - if (TryGetValue(path, out var value)) + if (TryReadData(path, out var value)) { return value; } @@ -35,44 +71,67 @@ CancellationToken cancellationToken await UniTask.SwitchToMainThread(cancellationToken); } - return default; + throw new Exception($"Could not read data from path: {path}"); } - public virtual bool TryGetValue(string path, out TValue value) + public async Task ReadDataStreamAsync( + string path, + CancellationToken cancellationToken = default + ) { - value = default; + if (TryGetFullPath(path, out var fullPath) == false) + { + return Stream.Null; + } + + await UniTask.SwitchToThreadPool(); - if (TryGetFormattedPath(path, out var formattedPath) == false) + try { - return false; + return ReadStream(fullPath); } + finally + { + await UniTask.SwitchToMainThread(cancellationToken); + } + } - var stringValue = GetString(formattedPath); - if (string.IsNullOrWhiteSpace(stringValue)) + public virtual void SaveData(string path, TValue data) + { + if (TryGetFullPath(path, out var fullPath) == false) { - return false; + return; } - if (serializer.TryDeserializeValue(stringValue, out TValue deserializedValue) == false) + if (serializer.TrySerializeValue(data, out var serializedValue) == false) { - return false; + return; } - value = deserializedValue; - return true; + SaveString(fullPath, serializedValue); + } + + public void SaveDataStream(string path, Stream stream) + { + if (TryGetFullPath(path, out var fullPath) == false) + { + return; + } + + SaveStream(fullPath, stream); } - public async Task SetValueAsync( + public async Task SaveDataAsync( string path, - TValue value, - CancellationToken cancellationToken + TValue data, + CancellationToken cancellationToken = default ) { await UniTask.SwitchToThreadPool(); try { - SetValue(path, value); + SaveData(path, data); } finally { @@ -80,22 +139,41 @@ CancellationToken cancellationToken } } - public virtual void SetValue(string path, TValue value) + public async Task SaveDataStreamAsync( + string path, + Stream stream, + CancellationToken cancellationToken = default + ) { - if (TryGetFormattedPath(path, out var formattedPath) == false) + if (TryGetFullPath(path, out var fullPath) == false) { return; } - if (serializer.TrySerializeValue(value, out var serializedValue) == false) + await UniTask.SwitchToThreadPool(); + + try { - return; + SaveStream(fullPath, stream); } + finally + { + await UniTask.SwitchToMainThread(cancellationToken); + } + } - SetString(formattedPath, serializedValue); + public void DeleteData(string path) + { + if (TryGetFullPath(path, out var fullPath)) + { + Delete(fullPath); + } } - public async Task DeleteValueAsync(string path, CancellationToken cancellationToken) + public async Task DeleteDataAsync( + string path, + CancellationToken cancellationToken = default + ) { await UniTask.SwitchToThreadPool(); @@ -109,39 +187,36 @@ public async Task DeleteValueAsync(string path, CancellationToken cancellationTo } } - public void DeleteValue(string path) - { - if (TryGetFormattedPath(path, out var formattedPath)) - { - Delete(formattedPath); - } - } - /// - /// String retrieved using given . + /// Stream retrieved using given . /// - protected abstract string GetString(string path); + protected abstract Stream ReadStream(string path); + + /// + /// Store a to given . + /// + protected abstract void SaveString(string path, string value); /// - /// Store a at given . + /// Store a to given . /// - protected abstract void SetString(string path, string value); + protected abstract void SaveStream(string path, Stream stream); /// /// Delete value at given . /// protected abstract void Delete(string path); - private bool TryGetFormattedPath(string path, out string formattedPath) + private bool TryGetFullPath(string path, out string fullPath) { - formattedPath = default; + fullPath = default; if (string.IsNullOrWhiteSpace(path)) { return false; } - formattedPath = pathPrefix + path; + fullPath = pathPrefix + path; return true; } } diff --git a/Packages/com.chark.game-management/Runtime/Storage/Storage.cs.meta b/Packages/com.chark.game-management/Runtime/Storage/Storage.cs.meta index d4cbf81..710aaf9 100644 --- a/Packages/com.chark.game-management/Runtime/Storage/Storage.cs.meta +++ b/Packages/com.chark.game-management/Runtime/Storage/Storage.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 2bff78b09def57040bc3ed7af6a9ae3c +guid: ea91cadd8a0c4a14b19e67360efea6bf timeCreated: 1669919761 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Runtime/Utilities/Logging.cs b/Packages/com.chark.game-management/Runtime/Utilities/Logging.cs index 60987af..47fc2c0 100644 --- a/Packages/com.chark.game-management/Runtime/Utilities/Logging.cs +++ b/Packages/com.chark.game-management/Runtime/Utilities/Logging.cs @@ -74,5 +74,15 @@ internal static void LogError(string message, Object owner) Debug.LogError($"[{name}]: {message}", owner); #endif } + + internal static void LogException(Exception exception, Type owner) + { + Debug.LogException(exception); + } + + internal static void LogException(Exception exception, Object owner) + { + Debug.LogException(exception, owner); + } } } diff --git a/Packages/com.chark.game-management/Samples/Counters/Scripts/StorageSystem.cs b/Packages/com.chark.game-management/Samples/Counters/Scripts/StorageSystem.cs index f3e87e1..1e77a35 100644 --- a/Packages/com.chark.game-management/Samples/Counters/Scripts/StorageSystem.cs +++ b/Packages/com.chark.game-management/Samples/Counters/Scripts/StorageSystem.cs @@ -6,13 +6,13 @@ internal sealed class StorageSystem : SimpleSystem, IStorageSystem { public long LoadFixedUpdateCount() { - var key = GetStorageKey(nameof(CounterSystem.FixedUpdateCount)); + var path = GetPath(nameof(CounterSystem.FixedUpdateCount)); // Game Manager provides an abstraction for retrieving values. It also supports json // serialization by default. - if (GameManager.TryGetRuntimeValue(key, out var value)) + if (GameManager.TryReadData(path, out var data)) { - return value; + return data; } return 0; @@ -20,10 +20,10 @@ public long LoadFixedUpdateCount() public long LoadUpdateCount() { - var key = GetStorageKey(nameof(CounterSystem.UpdateCount)); - if (GameManager.TryGetRuntimeValue(key, out var value)) + var path = GetPath(nameof(CounterSystem.UpdateCount)); + if (GameManager.TryReadData(path, out var data)) { - return value; + return data; } return 0; @@ -31,17 +31,17 @@ public long LoadUpdateCount() public void SaveFixedUpdateCount(long count) { - var key = GetStorageKey(nameof(CounterSystem.FixedUpdateCount)); - GameManager.SetRuntimeValue(key, count); + var path = GetPath(nameof(CounterSystem.FixedUpdateCount)); + GameManager.SaveData(path, count); } public void SaveUpdateCount(long count) { - var key = GetStorageKey(nameof(CounterSystem.UpdateCount)); - GameManager.SetRuntimeValue(key, count); + var path = GetPath(nameof(CounterSystem.UpdateCount)); + GameManager.SaveData(path, count); } - private static string GetStorageKey(string suffix) + private static string GetPath(string suffix) { return $"{nameof(StorageSystem)}_${suffix}"; } diff --git a/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs b/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs index 83cbbfc..4b4c154 100644 --- a/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs +++ b/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs @@ -4,6 +4,7 @@ namespace CHARK.GameManagement.Tests.Editor { + // ReSharper disable once UnusedType.Global internal sealed class EditorPrefsStorageTest : StorageTest { protected override IStorage CreateStorage() @@ -11,19 +12,19 @@ protected override IStorage CreateStorage() return new EditorPrefsStorage(DefaultSerializer.Instance); } - protected override string GetString(string key) + protected override string ReadString(string path) { - return EditorPrefs.GetString(key); + return EditorPrefs.GetString(path); } - protected override void SetString(string key, string value) + protected override void SaveString(string path, string data) { - EditorPrefs.SetString(key, value); + EditorPrefs.SetString(path, data); } - protected override void DeleteString(string key) + protected override void DeleteString(string path) { - EditorPrefs.DeleteKey(key); + EditorPrefs.DeleteKey(path); } } } diff --git a/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs.meta index 733cb6a..2bd5415 100644 --- a/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs.meta +++ b/Packages/com.chark.game-management/Tests/Editor/EditorPrefsStorageTest.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 2368299ec355e784089fd85a2f2624a5 +guid: 43a0363377e54882ad5aa7514c99a3e9 timeCreated: 1669924783 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs b/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs new file mode 100644 index 0000000..9d1d1fd --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs @@ -0,0 +1,51 @@ +using System.IO; +using CHARK.GameManagement.Serialization; +using CHARK.GameManagement.Storage; +using UnityEngine; + +namespace CHARK.GameManagement.Tests.Editor +{ + // ReSharper disable once UnusedType.Global + internal sealed class FileStorageTest : StorageTest + { + protected override IStorage CreateStorage() + { + return new FileStorage( + serializer: DefaultSerializer.Instance, + profile: GameManagerTestProfile.Instance, + persistentDataPath: Application.persistentDataPath, + pathPrefix: "Tests/" + ); + } + + protected override string ReadString(string path) + { + var actualPath = GetTestPath(path); + return File.ReadAllText(actualPath); + } + + protected override void SaveString(string path, string data) + { + var actualPath = GetTestPath(path); + var directory = Path.GetDirectoryName(actualPath); + + if (string.IsNullOrWhiteSpace(directory) == false) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(actualPath, data); + } + + protected override void DeleteString(string path) + { + var actualPath = GetTestPath(path); + File.Delete(actualPath); + } + + private static string GetTestPath(string path) + { + return Path.Combine(Application.persistentDataPath, "Tests", path); + } + } +} diff --git a/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs.meta new file mode 100644 index 0000000..cfdfc20 --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/FileStorageTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5634da6184fd4af3aa9bd12873bc4b66 +timeCreated: 1697718487 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs b/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs new file mode 100644 index 0000000..76980ce --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs @@ -0,0 +1,29 @@ +using CHARK.GameManagement.Settings; + +namespace CHARK.GameManagement.Tests.Editor +{ + internal sealed class GameManagerTestProfile : IGameManagerSettingsProfile + { + internal static GameManagerTestProfile Instance { get; } = new(); + + private GameManagerTestProfile() + { + } + + public string Name => "Test Profile"; + + public bool IsInstantiateAutomatically => false; + + public InstantiationMode InstantiationMode => InstantiationMode.BeforeSceneLoad; + + public bool IsDontDestroyOnLoad => false; + + public bool IsVerboseLogging => true; + + public bool TryGetGameManagerPrefab(out GameManager prefab) + { + prefab = default; + return false; + } + } +} diff --git a/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs.meta b/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs.meta new file mode 100644 index 0000000..514511b --- /dev/null +++ b/Packages/com.chark.game-management/Tests/Editor/GameManagerTestProfile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 79d514ff5ec84dff97c18442e169a573 +timeCreated: 1697718546 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs b/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs index 977ac6b..8856024 100644 --- a/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs +++ b/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs @@ -4,6 +4,7 @@ namespace CHARK.GameManagement.Tests.Editor { + // ReSharper disable once UnusedType.Global internal sealed class PlayerPrefsStorageTest : StorageTest { protected override IStorage CreateStorage() @@ -11,19 +12,19 @@ protected override IStorage CreateStorage() return new PlayerPrefsStorage(DefaultSerializer.Instance); } - protected override string GetString(string key) + protected override string ReadString(string path) { - return PlayerPrefs.GetString(key); + return PlayerPrefs.GetString(path); } - protected override void SetString(string key, string value) + protected override void SaveString(string path, string data) { - PlayerPrefs.SetString(key, value); + PlayerPrefs.SetString(path, data); } - protected override void DeleteString(string key) + protected override void DeleteString(string path) { - PlayerPrefs.DeleteKey(key); + PlayerPrefs.DeleteKey(path); } } } diff --git a/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs.meta index 50e9f62..32ed0ae 100644 --- a/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs.meta +++ b/Packages/com.chark.game-management/Tests/Editor/PlayerPrefsStorageTest.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: dae3f11f888aa314394e2513084cbbd5 +guid: 79ddf9e1bf7c4a8dab7a9a635b247d1c timeCreated: 1669922570 \ No newline at end of file diff --git a/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs b/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs index a41aa7c..ca91af1 100644 --- a/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs +++ b/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs @@ -1,4 +1,6 @@ using System.Globalization; +using System.IO; +using System.Text; using CHARK.GameManagement.Storage; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -13,7 +15,7 @@ internal abstract class StorageTest ContractResolver = new CamelCasePropertyNamesContractResolver(), }; - private string Key => GetType().Name; + private string Path => GetType().Name; private IStorage storage; @@ -26,122 +28,201 @@ public void SetUp() [TearDown] public void TearDown() { - DeleteString(Key); + DeleteString(Path); } [Test] - public void ShouldGetPrimitiveValue() + public void ShouldReadPrimitiveData() { // Given: - // - Persisted primitive float value of 1. - const float expectedValue = 1f; - SetString(Key, expectedValue.ToString(CultureInfo.InvariantCulture)); + // - Persisted primitive float data of 1. + const float expectedData = 1f; + SaveString(Path, expectedData.ToString(CultureInfo.InvariantCulture)); // When: - // - Retrieving value from storage. - if (storage.TryGetValue(Key, out float actualValue) == false) + // - Retrieving data from storage. + if (storage.TryReadData(Path, out float actualData) == false) { - Assert.Fail("Could not get Runtime primitive value"); + Assert.Fail($"Could not {nameof(storage.TryReadData)} from primitive"); } // Then: - // - Value should be retrieved properly. - Assert.AreEqual(expectedValue, actualValue); + // - Data should be retrieved properly. + Assert.AreEqual(expectedData, actualData); } [Test] - public void ShouldGetStructValue() + public void ShouldReadStructData() { // Given: - // - Persisted struct value. - var expectedValue = new TestStructData("hello"); - SetString(Key, SerializeObject(expectedValue)); + // - Persisted struct data. + var expectedData = new TestStructData("hello"); + SaveString(Path, SerializeObject(expectedData)); // When: - // - Retrieving value from storage. - if (storage.TryGetValue(Key, out TestStructData actualValue) == false) + // - Retrieving data from storage. + if (storage.TryReadData(Path, out TestStructData actualData) == false) { - Assert.Fail("Could not get Runtime struct value"); + Assert.Fail($"Could not {nameof(storage.TryReadData)} from struct"); } // Then: - // - Value (contents) should be retrieved properly. - Assert.AreEqual(expectedValue.Text, actualValue.Text); + // - Data (contents) should be retrieved properly. + Assert.AreEqual(expectedData.Text, actualData.Text); } [Test] - public void ShouldGetObjectValue() + public void ShouldReadObjectData() { // Given: - // - Persisted object value. - var expectedValue = new TestObjectData("hello"); - SetString(Key, SerializeObject(expectedValue)); + // - Persisted object data. + var expectedData = new TestObjectData("hello"); + SaveString(Path, SerializeObject(expectedData)); // When: - // - Retrieving value from storage. - if (storage.TryGetValue(Key, out TestObjectData actualValue) == false) + // - Retrieving data from storage. + if (storage.TryReadData(Path, out TestObjectData actualData) == false) { - Assert.Fail("Could not get Runtime object value"); + Assert.Fail($"Could not {nameof(storage.TryReadData)} from object"); } // Then: - // - Value (contents) should be retrieved properly. - Assert.AreEqual(expectedValue.Text, actualValue.Text); + // - Data (contents) should be retrieved properly. + Assert.AreEqual(expectedData.Text, actualData.Text); } [Test] - public void ShouldSetPrimitiveValue() + public void ShouldReadNoDataOnMissingData() { // Given: - // - Primitive float value of 1. - const float expectedValue = 1f; + // - No persisted data. + DeleteString(Path); // When: - // - Persisting value. - storage.SetValue(Key, expectedValue); + // - Retrieving data from storage. + var isRead = storage.TryReadData(Path, out var data); // Then: - // - Value should be persisted properly. - var actualValue = float.Parse(GetString(Key)); - Assert.AreEqual(expectedValue, actualValue); + // - Data should not be retrieved. + Assert.IsFalse(isRead); + Assert.IsNull(data); } [Test] - public void ShouldSetStructValue() + public void ShouldReadStreamObjectData() { // Given: - // - Struct value. - var value = new TestStructData("hello there"); + // - Persisted object data. + var expectedData = new TestObjectData("hello"); + SaveString(Path, SerializeObject(expectedData)); // When: - // - Persisting value. - storage.SetValue(Key, value); + // - Retrieving data from storage. + var actualStream = storage.ReadDataStream(Path); + if (actualStream.Length == 0) + { + Assert.Fail($"Could not {nameof(storage.ReadDataStream)} from object"); + } + + // Then: + // - Data (contents) should be retrieved properly. + var actualData = DeserializeObject(actualStream); + + Assert.AreEqual(expectedData.Text, actualData.Text); + } + + [Test] + public void ShouldReadEmptyStreamOnMissingData() + { + // Given: + // - No persisted data. + DeleteString(Path); + + // When: + // - Retrieving data from storage. + var stream = storage.ReadDataStream(Path); // Then: - // - Value should be persisted properly. - var expectedValue = SerializeObject(value); - var actualValue = GetString(Key); + // - Data (contents) should be retrieved properly. + Assert.AreEqual(0, stream.Length); + } + + [Test] + public void ShouldSavePrimitiveData() + { + // Given: + // - Primitive float data of 1. + const float expectedData = 1f; + + // When: + // - Persisting data. + storage.SaveData(Path, expectedData); - Assert.AreEqual(expectedValue, actualValue); + // Then: + // - Data should be persisted properly. + var actualData = float.Parse(ReadString(Path)); + Assert.AreEqual(expectedData, actualData); } [Test] - public void ShouldSetObjectValue() + public void ShouldSaveStructData() { // Given: - // - Object value. - var value = new TestObjectData("hello there"); + // - Struct data. + var data = new TestStructData("hello there"); // When: - // - Persisting value. - storage.SetValue(Key, value); + // - Persisting data. + storage.SaveData(Path, data); // Then: - // - Value should be persisted properly. - var expectedValue = SerializeObject(value); - var actualValue = GetString(Key); + // - Data should be persisted properly. + var expectedData = SerializeObject(data); + var actualData = ReadString(Path); - Assert.AreEqual(expectedValue, actualValue); + Assert.AreEqual(expectedData, actualData); + } + + [Test] + public void ShouldSaveObjectData() + { + // Given: + // - Data object. + var data = new TestObjectData("hello there"); + + // When: + // - Persisting data. + storage.SaveData(Path, data); + + // Then: + // - Data should be persisted properly. + var expectedData = SerializeObject(data); + var actualData = ReadString(Path); + + Assert.AreEqual(expectedData, actualData); + } + + [Test] + public void ShouldSaveStreamObjectData() + { + // Given: + // - Object data. + var data = new TestObjectData("hello there"); + var expectedData = SerializeObject(data); + + // When: + // - Persisting data. + using var stream = new MemoryStream( + Encoding.UTF8.GetBytes(expectedData) + ); + + storage.SaveDataStream(Path, stream); + + // Then: + // - Data should be persisted properly. + var actualData = ReadString(Path); + + Assert.AreEqual(expectedData, actualData); } /// @@ -150,26 +231,31 @@ public void ShouldSetObjectValue() protected abstract IStorage CreateStorage(); /// - /// Raw json value retrieved by given . + /// Raw json data retrieved by given . /// - protected abstract string GetString(string key); + protected abstract string ReadString(string path); /// - /// Set raw json value retrieved at given . + /// Set raw json data retrieved at given . /// - protected abstract void SetString(string key, string value); + protected abstract void SaveString(string path, string data); /// - /// Delete raw json stored at given . + /// Delete raw json stored at given . /// - protected abstract void DeleteString(string key); + protected abstract void DeleteString(string path); - /// - /// json string created from given - /// - protected string SerializeObject(T obj) + private static TObject DeserializeObject(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var @object = reader.ReadToEnd(); + + return JsonConvert.DeserializeObject(@object, SerializerSettings); + } + + private static string SerializeObject(TObject @object) { - return JsonConvert.SerializeObject(obj, SerializerSettings); + return JsonConvert.SerializeObject(@object, SerializerSettings); } private class TestObjectData diff --git a/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs.meta b/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs.meta index a4966bc..e909ecd 100644 --- a/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs.meta +++ b/Packages/com.chark.game-management/Tests/Editor/StorageTest.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 84adec182f69dc3449510989d0ddb724 +guid: 5e33c627119c4b06a548b9e3ef9242b6 timeCreated: 1670270154 \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index 87310e9..b66c295 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,6 +2,7 @@ "dependencies": { "com.unity.ide.rider": "3.0.25", "com.unity.test-framework": "1.1.33", - "com.unity.textmeshpro": "3.0.6" + "com.unity.textmeshpro": "3.0.6", + "com.unity.modules.unitywebrequest": "1.0.0" } } diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 12c6b3f..b612f30 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -77,6 +77,12 @@ "depth": 2, "source": "builtin", "dependencies": {} + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} } } }