hellbound/Assets/Scripts/Game/Save/GameData.cs

351 lines
10 KiB
C#
Raw Normal View History

2021-11-26 11:16:25 +03:00
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using game;
public partial class GameData /*: IServerInstallation*/
{
public static readonly string StateFileExt = ".dat";
//NOTE: they are property accessors, not "static readonly" fields
//because you can't access persistentDataPath in static constructor
public static string StateFileDir => Application.persistentDataPath;
public static string DefaultDataFile => $"{StateFileDir}/{Settings.DATA_FILE}";
public string DataFile { get; private set; }
public string AesSecret { get; private set; }
public string ClientVersion { get; private set; }
public bool SaveInProgress { get; private set; }
public string ExtID => _lastSave.local.extId;
public uint PlayerID => _lastSave.local.player.id;
private int _forbidSaves = 0;
private DataSave _lastSave = new DataSave();
private readonly List<IPersistent> _persistentStructures = new List<IPersistent>();
public GameData(string clientVersion)
: this(DefaultDataFile, clientVersion)
{
}
public GameData(string dataFile, string clientVersion)
{
DataFile = dataFile;
ClientVersion = clientVersion;
AesSecret = SystemInfo.deviceUniqueIdentifier.Reverse();
_lastSave.reset();
}
public SaveDiagnostics CollectSaveDiagnostics(PersistError persist_error)
{
var d = new SaveDiagnostics();
d.LastSaveAppVersion = PlayerPrefs.GetString("last_save_app_version", "");
d.LastSaveFile = PlayerPrefs.GetString("last_save_file", "");
d.LastSaveDeviceID = PlayerPrefs.GetString("last_save_device_id", "");
d.SaveFile = DataFile;
d.SaveDir = Path.GetDirectoryName(DataFile);
d.IsDirWritable = IsDirectoryWritable(d.SaveDir);
d.PersistError = persist_error;
return d;
}
public void RememberSuccessfulSave()
{
PlayerPrefs.SetString("last_save_ext_id", ExtID);
PlayerPrefs.SetString("last_save_file", DataFile);
PlayerPrefs.SetString("last_save_device_id", SystemInfo.deviceUniqueIdentifier);
PlayerPrefs.SetString("last_save_app_version", Settings.VERSION);
}
public string GetLastSuccessfulSaveExtId()
{
return PlayerPrefs.GetString("last_save_ext_id", "");
}
void ResetLastSuccessfulSave()
{
PlayerPrefs.DeleteKey("last_save_ext_id");
PlayerPrefs.DeleteKey("last_save_file");
PlayerPrefs.DeleteKey("last_save_device_id");
PlayerPrefs.DeleteKey("last_save_app_version");
}
public static bool IsDirectoryWritable(string dir)
{
try
{
using (var fs = File.Create(
Path.Combine(dir, Path.GetRandomFileName()),
1,
FileOptions.DeleteOnClose)
){}
return true;
}
catch
{
return false;
}
}
public bool CanSave()
{
return _forbidSaves == 0 && _persistentStructures.Count > 0;
}
public void ForbidSaves()
{
_forbidSaves++;
Log.Debug($"ForbidSaves, counter: {_forbidSaves}");
}
public void AllowSaves()
{
_forbidSaves--;
if (_forbidSaves < 0)
_forbidSaves = 0;
Log.Debug($"AllowSaves, counter: {_forbidSaves}");
}
public void ResetForbidSaves()
{
_forbidSaves = 0;
Log.Debug("All saves allowed");
}
public bool IsAttachedToServer()
{
return string.IsNullOrEmpty(ExtID) == false;
}
private bool IsRemoteStateSynced()
{
return string.IsNullOrEmpty(_lastSave.remote.extId) == false;
}
//TODO: not needed at the moment
//public void AttachToServer(uint player_id, string ext_id, uint reg_time)
//{
// Log.Warn("Attached to server as " + player_id + ", ext.id " + ext_id);
// last_save.local.ext_id = ext_id;
// last_save.local.player.id = player_id;
// last_save.local.player.reg_time = reg_time;
// last_save.remote.reset();
//}
private static DataGame CreateInitial()
{
return new DataGame();
}
public void RegisterPersistentStructure(IPersistent persistent)
{
Error.Assert(persistent != null);
_persistentStructures.Add(persistent);
}
public void RegisterPersistentStructures(params IPersistent[] structures)
{
for (int i = 0; i < structures.Length; i++)
RegisterPersistentStructure(structures[i]);
}
public void UnregisterPersistentStructures()
{
_persistentStructures.Clear();
}
public void Load()
{
var save = new DataSave();
if (Persister.LoadFromFile(ref save, DataFile, AesSecret) != 0)
return;
Load(save);
}
private void Load(DataSave save)
{
if (!ValidateSave(save))
{
Log.Warn("GameData.Load: State validation error, resetting remote state");
save.remote.reset();
}
_lastSave = save;
Log.Debug("GameData.Load: Loaded save with ext.id " + ExtID);
for (int i = 0; i < _persistentStructures.Count; i++)
_persistentStructures[i].Load(_lastSave.local);
}
public LoadOrCreateResult TryLoadOrCreateNew()
{
var save = new DataSave();
var err = Persister.LoadFromFile(ref save, DataFile, AesSecret);
var res = new LoadOrCreateResult();
res.PersistError = err;
res.Status = LoadOrCreateStatus.LOAD;
if (err != 0)
{
save.local = CreateInitial();
save.remote = (DataGame) save.local.clone();
res.Status = LoadOrCreateStatus.NEW;
}
Load(save);
return res;
}
private void FillState(ref DataGame new_state)
{
new_state.reset();
for (int i = 0; i < _persistentStructures.Count; i++)
_persistentStructures[i].Save(new_state);
//NOTE: need to preserve existing data
new_state.extId = _lastSave.local.extId;
new_state.player.id = _lastSave.local.player.id;
}
public PersistError SaveLocal()
{
PersistError err = new PersistError();
if (!CanSave())
return err;
var new_state = new DataGame();
FillState(ref new_state);
//NOTE: not checking delta diff here on purpose,
// this is a save which must be done
// for sure
var new_save = _lastSave;
new_save.local = new_state;
err = Persister.SaveToFile(new_save, DataFile, AesSecret);
if (err != 0)
_lastSave = new_save;
return err;
}
public void ResetProgress(bool preserve_identity)
{
//preserving essential stuff if it's needed by keep_ext_id flag
var ext_id = _lastSave.local.extId;
var player_id = _lastSave.local.player.id;
_lastSave.local = CreateInitial();
if (preserve_identity)
{
//..and restoring it back if necessary
_lastSave.local.extId = ext_id;
_lastSave.local.player.id = player_id;
}
else
{
//disabling emergency restore
ResetLastSuccessfulSave();
}
_lastSave.remote.reset();
var err = Persister.SaveToFile(_lastSave, DataFile, AesSecret);
if (err == 0)
Log.Warn("Progress was reset");
//we unregister them so that on the next save no data will be
//collected
UnregisterPersistentStructures();
}
//TODO: not needed at the moment
//async public UniTask<PersistError> Restore(bool keep_my_data, DataGame restored)
//{
// if(save_in_progress)
// await UniTask.WaitUntil(() => !save_in_progress);
// if(!keep_my_data)
// {
// Error.Assert(restored.player.id > 0, "Bad id");
// Error.Assert(!string.IsNullOrEmpty(restored.ext_id), "Bad ext.id");
// Log.Warn("Restoring from player id " + restored.player.id + ", ext.id " + restored.ext_id);
// last_save.local.copyFrom(restored);
// last_save.remote.copyFrom(restored);
// }
// else
// {
// Log.Warn("Keeping my data as player id " + PLAYER_ID);
// //we need to make the full remote save next time
// last_save.remote.reset();
// }
// return Persister.SaveToFile(last_save, DATA_FILE, AES_SEKRET);
//}
public static bool ValidateSave(DataSave save)
{
return true;
}
#if UNITY_EDITOR
public static void ClearAll()
{
Error.Assert(!UnityEditor.EditorApplication.isPlaying, "Removed state files might be rewritten by running game");
DeleteAllExceptBackupSaves();
PlayerPrefs.SetString("last_save_file", "");
PlayerPrefs.SetString("last_save_ext_id", "");
}
static void DeleteAllExceptBackupSaves()
{
string[] files = Directory.GetFiles(StateFileDir);
foreach (string path in files)
{
File.Delete(path);
}
string[] dirs = Directory.GetDirectories(StateFileDir);
foreach (string path in dirs)
{
if (path.EndsWith("/backup"))
continue;
Directory.Delete(path, recursive: true);
}
Directory.Delete(Application.temporaryCachePath, recursive: true);
}
public PersistError DevWriteToFile(string path, DataGame state)
{
var save = new DataSave();
save.reset();
save.local.copyFrom(state);
save.remote.copyFrom(state);
return Persister.SaveToFile(save, path, AesSecret);
}
public DataGame DevGetState()
{
return _lastSave.local;
}
#endif
}