using NeoSaveGames.Serialization; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using UnityEngine; using UnityEngine.SceneManagement; using NeoSaveGames.SceneManagement; namespace NeoSaveGames { [CreateAssetMenu(fileName = "FpsManager_SaveGames", menuName = "NeoFPS/Managers/Save Games", order = NeoFPS.NeoFpsMenuPriorities.manager_savegames)] [HelpURL("https://docs.neofps.com/manual/savegamesref-so-savegamemanager.html")] public class SaveGameManager : NeoFPS.NeoFpsManager { private static readonly NeoSerializationKey k_MainSceneKey = new NeoSerializationKey("mainScene"); private static readonly NeoSerializationKey k_SubScenesKey = new NeoSerializationKey("subScenes"); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void LoadSaveGameManager() { GetInstance("FpsManager_SaveGames"); } protected override void Initialise() { GetBehaviourProxy(); // Check folder exists CheckSaveFolder(); // Initialise InitialiseThreading(); InitialiseSerialization(); RefreshAvailableSaves(); // Register dynamic objects NeoSerializedObjectFactory.RegisterPrefabs(m_Prefabs); for (int i = 0; i < m_Assets.Length; ++i) { var cast = m_Assets[i] as INeoSerializableAsset; if (cast != null) NeoSerializedObjectFactory.RegisterAsset(cast); } } public override bool IsValid() { return true; } protected override void OnDestroy() { base.OnDestroy(); // Unregister dynamic objects NeoSerializedObjectFactory.UnregisterPrefabs(m_Prefabs); for (int i = 0; i < m_Assets.Length; ++i) { var cast = m_Assets[i] as INeoSerializableAsset; if (cast != null) NeoSerializedObjectFactory.UnregisterAsset(cast); } DestroyThreading(); } #region THREADING private Thread m_Thread = null; private EventWaitHandle m_ThreadWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); private volatile bool m_Destroyed = false; private bool m_SideThreadBusy = false; private bool m_MainThreadBusy = false; private object m_LockObject = new object(); private Queue m_JobQueue = new Queue(); private IEnumerator m_MainThreadTask = null; public static bool inProgress { get { if (instance == null) return false; if (instance.m_MainThreadBusy) return true; // Locks might not be required, depending on if optimisations // mess with order of instructions in thread loop lock (instance.m_LockObject) return instance.m_SideThreadBusy; } } class RuntimeBehaviour : MonoBehaviour { void Start() { StartCoroutine(instance.RefreshAvailableSaves()); } void Update() { if (instance.m_MainThreadTask != null) StartCoroutine(TaskCoroutine()); } IEnumerator TaskCoroutine() { yield return StartCoroutine(instance.m_MainThreadTask); instance.m_MainThreadTask = null; instance.m_ThreadWaitHandle.Set(); } } void InitialiseThreading() { m_Thread = new Thread(ThreadLoop) { Name = "NeoFPS Save System Thread", IsBackground = true }; m_Thread.Start(); // Reset variables that might have been muddied by the editor m_SideThreadBusy = false; m_MainThreadBusy = false; m_JobQueue.Clear(); m_MainThreadTask = null; } void DestroyThreading() { m_Destroyed = true; m_ThreadWaitHandle.Set(); bool stopped = false; try { if (!m_Thread.Join(TimeSpan.FromSeconds(15))) { Debug.Log("Unable to stop save game thread gracefully"); } else stopped = true; } finally { // abort in case it has not been stopped yet if (!stopped) m_Thread.Abort(); // dispose the wait handle because it would otherwise leak memory m_ThreadWaitHandle.Dispose(); } } void ThreadLoop() { while (!m_Destroyed) { lock (m_LockObject) m_SideThreadBusy = false; m_ThreadWaitHandle.WaitOne(); lock (m_LockObject) m_SideThreadBusy = true; // Clear the job pool while (true) { var job = GetJob(); if (job != null) job.Start(); else break; } } var thread = m_Thread; m_Thread = null; thread.Abort(); } void AddJob(AsyncSaveLoadJob job) { lock (m_LockObject) m_JobQueue.Enqueue(job); m_ThreadWaitHandle.Set(); } AsyncSaveLoadJob GetJob() { lock (m_LockObject) { if (m_JobQueue.Count > 0) return m_JobQueue.Dequeue(); } return null; } private abstract class AsyncSaveLoadJob { public SaveGameManager manager { get; private set; } public bool completed { get; protected set; } public AsyncSaveLoadJob(SaveGameManager m) { manager = m; completed = false; } public void Start() { while (!completed) Process(); } protected void WaitOnMainThreadTask(IEnumerator task) { manager.m_MainThreadTask = task; manager.m_ThreadWaitHandle.WaitOne(); } protected abstract void Process(); public abstract void Abort(); } #endregion #region SERIALIZATION public INeoSerializer serializer { get; private set; } public INeoDeserializer deserializer { get; private set; } void InitialiseSerialization() { #if UNITY_WEBGL serializer = new SafeSerializer(); deserializer = new SafeDeserializer(); #else serializer = new BinarySerializer(); deserializer = new BinaryDeserializer(); #endif } #endregion #region VALIDATION private void OnValidate() { ValidatePrefabs(); ValidateAssets(); ValidatePath(); } void ValidatePrefabs() { int valid = 0; for (int i = 0; i < m_Prefabs.Length; ++i) { if (m_Prefabs[i] != null) ++valid; } if (valid != m_Prefabs.Length) { var rebuilt = new NeoSerializedGameObject[valid]; int itr = 0; for (int i = 0; i < m_Prefabs.Length; ++i) { if (m_Prefabs[i] != null) { rebuilt[itr] = m_Prefabs[i]; ++itr; } } m_Prefabs = rebuilt; } } void ValidateAssets() { int valid = 0; for (int i = 0; i < m_Assets.Length; ++i) { if (m_Assets[i] != null) ++valid; } if (valid != m_Assets.Length) { var rebuilt = new ScriptableObject[valid]; int itr = 0; for (int i = 0; i < m_Assets.Length; ++i) { if (m_Assets[i] != null) { rebuilt[itr] = m_Assets[i]; ++itr; } } m_Assets = rebuilt; } } #endregion #region REGISTERED ITEMS [SerializeField, Tooltip("")] private NeoSerializedGameObject[] m_Prefabs = new NeoSerializedGameObject[0]; [SerializeField, Tooltip("")] private ScriptableObject[] m_Assets = new ScriptableObject[0]; #if UNITY_EDITOR public bool CheckIsPrefabRegistered(NeoSerializedGameObject prefab) { if (m_Prefabs != null && prefab != null) { for (int i = 0; i < m_Prefabs.Length; ++i) { if (m_Prefabs[i] != null && m_Prefabs[i].prefabStrongID == prefab.prefabStrongID) return true; } } return false; //return (Array.IndexOf(m_Prefabs, prefab) != -1); } public void RegisterPrefab(NeoSerializedGameObject prefab) { if (Array.IndexOf(m_Prefabs, prefab) == -1) { var so = new UnityEditor.SerializedObject(this); var prop = so.FindProperty("m_Prefabs"); ++prop.arraySize; prop = prop.GetArrayElementAtIndex(prop.arraySize - 1); prop.objectReferenceValue = prefab; so.ApplyModifiedProperties(); } } #endif #endregion #region QUICK SAVE [Header("Quick-Save")] [SerializeField, Tooltip("Sets whether the quick-save system is enabled in this project")] private bool m_CanQuickSave = true; [SerializeField, Tooltip("If true, quick loading will load the latest quick/auto/manual save. If not then it will only load the latest quick-save")] private bool m_QuickLoadAll = true; [SerializeField, Range(1, 10), Tooltip("The number of quicksaves to maintain. If the number exceeds this value, the oldest saves will be deleted.")] private int m_NumQuicksaves = 3; public static bool quickSaveEnabled { get { return instance != null && instance.m_CanQuickSave; } } public static bool canQuickSave { get { return instance != null && instance.m_CanQuickSave && !inProgress && instance.m_MainScene != null; } } public static bool canQuickLoad { get { if (instance == null || inProgress) return false; if (instance.m_QuickLoadAll) return hasAvailableSaves; else return (availableQuicksaves.Length > 0); } } public static bool QuickSave() { if (canQuickSave) return instance.SaveGameInternal(SaveGameType.Quicksave, mainScene.displayName, null); else { if (onSaveFailed != null) onSaveFailed(SaveGameType.Quicksave); return false; } } public static bool QuickLoad() { if (canQuickLoad) { if (instance.m_QuickLoadAll) return LoadGame(GetLatestSave(SaveGameTypeFilter.All)); else return LoadGame(GetLatestSave(SaveGameTypeFilter.Quicksave)); } else return false; } #endregion #region AUTO SAVE [Header("Auto-Save")] [SerializeField, Range(1, 10), Tooltip("The number of autosaves to maintain. If the number exceeds this value, the oldest saves will be deleted.")] private int m_NumAutosaves = 3; public static bool canAutoSave { get { return instance != null && !inProgress && instance.m_MainScene != null; } } public static bool canAutoLoad { get { return instance != null && availableAutosaves.Length > 0 && !inProgress; } } public static bool AutoSave() { if (canAutoSave) return instance.SaveGameInternal(SaveGameType.Autosave, mainScene.displayName, null); else { if (onSaveFailed != null) onSaveFailed(SaveGameType.Autosave); return false; } } public static bool AutoLoad() { if (canAutoLoad) return LoadGame(GetLatestSave(SaveGameTypeFilter.Autosave)); else return false; } #endregion #region MANUAL SAVE [Header("Manual Save")] [SerializeField, Tooltip("Sets whether the player can manually save the game in this project")] private bool m_CanManualSave = true; public static bool manualSaveEnabled { get { return instance != null && instance.m_CanManualSave; } } public static bool canManualSave { get { return instance != null && instance.m_CanManualSave && !inProgress && instance.m_MainScene != null; } } public static bool SaveGame(string title, FileInfo replaces = null) { if (!canManualSave) { if (onSaveFailed != null) onSaveFailed(SaveGameType.Manual); return false; } return instance.SaveGameInternal(SaveGameType.Manual, title, () => { if (replaces != null) replaces.Delete(); }); } #endregion #region CONTINUE [Header("Continue")] [SerializeField, Tooltip("The types of saves to use when continuing gameplay from the main menu")] private ContinueType m_ContinueFrom = ContinueType.AutoSaveOnly; public enum ContinueType { None, All, AutoSaveOnly } public static bool canContinue { get { if (instance == null || inProgress) return false; switch (instance.m_ContinueFrom) { case ContinueType.None: return false; case ContinueType.AutoSaveOnly: return availableAutosaves.Length > 0; case ContinueType.All: return hasAvailableSaves; } return false; } } public static bool Continue() { if (canContinue) { switch (instance.m_ContinueFrom) { case ContinueType.AutoSaveOnly: return LoadGame(GetLatestSave(SaveGameTypeFilter.Autosave)); case ContinueType.All: return LoadGame(GetLatestSave(SaveGameTypeFilter.All)); default: return false; } } else return false; } #endregion #region SAVE TO BUFFER private static MemoryStream s_TempDataStream = null; private static readonly NeoSerializationKey k_KeysKey = new NeoSerializationKey("keys"); public static bool SaveGameObjectsToBuffer(NeoSerializedGameObject[] objects, SaveMode saveMode) { // Basic checks (since anyone can call this) if (instance == null || inProgress || objects == null) return false; // Begin the serialization process var writer = instance.serializer; writer.BeginSerialization(); // Write an array of keys in the order provided Vector2Int[] keys = new Vector2Int[objects.Length]; for (int i = 0; i < objects.Length; ++i) { if (objects[i] == null) { keys[i] = Vector2Int.zero; } else { if (!objects[i].wasRuntimeInstantiated || objects[i].prefabStrongID == 0) { Debug.LogError("Game objects saved to buffer must be runtime instantiated prefab instances in order to be rebuilt correctly in the new scene. Invalid: " + objects[i].name); writer.EndSerialization(); return false; } keys[i].x = objects[i].serializationKey; keys[i].y = objects[i].prefabStrongID; } } writer.WriteValues(k_KeysKey, keys); // Serialize each of the game objects for (int i = 0; i < objects.Length; ++i) { if (objects[i] != null) { writer.PushContext(SerializationContext.GameObject, objects[i].serializationKey); objects[i].WriteGameObject(writer, saveMode); writer.PopContext(SerializationContext.GameObject); } } // End the serialization process and write to stream writer.EndSerialization(); s_TempDataStream = new MemoryStream(writer.byteLength); writer.WriteToStream(s_TempDataStream); s_TempDataStream.Position = 0; return true; } public static NeoSerializedGameObject[] LoadGameObjectsFromBuffer(NeoSerializedGameObjectContainerBase container) { if (instance == null || inProgress || s_TempDataStream == null) return null; // Read from stream var reader = instance.deserializer; reader.ReadFromStream(s_TempDataStream); //s_TempDataStream.Close(); s_TempDataStream = null; // Begin the deserialization process reader.BeginDeserialization(); // Read the keys array Vector2Int[] keys = null; if (!reader.TryReadValues(k_KeysKey, out keys, null)) return null; // Instantiate each of the objects var results = new NeoSerializedGameObject[keys.Length]; for (int i = 0; i < keys.Length; ++i) { if (keys[i].x == 0) results[i] = null; else results[i] = NeoSerializedObjectFactory.Instantiate(keys[i].y, keys[i].x, container); } // Deserialize each of the game objects for (int i = 0; i < results.Length; ++i) { if (results[i] != null) { if (reader.PushContext(SerializationContext.GameObject, results[i].serializationKey)) { results[i].ReadGameObjectHierarchy(reader); reader.PopContext(SerializationContext.GameObject, results[i].serializationKey); } } } for (int i = 0; i < results.Length; ++i) { if (results[i] != null) { if (reader.PushContext(SerializationContext.GameObject, results[i].serializationKey)) { results[i].ReadGameObjectProperties(reader); reader.PopContext(SerializationContext.GameObject, results[i].serializationKey); } } } // End the deserialization process reader.EndDeserialization(); return results; } public static bool LoadGameObjectsFromBuffer(NeoSerializedGameObject[] objects) { if (instance == null || inProgress || objects == null || s_TempDataStream == null) return false; // Read from stream var reader = instance.deserializer; reader.ReadFromStream(s_TempDataStream); //s_TempDataStream.Close(); s_TempDataStream = null; // Begin the deserialization process reader.BeginDeserialization(); // Deserialize each of the game objects for (int i = 0; i < objects.Length; ++i) { if (objects[i] != null) { if (reader.PushContext(SerializationContext.GameObject, i)) { objects[i].ReadGameObjectHierarchy(reader); reader.PopContext(SerializationContext.GameObject, i); } } } for (int i = 0; i < objects.Length; ++i) { if (objects[i] != null) { if (reader.PushContext(SerializationContext.GameObject, i)) { objects[i].ReadGameObjectProperties(reader); reader.PopContext(SerializationContext.GameObject, i); } } } // End the deserialization process reader.EndDeserialization(); return false; } #endregion #region THUMBNAIL [Header("Thumbnails")] [SerializeField, Tooltip("Where to get the tumbnail texture for saved scenes")] private Thumbnail m_QuicksaveThumbnail = Thumbnail.None; [SerializeField, Tooltip("The thumbnail to use for saved scenes")] private Texture2D m_QsThumbnailTexture = null; [SerializeField, Tooltip("Where to get the tumbnail texture for saved scenes")] private Thumbnail m_AutosaveThumbnail = Thumbnail.None; [SerializeField, Tooltip("The thumbnail to use for saved scenes")] private Texture2D m_AsThumbnailTexture = null; [SerializeField, Tooltip("Where to get the tumbnail texture for saved scenes")] private Thumbnail m_ManualSaveThumbnail = Thumbnail.None; [SerializeField, Tooltip("The thumbnail to use for saved scenes")] private Texture2D m_MsThumbnailTexture = null; [SerializeField, Tooltip("The width of any save game screenshots")] private Vector2Int m_ScreenshotSize = new Vector2Int(256, 256); [SerializeField, Tooltip("Should the save game screenshot be compressed")] private bool m_ScreenshotCompression = true; [SerializeField, Tooltip("Is the game using linear rendering? Screenshots need to match")] private bool m_UsingLinearRendering = true; public enum Thumbnail { None, Texture, TextureFromScene, Screenshot } public Texture2D GetThumbnail(SaveGameType saveType) { Thumbnail thumbnail = Thumbnail.None; Texture2D thumbnailTexture = null; // Get settings from type switch (saveType) { case SaveGameType.Quicksave: thumbnail = m_QuicksaveThumbnail; thumbnailTexture = m_QsThumbnailTexture; break; case SaveGameType.Autosave: thumbnail = m_AutosaveThumbnail; thumbnailTexture = m_AsThumbnailTexture; break; case SaveGameType.Manual: thumbnail = m_ManualSaveThumbnail; thumbnailTexture = m_MsThumbnailTexture; break; } // Get texture switch (thumbnail) { case Thumbnail.TextureFromScene: if (mainScene != null && mainScene.thumbnailTexture != null) thumbnailTexture = mainScene.thumbnailTexture; break; case Thumbnail.Screenshot: var screenshot = GetScreenshot(m_ScreenshotSize, m_ScreenshotCompression); if (screenshot != null) thumbnailTexture = screenshot; break; } return thumbnailTexture; } Texture2D GetScreenshot(Vector2Int size, bool compressed) { // Based on: https://pastebin.com/qkkhWs2J var capture = ScreenCapture.CaptureScreenshotAsTexture(); // - Could try replacing with a version which renders the main camera direct into a render texture // - Or use ReadPixels() instead of ScreenCapture // We need the source texture in VRAM because we render with it capture.filterMode = FilterMode.Bilinear; //capture.alphaIsTransparency = true; capture.Apply(true); // Set the RTT in order to render to it RenderTexture rtt = new RenderTexture(size.x, size.y, 32); rtt.autoGenerateMips = false; Graphics.SetRenderTarget(rtt); // Setup 2D matrix in range 0..1, so nobody needs to care about sized GL.LoadPixelMatrix(0, 1, 1, 0); // Then clear & draw the texture to fill the entire RTT. GL.Clear(true, true, new Color(0, 0, 0, 0)); Graphics.DrawTexture(new Rect(0, 0, 1, 1), capture); // Update new texture #if UNITY_2021_2_OR_NEWER capture.Reinitialize(size.x, size.y); #else capture.Resize(size.x, size.y); #endif capture.ReadPixels(new Rect(0, 0, size.x, size.y), 0, 0); // Hacky workaround for issue with CaptureScreenshotAsTexture returning texture // in wrong colour space when using linear rendering if (m_UsingLinearRendering) { var pixels = capture.GetPixels(); for (int i = 0; i < pixels.Length; ++i) { //#if UNITY_EDITOR pixels[i] = pixels[i].linear; //#endif pixels[i].a = 1f; } capture.SetPixels(pixels); } capture.Apply(); // Compress the new texture if (compressed) capture.Compress(false); return capture; } #endregion #region SAVE FILE MANAGEMENT [Header("Location")] [SerializeField, Tooltip("")] private SavePathRoot m_SavePath = SavePathRoot.PersistantDataPath; [SerializeField, Tooltip("")] private string m_SaveSubFolder = "SaveFiles"; const string k_Extension = "saveData"; const string k_TypeStringQuick = "quick"; const string k_TypeStringAuto = "auto"; const string k_TypeStringManual = "manual"; public static bool hasAvailableSaves { get { if (instance == null) return false; // Check initial count has been retrieved if (availableQuicksaves == null || availableAutosaves == null || availableManualSaves == null) return false; // Check there's more than 1 save file available return (availableQuicksaves.Length > 0 || availableAutosaves.Length > 0 || availableManualSaves.Length > 0); } } public static FileInfo[] availableQuicksaves { get; private set; } public static FileInfo[] availableAutosaves { get; private set; } public static FileInfo[] availableManualSaves { get; private set; } public static FileInfo GetLatestSave(SaveGameTypeFilter filter) { if (instance == null) return null; FileInfo result = null; if ((filter & SaveGameTypeFilter.Quicksave) != SaveGameTypeFilter.None && availableQuicksaves.Length > 0) { if (result == null || result.CreationTime < availableQuicksaves[0].CreationTime) result = availableQuicksaves[0]; } if ((filter & SaveGameTypeFilter.Autosave) != SaveGameTypeFilter.None && availableAutosaves.Length > 0) { if (result == null || result.CreationTime < availableAutosaves[0].CreationTime) result = availableAutosaves[0]; } if ((filter & SaveGameTypeFilter.Manual) != SaveGameTypeFilter.None && availableManualSaves.Length > 0) { if (result == null || result.CreationTime < availableManualSaves[0].CreationTime) result = availableManualSaves[0]; } return result; } public static FileInfo[] GetAvailableSaves(SaveGameTypeFilter filter) { if (instance == null || filter == SaveGameTypeFilter.None) return new FileInfo[0]; List collected = new List(); if ((filter & SaveGameTypeFilter.Quicksave) != SaveGameTypeFilter.None) collected.AddRange(availableQuicksaves); if ((filter & SaveGameTypeFilter.Autosave) != SaveGameTypeFilter.None) collected.AddRange(availableAutosaves); if ((filter & SaveGameTypeFilter.Manual) != SaveGameTypeFilter.None) collected.AddRange(availableManualSaves); collected.Sort((FileInfo f1, FileInfo f2) => { return f2.CreationTime.CompareTo(f1.CreationTime); }); return collected.ToArray(); } static FileInfo[] CheckAvailableSaveFiles(SaveGameTypeFilter filter) { if (instance != null) { DirectoryInfo directory = new DirectoryInfo(instance.GetSaveFolder()); if (directory != null && filter != SaveGameTypeFilter.None) { FileInfo[] result = null; switch (filter) { case SaveGameTypeFilter.All: result = directory.GetFiles("*." + k_Extension); break; case SaveGameTypeFilter.Quicksave: result = directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringQuick, k_Extension)); break; case SaveGameTypeFilter.Autosave: result = directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringAuto, k_Extension)); break; case SaveGameTypeFilter.Manual: result = directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringManual, k_Extension)); break; default: { List collected = new List(); if ((filter & SaveGameTypeFilter.Quicksave) != SaveGameTypeFilter.None) collected.AddRange(directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringQuick, k_Extension))); if ((filter & SaveGameTypeFilter.Autosave) != SaveGameTypeFilter.None) collected.AddRange(directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringAuto, k_Extension))); if ((filter & SaveGameTypeFilter.Manual) != SaveGameTypeFilter.None) collected.AddRange(directory.GetFiles(string.Format("*_{0}.{1}", k_TypeStringManual, k_Extension))); result = collected.ToArray(); break; } } Array.Sort(result, (FileInfo f1, FileInfo f2) => { return f2.CreationTime.CompareTo(f1.CreationTime); }); return result; } } return new FileInfo[0]; } IEnumerator RefreshAvailableSaves() { yield return null; // Get quicksaves (and trim excess) var files = new List(CheckAvailableSaveFiles(SaveGameTypeFilter.Quicksave)); files.Sort((FileInfo f1, FileInfo f2) => { return f2.CreationTime.CompareTo(f1.CreationTime); }); if (files.Count > m_NumQuicksaves) { for (int i = files.Count; i > m_NumQuicksaves; --i) { files[i - 1].Delete(); files.RemoveAt(i - 1); } } availableQuicksaves = files.ToArray(); // Get autosaves (and trim excess) files = new List(CheckAvailableSaveFiles(SaveGameTypeFilter.Autosave)); files.Sort((FileInfo f1, FileInfo f2) => { return f2.CreationTime.CompareTo(f1.CreationTime); }); if (files.Count > m_NumAutosaves) { for (int i = files.Count; i > m_NumAutosaves; --i) { files[i - 1].Delete(); files.RemoveAt(i - 1); } } availableAutosaves = files.ToArray(); // Get manualsaves availableManualSaves = CheckAvailableSaveFiles(SaveGameTypeFilter.Manual); Array.Sort(availableManualSaves, (FileInfo f1, FileInfo f2) => { return f2.CreationTime.CompareTo(f1.CreationTime); }); } public void CheckSaveFolder() { string folder = GetSaveFolder(); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); } public string GetSaveFolder() { switch (m_SavePath) { case SavePathRoot.PersistantDataPath: if (string.IsNullOrEmpty(m_SaveSubFolder)) return Application.persistentDataPath + '/'; else return string.Format("{0}/{1}/", Application.persistentDataPath, m_SaveSubFolder); case SavePathRoot.DataPath: if (string.IsNullOrEmpty(m_SaveSubFolder)) return Application.dataPath + '/'; else return string.Format("{0}/{1}/", Application.dataPath, m_SaveSubFolder); default: return m_SaveSubFolder; } } string GetSavePath(DateTime time, SaveGameType type) { string typeString = string.Empty; switch (type) { case SaveGameType.Quicksave: typeString = k_TypeStringQuick; break; case SaveGameType.Autosave: typeString = k_TypeStringAuto; break; case SaveGameType.Manual: typeString = k_TypeStringManual; break; } return string.Format("{0}{1}{2}{3}{4}{5}{6}_{7}.{8}", GetSaveFolder(), time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, typeString, k_Extension ); } void ValidatePath() { // Check for empty string if (string.IsNullOrEmpty(m_SaveSubFolder)) return; #if UNITY_EDITOR var input = m_SaveSubFolder.Replace('\\', '/'); var filtered = new List(input.Length); var invalid = Path.GetInvalidPathChars(); foreach (var c in input) { if (Array.IndexOf(invalid, c) == -1) filtered.Add(c); } m_SaveSubFolder = new string(filtered.ToArray()); #endif } #endregion #region META-DATA private static LoadMetaDataJob m_LastMetaDataLoadJob = null; public static SaveFileMetaData[] LoadFileMetaData(SaveGameTypeFilter filter) { // Can't return meta-data while saving or loading if (instance == null || inProgress) return new SaveFileMetaData[0]; // Get the file infos and prep array var files = GetAvailableSaves(filter); var result = new SaveFileMetaData[files.Length]; for (int i = 0; i < files.Length; ++i) result[i] = new SaveFileMetaData(files[i]); // Add async load job m_LastMetaDataLoadJob = new LoadMetaDataJob(instance, result); instance.AddJob(m_LastMetaDataLoadJob); return result; } public static void CancelLoadingFileMetaData() { if (m_LastMetaDataLoadJob != null) { if (!m_LastMetaDataLoadJob.completed) m_LastMetaDataLoadJob.Abort(); m_LastMetaDataLoadJob = null; } } class LoadMetaDataJob : AsyncSaveLoadJob { private Queue m_Queue = new Queue(); private SaveFileMetaData m_CurrentMeta = null; public LoadMetaDataJob(SaveGameManager m, SaveFileMetaData[] meta) : base(m) { for (int i = 0; i < meta.Length; ++i) m_Queue.Enqueue(meta[i]); } protected override void Process() { // Get the meta data to load this iteration m_CurrentMeta = null; lock (m_Queue) { if (m_Queue.Count > 0) m_CurrentMeta = m_Queue.Dequeue(); } if (m_CurrentMeta == null) completed = true; else { using (var stream = m_CurrentMeta.saveFile.OpenRead()) { if (manager.deserializer.ReadFromStream(stream)) WaitOnMainThreadTask(ReadProperties()); } // Pause to allow for cleanup before loading the next WaitOnMainThreadTask(Pause(0.5f)); } } IEnumerator ReadProperties() { yield return null; var reader = manager.deserializer; reader.BeginDeserialization(); if (reader.PushContext(SerializationContext.MetaData, 0)) { m_CurrentMeta.ReadProperties(reader); reader.PopContext(SerializationContext.MetaData, 0); } reader.EndDeserialization(); } IEnumerator Pause(float duration) { float timer = 0f; while (timer < duration) { yield return null; timer += Time.unscaledDeltaTime; } } public override void Abort() { lock (m_Queue) m_Queue.Clear(); } } #endregion #region SAVING public static event Action onSaveInProgess; public static event Action onSaveFailed; bool SaveGameInternal(SaveGameType type, string title, Action onComplete) { // Basic checks (since anyone can call this) if (inProgress || m_MainScene == null || (type == SaveGameType.Manual && !canManualSave)) { if (onSaveFailed != null) onSaveFailed(type); return false; } // Add the file write job AddJob(new SaveGameJob(instance, title, type, onComplete)); // Invoke save in-progress event if (onSaveInProgess != null) onSaveInProgess(type); return true; } class SaveGameJob : AsyncSaveLoadJob { private DateTime m_SaveTime = new DateTime(); private SaveGameType m_SaveType = SaveGameType.Manual; private Action m_OnComplete = null; private string m_SaveTitle = string.Empty; private string m_SaveFilePath = string.Empty; public SaveGameJob(SaveGameManager m, string title, SaveGameType type, Action onComplete) : base(m) { m_SaveType = type; m_SaveTitle = title; m_SaveTime = DateTime.Now; m_OnComplete = onComplete; } protected override void Process() { WaitOnMainThreadTask(SaveGameCoroutine(m_SaveType, m_SaveTitle)); // Write to stream using (var fstream = File.Create(m_SaveFilePath)) { manager.serializer.WriteToStream(fstream); } // Perform the onComplete if (m_OnComplete != null) m_OnComplete(); // Refresh available saves (also deletes excess) WaitOnMainThreadTask(manager.RefreshAvailableSaves()); // Signal completed completed = true; } IEnumerator SaveGameCoroutine(SaveGameType type, string title) { yield return new WaitForEndOfFrame(); m_SaveFilePath = manager.GetSavePath(m_SaveTime, m_SaveType); var serializer = manager.serializer; serializer.BeginSerialization(); // Write metadata var meta = new SaveFileMetaData(title, type, m_SaveTime, manager.GetThumbnail(type)); serializer.PushContext(SerializationContext.MetaData, 0); meta.WriteProperties(serializer); serializer.PopContext(SerializationContext.MetaData); // Save persistant (non-scene) data // ... // Save dont destroy on load scene contents // (requires adding a NeoSerializedScene based component to the SceneManager object) //var managerScene = gameObject.GetComponent(); //if (managerScene != null) //{ // serializer.PushContext(SerializationContext.Scene, -1); // managerScene.WriteData(serializer); // serializer.PopContext(SerializationContext.Scene); //} var scenes = manager.m_Scenes; // Prep for scene saves int sceneCount = scenes.Count; var m_FilteredPaths = new List(sceneCount); var m_FilteredScenes = new List(sceneCount); // Get main scene if (mainScene != null) { //Debug.Log("Writing main scene path"); serializer.WriteValue(k_MainSceneKey, mainScene.scene.path); m_FilteredScenes.Add(mainScene); } else { serializer.WriteValue(k_MainSceneKey, string.Empty); } // Filter sub-scenes for (int i = 0; i < sceneCount; ++i) { if (scenes[i] != mainScene) { m_FilteredScenes.Add(scenes[i]); m_FilteredPaths.Add(scenes[i].scene.path); } } // Write sub-scenes serializer.WriteValues(k_SubScenesKey, m_FilteredPaths); for (int i = 0; i < m_FilteredScenes.Count; ++i) { //Debug.Log("Writing scene: " + m_FilteredScenes[i].scene.path + ", hash: " + NeoSerializationUtilities.StringToHash(m_FilteredScenes[i].scene.path)); serializer.PushContext(SerializationContext.Scene, NeoSerializationUtilities.StringToHash(m_FilteredScenes[i].scene.path)); m_FilteredScenes[i].WriteData(serializer); serializer.PopContext(SerializationContext.Scene); } serializer.EndSerialization(); yield return null; } public override void Abort() { } } #endregion #region LOADING private string m_LoadingScenePath = null; public static bool LoadGame(FileInfo saveFile) { if (instance == null || inProgress) return false; // Read from stream var reader = instance.deserializer; using (var fstream = saveFile.OpenRead()) { reader.ReadFromStream(fstream); } reader.BeginDeserialization(); // Load the main scene string mainScene; if (reader.TryReadValue(k_MainSceneKey, out mainScene, null)) { // Add multi-scene loading later //string[] subScenes; //reader.TryReadValues(k_SubScenesKey, out subScenes, null); // Record scene name instance.m_LoadingScenePath = mainScene; instance.m_MainThreadBusy = true; NeoSceneManager.LoadScene(mainScene, OnCompleteLoad); } else { Debug.LogError("No main scene found"); reader.EndDeserialization(); } return true; } public static void NotifySceneLoaded(NeoSerializedScene scene) { RegisterScene(scene); // This method should be called by the NeoSerializedScene itself on Awake(), // And the NeoSerializedScene is set to execute Awake() before all other classes. var s = scene.scene; if (instance.m_LoadingScenePath == s.path) { var reader = instance.deserializer; if (reader.isDeserializing && reader.PushContext(SerializationContext.Scene, NeoSerializationUtilities.StringToHash(s.path))) { try { var objects = s.GetRootGameObjects(); foreach (var obj in objects) { var sceneInfo = obj.GetComponent(); if (sceneInfo != null) { sceneInfo.ReadData(reader); break; } } } //catch (Exception e) //{ // Debug.LogError("Scene activation callback failed. There was an issue loading the scene: " + e.Message); //} finally { reader.PopContext(SerializationContext.Scene, NeoSerializationUtilities.StringToHash(s.path)); } } instance.m_LoadingScenePath = null; } } static void OnCompleteLoad() { instance.m_MainThreadBusy = false; instance.deserializer.EndDeserialization(); } #endregion #region SCENES private List m_Scenes = new List(); private SceneSaveInfo m_MainScene = null; public static SceneSaveInfo mainScene { get { if (instance != null) return instance.m_MainScene; else return null; } } public static void RegisterScene(NeoSerializedScene scene) { if (instance != null && scene != null) { if (scene.isMainScene) instance.m_MainScene = scene as SceneSaveInfo; instance.m_Scenes.Add(scene); NeoSerializedObjectFactory.RegisterPrefabs(scene.registeredPrefabs); for (int i = 0; i < scene.registeredAssets.Length; ++i) { var cast = scene.registeredAssets[i] as INeoSerializableAsset; if (cast != null) NeoSerializedObjectFactory.RegisterAsset(cast); } } } public static void UnregisterScene(NeoSerializedScene scene) { if (instance != null && scene != null) { NeoSerializedObjectFactory.UnregisterPrefabs(scene.registeredPrefabs); for (int i = 0; i < scene.registeredAssets.Length; ++i) { var cast = scene.registeredAssets[i] as INeoSerializableAsset; if (cast != null) NeoSerializedObjectFactory.UnregisterAsset(cast); } instance.m_Scenes.Remove(scene); if (instance.m_MainScene == scene) instance.m_MainScene = null; } } #endregion } }