using System; using System.Collections.Generic; using System.IO; using System.Linq; using Nullframes.Intrigues.Utils; using Nullframes.Intrigues.Graph; using UnityEngine; namespace Nullframes.Intrigues.SaveSystem { [DefaultExecutionOrder(-350)] [AddComponentMenu("Intrigues/Save System")] public class IntrigueSaveSystem : MonoBehaviour { /// /// Indicates whether to pretty print the JSON data for better readability. /// public bool PrettyPrint; /// /// If true, the save file will be encrypted. /// public bool Encrypt; /// /// The password used to encrypt the save file. /// public string encryptionPassword = "My Password"; /// /// Returns the directory where the save files are stored. /// private static string Path => $"{Application.persistentDataPath}/"; /// /// Generates and returns a name for the save file using a unique ID with a .null extension. /// private static string FileNameExtension => "null"; /// /// Retrieves the most recently saved file based on its last write time. /// public SaveFileInfo GET_LATEST_SAVE { get { var fileInfo = new DirectoryInfo(Path).GetFiles(@$"*.{FileNameExtension}") .OrderByDescending(f => f.LastWriteTime).FirstOrDefault(); return fileInfo == null ? null : new SaveFileInfo(System.IO.Path.GetFileNameWithoutExtension(fileInfo.Name), fileInfo); } } /// /// Retrieves all save files sorted by their last write time in descending order. /// public IEnumerable GET_SAVE_FILES { get { return new DirectoryInfo(Path).GetFiles(@$"*.{FileNameExtension}") .OrderByDescending(f => f.LastWriteTime) .Select(f => new SaveFileInfo(System.IO.Path.GetFileNameWithoutExtension(f.Name), f)).ToList(); } } /// /// Returns a FileInfo object for a given save file name. /// /// Name of the save file. public FileInfo SaveFileInfo(string saveFileName) => new DirectoryInfo(Path) .GetFiles(@$"*.{FileNameExtension}") .FirstOrDefault(f => System.IO.Path.GetFileNameWithoutExtension(f.Name) == saveFileName); /// /// Checks if a save file with the given name exists. /// /// Name of the save file to check. public bool SaveFileExists(string saveFileName) => SaveFileInfo(saveFileName) != null; private SaveData loadedData; private string currentSaveFile; public static IntrigueSaveSystem Instance; /// /// Checks if any save file exists. /// public bool AnySaveFileExists => GET_LATEST_SAVE != null; /// /// Provides access to the loaded save data. /// public SaveData Data => loadedData; /// /// Returns the name of the currently active save file. /// public string ActiveSaveFile => currentSaveFile; /// /// Event triggered when all Runtime Actors have been loaded. /// public static event Action onRuntimeActorsLoaded; /// /// Event triggered when data is saved. /// public static event Action onSave; /// /// Event triggered when data is loaded. /// public static event Action onLoad; private void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(this); return; } Destroy(this); } /// /// Clears the loaded save data. Typically used when starting a new game or similar scenarios. /// public void NewGame() { loadedData = null; IM.Load(null); } /// /// Saves the game data. If a file name is provided, it will use that; otherwise, it might use a default naming mechanism. /// /// Optional name for the save file. public string Save(string fileName) { if (string.IsNullOrEmpty(fileName)) { fileName = null; } loadedData = new SaveData(); //Save Actor Data foreach (var actor in IM.ActorDictionary.Values) { var actorData = new ActorData(); actorData.Init(actor); loadedData.actorData.Add(actorData); } //Save Game Data var gameData = new GameData(); gameData.Init(); loadedData.gameData = gameData; var jsonString = JsonUtility.ToJson(loadedData, PrettyPrint); var fName = fileName ?? NullUtils.GenerateID(); File.WriteAllText(Path + fName + "." + FileNameExtension, Encrypt ? Encryption.Encrypt(jsonString, encryptionPassword) : jsonString); currentSaveFile = fName; NDebug.Log($"Save successful. Save name: '{currentSaveFile}'"); onSave?.Invoke(); return fName; } /// /// Saves the game data. If a file name is provided, it will use that; otherwise, it might use a default naming mechanism. /// public void Save() => Save(null); /// /// Overwrites the currently active save file. If there's no active save file, it creates a new save. /// public void SaveOverwrite() { if (string.IsNullOrEmpty(currentSaveFile)) { Save(); return; } NDebug.Log($"The save file named '{currentSaveFile}' was overwritten."); Save(currentSaveFile); } /// /// Loads the game data from the specified save file. /// /// The name of the save file to load. public void Load(string fileName) { if (string.IsNullOrEmpty(fileName)) return; fileName = System.IO.Path.GetFileNameWithoutExtension(fileName); if (!SaveFileExists(fileName)) return; string content = File.ReadAllText(Path + fileName + "." + FileNameExtension); loadedData = JsonUtility.FromJson(Encryption.Decrypt(content, encryptionPassword)); if (loadedData == null) return; IM.Load(loadedData); currentSaveFile = fileName; IRuntimeLoader.Load(); foreach (var actor in IM.ActorDictionary.Values) { var actorSaveData = loadedData.actorData.Find(a => a.id == actor.ID); if (actorSaveData == null) continue; actor.LoadActor(actorSaveData); } NDebug.Log($"The save named '{currentSaveFile}' was successfully loaded."); onLoad?.Invoke(); } /// /// Loads the runtime actors. /// public void LoadRuntimeActors() { if (loadedData == null) return; //Create Runtime Actors foreach (var runtimeActor in loadedData.actorData) // Where(a => a.classType == 1) { if (IM.Actors.Any(a => a.ID == runtimeActor.id)) continue; RuntimeActor.LoadRuntimeActor(runtimeActor); } onRuntimeActorsLoaded?.Invoke(); NDebug.Log("RuntimeActors were successfully loaded."); } /// /// Loads the most recent save based on its last write time. /// public void LoadLatest() { Load(GET_LATEST_SAVE.Name); } public void Delete(string saveFileName) { if (SaveFileExists(saveFileName)) { File.Delete(Path + saveFileName + "." + FileNameExtension); NDebug.Log($"The save file named '{saveFileName}' was deleted."); } } public void DeleteCurrent() { if (SaveFileExists(currentSaveFile)) { File.Delete(Path + currentSaveFile + "." + FileNameExtension); NDebug.Log($"The save file named '{currentSaveFile}' was deleted."); } } } [Serializable] public class SaveData { public List actorData = new(); public GameData gameData; } [Serializable] public class CultureData { public string id; public string name; public Sprite icon; public string description; public List femaleNameList; public List maleNameList; public void Init(Culture culture) { id = culture.ID; name = culture.CultureName; icon = culture.Icon; description = culture.DescriptionWithoutLocalisation; femaleNameList = new List(culture.FemaleNames); maleNameList = new List(culture.MaleNames); } } [Serializable] public class FamilyData { public string id; public string name; public Sprite icon; public string description; public string cultureId; public List policies; public List variableData; public void Init(Family family) { id = family.ID; name = family.FamilyName; icon = family.Icon; description = family.DescriptionWithoutLocalisation; cultureId = family.Culture?.ID; policies = new List(); variableData = new List(); foreach (var variable in family.Variables) { var varData = new NVarData(); varData.Init(variable); variableData.Add(varData); } foreach (var policy in family.Policies) { policies.Add(policy.ID); } } } [Serializable] public class ClanData { public string id; public string name; public Sprite icon; public string description; public string cultureId; public List policies; public List variableData; public void Init(Clan clan) { id = clan.ID; name = clan.ClanName; icon = clan.Icon; description = clan.DescriptionWithoutLocalisation; cultureId = clan.Culture?.ID; policies = new List(); variableData = new List(); foreach ( var variable in clan.Variables ) { var varData = new NVarData(); varData.Init(variable); variableData.Add(varData); } foreach (var policy in clan.Policies) { policies.Add(policy.ID); } } } [Serializable] public class PolicyData { public string id; public string name; public string description; public PolicyType type; public Sprite icon; public void Init(Policy policy) { id = policy.ID; name = policy.PolicyName; description = policy.DescriptionWithoutLocalisation; type = policy.Type; icon = policy.Icon; } } [Serializable] public class RoleData { public string id; public string name; public string description; public string titleForFemale; public string titleForMale; public string heirFilterId; public bool inheritance; public int capacity; public int priority; public Sprite icon; public void Init(Role role) { id = role.ID; name = role.RoleName; description = role.DescriptionWithoutLocalisation; titleForFemale = role.TitleForFemale; titleForMale = role.TitleForMale; heirFilterId = role.FilterID; capacity = role.Capacity; inheritance = role.Inheritance; priority = role.Priority; icon = role.Icon; } } [Serializable] public class GameData { public List variableData; public List cultureData; public List policyData; public List familyData; public List clanData; public List roleData; public void Init() { variableData = new List(); cultureData = new List(); policyData = new List(); roleData = new List(); familyData = new List(); clanData = new List(); foreach (var variable in IM.Variables) { var data = new NVarData(); data.Init(variable); variableData.Add(data); } foreach (var culture in IM.Cultures) { var data = new CultureData(); data.Init(culture); cultureData.Add(data); } foreach (var policy in IM.Policies) { var data = new PolicyData(); data.Init(policy); policyData.Add(data); } foreach (var role in IM.Roles) { var data = new RoleData(); data.Init(role); roleData.Add(data); } foreach (var family in IM.Families) { var data = new FamilyData(); data.Init(family); familyData.Add(data); } foreach (var clan in IM.Clans) { var data = new ClanData(); data.Init(clan); clanData.Add(data); } } } [Serializable] public class ActorData { public string id; public int classType; public bool isPlayer; public bool inheritor; public string name; public int age; public string culture; public string clan; public string role; public Actor.IGender gender; public Actor.IState state; public List parents; public List children; public List spouses; public string family; public string familyOrigin; public string overrideHeir; public Sprite portrait; public List variableData; public List schemeData; public List relationVariableData; public void Init(Actor actor) { id = actor.ID; name = actor.Name; age = actor.Age; gender = actor.Gender; inheritor = actor.Inheritor; culture = actor.Culture?.ID; clan = actor.Clan?.ID; role = actor.Role?.ID; state = actor.State; overrideHeir = actor.HeirOverridden ? actor.Heir.ID : null; portrait = actor.Portrait; familyOrigin = actor.Origin?.ID; family = actor.Family?.ID; isPlayer = actor.IsPlayer; classType = actor switch { InitialActor => 0, RuntimeActor => 1, _ => classType }; parents = new List(); children = new List(); spouses = new List(); variableData = new List(); relationVariableData = new List(); schemeData = new List(); foreach (var parent in actor.Parents()) { parents.Add(parent.ID); } foreach (var child in actor.Children()) { children.Add(child.ID); } foreach (var spouse in actor.Spouses()) { spouses.Add(spouse.ID); } foreach (var variable in actor.Variables) { var varData = new NVarData(); varData.Init(variable); variableData.Add(varData); } foreach (var specificVariable in actor.RelationVariables) { var specificData = new RelationVariableData() { actorId = specificVariable.Key.ID, varData = new List() }; foreach (var variable in specificVariable.Value) { var varData = new NVarData(); varData.Init(variable); specificData.varData.Add(varData); } relationVariableData.Add(specificData); } foreach (var scheme in actor.Schemes) { if (scheme.Schemer == null) continue; var _schemeData = new SchemeData(); _schemeData.Init(scheme); this.schemeData.Add(_schemeData); } } } [Serializable] public class RelationVariableData { public string actorId; public List varData; } [Serializable] public class NVarData { public string id; public string name; public NType type; public string stringValue; public int integerValue; public bool boolValue; public float floatValue; public string actorId; public UnityEngine.Object objectValue; public int enumIndex; public void Init(NVar variable) { id = variable.id; name = variable.name; type = variable.Type; switch (variable) { case NString nString: stringValue = nString.Value; break; case NInt nInt: integerValue = nInt.Value; break; case NBool nBool: boolValue = nBool.Value; break; case NFloat nFloat: floatValue = nFloat.Value; break; case NObject nObject: objectValue = nObject.Value; break; case NEnum nEnum: enumIndex = nEnum.Index; break; case NActor nActor: actorId = nActor.ActorID; break; } } } [Serializable] public class SchemeData { public string id; public string targetId; public bool isPaused; public string objective; public bool isEnded; public List variables; public List activeNodes; public void Init(Scheme scheme) { id = scheme.ID; targetId = scheme.Schemer.Target != null ? scheme.Schemer.Target.ID : null; objective = scheme.CurrentObjective; if (scheme.Schemer != null) { isPaused = scheme.Schemer.IsPaused; isEnded = scheme.Schemer.IsEnded; } activeNodes = new List(); foreach (var nodeInfo in scheme.Schemer.ActiveNodeList) { var nInfo = new NodeInfoData(); nInfo.Init(nodeInfo); activeNodes.Add(nInfo); } variables = new List(); foreach (var _nVar in scheme.Schemer.Variables) { var nVar = new NVarData(); nVar.Init(_nVar); nVar.id = _nVar.name; variables.Add(nVar); } } } [Serializable] public class NodeInfoData { public string id; public string nodeId; public bool bgWorker; public string inputName; public string actorId; public string actorId1; public string actorId2; public string clanId; public string familyId; public float time; public int index; public string sequencerId; public string repeaterId; public string validatorId; public int repeatCount; public List delays; public List indexes; public void Init(NodeInfo nodeInfo) { id = nodeInfo.id; nodeId = nodeInfo.node.ID; bgWorker = nodeInfo.bgWorker; actorId = nodeInfo.actor != null ? nodeInfo.actor.ID : null; actorId1 = nodeInfo.dualActor.Item1 != null ? nodeInfo.dualActor.Item1.ID : null; actorId2 = nodeInfo.dualActor.Item2 != null ? nodeInfo.dualActor.Item2.ID : null; clanId = nodeInfo.clan?.ID; familyId = nodeInfo.family?.ID; inputName = nodeInfo.inputName; time = nodeInfo.time; index = nodeInfo.index; sequencerId = nodeInfo.sequencer?.id; repeaterId = nodeInfo.repeater?.id; validatorId = nodeInfo.validator?.id; repeatCount = nodeInfo.repeatCount; delays = new List(nodeInfo.delays); indexes = new List(nodeInfo.indexes); } } public class SaveFileInfo { public string Name { get; } public FileInfo FileInfo { get; } public SaveFileInfo(string name, FileInfo fileInfo) { Name = name; FileInfo = fileInfo; } } }