projectEli/Assets/NeoFPS/Core/NeoSaveGames/SaveGameManager.cs

1478 lines
50 KiB
C#
Raw Normal View History

2022-11-07 01:28:33 +00:00
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<SaveGameManager>
{
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<RuntimeBehaviour>();
// 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<AsyncSaveLoadJob> m_JobQueue = new Queue<AsyncSaveLoadJob>();
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<FileInfo> collected = new List<FileInfo>();
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<FileInfo> collected = new List<FileInfo>();
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<FileInfo>(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<FileInfo>(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<char>(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<SaveFileMetaData> m_Queue = new Queue<SaveFileMetaData>();
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<SaveGameType> onSaveInProgess;
public static event Action<SaveGameType> 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<NeoSerializedScene>();
//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<string>(sceneCount);
var m_FilteredScenes = new List<NeoSerializedScene>(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<SceneSaveInfo>();
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<NeoSerializedScene> m_Scenes = new List<NeoSerializedScene>();
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
}
}