using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Xml; using UnityEngine; using UnityEngine.SceneManagement; namespace OctoCore.Save { /// /// Save system module for managing persistent game data. /// Handles saving and loading of boolean, integer, float, string, and vector data to XML files. /// [System.Serializable] public class OctoSave : IOctoModule { public class Data { [HideInInspector] public string Id; } [System.Serializable] public class DataBool : Data { [OctoReadOnly] public global::OctoCore.Save.Data.OctoDataBool Data; public bool Value; public Action OnValueChanged; public void Set(bool value) { bool valueChanged = Value != value; Value = value; if (valueChanged) OnValueChanged?.Invoke(Value); } public void Set(OctoVirtual.OctoVirtualBool virtualBool) { if (virtualBool != null) Set(virtualBool.Value); else if (OctoCore.Instance.Development.Log) Debug.Log($"OctoVirtualBool is null"); } } [System.Serializable] public class DataInt : Data { [OctoReadOnly] public global::OctoCore.Save.Data.OctoDataInt Data; public int Value; public Action OnValueChanged; public void Set(int value) { bool valueChanged = Value != value; Value = value; if (valueChanged) OnValueChanged?.Invoke(Value); } public void Set(OctoVirtual.OctoVirtualInt virtualInt) { if (virtualInt != null) Set(virtualInt.Value); else if (OctoCore.Instance.Development.Log) Debug.Log($"OctoVirtualInt is null"); } } [System.Serializable] public class DataFloat : Data { [OctoReadOnly] public global::OctoCore.Save.Data.OctoDataFloat Data; public float Value; public Action OnValueChanged; public void Set(float value) { bool valueChanged = Value != value; Value = value; if (valueChanged) OnValueChanged?.Invoke(Value); } public void Set(OctoVirtual.OctoVirtualFloat virtualFloat) { if (virtualFloat != null) Set(virtualFloat.Value); else if (OctoCore.Instance.Development.Log) Debug.Log($"OctoVirtualFloat is null"); } } [System.Serializable] public class DataString : Data { [OctoReadOnly] public global::OctoCore.Save.Data.OctoDataString Data; public string Value; public Action OnValueChanged; public void Set(string value) { bool valueChanged = Value != value; Value = value; if (valueChanged) OnValueChanged?.Invoke(Value); } public void Set(OctoVirtual.OctoVirtualString virtualString) { if (virtualString != null) Set(virtualString.Value); else if (OctoCore.Instance.Development.Log) Debug.Log($"OctoVirtualString is null"); } } [System.Serializable] public class DataVector : Data { [OctoReadOnly] public global::OctoCore.Save.Data.OctoDataVector Data; public Vector4 Value; public Action OnValueChanged; public void Set(Vector4 value) { bool valueChanged = Value != value; Value = value; if (valueChanged) OnValueChanged?.Invoke(Value); } public void Set(OctoVirtual.OctoVirtualVector virtualVector) { if (virtualVector != null) Set(virtualVector.Value); else if (OctoCore.Instance.Development.Log) Debug.Log($"OctoVirtualVector is null"); } } public DataBool[] Booleans; public DataInt[] Integers; public DataFloat[] Floats; public DataString[] Strings; public DataVector[] Vectors; private Dictionary _booleansId; private Dictionary _integersId; private Dictionary _floatsId; private Dictionary _stringsId; private Dictionary _vectorsId; public void Init(OctoSettings settings) { if (!settings) { Debug.Log("No Settings Registered."); return; } #if UNITY_EDITOR // Get all data settings.GetAllDatas(); #endif // Init values List tmpBooleans = new List(); List tmpIntegers = new List(); List tmpFloats = new List(); List tmpStrings = new List(); List tmpVectors = new List(); foreach (global::OctoCore.Save.Data.OctoDataBool dataBool in settings.Save.Booleans) tmpBooleans.Add(new DataBool() { Id = dataBool.Id, Data = dataBool, Value = dataBool.Value }); foreach (global::OctoCore.Save.Data.OctoDataInt dataInt in settings.Save.Integers) tmpIntegers.Add(new DataInt() { Id = dataInt.Id, Data = dataInt, Value = dataInt.Value }); foreach (global::OctoCore.Save.Data.OctoDataFloat dataFloat in settings.Save.Floats) tmpFloats.Add(new DataFloat() { Id = dataFloat.Id, Data = dataFloat, Value = dataFloat.Value }); foreach (global::OctoCore.Save.Data.OctoDataString dataString in settings.Save.Strings) tmpStrings.Add(new DataString() { Id = dataString.Id, Data = dataString, Value = dataString.Value }); foreach (global::OctoCore.Save.Data.OctoDataVector dataVector in settings.Save.Vectors) tmpVectors.Add(new DataVector() { Id = dataVector.Id, Data = dataVector, Value = dataVector.Value }); Booleans = tmpBooleans.ToArray(); Integers = tmpIntegers.ToArray(); Floats = tmpFloats.ToArray(); Strings = tmpStrings.ToArray(); Vectors = tmpVectors.ToArray(); tmpBooleans.Clear(); tmpIntegers.Clear(); tmpFloats.Clear(); tmpStrings.Clear(); tmpVectors.Clear(); // Load values Load(); // Adress datas _booleansId = new Dictionary(); _integersId = new Dictionary(); _floatsId = new Dictionary(); _stringsId = new Dictionary(); _vectorsId = new Dictionary(); for (int i = 0; i < Booleans.Length; i++) _booleansId.Add(Booleans[i].Id, i); for (int i = 0; i < Integers.Length; i++) _integersId.Add(Integers[i].Id, i); for (int i = 0; i < Floats.Length; i++) _floatsId.Add(Floats[i].Id, i); for (int i = 0; i < Strings.Length; i++) _stringsId.Add(Strings[i].Id, i); for (int i = 0; i < Vectors.Length; i++) _vectorsId.Add(Vectors[i].Id, i); // Callback OctoEvents if (OctoEvents.Instance) OctoEvents.Instance.SetSaveInitialized(); } public void Tick() { } public void OnSceneChanged(Scene current, Scene next) { } public void OnQuit() { Upgrade(); } public void Load() { string path = GetPath(); bool saveLoaded = false; if (File.Exists(path)) { OctoVirtual virtualSave = new OctoVirtual { Booleans = ReadGroup(path, "Booleans"), Integers = ReadGroup(path, "Integers"), Floats = ReadGroup(path, "Floats"), Strings = ReadGroup(path, "Strings"), Vectors = ReadGroup(path, "Vectors") }; foreach (DataBool dataBool in Booleans) dataBool.Set(virtualSave.Booleans.Where(v => v.Id == dataBool.Id).FirstOrDefault()); foreach (DataInt dataInt in Integers) dataInt.Set(virtualSave.Integers.Where(v => v.Id == dataInt.Id).FirstOrDefault()); foreach (DataFloat dataFloat in Floats) dataFloat.Set(virtualSave.Floats.Where(v => v.Id == dataFloat.Id).FirstOrDefault()); foreach (DataString dataString in Strings) dataString.Set(virtualSave.Strings.Where(v => v.Id == dataString.Id).FirstOrDefault()); foreach (DataVector dataVector in Vectors) dataVector.Set(virtualSave.Vectors.Where(v => v.Id == dataVector.Id).FirstOrDefault()); saveLoaded = true; if (OctoCore.Instance.Development.Log) { if (saveLoaded) Debug.Log($"Save read from: {path}"); else Debug.Log($"Save corrupted: {path}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"No save found at: {path}"); } T[] ReadGroup(string xmlPath, string groupName) where T : OctoVirtual, new() { List list = new List(); using (XmlReader reader = XmlReader.Create(xmlPath)) { // Jump to node while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == groupName) { string id = null; string value = null; while (reader.Read()) { // Stop if end of group reached if (reader.NodeType == XmlNodeType.EndElement && reader.Name == groupName) break; if (reader.NodeType == XmlNodeType.Element) { if (reader.Name == "ID") id = reader.ReadElementContentAsString(); else if (reader.Name == "Value") value = reader.ReadElementContentAsString(); // Create entry if (id != null && value != null) { T entry = new T { Id = id }; switch (entry) { case OctoVirtual.OctoVirtualBool virtualBool: virtualBool.Value = bool.Parse(value); break; case OctoVirtual.OctoVirtualInt virtualInt: virtualInt.Value = int.Parse(value); break; case OctoVirtual.OctoVirtualFloat virtualFloat: virtualFloat.Value = float.Parse(value); break; case OctoVirtual.OctoVirtualString virtualString: virtualString.Value = value; break; case OctoVirtual.OctoVirtualVector virtualVector: virtualVector.Value = StringToVector4(value); break; } list.Add(entry); id = null; value = null; } } } break; } } } return list.ToArray(); } Vector4 StringToVector4(string s) { s = s.Trim('(', ')').Replace(" ", ""); string[] parts = s.Split(','); if (parts.Length != 4) { return Vector4.zero; } else { return new Vector4( float.Parse(parts[0], CultureInfo.InvariantCulture), float.Parse(parts[1], CultureInfo.InvariantCulture), float.Parse(parts[2], CultureInfo.InvariantCulture), float.Parse(parts[3], CultureInfo.InvariantCulture) ); } } } public void Upgrade() { // Set virtual save OctoVirtual virtualSave = new OctoVirtual() { Booleans = new OctoVirtual.OctoVirtualBool[Booleans.Length], Integers = new OctoVirtual.OctoVirtualInt[Integers.Length], Floats = new OctoVirtual.OctoVirtualFloat[Floats.Length], Strings = new OctoVirtual.OctoVirtualString[Strings.Length], Vectors = new OctoVirtual.OctoVirtualVector[Vectors.Length] }; for (int i = 0; i < virtualSave.Booleans.Length; i++) { virtualSave.Booleans[i] = new OctoVirtual.OctoVirtualBool(); virtualSave.Booleans[i].Id = Booleans[i].Id; virtualSave.Booleans[i].Value = Booleans[i].Value; } for (int i = 0; i < virtualSave.Integers.Length; i++) { virtualSave.Integers[i] = new OctoVirtual.OctoVirtualInt(); virtualSave.Integers[i].Id = Integers[i].Id; virtualSave.Integers[i].Value = Integers[i].Value; } for (int i = 0; i < virtualSave.Floats.Length; i++) { virtualSave.Floats[i] = new OctoVirtual.OctoVirtualFloat(); virtualSave.Floats[i].Id = Floats[i].Id; virtualSave.Floats[i].Value = Floats[i].Value; } for (int i = 0; i < virtualSave.Strings.Length; i++) { virtualSave.Strings[i] = new OctoVirtual.OctoVirtualString(); virtualSave.Strings[i].Id = Strings[i].Id; virtualSave.Strings[i].Value = Strings[i].Value; } for (int i = 0; i < virtualSave.Vectors.Length; i++) { virtualSave.Vectors[i] = new OctoVirtual.OctoVirtualVector(); virtualSave.Vectors[i].Id = Vectors[i].Id; virtualSave.Vectors[i].Value = Vectors[i].Value; } // Save file string path = GetPath(); using (XmlWriter writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true })) { writer.WriteStartDocument(); writer.WriteStartElement("OctoSave"); void WriteGroup(string groupName, T[] array) where T : OctoVirtual { if (array == null || array.Length == 0) return; writer.WriteStartElement(groupName); foreach (var item in array) { writer.WriteElementString("ID", item.Id); switch (item) { case OctoVirtual.OctoVirtualBool virtualBool: writer.WriteElementString("Value", virtualBool.Value.ToString().ToLower()); break; case OctoVirtual.OctoVirtualInt virtualInt: writer.WriteElementString("Value", virtualInt.Value.ToString()); break; case OctoVirtual.OctoVirtualFloat virtualFloat: writer.WriteElementString("Value", virtualFloat.Value.ToString()); break; case OctoVirtual.OctoVirtualString virtualString: writer.WriteElementString("Value", virtualString.Value); break; case OctoVirtual.OctoVirtualVector virtualVector: writer.WriteElementString("Value", virtualVector.Value.ToString()); break; } } writer.WriteEndElement(); } WriteGroup("Booleans", virtualSave.Booleans); WriteGroup("Integers", virtualSave.Integers); WriteGroup("Floats", virtualSave.Floats); WriteGroup("Strings", virtualSave.Strings); WriteGroup("Vectors", virtualSave.Vectors); writer.WriteEndElement(); writer.WriteEndDocument(); } if (OctoCore.Instance.Development.Log) Debug.Log($"Saved to: {path}"); } public void Unsubscribe(global::OctoCore.Save.Data.OctoData data, Action action) { Subscribe(data, action, false); } public void Subscribe(global::OctoCore.Save.Data.OctoData data, Action handler, bool subscribe = true) { if (data == null || string.IsNullOrEmpty(data.Id)) { if (OctoCore.Instance.Development.Log) Debug.Log("Data is null or has no ID"); return; } switch (data) { case global::OctoCore.Save.Data.OctoDataBool _ when _booleansId.TryGetValue(data.Id, out int iBool): if (subscribe) Booleans[iBool].OnValueChanged += handler as Action; else Booleans[iBool].OnValueChanged -= handler as Action; break; case global::OctoCore.Save.Data.OctoDataInt _ when _integersId.TryGetValue(data.Id, out int iInt): if (subscribe) Integers[iInt].OnValueChanged += handler as Action; else Integers[iInt].OnValueChanged -= handler as Action; break; case global::OctoCore.Save.Data.OctoDataFloat _ when _floatsId.TryGetValue(data.Id, out int iFloat): if (subscribe) Floats[iFloat].OnValueChanged += handler as Action; else Floats[iFloat].OnValueChanged -= handler as Action; break; case global::OctoCore.Save.Data.OctoDataString _ when _stringsId.TryGetValue(data.Id, out int iString): if (subscribe) Strings[iString].OnValueChanged += handler as Action; else Strings[iString].OnValueChanged -= handler as Action; break; case global::OctoCore.Save.Data.OctoDataVector _ when _vectorsId.TryGetValue(data.Id, out int iVector): if (subscribe) Vectors[iVector].OnValueChanged += handler as Action; else Vectors[iVector].OnValueChanged -= handler as Action; break; default: if (OctoCore.Instance.Development.Log) Debug.Log($"Unsupported or missing data: {data.Id}"); break; } } #region GET public bool Get(global::OctoCore.Save.Data.OctoDataBool dataBool) { if (dataBool) { if (_booleansId.ContainsKey(dataBool.Id)) { return Booleans[_booleansId[dataBool.Id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Bool data not found: {dataBool.Id}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Bool data is null"); } return false; } public int Get(global::OctoCore.Save.Data.OctoDataInt dataInt) { if (dataInt) { if (_integersId.ContainsKey(dataInt.Id)) { return Integers[_integersId[dataInt.Id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data not found: {dataInt.Id}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data is null"); } return 0; } public float Get(global::OctoCore.Save.Data.OctoDataFloat dataFloat) { if (dataFloat) { if (_floatsId.ContainsKey(dataFloat.Id)) { return Floats[_floatsId[dataFloat.Id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data not found: {dataFloat.Id}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data is null"); } return 0; } public string Get(global::OctoCore.Save.Data.OctoDataString dataString) { if (dataString) { if (_stringsId.ContainsKey(dataString.Id)) { return Strings[_stringsId[dataString.Id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data not found: {dataString.Id}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data is null"); } return string.Empty; } public Vector4 Get(global::OctoCore.Save.Data.OctoDataVector dataVector) { if (dataVector) { if (_vectorsId.ContainsKey(dataVector.Id)) { return Vectors[_vectorsId[dataVector.Id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Vector data not found: {dataVector.Id}"); } } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Vector data is null"); } return Vector4.zero; } public bool GetBool(string id) { if (_booleansId.ContainsKey(id)) { return Booleans[_booleansId[id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Bool data not found: {id}"); } return false; } public int GetInt(string id) { if (_integersId.ContainsKey(id)) { return Integers[_integersId[id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data not found: {id}"); } return 0; } public float GetFloat(string id) { if (_floatsId.ContainsKey(id)) { return Floats[_floatsId[id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Float data not found: {id}"); } return 0; } public string GetString(string id) { if (_stringsId.ContainsKey(id)) { return Strings[_stringsId[id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"String data not found: {id}"); } return string.Empty; } public Vector4 GetVector(string id) { if (_vectorsId.ContainsKey(id)) { return Vectors[_vectorsId[id]].Value; } else if (OctoCore.Instance.Development.Log) { Debug.Log($"String data not found: {id}"); } return Vector4.zero; } #endregion #region SET public void Set(global::OctoCore.Save.Data.OctoDataBool dataBool, bool value) { if (dataBool != null && _booleansId.ContainsKey(dataBool.Id)) { Booleans[_booleansId[dataBool.Id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Bool data is null or not found"); } } public void Set(global::OctoCore.Save.Data.OctoDataInt dataInt, int value) { if (dataInt != null && _integersId.ContainsKey(dataInt.Id)) { Integers[_integersId[dataInt.Id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data is null or not found"); } } public void Set(global::OctoCore.Save.Data.OctoDataFloat dataFloat, float value) { if (dataFloat != null && _floatsId.ContainsKey(dataFloat.Id)) { Floats[_floatsId[dataFloat.Id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Float data is null or not found"); } } public void Set(global::OctoCore.Save.Data.OctoDataString dataString, string value) { if (dataString != null && _stringsId.ContainsKey(dataString.Id)) { Strings[_stringsId[dataString.Id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"String data is null or not found"); } } public void Set(global::OctoCore.Save.Data.OctoDataVector dataVector, Vector4 value) { if (dataVector != null && _vectorsId.ContainsKey(dataVector.Id)) { Vectors[_vectorsId[dataVector.Id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Vector data is null or not found"); } } public void SetBool(string id, bool value) { if (_booleansId.ContainsKey(id)) { Booleans[_booleansId[id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Bool data not found: {id}"); } } public void SetInt(string id, int value) { if (_booleansId.ContainsKey(id)) { Integers[_integersId[id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Int data not found: {id}"); } } public void SetFloat(string id, float value) { if (_floatsId.ContainsKey(id)) { Floats[_floatsId[id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"Float data not found: {id}"); } } public void SetString(string id, string value) { if (_stringsId.ContainsKey(id)) { Strings[_stringsId[id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"String data not found: {id}"); } } public void SetVector(string id, Vector4 value) { if (_vectorsId.ContainsKey(id)) { Vectors[_vectorsId[id]].Set(value); } else if (OctoCore.Instance.Development.Log) { Debug.Log($"String data not found: {id}"); } } #endregion private string GetPath() { Platform.OctoPlatforms platform = OctoCore.Instance ? OctoCore.Instance.Platform.Current : Platform.OctoPlatforms.Standalone; switch (platform) { default: return Path.Combine(Application.persistentDataPath, "Save.xml"); case Platform.OctoPlatforms.XboxOne: return Path.Combine(Application.persistentDataPath, "Save.xml"); case Platform.OctoPlatforms.XboxSeries: return Path.Combine(Application.persistentDataPath, "Save.xml"); case Platform.OctoPlatforms.PS4: return Path.Combine(Application.persistentDataPath, "Save.xml"); case Platform.OctoPlatforms.PS5: return Path.Combine(Application.persistentDataPath, "Save.xml"); case Platform.OctoPlatforms.Switch: return Path.Combine(Application.persistentDataPath, "Save.xml"); } } } }