initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f1c7553ca7b3bb4399eab9e93b9a6ff
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class GameData
|
||||
{
|
||||
public long lastUpdated;
|
||||
|
||||
public PlayerData player = new PlayerData();
|
||||
|
||||
public List<ShelfData> shelves = new List<ShelfData>();
|
||||
|
||||
// the values defined in this constructor will be the default values
|
||||
// the game starts with when there's no data to load
|
||||
public GameData()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89cbdf7a2dcd8ab40a929072c7493273
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class InventoryData
|
||||
{
|
||||
public int capacity = 0;
|
||||
|
||||
public SerializableDictionary<string, int> items = new SerializableDictionary<string, int>();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bcd40c816f3fb24a8aa90a6438d53a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
[System.Serializable]
|
||||
public class PlayerData
|
||||
{
|
||||
public InventoryData inventory = new InventoryData();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff587676af26ad344af1761388bb9016
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class ShelfData
|
||||
{
|
||||
public string type = "shelf";
|
||||
public Vector2 location = Vector2.zero;
|
||||
public float angle = 0;
|
||||
public InventoryData inventory = new InventoryData();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e220bc2634b66924c99448f4bd3a824c
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class DataPersistenceManager : MonoBehaviour
|
||||
{
|
||||
// [Header("Debugging")]
|
||||
// [SerializeField] private bool initializeDataIfNull = false;
|
||||
|
||||
[Header("File Storage Config")]
|
||||
[SerializeField] private string fileName;
|
||||
[SerializeField] private bool useEncryption;
|
||||
|
||||
[Header("Auto Saving Configuration")]
|
||||
[SerializeField] private float autoSaveTimeSeconds = 60f;
|
||||
|
||||
private GameData gameData = new GameData();
|
||||
private List<IDataPersistence<GameData>> dataPersistenceObjects;
|
||||
private FileDataHandler dataHandler;
|
||||
|
||||
private Coroutine autoSaveCoroutine;
|
||||
|
||||
public static DataPersistenceManager instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null)
|
||||
{
|
||||
Debug.Log("Found more than one Data Persistence Manager in the scene. Destroying the newest one.");
|
||||
Destroy(this.gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
DontDestroyOnLoad(this.gameObject);
|
||||
|
||||
this.dataHandler = new FileDataHandler(Application.persistentDataPath, fileName, useEncryption);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
|
||||
public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
// Start();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
LoadGame();
|
||||
|
||||
// start up the auto saving coroutine
|
||||
if (autoSaveCoroutine != null)
|
||||
{
|
||||
StopCoroutine(autoSaveCoroutine);
|
||||
}
|
||||
autoSaveCoroutine = StartCoroutine(AutoSave());
|
||||
}
|
||||
|
||||
public void NewGame()
|
||||
{
|
||||
Debug.Log("new game");
|
||||
this.gameData = new GameData();
|
||||
}
|
||||
|
||||
public void LoadGame()
|
||||
{
|
||||
Debug.Log("Loading game");
|
||||
|
||||
this.dataPersistenceObjects = FindAllDataPersistenceObjects();
|
||||
|
||||
// load any saved data from a file using the data handler
|
||||
this.gameData = dataHandler.Load();
|
||||
|
||||
// start a new game if the data is null and we're configured to initialize data for debugging purposes
|
||||
if (this.gameData == null)
|
||||
{
|
||||
NewGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// if no data can be loaded, don't continue
|
||||
// if (this.gameData == null)
|
||||
// {
|
||||
// Debug.Log("No data was found. A New Game needs to be started before data can be loaded.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// push the loaded data to all other scripts that need it
|
||||
foreach (IDataPersistence<GameData> dataPersistenceObj in dataPersistenceObjects)
|
||||
{
|
||||
dataPersistenceObj.LoadData(gameData);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveGame()
|
||||
{
|
||||
Debug.Log("Saving game");
|
||||
|
||||
this.dataPersistenceObjects = FindAllDataPersistenceObjects();
|
||||
|
||||
// if we don't have any data to save, log a warning here
|
||||
if (this.gameData == null)
|
||||
{
|
||||
Debug.LogWarning("No data was found. A New Game needs to be started before data can be saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
// pass the data to other scripts so they can update it
|
||||
foreach (IDataPersistence<GameData> dataPersistenceObj in dataPersistenceObjects)
|
||||
{
|
||||
dataPersistenceObj.SaveData(gameData);
|
||||
}
|
||||
|
||||
// timestamp the data so we know when it was last saved
|
||||
gameData.lastUpdated = System.DateTime.Now.ToBinary();
|
||||
|
||||
// save that data to a file using the data handler
|
||||
dataHandler.Save(gameData);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
SaveGame();
|
||||
}
|
||||
|
||||
private List<IDataPersistence<GameData>> FindAllDataPersistenceObjects()
|
||||
{
|
||||
// FindObjectsofType takes in an optional boolean to include inactive gameobjects
|
||||
IEnumerable<IDataPersistence<GameData>> dataPersistenceObjects = FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None)
|
||||
.OfType<IDataPersistence<GameData>>();
|
||||
|
||||
var list = new List<IDataPersistence<GameData>>(dataPersistenceObjects);
|
||||
return list;
|
||||
}
|
||||
|
||||
private IEnumerator AutoSave()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return new WaitForSeconds(autoSaveTimeSeconds);
|
||||
SaveGame();
|
||||
Debug.Log("Auto Saved Game");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a43c513eda07bf94db46b159ebbc8b51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,185 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
public class FileDataHandler
|
||||
{
|
||||
private string dataDirPath = "";
|
||||
private string dataFileName = "";
|
||||
private bool useEncryption = false;
|
||||
private readonly string encryptionCodeWord = "word";
|
||||
private readonly string backupExtension = ".bak";
|
||||
|
||||
public FileDataHandler(string dataDirPath, string dataFileName, bool useEncryption)
|
||||
{
|
||||
this.dataDirPath = dataDirPath;
|
||||
this.dataFileName = dataFileName;
|
||||
this.useEncryption = useEncryption;
|
||||
}
|
||||
|
||||
public GameData Load(bool allowRestoreFromBackup = true)
|
||||
{
|
||||
// use Path.Combine to account for different OS's having different path separators
|
||||
string fullPath = Path.Combine(dataDirPath, dataFileName);
|
||||
GameData loadedData = null;
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
// load the serialized data from the file
|
||||
string dataToLoad = "";
|
||||
using (FileStream stream = new FileStream(fullPath, FileMode.Open))
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(stream))
|
||||
{
|
||||
dataToLoad = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// optionally decrypt the data
|
||||
if (useEncryption)
|
||||
{
|
||||
dataToLoad = EncryptDecrypt(dataToLoad);
|
||||
}
|
||||
|
||||
// deserialize the data from Json back into the C# object
|
||||
loadedData = JsonUtility.FromJson<GameData>(dataToLoad);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// since we're calling Load(..) recursively, we need to account for the case where
|
||||
// the rollback succeeds, but data is still failing to load for some other reason,
|
||||
// which without this check may cause an infinite recursion loop.
|
||||
if (allowRestoreFromBackup)
|
||||
{
|
||||
Debug.LogWarning("Failed to load data file. Attempting to roll back.\n" + e);
|
||||
bool rollbackSuccess = AttemptRollback(fullPath);
|
||||
if (rollbackSuccess)
|
||||
{
|
||||
// try to load again recursively
|
||||
loadedData = Load(false);
|
||||
}
|
||||
}
|
||||
// if we hit this else block, one possibility is that the backup file is also corrupt
|
||||
else
|
||||
{
|
||||
Debug.LogError("Error occured when trying to load file at path: "
|
||||
+ fullPath + " and backup did not work.\n" + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return loadedData;
|
||||
}
|
||||
|
||||
public void Save(GameData data)
|
||||
{
|
||||
// use Path.Combine to account for different OS's having different path separators
|
||||
string fullPath = Path.Combine(dataDirPath, dataFileName);
|
||||
Debug.Log("Saving to " + fullPath);
|
||||
string backupFilePath = fullPath + backupExtension;
|
||||
try
|
||||
{
|
||||
// create the directory the file will be written to if it doesn't already exist
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
|
||||
|
||||
// serialize the C# game data object into Json
|
||||
string dataToStore = JsonUtility.ToJson(data, true);
|
||||
|
||||
// optionally encrypt the data
|
||||
if (useEncryption)
|
||||
{
|
||||
dataToStore = EncryptDecrypt(dataToStore);
|
||||
}
|
||||
|
||||
// write the serialized data to the file
|
||||
using (FileStream stream = new FileStream(fullPath, FileMode.Create))
|
||||
{
|
||||
using (StreamWriter writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.Write(dataToStore);
|
||||
}
|
||||
}
|
||||
|
||||
// verify the newly saved file can be loaded successfully
|
||||
GameData verifiedGameData = Load();
|
||||
// if the data can be verified, back it up
|
||||
if (verifiedGameData != null)
|
||||
{
|
||||
File.Copy(fullPath, backupFilePath, true);
|
||||
}
|
||||
// otherwise, something went wrong and we should throw an exception
|
||||
else
|
||||
{
|
||||
throw new Exception("Save file could not be verified and backup could not be created.");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Error occured when trying to save data to file: " + fullPath + "\n" + e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
string fullPath = Path.Combine(dataDirPath, dataFileName);
|
||||
try
|
||||
{
|
||||
// ensure the data file exists at this path before deleting the directory
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
// delete the profile folder and everything within it
|
||||
Directory.Delete(Path.GetDirectoryName(fullPath), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Tried to delete profile data, but data was not found at path: " + fullPath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Failed to delete data at path: " + fullPath + "\n" + e);
|
||||
}
|
||||
}
|
||||
|
||||
// the below is a simple implementation of XOR encryption
|
||||
private string EncryptDecrypt(string data)
|
||||
{
|
||||
string modifiedData = "";
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
modifiedData += (char) (data[i] ^ encryptionCodeWord[i % encryptionCodeWord.Length]);
|
||||
}
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
private bool AttemptRollback(string fullPath)
|
||||
{
|
||||
bool success = false;
|
||||
string backupFilePath = fullPath + backupExtension;
|
||||
try
|
||||
{
|
||||
// if the file exists, attempt to roll back to it by overwriting the original file
|
||||
if (File.Exists(backupFilePath))
|
||||
{
|
||||
File.Copy(backupFilePath, fullPath, true);
|
||||
success = true;
|
||||
Debug.LogWarning("Had to roll back to backup file at: " + backupFilePath);
|
||||
}
|
||||
// otherwise, we don't yet have a backup file - so there's nothing to roll back to
|
||||
else
|
||||
{
|
||||
throw new Exception("Tried to roll back, but no backup file exists to roll back to.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Error occured when trying to roll back to backup file at: "
|
||||
+ backupFilePath + "\n" + e);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4d960248b76a174e8de8eb7d18afdb5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
public interface IDataPersistenceBase
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public interface IDataPersistence<TData> : IDataPersistenceBase
|
||||
{
|
||||
void LoadData(TData data);
|
||||
|
||||
// The 'ref' keyword was removed from here as it is not needed.
|
||||
// In C#, non-primitive types are automatically passed by reference.
|
||||
void SaveData(TData data);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dadb1cc8e7af5394ca73356b1c0f1289
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e335e31db871f2246b93b4540f64cb92
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class SerializableDictionaryEntry<TKey, TValue>
|
||||
{
|
||||
public TKey key;
|
||||
public TValue value;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class SerializableDictionary<TKey, TValue>
|
||||
: Dictionary<TKey, TValue>,
|
||||
ISerializationCallbackReceiver
|
||||
{
|
||||
[SerializeField]
|
||||
private List<SerializableDictionaryEntry<TKey, TValue>> entries =
|
||||
new List<SerializableDictionaryEntry<TKey, TValue>>();
|
||||
|
||||
public SerializableDictionary() { }
|
||||
|
||||
public SerializableDictionary(Dictionary<TKey, TValue> source)
|
||||
{
|
||||
foreach (var item in source)
|
||||
Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
entries.Clear();
|
||||
foreach (KeyValuePair<TKey, TValue> pair in this)
|
||||
{
|
||||
entries.Add(
|
||||
new SerializableDictionaryEntry<TKey, TValue>()
|
||||
{
|
||||
key = pair.Key,
|
||||
value = pair.Value,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// load the dictionary from lists
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
this.Clear();
|
||||
|
||||
foreach (SerializableDictionaryEntry<TKey, TValue> entry in entries)
|
||||
{
|
||||
this.Add(entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9dc07c3462db23d45a4aa37b7d15244c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user