Files
ihob/Assets/Extensions/TinySave/TinySaveAPI.cs

385 lines
17 KiB
C#
Raw Normal View History

2026-02-21 17:04:05 -08:00
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Debug = TinySaveAPI.Logger;
namespace TinySaveAPI
{
public static class TinySave
{
// ---------------------------------------------------------------------------- Static Variables and Properties
// The security key has to be either 16 or 24 characters in length.
public static string SecurityKey { get; set; } = "Security Key... Change!!";
public static SerializationType SerializationType { get; set; } = SerializationType.Binary;
public static bool IsWebGL =>
#if UNITY_WEBGL
true;
#else
false;
#endif
public static bool IsDebug
{ get; set; } =
#if DEBUG
true;
#else
false;
#endif
// ---------------------------------------------------------------------------- Private Variables
private static BinaryFormatter binaryFormatter;
// ---------------------------------------------------------------------------- Static Methods
/// <summary>
/// Create a new item T from the serialised data.
/// </summary>
/// <typeparam name="T">A class or struct. Cannot be a UnityEngine.Object, e.g. GameObject or ScriptableObject.</typeparam>
/// <param name="name">The name of the item you wish to load. Must match exactly the name that was used to serialise the data.</param>
/// <param name="serializationOverride">An optional serialisation type override.</param>
/// <returns>A Tuple containing an operation success Response and the newly created (T)Item.</returns>
public static async Task<(Response response, T item)> LoadAsync<T> ( string name, SerializationType serializationOverride = SerializationType.None )
{
if ( !typeof ( T ).IsSerializable )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) {typeof ( T ).Name} is not Serializable!" );
return (Response.InvalidParameter, default);
}
if ( typeof ( T ).IsAssignableFrom ( typeof ( UnityEngine.Object ) ) )
{
// We can't create "new" UnityEngine Objects.
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ). Cannot deserialize to a new UnityEngine.Object." );
return (Response.InvalidParameter, default);
}
if ( serializationOverride == SerializationType.None ) serializationOverride = SerializationType;
byte [ ] result;
int bytesRead = 0;
if ( IsWebGL )
{
if ( serializationOverride != SerializationType.Binary )
{
serializationOverride = SerializationType.Binary;
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) Only Binary formatting supported for WebGL. No action required." );
}
var fileName = $"{( uint ) name.GetHashCode ( )}";
var base64String = PlayerPrefs.GetString ( fileName, string.Empty );
if ( string.IsNullOrEmpty ( base64String ) )
return (Response.Empty, default);
try
{
result = Convert.FromBase64String ( base64String );
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) Exception caught.\n{e.Message}" );
return (Response.Exception, default);
}
bytesRead = result.Length;
}
else
{
if ( binaryFormatter == null ) binaryFormatter = new BinaryFormatter ( );
var fileName = $"{Application.persistentDataPath}/{( uint ) name.GetHashCode ( )}.dat";
if ( !File.Exists ( fileName ) )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) File Not Found : {fileName}" );
return (Response.FileNotFound, default);
}
using ( FileStream file = File.Open ( fileName, FileMode.Open, FileAccess.Read, FileShare.None ) )
{
result = new byte [ file.Length ];
bytesRead = await file.ReadAsync ( result, 0, ( int ) file.Length );
}
}
Debug.Log ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) {bytesRead} bytes read." );
if ( bytesRead == 0 )
{
Debug.Log ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) {bytesRead} bytes read." );
return (Response.Empty, default);
}
switch ( serializationOverride )
{
case SerializationType.Binary:
try
{
using ( MemoryStream stream = new MemoryStream ( result ) )
return (Response.Success, ( T ) binaryFormatter.Deserialize ( stream ) );
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) [{serializationOverride}] Exception caught.\n{e.Message}" );
return (Response.Exception, default);
}
case SerializationType.JsonEncrypted:
case SerializationType.Json:
try
{
string jsonString = serializationOverride == SerializationType.JsonEncrypted ? Decrypt ( result ) : Encoding.UTF8.GetString ( result );
return (Response.Success, JsonUtility.FromJson<T> ( jsonString ));
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\" ) [{serializationOverride}] Exception caught.\n{e.Message}" );
return (Response.Exception, default);
}
}
return (Response.None, default);
}
/// <summary>
/// Fill a given instance of an object from the serialized data. This form of the method only accepts UnityEngine.Objects, and works with encrypted or unencrypted JSON.
/// </summary>
/// <typeparam name="T">A class that derives from UnityEngine.Object, e.g. GameObject or ScriptableObject.</typeparam>
/// <param name="name">The name of the item you wish to load. Must match exactly the name that was used to serialise the data.</param>
/// <param name="serializationOverride">An optional serialisation type override.</param>
/// <returns>A Response indicating whether or not the operation succeeded.</returns>
public static async Task<Response> LoadAsync<T> ( string name, T item, SerializationType serializationOverride = SerializationType.None ) where T : UnityEngine.Object
{
if ( serializationOverride == SerializationType.None ) serializationOverride = SerializationType;
if ( serializationOverride != SerializationType.Json && serializationOverride != SerializationType.JsonEncrypted )
{
serializationOverride = SerializationType.JsonEncrypted;
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\", {typeof ( T ).Name} ) Only Json or JsonEncrypted formatting supported for WebGL. No action required, defaulting to JsonEncrypted." );
}
if ( !typeof ( MonoBehaviour ).IsAssignableFrom ( typeof ( T ) ) && !typeof ( ScriptableObject ).IsAssignableFrom ( typeof ( T ) ) )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\", {typeof ( T ).Name} ) Method requires either a MonoBehaviour or ScriptableObject." );
return Response.InvalidParameter;
}
byte [ ] result;
int bytesRead = 0;
string json = string.Empty;
if ( IsWebGL )
{
var fileName = $"{( uint ) name.GetHashCode ( )}";
json = PlayerPrefs.GetString ( fileName, string.Empty );
if ( json == string.Empty || json == "{}" ) return Response.Empty;
bytesRead = json.Length;
if ( serializationOverride == SerializationType.JsonEncrypted ) json = Decrypt ( Convert.FromBase64String ( json ) );
}
else
{
var fileName = $"{Application.persistentDataPath}/{( uint ) name.GetHashCode ( )}.dat";
if ( !File.Exists ( fileName ) )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\", {typeof ( T ).Name} ) File Not Found : {fileName}" );
return Response.FileNotFound;
}
using ( FileStream file = File.Open ( fileName, FileMode.Open, FileAccess.Read, FileShare.None ) )
{
result = new byte [ file.Length ];
bytesRead = await file.ReadAsync ( result, 0, ( int ) file.Length );
}
if ( serializationOverride == SerializationType.JsonEncrypted ) json = Decrypt ( result );
}
if ( bytesRead == 0 )
{
Debug.Log ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\", {typeof ( T ).Name} ) {bytesRead} bytes read." );
return Response.Empty;
}
try
{
JsonUtility.FromJsonOverwrite ( json, item );
return Response.Success;
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] LoadAsync<{typeof ( T ).Name}> ( \"{name}\", {typeof ( T ).Name} ) [{serializationOverride}] Exception caught.\n\n{e.Message}" );
return Response.Exception;
}
}
/// <summary>
/// Save the supplied object, using the given name.
/// </summary>
/// <param name="name">The name used as the filename, or key if using PlayerPrefs (WebGL).</param>
/// <param name="obj">The object to be serialised.</param>
/// <param name="securityKeyOverride">If left null, uses the static SecurityKey value.</param>
/// <returns>A Response indicating whether or not the operation succeeded.</returns>
public static async Task<Response> SaveAsync ( string name, object obj, SerializationType serializationOverride = SerializationType.None )
{
if ( serializationOverride == SerializationType.None ) serializationOverride = SerializationType;
var objectType = obj.GetType ( );
if ( string.IsNullOrEmpty ( name ) || obj == null )
{
Debug.LogWarning ( $"[DEBUG] SaveAsync ( \"{name}\", {objectType.Name} ) [{serializationOverride}] name or object are empty/null." );
return Response.InvalidParameter;
}
if ( objectType.IsAssignableFrom ( typeof ( UnityEngine.Object ) ) && !objectType.IsSerializable )
{
Debug.LogWarning ( $"[DEBUG] SaveAsync ( \"{name}\", {objectType.Name} ) [{serializationOverride}] is not Serializable!" );
return Response.InvalidParameter;
}
var fileName = $"{( uint ) name.GetHashCode ( )}";
if ( !IsWebGL )
{
if ( binaryFormatter == null ) binaryFormatter = new BinaryFormatter ( );
fileName = $"{Application.persistentDataPath}/{fileName}.dat";
}
switch ( serializationOverride )
{
case SerializationType.Binary:
try
{
if ( IsWebGL )
{
using ( MemoryStream stream = new MemoryStream ( ) )
{
binaryFormatter.Serialize ( stream, obj );
PlayerPrefs.SetString ( fileName, Convert.ToBase64String ( stream.ToArray ( ) ) );
}
}
else
{
using ( FileStream file = File.Open ( fileName, FileMode.Create ) )
binaryFormatter.Serialize ( file, obj );
}
return Response.Success;
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] SaveAsync ( \"{name}\", {objectType.Name} ) [{serializationOverride}] Exception caught.\n\n{e.Message}" );
return Response.Exception;
}
case SerializationType.Json:
case SerializationType.JsonEncrypted:
try
{
var json = JsonUtility.ToJson ( obj );
if ( json == "{}" )
return Response.UnableToSerialise;
if ( IsWebGL )
{
if ( serializationOverride == SerializationType.JsonEncrypted ) json = Convert.ToBase64String ( Encrypt ( json ) );
PlayerPrefs.SetString ( fileName, json );
}
else
{
using ( FileStream file = File.Open ( fileName, FileMode.Create ) )
{
byte [ ] bytes = serializationOverride == SerializationType.JsonEncrypted ? Encrypt ( json ) : Encoding.UTF8.GetBytes ( json );
await file.WriteAsync ( bytes, 0, bytes.Length );
}
}
return Response.Success;
}
catch ( Exception e )
{
Debug.LogWarning ( $"[DEBUG] SaveAsync ( \"{name}\", {objectType.Name} ) [{serializationOverride}] Exception caught.\n\n{e.Message}" );
return Response.Exception;
}
}
return Response.Empty;
}
/// <summary>
/// Encrypt a string, using a security key, into a byte array.
/// </summary>
/// <param name="dataString">The string data to encrypt.</param>
/// <param name="securityKeyOverride">If left null, uses the static SecurityKey value.</param>
/// <returns>A byte array containing the encrypted string.</returns>
public static byte [ ] Encrypt ( string dataString, string securityKeyOverride = null )
{
if ( string.IsNullOrEmpty ( securityKeyOverride ) )
{
if ( SecurityKey == null ) return null;
securityKeyOverride = SecurityKey;
}
var keyArray = Encoding.UTF8.GetBytes ( securityKeyOverride );
var dataBytesArray = Encoding.UTF8.GetBytes ( dataString );
using ( var provider = new TripleDESCryptoServiceProvider
{
Key = keyArray,
Mode = CipherMode.ECB,
Padding = PaddingMode.PKCS7
} )
{
using ( var encryptor = provider.CreateEncryptor ( ) )
{
var resultArray = encryptor.TransformFinalBlock ( dataBytesArray, 0, dataBytesArray.Length );
provider.Clear ( );
return resultArray;
}
}
}
/// <summary>
/// Decrypt a byte array, using a security key, into a string.
/// </summary>
/// <param name="dataBytesArray">The byte array containing the encrypted data.</param>
/// <param name="securityKeyOverride">If left null, uses the static SecurityKey value.</param>
/// <returns>A string containing the unencrypted data.</returns>
public static string Decrypt ( byte [ ] dataBytesArray, string securityKeyOverride = null )
{
if ( string.IsNullOrEmpty ( securityKeyOverride ) )
{
if ( SecurityKey == null ) return null;
securityKeyOverride = SecurityKey;
}
var keyArray = Encoding.UTF8.GetBytes ( securityKeyOverride );
using ( var provider = new TripleDESCryptoServiceProvider
{
Key = keyArray,
Mode = CipherMode.ECB,
Padding = PaddingMode.PKCS7
} )
{
using ( var decryptor = provider.CreateDecryptor ( ) )
{
byte [ ] resultArray = decryptor.TransformFinalBlock ( dataBytesArray, 0, dataBytesArray.Length );
provider.Clear ( );
return Encoding.UTF8.GetString ( resultArray );
}
}
}
}
}