351 lines
10 KiB
C#
351 lines
10 KiB
C#
|
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
|
|||
|
}
|