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 _persistentStructures = new List(); 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 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 }