using System.Collections;
using UnityEngine;
using System.Threading;
using UnityEngine.Events;
using OctoberStudio.Easing;
using System.IO;
#if UNITY_WEBGL && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
namespace OctoberStudio.Save
{
[DefaultExecutionOrder(-100)]
public class SaveManager : MonoBehaviour, ISaveManager
{
public static readonly string SAVE_FILE_NAME = "game_save";
private static SaveManager instance;
[SerializeField] SaveType saveType = SaveType.SaveFile;
[Space]
[SerializeField] bool clearSave;
[Space]
[SerializeField] bool autoSaveEnabled;
[SerializeField] float autoSaveDelay;
private SaveDatabase SaveDatabase { get; set; }
public bool IsSaveLoaded { get; private set; }
public event UnityAction OnSaveLoaded;
private Coroutine saveCoroutine;
private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
if (clearSave)
{
InitClear();
}
else
{
Load();
}
if (autoSaveEnabled)
{
StartCoroutine(AutoSaveCoroutine());
}
GameController.RegisterSaveManager(this);
}
///
/// Returns an instance from the save database, or creates a new one
///
/// Should implement ISave interface
/// The unique identifier of the object you want to retrieve
///
public T GetSave(int hash) where T : ISave, new()
{
if (!IsSaveLoaded)
{
Debug.LogError("Save file has not been loaded yet");
return default;
}
return SaveDatabase.GetSave(hash);
}
///
/// Returns an instance from the save database, or creates a new one
///
/// Should implement ISave interface
/// The unique identifier of the object you want to retrieve
///
public T GetSave(string uniqueName) where T : ISave, new()
{
return GetSave(uniqueName.GetHashCode());
}
private void InitClear()
{
SaveDatabase = new SaveDatabase();
SaveDatabase.Init();
Debug.Log("New save is created");
IsSaveLoaded = true;
}
private void Load()
{
if (IsSaveLoaded)
return;
if(saveType == SaveType.SaveFile)
{
// Try to read and deserialize file or create new one
SaveDatabase = LoadSave();
SaveDatabase.Init();
Debug.Log("Save file is loaded");
} else
{
var json = PlayerPrefs.GetString("save");
SaveDatabase = JsonUtility.FromJson(json);
if (SaveDatabase == null) SaveDatabase = new SaveDatabase();
SaveDatabase.Init();
Debug.Log("Loaded SaveDatabase from PlayerPrefs");
}
IsSaveLoaded = true;
OnSaveLoaded?.Invoke();
}
private SaveDatabase LoadSave()
{
#if UNITY_WEBGL && !UNITY_EDITOR
string jsonObject = load(SAVE_FILE_NAME);
if(!string.IsNullOrEmpty(jsonObject))
{
try
{
SaveDatabase deserializedObject = JsonUtility.FromJson(jsonObject);
return deserializedObject;
}
catch (System.Exception ex)
{
Debug.LogError(ex.Message);
}
}
return new SaveDatabase();
#else
return SerializationHelper.DeserializePersistent(SAVE_FILE_NAME, useLogs: false);
#endif
}
private IEnumerator SaveCoroutine(bool multithreading = false)
{
var wait = new WaitForSeconds(0.2f);
while (SerializationHelper.IsFileLocked(SAVE_FILE_NAME))
{
yield return wait;
}
if (multithreading)
{
var saveThread = new Thread(() => {
SerializationHelper.SerializePersistent(SaveDatabase, SAVE_FILE_NAME);
});
saveThread.Start();
}
else
{
SerializationHelper.SerializePersistent(SaveDatabase, SAVE_FILE_NAME);
}
Debug.Log("Save file is updated");
saveCoroutine = null;
}
private void ForceSave()
{
if (SaveDatabase == null) return;
SaveDatabase.Flush();
if(saveType == SaveType.PlayerPrefs)
{
PlayerPrefs.SetString("save", JsonUtility.ToJson(SaveDatabase));
PlayerPrefs.Save();
Debug.Log("Save Database is sent to PlayerPrefs");
} else
{
#if UNITY_WEBGL && !UNITY_EDITOR
WebGLSave(SaveDatabase, SAVE_FILE_NAME);
Debug.Log("Save file is updated");
#else
if (!SerializationHelper.IsFileLocked(SAVE_FILE_NAME))
{
SerializationHelper.SerializePersistent(SaveDatabase, SAVE_FILE_NAME);
Debug.Log("Save file is updated");
}
#endif
}
}
///
/// Saves the current state of the game to the file system
///
/// if true, saves the file in another thread. Do not use multitherading in OnDestroy
public void Save(bool multithreading = false)
{
if (SaveDatabase == null) return;
SaveDatabase.Flush();
if (saveType == SaveType.PlayerPrefs)
{
PlayerPrefs.SetString("save", JsonUtility.ToJson(SaveDatabase));
PlayerPrefs.Save();
Debug.Log("Save Database is sent to PlayerPrefs");
} else
{
#if UNITY_WEBGL && !UNITY_EDITOR
WebGLSave(SaveDatabase, SAVE_FILE_NAME);
Debug.Log("Save file is updated");
#else
if (saveCoroutine == null) saveCoroutine = StartCoroutine(SaveCoroutine(multithreading));
#endif
}
}
#if UNITY_WEBGL && !UNITY_EDITOR
private void WebGLSave(SaveDatabase saveDatabase, string fileName)
{
string jsonObject = JsonUtility.ToJson(saveDatabase);
save(fileName, jsonObject);
}
#endif
private IEnumerator AutoSaveCoroutine()
{
var wait = new WaitForSecondsRealtime(autoSaveDelay);
while (true)
{
yield return wait;
Save(true);
}
}
public static void DeleteSaveFile()
{
#if UNITY_WEBGL && !UNITY_EDITOR
deleteItem(SAVE_FILE_NAME);
#else
SerializationHelper.DeletePersistent(SAVE_FILE_NAME);
#endif
PlayerPrefs.DeleteAll();
Debug.Log("Save file is deleted!");
}
private void OnDestroy()
{
ForceSave();
}
private void OnDisable()
{
#if UNITY_WEBGL && !UNITY_EDITOR
ForceSave();
#endif
}
///
/// Android and IOS Phones minimize applications instead of destroying them
///
///
private void OnApplicationFocus(bool focus)
{
#if !UNITY_EDITOR
if(!focus) ForceSave();
#endif
}
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern string load(string keyName);
[DllImport("__Internal")]
private static extern void save(string keyName, string data);
[DllImport("__Internal")]
private static extern void deleteItem(string keyName);
#endif
}
public enum SaveType
{
SaveFile,
PlayerPrefs,
}
}