using System.Collections.Generic; using UnityEngine; namespace Ubisoft { /// /// Implementation of HtPlayerPrefs that persists data in UnityEngine.PlayerPrefs. /// Get/set methods in this class can be invoked from secondary threads, since all modifications are cached until Update method is called. /// Update method needs to be called from the main thread, since it's a requirement to access UnityEngine.PlayerPrefs. /// internal class HtPlayerPrefs_Unity_Cached : HtPlayerPrefs_Unity_Base { protected const string KEY_STORED_KEYS = "__storedKeys"; private class KeyMetadata { public enum Status { OK, DIRTY, DELETED } internal IHtPlayerPrefs.EType Type { get; } internal string TypeAsKey { get { return ETypeToKey(Type); } } internal string FullKey { get; } internal Status ValueStatus { get; set; } public KeyMetadata(IHtPlayerPrefs.EType type, string fullKey, Status status = Status.OK) { Type = type; FullKey = fullKey; ValueStatus = status; } } private string StoredKeysFullKey { get; } private Dictionary StoredKeys { get; set; } private Dictionary BoolValues { get; set; } private Dictionary FloatValues { get; set; } private Dictionary IntValues { get; set; } private Dictionary LongValues { get; set; } private Dictionary StringValues { get; set; } private bool IsDirty { get; set; } /// /// Create an HtPlayerPrefs_Unity_ThreadSafe object to store data in the channel passed in. /// Its behaviour is the same as HtPlayerPrefs_Unity, but this version can be used with threads. /// /// Name of the channel to use to store data saved through this object. /// An HtLogger.Channel object to use to log data to. /// If no object is passed in then a default one called 'HT_PlayerPrefs('channel')' is created. /// When true logs are printed out. Use it only for debug purposes. /// Make sure that Trace is the minimum log level set in your Logger configuration. public HtPlayerPrefs_Unity_Cached(string channel, HtLogger.Channel logger = null, bool isVerbose = false) : base(channel, logger, isVerbose) { StoredKeys = new Dictionary(); StoredKeysFullKey = GetFullKey(KEY_STORED_KEYS); BoolValues = new Dictionary(); FloatValues = new Dictionary(); IntValues = new Dictionary(); LongValues = new Dictionary(); StringValues = new Dictionary(); LoadStoredKeys(); LoadCache(); } #region HtPlayerPrefs /// /// Updates all modified and deleted values in PlayerPrefs /// This method must be called from the main thread /// public override void Update() { if (IsDirty) { IsDirty = false; List toRemove = new List(); foreach (KeyValuePair pair in StoredKeys) { switch (pair.Value.ValueStatus) { case KeyMetadata.Status.DIRTY: UpdateValue(pair); pair.Value.ValueStatus = KeyMetadata.Status.OK; break; case KeyMetadata.Status.DELETED: PlayerPrefs.DeleteKey(GetFullKey(pair.Key)); toRemove.Add(pair.Key); break; default: break; } } foreach(string key in toRemove) { _ = StoredKeys.Remove(key); } UpdateRawStoredKeys(); } } public override bool GetBool(string key, bool defaultValue = false) { _ = BoolValues.TryGetValue(key, out defaultValue); return defaultValue; } public override float GetFloat(string key, float defaultValue = 0f) { _ = FloatValues.TryGetValue(key, out defaultValue); return defaultValue; } public override int GetInt(string key, int defaultValue = 0) { _ = IntValues.TryGetValue(key, out defaultValue); return defaultValue; } public override long GetLong(string key, long defaultValue = 0) { _ = LongValues.TryGetValue(key, out defaultValue); return defaultValue; } public override string GetString(string key, string defaultValue = "") { _ = StringValues.TryGetValue(key, out defaultValue); return defaultValue; } public override IHtPlayerPrefs.EType GetType(string key) { return StoredKeys.ContainsKey(key) ? StoredKeys[key].Type : IHtPlayerPrefs.EType.None; } public override bool HasKey(string key) { return StoredKeys.ContainsKey(key); } protected override void ExtendedDeleteAll() { foreach (KeyValuePair pair in StoredKeys) { pair.Value.ValueStatus = KeyMetadata.Status.DELETED; } BoolValues.Clear(); FloatValues.Clear(); IntValues.Clear(); LongValues.Clear(); StringValues.Clear(); IsDirty = true; } protected override void ExtendedDeleteKey(string key) { if (StoredKeys.ContainsKey(key)) { IHtPlayerPrefs.EType type = StoredKeys[key].Type; switch (type) { case IHtPlayerPrefs.EType.Bool: if (BoolValues.ContainsKey(key)) { _ = BoolValues.Remove(key); } break; case IHtPlayerPrefs.EType.Float: if (FloatValues.ContainsKey(key)) { _ = FloatValues.Remove(key); } break; case IHtPlayerPrefs.EType.Int: if (IntValues.ContainsKey(key)) { _ = IntValues.Remove(key); } break; case IHtPlayerPrefs.EType.Long: if (LongValues.ContainsKey(key)) { _ = LongValues.Remove(key); } break; case IHtPlayerPrefs.EType.String: if (StringValues.ContainsKey(key)) { _ = StringValues.Remove(key); } break; } StoredKeys[key].ValueStatus = KeyMetadata.Status.DELETED; IsDirty = true; } } protected override void ExtendedSetBool(string key, bool value) { if (BoolValues.ContainsKey(key)) { BoolValues[key] = value; } else { BoolValues.Add(key, value); AddStoredKey(key, IHtPlayerPrefs.EType.Bool); } StoredKeys[key].ValueStatus = KeyMetadata.Status.DIRTY; IsDirty = true; } protected override void ExtendedSetFloat(string key, float value) { if (FloatValues.ContainsKey(key)) { FloatValues[key] = value; } else { FloatValues.Add(key, value); AddStoredKey(key, IHtPlayerPrefs.EType.Float); } StoredKeys[key].ValueStatus = KeyMetadata.Status.DIRTY; IsDirty = true; } protected override void ExtendedSetInt(string key, int value) { if (IntValues.ContainsKey(key)) { IntValues[key] = value; } else { IntValues.Add(key, value); AddStoredKey(key, IHtPlayerPrefs.EType.Int); } StoredKeys[key].ValueStatus = KeyMetadata.Status.DIRTY; IsDirty = true; } protected override void ExtendedSetLong(string key, long value) { if (LongValues.ContainsKey(key)) { LongValues[key] = value; } else { LongValues.Add(key, value); AddStoredKey(key, IHtPlayerPrefs.EType.Long); } StoredKeys[key].ValueStatus = KeyMetadata.Status.DIRTY; IsDirty = true; } protected override void ExtendedSetString(string key, string value) { if (StringValues.ContainsKey(key)) { StringValues[key] = value; } else { StringValues.Add(key, value); AddStoredKey(key, IHtPlayerPrefs.EType.String); } StoredKeys[key].ValueStatus = KeyMetadata.Status.DIRTY; IsDirty = true; } public override string ToString() { return $"StoredKeys: {RawStoredKeys}\nContent: {ContentAsString()}"; } #endregion #region Private Methods private string ContentAsString() { string returnValue = ""; string valueAsString; string status; foreach (KeyValuePair pair in StoredKeys) { if (HasKey(pair.Key)) { if (pair.Value.Type != IHtPlayerPrefs.EType.None) { valueAsString = null; status = pair.Value.ValueStatus != KeyMetadata.Status.OK ? $" ({pair.Value.ValueStatus})" : ""; switch (pair.Value.Type) { case IHtPlayerPrefs.EType.Bool: valueAsString = $"{ GetBool(pair.Key)}{status}"; break; case IHtPlayerPrefs.EType.Int: valueAsString = $"{ GetInt(pair.Key)}{status}"; break; case IHtPlayerPrefs.EType.Long: valueAsString = $"{ GetLong(pair.Key)}{status}"; break; case IHtPlayerPrefs.EType.Float: valueAsString = $"{ GetFloat(pair.Key)}{status}"; break; case IHtPlayerPrefs.EType.String: valueAsString = $"{ GetString(pair.Key)}{status}"; break; } if (valueAsString != null) { if (!string.IsNullOrEmpty(returnValue)) { returnValue += $", "; } returnValue += $"{pair.Key}:{valueAsString}"; } } } } return returnValue; } /// /// Reads KEY_STORED_KEYS from PlayerPrefs, splits the key-values and adds them to StoredKeys /// The raw value is cached in RawStoredKeys /// private void LoadStoredKeys() { string key; string rawStoredKeys = PlayerPrefs.GetString(StoredKeysFullKey, ""); if (!string.IsNullOrEmpty(rawStoredKeys)) { bool needsToCorrectRawStoredKeys = false; string[] keys = rawStoredKeys.Split(STORED_KEYS_KEY_SEPARATOR); string[] tokens; int count = keys.Length; for (int i = 0; i < count; ++i) { key = keys[i].Trim(); if (!string.IsNullOrEmpty(key)) { tokens = key.Split(STORED_KEYS_TOKEN_SEPARATOR); if (tokens.Length < 2) { Logger.LogError($"Malformed raw stored key {Debug.FormatTextInUserContext(key)}"); needsToCorrectRawStoredKeys = true; } else if (StoredKeys.ContainsKey(tokens[0])) { Logger.LogError($"Duplicate raw stored key {Debug.FormatTextInUserContext(key)} in {Debug.FormatTextInUserContext(rawStoredKeys)}"); needsToCorrectRawStoredKeys = true; } else { IHtPlayerPrefs.EType type = KeyToEType(tokens[1]); if (type == IHtPlayerPrefs.EType.None) { Logger.LogError($"Unsupported type {Debug.FormatTextInUserContext(tokens[1])} in raw stored key {Debug.FormatTextInUserContext(key)}"); needsToCorrectRawStoredKeys = true; } else { StoredKeys.Add(tokens[0], new KeyMetadata(type, GetFullKey(tokens[0]))); } } } else { needsToCorrectRawStoredKeys = true; Logger.LogError($"Empty raw stored key in {Debug.FormatTextInUserContext(rawStoredKeys)}"); } } if (needsToCorrectRawStoredKeys) { UpdateRawStoredKeys(); Logger.LogWarning($"Correct raw stored keys from: {Debug.FormatTextInUserContext(rawStoredKeys)} to {Debug.FormatTextInUserContext(RawStoredKeys)}"); } RawStoredKeys = rawStoredKeys; } } private void AddStoredKey(string key, IHtPlayerPrefs.EType type) { if (!StoredKeys.ContainsKey(key) && type != IHtPlayerPrefs.EType.None) { StoredKeys.Add(key, new KeyMetadata(type, GetFullKey(key))); RawStoredKeys = AddRawStoredKey(key, type, RawStoredKeys); } } private string RawStoredKeys { get; set; } private void UpdateRawStoredKeys() { string value = ""; foreach (KeyValuePair pair in StoredKeys) { value = AddRawStoredKey(pair.Key, pair.Value.Type, value); } RawStoredKeys = value; // Save StoredKeys to PlayerPrefs PlayerPrefs.SetString(StoredKeysFullKey, value); NeedsToSave = true; } /// /// Loads the key-values from StoredKeys into the corresponding Dictionary based on its type /// private void LoadCache() { foreach (KeyValuePair pair in StoredKeys) { IHtPlayerPrefs.EType type = pair.Value.Type; switch (type) { case IHtPlayerPrefs.EType.Bool: int bValue = PlayerPrefs.GetInt(pair.Value.FullKey, BoolToInt(false)); BoolValues.Add(pair.Key, IntToBool(bValue)); break; case IHtPlayerPrefs.EType.Float: float fValue = PlayerPrefs.GetFloat(pair.Value.FullKey, 0f); FloatValues.Add(pair.Key, fValue); break; case IHtPlayerPrefs.EType.Int: int iValue = PlayerPrefs.GetInt(pair.Value.FullKey, 0); IntValues.Add(pair.Key, iValue); break; case IHtPlayerPrefs.EType.Long: string slValue = PlayerPrefs.GetString(pair.Value.FullKey, LongToString(0)); if (StringToLong(pair.Key, slValue, out long lValue)) { LongValues.Add(pair.Key, lValue); } break; case IHtPlayerPrefs.EType.String: string sValue = PlayerPrefs.GetString(pair.Value.FullKey, ""); StringValues.Add(pair.Key, sValue); break; } } } /// /// Updates values in PlayerPrefs /// This method should only be called from the main thread /// /// private void UpdateValue(KeyValuePair pair) { switch (pair.Value.Type) { case IHtPlayerPrefs.EType.Bool: if (BoolValues.TryGetValue(pair.Key, out bool bValue)) { // Stored as int because Unity PlayerPrefs doesn't support bool type PlayerPrefs.SetInt(GetFullKey(pair.Key), BoolToInt(bValue)); } break; case IHtPlayerPrefs.EType.Float: if (FloatValues.TryGetValue(pair.Key, out float fValue)) { PlayerPrefs.SetFloat(GetFullKey(pair.Key), fValue); } break; case IHtPlayerPrefs.EType.Int: if (IntValues.TryGetValue(pair.Key, out int iValue)) { PlayerPrefs.SetInt(GetFullKey(pair.Key), iValue); } break; case IHtPlayerPrefs.EType.Long: if (LongValues.TryGetValue(pair.Key, out long lValue)) { // Stored as string because Unity PlayerPrefs doesn't support long type PlayerPrefs.SetString(GetFullKey(pair.Key), LongToString(lValue)); } break; case IHtPlayerPrefs.EType.String: if (StringValues.TryGetValue(pair.Key, out string sValue)) { PlayerPrefs.SetString(GetFullKey(pair.Key), sValue); } break; } } protected string GetFullKey(string key) { return StoredKeys.ContainsKey(key) ? StoredKeys[key].FullKey : $"{Channel}.{key}"; } #endregion } }