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");
}
}
}
}