using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
///
/// PersistentDataController singleton
///
public class PersistentDataController
{
///
/// The instance controlling the singleton
///
private static PersistentDataController instance = null;
///
/// Current implementation version of the PersistentDataController
///
/// MSB represent sprint version, LSB represent subversion
public const int VERSION = 0x04_03;
///
/// Path of the .json-file to store all serialized data
///
public static string PATH = null;
///
/// Class to hold a list of data records
///
[Serializable]
public class PersistentDataContainer
{
///
/// A helper class for handling the stored progress
///
[Serializable]
public class PersistentDataEntry
{
///
/// The key, used to reference the data object
///
public string key;
///
/// The object, representated as a list of byte (which can be serialized)
///
public List data = new List();
///
/// Create a new PersistentDataEntry
///
///
///
public PersistentDataEntry(string key, byte[] data) : this(key, data.ToList())
{ }
///
/// Create a new PersistentDataEntry
///
public PersistentDataEntry(string key, List data)
{
this.key = key;
this.data = data;
}
}
///
/// List of data records
///
public List entries = new List();
///
/// Update the value of a certain key,
/// or add a new value if the key was not present.
///
/// The type of the data to be added/updated
/// The key, used for referencing the data
/// The object of type
/// true if successful, false otherwise
public bool Set(string key, T data)
{
if (data == null)
return false;
PersistentDataEntry entry = entries.Find(x => x.key == key);
// Hacky serialization stuff
BinaryFormatter bf = new BinaryFormatter();
using (MemoryStream ms = new MemoryStream())
{
bf.Serialize(ms, data);
if (entry != null)
{
entry.data.Clear();
entry.data.AddRange(ms.ToArray());
}
else
{
entries.Add(new PersistentDataEntry(key, ms.ToArray()));
}
return true;
}
}
///
/// Get the data object of a certain key
///
/// The type of the data object
/// The key referencing the data object
/// The data, cast to a type
///
///
public T Get(string key)
{
BinaryFormatter bf = new BinaryFormatter();
using (MemoryStream ms = new MemoryStream())
{
// Find the correct key
foreach (PersistentDataEntry entry in entries)
{
if (entry.key == key)
{
// Hacky serialization stuff
byte[] data = entry.data.ToArray();
ms.Write(data, 0, data.Length);
ms.Seek(0, SeekOrigin.Begin);
return (T)bf.Deserialize(ms);
}
}
}
// Raise an exception when key is not found
throw new KeyNotFoundException();
}
///
/// Remove a key-value from the data.
///
/// The key referencing the data object
///
public void Remove(string key)
{
if (!Has(key))
throw new KeyNotFoundException();
entries.Remove(entries.Find(x => x.key == key));
}
///
/// Remove and return value from the data.
///
/// The type of the data object
/// The key referencing the data object
/// Whether the removal of the data should also be saved to disk
///
public T Pop(string key)
{
T data = Get(key);
Remove(key);
return data;
}
///
/// Check whether a key is present
///
/// The key to check
/// true if a item can be found with the specified key
public bool Has(string key)
{
return entries.Find(x => x.key == key) != null;
}
}
///
/// Stored user data record
///
[Serializable]
public class SavedUserData : PersistentDataContainer
{
///
/// The user's username
///
public string username = null;
///
/// The index of the user's avatar in the UserList.AVATARS list
///
public int avatarIndex = -1;
///
/// The total playtime of the user
///
/// Not implemented yet
public double playtime = 0.0;
///
/// A list of progress on minigames the user has
///
public List minigames = new List();
///
/// A list of progress on courses the user has
///
public List courses = new List();
}
///
/// Stored course progress data record
///
[Serializable]
public class SavedCourseProgress : PersistentDataContainer
{
///
/// Update the progress value of the SavedLearnableProgress with the given learnableName.
///
///
///
public void UpdateLearnable(string learnableName, float addValue)
{
SavedLearnableProgress learnable = learnables.Find(l => l.name == learnableName);
if (learnable == null)
throw new KeyNotFoundException();
// Update the progress value of the SavedLearnableProgress
learnable.progress += addValue;
// crop the learnable progress around -5 and 5
if (learnable.progress > 5.0f)
learnable.progress = 5.0f;
else if (learnable.progress < -5.0f)
learnable.progress = -5.0f;
// if learnable progress is big enough it is "completed"
if (learnable.progress > 3)
completedLearnables++;
}
///
/// Check whether there are enough inUse Learnables
///
///
private bool EnoughLearnables()
{
// There need to be more then 5 non completed learnables
return inUseLearnables - completedLearnables > 5 || totalLearnables == inUseLearnables;
}
///
/// Find a SavedLearnableProgress with the given name
///
///
/// SavedLearnableProgress with the given name
public SavedLearnableProgress FindLearnable(string name)
{
return learnables.Find(l => l.name == name);
}
///
/// Find learnable in learnables which is not yet in use, and set it active
///
/// SavedLearnableProgress learnable
private SavedLearnableProgress UseUnusedLearnable()
{
SavedLearnableProgress learnable = learnables.Find(l => !l.inUse);
if (learnable == null)
return null;
learnable.inUse = true;
inUseLearnables++;
return learnable;
}
///
/// Gets a random inUse learnable
///
/// a randomly selected inUse SavedLearnable which is not yet completed
public SavedLearnableProgress GetRandomLearnable()
{
if (!EnoughLearnables())
return UseUnusedLearnable();
// only select inUse learnables which are not yet completed (progress < 3.5f)
List inUseLearnables = learnables.FindAll(l => l.inUse && l.progress <= 3.5f);
if (inUseLearnables.Count == 0)
return null;
// Select a random index from the in-use learnables list
int randomIndex = UnityEngine.Random.Range(0, inUseLearnables.Count);
return inUseLearnables[randomIndex];
}
///
/// Create new SavedLearnableProgress object and assigns the index and name values
///
///
///
/// bool which indicates the success of the function
public bool AddLearnable(string name, int index)
{
if (learnables.Any(learnable => learnable.name == name || learnable.index == index))
return false;
SavedLearnableProgress savedLearnableProgress = new SavedLearnableProgress();
savedLearnableProgress.index = index;
savedLearnableProgress.name = name;
learnables.Add(savedLearnableProgress);
totalLearnables++;
return true;
}
public CourseIndex courseIndex;
public float progress = -1.0f;
public int completedLearnables = 0;
public int inUseLearnables = 0;
public int totalLearnables = 0;
public List learnables = new List();
}
///
/// Stored individual learnable progress
///
[Serializable]
public class SavedLearnableProgress : PersistentDataContainer
{
///
/// Index of the Learnbable in its Theme
///
public int index;
///
/// Bool that indicated whether the user already started learning this Learnable
///
public bool inUse = false;
///
/// Display name of the Learnable
///
public string name;
///
/// Progress of the learnabe, a number between -5.0 and +5.0
///
public float progress = 0.0f;
}
///
/// Stored minigame progress data record
///
[Serializable]
public class SavedMinigameProgress : PersistentDataContainer
{
///
/// Index of the minigame
///
public MinigameIndex minigameIndex;
///
/// The 10 last scores of a user
///
public List latestScores = new List();
///
/// Top 10 scores of a user
///
public List highestScores = new List();
}
///
/// Stored WeSign data record
///
[Serializable]
private class SavedDataStructure
{
///
/// The version of the PersistentDataController with which this savefile is created
///
public int version = VERSION;
///
/// A list of all users
///
public List users = new List();
///
/// The index of the current user in the this.users list
///
public int currentUser = -1;
///
/// The index of the current minigame
///
public MinigameIndex currentMinigame;
///
/// The index of the current course
///
public CourseIndex currentCourse;
///
/// The index of the current theme
///
public ThemeIndex currentTheme;
///
/// The use hardware acceleration user preferences
///
public bool useGPU = false;
///
/// Initiate the SavedDataStructure, by setting the user preferences
///
public SavedDataStructure()
{
RestoreSettings();
}
///
/// Reset the user preferences to the default values
///
public void RestoreSettings()
{
useGPU = false;
}
}
///
/// The object holding the data references
///
private SavedDataStructure json = new SavedDataStructure();
///
/// Get the instance loaded by the singleton
///
/// PersistentDataController instance
public static PersistentDataController GetInstance()
{
// Create a new instance if non exists
if (instance == null || PATH == null)
{
if (PATH == null)
PersistentDataController.PATH = $"{Application.persistentDataPath}/wesign_saved_data.json";
instance = new PersistentDataController();
}
return instance;
}
///
/// PersistentDataController contructor
///
private PersistentDataController()
{
Load();
}
///
/// Clear everything stored in the PersistentDataController, won't save to disk
///
public void Clear()
{
json.users.Clear();
json.currentUser = -1;
json.useGPU = false;
}
///
/// Save all data to disk
///
public void Save()
{
string text = JsonUtility.ToJson(json);
File.CreateText(PATH).Close();
File.WriteAllText(PATH, text);
}
///
/// Override current data with the data from disk, will just clear if no data was found.
///
/// true if you want to override the existing file if it exists and the loading failed.
/// If the data on disk is outdated (version number is lower than the current version), the loading will also fail
/// true if successful, false otherwise
public bool Load(bool overrideOnFail = true)
{
Clear();
if (!File.Exists(PATH))
goto failed;
try
{
string text = File.ReadAllText(PATH);
SavedDataStructure newJson = JsonUtility.FromJson(text);
if (newJson == null || newJson.version != VERSION)
goto failed;
json = newJson;
return true;
}
catch (Exception) { goto failed; }
failed:
if (overrideOnFail)
Save();
return false;
}
///
/// Add a user to the WeSign data record
///
/// User data record
/// Whether to save the addition immediately to disk
public void AddUser(SavedUserData user, bool save = true)
{
if (json.users.Count == 0)
json.currentUser = 0;
json.users.Add(user);
if (save)
Save();
}
///
/// Get a list of all user data records
///
///
public List GetUsers()
{
return json.users;
}
///
/// Get the index of the current user
///
///
public int GetCurrentUser()
{
return json.currentUser;
}
///
/// Set the index of the current record
///
/// New index
/// Whether to save the change immediately to disk
///
public void SetCurrentUser(int index, bool save = true)
{
if (index < 0 || json.users.Count <= index)
throw new IndexOutOfRangeException();
json.currentUser = index;
if (save)
Save();
}
///
/// Remove a user data record
///
/// Index of the user
/// Whether to save the deletion immediately to disk
///
public void DeleteUser(int index, bool save = true)
{
if (index < 0 || json.users.Count <= index)
throw new IndexOutOfRangeException();
if (0 < json.currentUser && index <= json.currentUser)
json.currentUser--;
json.users.RemoveAt(index);
if (save)
Save();
}
///
/// Get the current course
///
///
public CourseIndex GetCurrentCourse()
{
return json.currentCourse;
}
///
/// Set the current course
///
/// New course index
/// Whether to save the change immediately to disk
public void SetCurrentCourse(CourseIndex course, bool save = true)
{
json.currentCourse = course;
if (save)
Save();
}
///
/// Get the current minigame
///
///
public MinigameIndex GetCurrentMinigame()
{
return json.currentMinigame;
}
///
/// Set the current minigame
///
/// New minigame index
/// Whether to save the change immediately to disk
public void SetCurrentMinigame(MinigameIndex minigame, bool save = true)
{
json.currentMinigame = minigame;
if (save)
Save();
}
///
/// Get the current theme
///
///
public ThemeIndex GetCurrentTheme()
{
return json.currentTheme;
}
///
/// Set the current theme
///
/// New theme index
/// Whether to save the change immediately to disk
public void SetCurrentTheme(ThemeIndex theme, bool save = true)
{
json.currentTheme = theme;
if (save)
Save();
}
///
/// Whether the user wants to use hardware acceleration or not
///
public bool IsUsingGPU()
{
return json.useGPU;
}
///
/// Set the preference of the user for hardware acceleration
///
/// Value of the preference
/// Whether to save the change immediately to disk
public void SetGPUUsage(bool value, bool save = true)
{
json.useGPU = value;
if (save)
Save();
}
///
/// Restore preferences to default factory settings
///
/// Whether to save the change immediately to disk
public void RestoreSettings(bool save = true)
{
json.RestoreSettings();
if (save)
Save();
}
}