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