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(); public PersistentDataEntry(string key, byte[] data) : this(key, data.ToList()) { } 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 { public string username = null; public int avatarIndex = -1; public double playtime = 0.0; public List minigames = new List(); 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++; } /// /// /// /// bool which indicates if there are enough inUseLearnables 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 { public int index; public bool inUse = false; public string name; public float progress = 0.0f; } /// /// Stored minigame progress data record /// [Serializable] public class SavedMinigameProgress : PersistentDataContainer { public MinigameIndex minigameIndex; public List latestScores = new List(); public List highestScores = new List(); } /// /// Stored WeSign data record /// [Serializable] private class SavedDataStructure { public int version = VERSION; public List users = new List(); public int currentUser = -1; public MinigameIndex currentMinigame; public CourseIndex currentCourse; 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(); } }