2025-07-08 10:46:31 +00:00
using System.Collections.Generic ;
using System.Collections ;
using System.IO ;
using UnityEngine ;
using System ;
using ES3Types ;
using ES3Internal ;
public abstract class ES3Writer : IDisposable
{
public ES3Settings settings ;
protected HashSet < string > keysToDelete = new HashSet < string > ( ) ;
internal bool writeHeaderAndFooter = true ;
internal bool overwriteKeys = true ;
protected int serializationDepth = 0 ;
#region ES3Writer Abstract Methods
internal abstract void WriteNull ( ) ;
internal virtual void StartWriteFile ( )
{
serializationDepth + + ;
}
internal virtual void EndWriteFile ( )
{
serializationDepth - - ;
}
internal virtual void StartWriteObject ( string name )
{
serializationDepth + + ;
}
internal virtual void EndWriteObject ( string name )
{
serializationDepth - - ;
}
internal virtual void StartWriteProperty ( string name )
{
if ( name = = null )
throw new ArgumentNullException ( "Key or field name cannot be NULL when saving data." ) ;
ES3Debug . Log ( "<b>" + name + "</b> (writing property)" , null , serializationDepth ) ;
}
internal virtual void EndWriteProperty ( string name )
{
}
internal virtual void StartWriteCollection ( )
{
serializationDepth + + ;
}
internal virtual void EndWriteCollection ( )
{
serializationDepth - - ;
}
internal abstract void StartWriteCollectionItem ( int index ) ;
internal abstract void EndWriteCollectionItem ( int index ) ;
internal abstract void StartWriteDictionary ( ) ;
internal abstract void EndWriteDictionary ( ) ;
internal abstract void StartWriteDictionaryKey ( int index ) ;
internal abstract void EndWriteDictionaryKey ( int index ) ;
internal abstract void StartWriteDictionaryValue ( int index ) ;
internal abstract void EndWriteDictionaryValue ( int index ) ;
public abstract void Dispose ( ) ;
#endregion
#region ES3Writer Interface abstract methods
internal abstract void WriteRawProperty ( string name , byte [ ] bytes ) ;
internal abstract void WritePrimitive ( int value ) ;
internal abstract void WritePrimitive ( float value ) ;
internal abstract void WritePrimitive ( bool value ) ;
internal abstract void WritePrimitive ( decimal value ) ;
internal abstract void WritePrimitive ( double value ) ;
internal abstract void WritePrimitive ( long value ) ;
internal abstract void WritePrimitive ( ulong value ) ;
internal abstract void WritePrimitive ( uint value ) ;
internal abstract void WritePrimitive ( byte value ) ;
internal abstract void WritePrimitive ( sbyte value ) ;
internal abstract void WritePrimitive ( short value ) ;
internal abstract void WritePrimitive ( ushort value ) ;
internal abstract void WritePrimitive ( char value ) ;
internal abstract void WritePrimitive ( string value ) ;
internal abstract void WritePrimitive ( byte [ ] value ) ;
#endregion
protected ES3Writer ( ES3Settings settings , bool writeHeaderAndFooter , bool overwriteKeys )
{
this . settings = settings ;
this . writeHeaderAndFooter = writeHeaderAndFooter ;
this . overwriteKeys = overwriteKeys ;
}
/* User-facing methods used when writing randomly-accessible Key-Value pairs. */
#region Write ( key , value ) Methods
internal virtual void Write ( string key , Type type , byte [ ] value )
{
StartWriteProperty ( key ) ;
StartWriteObject ( key ) ;
WriteType ( type ) ;
WriteRawProperty ( "value" , value ) ;
EndWriteObject ( key ) ;
EndWriteProperty ( key ) ;
MarkKeyForDeletion ( key ) ;
}
/// <summary>Writes a value to the writer with the given key.</summary>
/// <param name="key">The key which uniquely identifies this value.</param>
/// <param name="value">The value we want to write.</param>
public virtual void Write < T > ( string key , object value )
{
if ( typeof ( T ) = = typeof ( object ) )
{
if ( value = = null )
Write ( typeof ( System . Object ) , key , null ) ;
else
Write ( value . GetType ( ) , key , value ) ;
}
else
Write ( typeof ( T ) , key , value ) ;
}
/// <summary>Writes a value to the writer with the given key, using the given type rather than the generic parameter.</summary>
/// <param name="key">The key which uniquely identifies this value.</param>
/// <param name="value">The value we want to write.</param>
/// <param name="type">The type we want to use for the header, and to retrieve an ES3Type.</param>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void Write ( Type type , string key , object value )
{
StartWriteProperty ( key ) ;
StartWriteObject ( key ) ;
WriteType ( type ) ;
WriteProperty ( "value" , value , ES3TypeMgr . GetOrCreateES3Type ( type ) , settings . referenceMode ) ;
EndWriteObject ( key ) ;
EndWriteProperty ( key ) ;
MarkKeyForDeletion ( key ) ;
}
#endregion
#region Write ( value ) & Write ( value , ES3Type ) Methods
/// <summary>Writes a value to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="value">The value we want to write.</param>
/// <param name="memberReferenceMode">Whether we want to write UnityEngine.Object fields and properties by reference, by value, or both.</param>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void Write ( object value , ES3 . ReferenceMode memberReferenceMode = ES3 . ReferenceMode . ByRef )
{
if ( value = = null ) { WriteNull ( ) ; return ; }
var type = ES3TypeMgr . GetOrCreateES3Type ( value . GetType ( ) ) ;
Write ( value , type , memberReferenceMode ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void Write ( object value , ES3Type type , ES3 . ReferenceMode memberReferenceMode = ES3 . ReferenceMode . ByRef )
{
// Note that we have to check UnityEngine.Object types for null by casting it first, otherwise
// it will always return false.
if ( value = = null | | ( ES3Reflection . IsAssignableFrom ( typeof ( UnityEngine . Object ) , value . GetType ( ) ) & & value as UnityEngine . Object = = null ) )
{
WriteNull ( ) ;
return ;
}
// Deal with System.Objects
if ( type = = null | | type . type = = typeof ( object ) )
{
var valueType = value . GetType ( ) ;
type = ES3TypeMgr . GetOrCreateES3Type ( valueType ) ;
if ( type = = null )
throw new NotSupportedException ( "Types of " + valueType + " are not supported. Please see the Supported Types guide for more information: https://docs.moodkie.com/easy-save-3/es3-supported-types/" ) ;
if ( ! type . isCollection & & ! type . isDictionary )
{
TryOnBeforeSerialize ( value ) ;
StartWriteObject ( null ) ;
WriteType ( valueType ) ;
type . Write ( value , this ) ;
EndWriteObject ( null ) ;
return ;
}
}
if ( type = = null )
throw new ArgumentNullException ( "ES3Type argument cannot be null." ) ;
if ( type . isUnsupported )
{
if ( type . isCollection | | type . isDictionary )
throw new NotSupportedException ( type . type + " is not supported because it's element type is not supported. Please see the Supported Types guide for more information: https://docs.moodkie.com/easy-save-3/es3-supported-types/" ) ;
else
throw new NotSupportedException ( "Types of " + type . type + " are not supported. Please see the Supported Types guide for more information: https://docs.moodkie.com/easy-save-3/es3-supported-types/" ) ;
}
if ( type . isPrimitive | | type . isEnum )
type . Write ( value , this ) ;
else if ( type . isCollection )
{
StartWriteCollection ( ) ;
( ( ES3CollectionType ) type ) . Write ( value , this , memberReferenceMode ) ;
EndWriteCollection ( ) ;
}
else if ( type . isDictionary )
{
StartWriteDictionary ( ) ;
( ( ES3DictionaryType ) type ) . Write ( value , this , memberReferenceMode ) ;
EndWriteDictionary ( ) ;
}
else
{
TryOnBeforeSerialize ( value ) ;
if ( type . type = = typeof ( GameObject ) )
( ( ES3Type_GameObject ) type ) . saveChildren = settings . saveChildren ;
StartWriteObject ( null ) ;
if ( type . isES3TypeUnityObject )
( ( ES3UnityObjectType ) type ) . WriteObject ( value , this , memberReferenceMode ) ;
else
type . Write ( value , this ) ;
EndWriteObject ( null ) ;
}
}
internal static void TryOnBeforeSerialize ( object obj )
{
if ( obj is ISerializationCallbackReceiver scr )
scr . OnBeforeSerialize ( ) ;
}
internal virtual void WriteRef ( UnityEngine . Object obj )
{
WriteRef ( obj , ES3ReferenceMgrBase . referencePropertyName ) ;
}
internal virtual void WriteRef ( UnityEngine . Object obj , string propertyName )
{
WriteRef ( obj , ES3ReferenceMgrBase . referencePropertyName , ES3ReferenceMgrBase . Current ) ;
}
internal virtual void WriteRef ( UnityEngine . Object obj , string propertyName , ES3ReferenceMgrBase refMgr )
{
if ( refMgr = = null )
throw new InvalidOperationException ( $"An Easy Save 3 Manager is required to save references. To add one to your scene, exit playmode and go to Tools > Easy Save 3 > Add Manager to Scene. Object being saved by reference is {obj.GetType()} with name {obj.name}." ) ;
// Get the reference ID, if it exists, and store it.
long id = refMgr . Get ( obj ) ;
// If reference ID doesn't exist, create reference.
if ( id = = - 1 )
id = refMgr . Add ( obj ) ;
WriteProperty ( propertyName , id . ToString ( ) ) ;
}
#endregion
/* Writes a property as a name value pair. */
#region WriteProperty ( name , value ) methods
/// <summary>Writes a field or property to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="name">The name of the field or property.</param>
/// <param name="value">The value we want to write.</param>
public virtual void WriteProperty ( string name , object value )
{
WriteProperty ( name , value , settings . memberReferenceMode ) ;
}
/// <summary>Writes a field or property to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="name">The name of the field or property.</param>
/// <param name="value">The value we want to write.</param>
/// <param name="memberReferenceMode">Whether we want to write the property by reference, by value, or both.</param>
public virtual void WriteProperty ( string name , object value , ES3 . ReferenceMode memberReferenceMode )
{
if ( SerializationDepthLimitExceeded ( ) )
return ;
StartWriteProperty ( name ) ;
Write ( value , memberReferenceMode ) ;
EndWriteProperty ( name ) ;
}
/// <summary>Writes a field or property to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="name">The name of the field or property.</param>
/// <param name="value">The value we want to write.</param>
public virtual void WriteProperty < T > ( string name , object value )
{
WriteProperty ( name , value , ES3TypeMgr . GetOrCreateES3Type ( typeof ( T ) ) , settings . memberReferenceMode ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void WriteProperty ( string name , object value , ES3Type type )
{
WriteProperty ( name , value , type , settings . memberReferenceMode ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void WriteProperty ( string name , object value , ES3Type type , ES3 . ReferenceMode memberReferenceMode )
{
if ( SerializationDepthLimitExceeded ( ) )
return ;
StartWriteProperty ( name ) ;
Write ( value , type , memberReferenceMode ) ;
EndWriteProperty ( name ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void WritePropertyByRef ( string name , UnityEngine . Object value )
{
if ( SerializationDepthLimitExceeded ( ) )
return ;
StartWriteProperty ( name ) ;
if ( value = = null )
{
WriteNull ( ) ;
return ; } ;
StartWriteObject ( name ) ;
WriteRef ( value ) ;
EndWriteObject ( name ) ;
EndWriteProperty ( name ) ;
}
/// <summary>Writes a private property to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="name">The name of the property.</param>
/// <param name="objectContainingProperty">The object containing the property we want to write.</param>
public void WritePrivateProperty ( string name , object objectContainingProperty )
{
var property = ES3Reflection . GetES3ReflectedProperty ( objectContainingProperty . GetType ( ) , name ) ;
if ( property . IsNull )
throw new MissingMemberException ( "A private property named " + name + " does not exist in the type " + objectContainingProperty . GetType ( ) ) ;
WriteProperty ( name , property . GetValue ( objectContainingProperty ) , ES3TypeMgr . GetOrCreateES3Type ( property . MemberType ) ) ;
}
/// <summary>Writes a private field to the writer. Note that this should only be called within an ES3Type.</summary>
/// <param name="name">The name of the field.</param>
/// <param name="objectContainingField">The object containing the property we want to write.</param>
public void WritePrivateField ( string name , object objectContainingField )
{
var field = ES3Reflection . GetES3ReflectedMember ( objectContainingField . GetType ( ) , name ) ;
if ( field . IsNull )
throw new MissingMemberException ( "A private field named " + name + " does not exist in the type " + objectContainingField . GetType ( ) ) ;
WriteProperty ( name , field . GetValue ( objectContainingField ) , ES3TypeMgr . GetOrCreateES3Type ( field . MemberType ) ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void WritePrivateProperty ( string name , object objectContainingProperty , ES3Type type )
{
var property = ES3Reflection . GetES3ReflectedProperty ( objectContainingProperty . GetType ( ) , name ) ;
if ( property . IsNull )
throw new MissingMemberException ( "A private property named " + name + " does not exist in the type " + objectContainingProperty . GetType ( ) ) ;
WriteProperty ( name , property . GetValue ( objectContainingProperty ) , type ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void WritePrivateField ( string name , object objectContainingField , ES3Type type )
{
var field = ES3Reflection . GetES3ReflectedMember ( objectContainingField . GetType ( ) , name ) ;
if ( field . IsNull )
throw new MissingMemberException ( "A private field named " + name + " does not exist in the type " + objectContainingField . GetType ( ) ) ;
WriteProperty ( name , field . GetValue ( objectContainingField ) , type ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void WritePrivatePropertyByRef ( string name , object objectContainingProperty )
{
var property = ES3Reflection . GetES3ReflectedProperty ( objectContainingProperty . GetType ( ) , name ) ;
if ( property . IsNull )
throw new MissingMemberException ( "A private property named " + name + " does not exist in the type " + objectContainingProperty . GetType ( ) ) ;
WritePropertyByRef ( name , ( UnityEngine . Object ) property . GetValue ( objectContainingProperty ) ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void WritePrivateFieldByRef ( string name , object objectContainingField )
{
var field = ES3Reflection . GetES3ReflectedMember ( objectContainingField . GetType ( ) , name ) ;
if ( field . IsNull )
throw new MissingMemberException ( "A private field named " + name + " does not exist in the type " + objectContainingField . GetType ( ) ) ;
WritePropertyByRef ( name , ( UnityEngine . Object ) field . GetValue ( objectContainingField ) ) ;
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public void WriteType ( Type type )
{
WriteProperty ( ES3Type . typeFieldName , ES3Reflection . GetTypeString ( type ) ) ;
}
#endregion
#region Create methods
/// <summary>Creates a new ES3Writer.</summary>
/// <param name="filePath">The relative or absolute path of the file we want to write to.</param>
/// <param name="settings">The settings we want to use to override the default settings.</param>
public static ES3Writer Create ( string filePath , ES3Settings settings )
{
return Create ( new ES3Settings ( filePath , settings ) ) ;
}
/// <summary>Creates a new ES3Writer.</summary>
/// <param name="settings">The settings we want to use to override the default settings.</param>
public static ES3Writer Create ( ES3Settings settings )
{
return Create ( settings , true , true , false ) ;
}
// Implicit Stream Methods.
internal static ES3Writer Create ( ES3Settings settings , bool writeHeaderAndFooter , bool overwriteKeys , bool append )
{
var stream = ES3Stream . CreateStream ( settings , ( append ? ES3FileMode . Append : ES3FileMode . Write ) ) ;
if ( stream = = null )
return null ;
return Create ( stream , settings , writeHeaderAndFooter , overwriteKeys ) ;
}
// Explicit Stream Methods.
internal static ES3Writer Create ( Stream stream , ES3Settings settings , bool writeHeaderAndFooter , bool overwriteKeys )
{
if ( stream . GetType ( ) = = typeof ( MemoryStream ) )
{
settings = ( ES3Settings ) settings . Clone ( ) ;
settings . location = ES3 . Location . InternalMS ;
}
// Get the baseWriter using the given Stream.
if ( settings . format = = ES3 . Format . JSON )
return new ES3JSONWriter ( stream , settings , writeHeaderAndFooter , overwriteKeys ) ;
else
return null ;
}
#endregion
/ *
* Checks whether serialization depth limit has been exceeded
* /
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected bool SerializationDepthLimitExceeded ( )
{
if ( serializationDepth > settings . serializationDepthLimit )
{
ES3Debug . LogWarning ( "Serialization depth limit of " + settings . serializationDepthLimit + " has been exceeded, indicating that there may be a circular reference.\nIf this is not a circular reference, you can increase the depth by going to Window > Easy Save 3 > Settings > Advanced Settings > Serialization Depth Limit" ) ;
return true ;
}
return false ;
}
/ *
* Marks a key for deletion .
* When merging files , keys marked for deletion will not be included .
* /
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public virtual void MarkKeyForDeletion ( string key )
{
keysToDelete . Add ( key ) ;
}
/ *
* Merges the contents of the non - temporary file with this ES3Writer ,
* ignoring any keys which are marked for deletion .
* /
protected void Merge ( )
{
using ( var reader = ES3Reader . Create ( settings ) )
{
if ( reader = = null )
return ;
Merge ( reader ) ;
}
}
/ *
* Merges the contents of the ES3Reader with this ES3Writer ,
* ignoring any keys which are marked for deletion .
* /
protected void Merge ( ES3Reader reader )
{
foreach ( KeyValuePair < string , ES3Data > kvp in reader . RawEnumerator )
if ( ! keysToDelete . Contains ( kvp . Key ) | | kvp . Value . type = = null ) // Don't add keys whose data is of a type which no longer exists in the project.
Write ( kvp . Key , kvp . Value . type . type , kvp . Value . bytes ) ;
}
/// <summary>Stores the contents of the writer and overwrites any existing keys if overwriting is enabled.</summary>
public virtual void Save ( )
{
Save ( overwriteKeys ) ;
}
/// <summary>Stores the contents of the writer and overwrites any existing keys if overwriting is enabled.</summary>
/// <param name="overwriteKeys">Whether we should overwrite existing keys.</param>
public virtual void Save ( bool overwriteKeys )
{
if ( overwriteKeys )
Merge ( ) ;
EndWriteFile ( ) ;
Dispose ( ) ;
// If we're writing to a location which can become corrupted, rename the backup file to the file we want.
// This prevents corrupt data.
if ( settings . location = = ES3 . Location . File | | settings . location = = ES3 . Location . PlayerPrefs )
ES3IO . CommitBackup ( settings ) ;
}
}