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
}
}