Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Resolved Use the JSON serilization for ScriptableObject with asset fields in Editor and Build

Discussion in 'Scripting' started by DSivtsov, Aug 24, 2022.

  1. DSivtsov


    Feb 20, 2019
    I tested the possibility to use the ScriptableObject to Store Game settings.
    The default values Stored in asset.
    Code (CSharp):
    1. [CreateAssetMenu(fileName ="GameSettingsSO",menuName = "SoundAndEffects/GameSettingsSO")]
    2. public class GameSettingsSO : ScriptableObject
    3. {
    4.     [Header("Game Options")]
    5.     [SerializeField] private ComplexitySO _complexityGame;
    6.     [SerializeField] private PlayMode _usedPlayMode;
    7.     [Header("Audio Options")]
    8.     [SerializeField] private float _masterVolume;
    9.     [SerializeField] private float _musicVolume;
    10.     [SerializeField] private float _effectVolume;
    11.     [SerializeField] private SequenceType _musicSequenceType;
    13.     public override string ToString() => JsonUtility.ToJson(this);
    15.     public PlayMode UsedPlayMode => _usedPlayMode;
    16.     public ComplexitySO ComplexityGame => _complexityGame;
    17. }
    The Changed Settings saved to disk through JSON serialization as string

    Was detected strange behavior:
    EDIT see note at next post

    When I store in Play mode in Editor the field "_complexityGame" serialized like as :{"instanceID":18942}
    Code (CSharp):
    1. {"_complexityGame":{"instanceID":18942},"_usedPlayMode":2,"_notCopyToGlobal":true,"_globalDefaultTopList":false,"_masterVolume":1.0,"_musicVolume":2.0,"_effectVolume":3.0,"_musicSequenceType":1}
    When I store in Play mode in Build the field "_complexityGame" serialized like as :{"m_FileID":2154,"m_PathID":0}
    Code (CSharp):
    1. {"_complexityGame":{"m_FileID":2154,"m_PathID":0},"_usedPlayMode":2,"_notCopyToGlobal":true,"_globalDefaultTopList":false,"_masterVolume":1.0,"_musicVolume":2.0,"_effectVolume":3.0,"_musicSequenceType":1}
    Every time it was contained the same value

    I can understand the reason of that, and for save setting it's not a big problem (you may not transfer this to build).
    But if you try to save something in processing of developing (as JSON) and how it can restore after in build, in case if it will not primitive values?
    Last edited: Aug 26, 2022
  2. spiney199


    Feb 11, 2021
    The short answer is you can't serialise out reference to other UnityEngine.Object assets, even using Unity's JSON Utility and restore them later.
    Bunny83 likes this.
  3. DSivtsov


    Feb 20, 2019
    I understand it now, but it was a little unexpected :).
    I plan to test the Odin JSON Serialization, or another variants, but they all based on fact that you write - exist the limitation of standard JSON Unity Serialization, and use the custom JSON Serialization ( ISerializationCallbackReceiver ).

    But the main problem of this issue with serialization (different algorithms of serialization links to assets in Editor and in Build) you detects it only if will use it. I don't remember that it was described.

    And that more interesting why Unity doesn't use the same algorithms of serialization of links to assets in Editor and in Build) (
    Last edited: Aug 26, 2022
  4. Bunny83


    Oct 18, 2010
    Well, Unity does not really have a proper representation of the AssetDatabase at runtime. It's essentially an editor feature for the authoring of your game. In the editor you only have asset references. So instances that are stored in the asset database. A single serialization unit may also use local FileIDs to refer to other instances within the same serialization unit. Unity uses YAML or it's custom binary asset format to store this information. The json serialization does not include FileIDs since json itself does not have a concept for that. Some json serializers do implement meta tags to store such information and technically that's of course possible. Though Unity's serialization system has grown quite "organically" over many years and it gets progressively harder to make radical changes.

    At runtime we only have instance IDs. Instance IDs are not globally unique and are usually restricted to a single serialization unit (scene, perfab, asset). I guess it's quite difficult to change the current system to be actually useful for runtime serialization.

    Don't get me wrong, I would also love to see a better assetdatabase access at runtime. Of course only read-only access. Though simply getting a GUID of an authored asset at runtime and looking up an asset based on that GUID would make it extremely easy to roll your own serialization. We now have the addressables system which is kind of a hack on top of the existing system. It still does not fill all the gaps. In order to be able to create a proper save system we simply need a way to identify assets stored in the asset database at runtime and we need a way to address those later. Likewise the SerializeReference attribute (which gives us a lot more flexibility) is only restricted to the serialization unit and can't be used for cross references. We have many tiny tools which all improve what we can do, but the above mentioned basic features are still missing in Unity.

    While I never really wanted a ready-to-use save and load solution from Unity like most other major engines may have, I prefer some simple basic features where we can build on top our own solutions. Most ready-to-use solutions have limitations or specific game types in mind (games like the old halflife actually store the whole bsp map with all dynamic changes in the save file. So loading a savegame does only partly load the original map). Unity is supposed to be the "allround" solution for games. It should not be stuck at a certain game type. Different games have vastly different save and load needs. So having basic tools to interact with the engine and its assets would be the best solution. We don't need a complicated and convoluted solution that tries to satisfy all needs and usually fail reaching that goal. We just need the basic tools. The individual solutions will be developed by the community / developers.

    That was an attempt to defend Unity and at the same time a rant why we still don't have the tools we need :)
  5. spiney199


    Feb 11, 2021
    You cannot, I repeat CANNOT serialise out Unity objects or references to them nor can you restore those. DO NOT waste your time trying to do so.

    Trust me I bought Odin with the intention of serialising out scriptable objects to learn that it couldn't. Upside is that I ended up with an awesome addon that does do a million other things.

    Your only choice is to devise a system to represent references to unity objects and the objects themselves with simple, serialisable forms. Basically with plain classes and structs, often ones that are 'serialisable surrogates' of your Unity centric types. There is no getting around this.
    Kurt-Dekker and Bunny83 like this.
  6. Bunny83


    Oct 18, 2010
    Right, it's sad that we have to do this, but currently it's the only real solution. Depending on the needs you essentially have to roll your own "asset database" of objects you want / need to reference and give them your own ID which you can look up. either simply an index into an array of objects or some other solution. Depending on the game you have to decide which granularity you need.
  7. DSivtsov


    Feb 20, 2019
    Thanks you @spiney199 and @Bunny83 for explanation the existent limitations and some issues with Unity Serialization.

    Also I must noted :) that I was initially wrong, because when I tested the serialize the objected with a asset reference in Editor :) I forgot to reloading the Editor self between Save and Restore (I was reloading Scene only).
    In this the case (the "instanceID" not changing :) as described it in Unity documentaion) and the Store& Restore of reference to asset was restoring "correctly". Now i see that is not possible without use a some workarounds and explanations from @spiney199 and @Bunny83
    Last edited: Aug 26, 2022
  8. Kurt-Dekker


    Mar 16, 2013
    We trivially work around this as follows:

    - we recognize that ScriptableObjects cannot be serialized.

    - we put ALL ScriptableObjects of a given type in a single directory to enforce name uniqueness

    - we make a Repository object to retrieve these ScriptableObjects by name

    - we modify our serializable structures so they:
    ----> don't serialize the SO itself
    ----> they serialize the name of the SO
    ----> they load the SO as a reference and look it up

    You cannot use Unity's "lite" JSON implementation to do this.

    The following example uses Newtonsoft JSON, available via the Unity Package Mangler:

    Code (csharp):
    1.     [System.Serializable]
    2.     public class MySerializableThingamajig
    3.     {
    4.         // this is what we drag into the inspector
    5.         [Newtonsoft.Json.JsonIgnore]
    6.         public VisualizationSO Visualization;
    8.         // being a property, this doesn't appear in the inspector
    9.         // being a proper JSON mechanism, NewtonSoft handles it correctly
    10.         public string VizualizationName
    11.         {
    12.             get
    13.             {
    14.                 return;
    15.             }
    16.             set
    17.             {
    18.                 Visualization = VisualizationSORepository.Load(value);
    19.             }
    20.         }
    21.     }
    This also has the side benefit of using an easy-to-reason-about name in the save data, something you can look up directly in the project rather than some GUID or instance ID or other ambiguous number.

    The big obvious hole here is if that name is serialized, the underlying ScriptableObject that the name references can:

    - change
    - become deleted
    - become invalid

    But since we control the repository of these objects, we can simply elect to invalidate old names, or provide lookup service to legacy names, or otherwise have appropriate graceful behaviour take place when stale data is deserialized.

    A serialization data format specifier can be useful here, such as a version number.
    Last edited: Aug 26, 2022
  9. spiney199


    Feb 11, 2021
    This is why I impose a unique identifier inside of SO's that I need to save out. Easy enough to make a small Utility class that generates a Guid when the SO is made, and use it to both save out and look up from a dictionary.

    I would post my example but you'd probably murder me for how over engineered it is. ;p
    Kurt-Dekker likes this.
  10. DSivtsov


    Feb 20, 2019
    and you call it by VisualizationSORepository.Load(value);

    and I didn't dive to load an assets in runtime (Asset Bundles/Addressable and so on) ... did understand right - you propose to base the VisualizationSORepository.Load on methods Resources.Load
  11. DSivtsov


    Feb 20, 2019
    I will not kill you :) exactly, because for me interesting your concept also, because when i thinking about surrogate for SO serialization (i thought about the same).
    But that will do the Wizard of 27K Level i don't knowo_O ... but i think he love the animals :) and may be peoples also:) (therefore you have a double chance :))
  12. spiney199


    Feb 11, 2021
    Well I was replying to Kurt there, but the code also uses a bit of Odin Inspector. Nonetheless, here's the utility class I use:

    Code (CSharp):
    1. namespace LizardBrainGames
    2. {
    3.     using System;
    4.     using UnityEngine;
    5.     using Sirenix.OdinInspector;
    7.     /// <summary>
    8.     /// Serializable class that handles the storing and generation of a GUID string.
    9.     /// Supports implicit conversion to a string.
    10.     /// </summary>
    11.     [Serializable, HideLabel]
    12.     public sealed class AssetGUID : IEquatable<AssetGUID>
    13.     {
    14.         #region Constructors
    16.         /// <summary>
    17.         /// Creates a new AssetGUID instance. If generateGUID is true, a new GUID will be generated immediately.
    18.         /// </summary>
    19.         public AssetGUID(bool generateGIUD = false)
    20.         {
    21.             if (generateGIUD == true) GenerateGUID();
    22.         }
    24.         #endregion
    26.         #region Inspector Fields
    28.         [HorizontalGroup("GUID", Width = 0.5f, MaxWidth = 1)]
    29.         [SerializeField, HideLabel, ReadOnly]
    30.         private string assetGUID;
    32.         #endregion
    34.         #region Properties
    36.         /// <summary>
    37.         /// Returns the asset GUID as a string.
    38.         /// </summary>
    39.         public string AssetGUIDString { get { return assetGUID; } }
    41.         /// <summary>
    42.         /// Does this AssetGUID already have a generated GUID?
    43.         /// </summary>
    44.         public bool HasGeneratedGUID { get { return !string.IsNullOrEmpty(assetGUID); } }
    46.         #endregion
    48.         #region GUID Generation Methods
    50.         /// <summary>
    51.         /// Call this to generate a new GUID. Will only generate a new GUID if none is already generated.
    52.         /// </summary>
    53.         [HorizontalGroup("GUID")]
    54.         [Button("Generate GUID"), ShowIf("@!HasGeneratedGUID")]
    55.         public void GenerateGUID()
    56.         {
    57.             if (!HasGeneratedGUID) assetGUID = Guid.NewGuid().ToString();
    58.         }
    60.         /// <summary>
    61.         /// Generates a new GUID even if one already exists.
    62.         /// </summary>
    63.         public void ForceGenerateGUID()
    64.         {
    65.             assetGUID = Guid.NewGuid().ToString();
    66.         }
    68.         #endregion
    70.         #region Interface Methods & Object Overloads
    72.         /// <summary>
    73.         /// Compares if two AssetGUID classes contain the same GUID string.
    74.         /// </summary>
    75.         public bool Equals(AssetGUID other)
    76.         {
    77.             if (other is null) return false;
    79.             if (ReferenceEquals(this, other)) return true;
    81.             return assetGUID == other.assetGUID;
    82.         }
    84.         public override bool Equals(object obj)
    85.         {
    86.             return obj is AssetGUID assetGUID && this.Equals(assetGUID);
    87.         }
    89.         public override int GetHashCode()
    90.         {
    91.             return assetGUID.GetHashCode();
    92.         }
    94.         public override string ToString()
    95.         {
    96.             return assetGUID;
    97.         }
    99.         #endregion
    101.         #region Operator Overloads
    103.         /// <summary>
    104.         /// Returns true if two Asset GUID classes contain the same GUID string.
    105.         /// </summary>
    106.         public static bool operator ==(AssetGUID left, AssetGUID right)
    107.         {
    108.             return left is null ? right is null : left.Equals(right);
    109.         }
    111.         /// <summary>
    112.         /// Returns true if two asset GUID classes do not contain the same GUID string.
    113.         /// </summary>
    114.         public static bool operator !=(AssetGUID left, AssetGUID right)
    115.         {
    116.             return !(left == right);
    117.         }
    119.         /// <summary>
    120.         /// Returns true if the string equals the AssetGUID's GUID string.
    121.         /// </summary>
    122.         public static bool operator ==(string text, AssetGUID assetGUID)
    123.         {
    124.             return assetGUID.assetGUID == text;
    125.         }
    127.         /// <summary>
    128.         /// Returns true if the string does not equal the AssetGUID's GUID string.
    129.         /// </summary>
    130.         public static bool operator !=(string text, AssetGUID assetGUID)
    131.         {
    132.             return !(text == assetGUID);
    133.         }
    135.         /// <summary>
    136.         /// Returns true if the string equals the AssetGUID's GUID string.
    137.         /// </summary>
    138.         public static bool operator ==(AssetGUID assetGUID, string text)
    139.         {
    140.             return text == assetGUID;
    141.         }
    143.         /// <summary>
    144.         /// Returns true if the string does not equal the AssetGUID's GUID string.
    145.         /// </summary>
    146.         public static bool operator !=(AssetGUID assetGUID, string text)
    147.         {
    148.             return !(text == assetGUID);
    149.         }
    151.         /// <summary>
    152.         /// Returns the Asset GUID string with an implicit conversion.
    153.         /// </summary>
    154.         public static implicit operator string(AssetGUID assetGUID)
    155.         {
    156.             return assetGUID.assetGUID;
    157.         }
    159.         #endregion
    160.     }
    161. }
    And I use an interface to make it more of a contract:

    Code (CSharp):
    1. namespace LizardBrainGames
    2. {
    3.     /// <summary>
    4.     /// Interface for assets to implement that want to express a unique identifier.
    5.     /// </summary>
    6.     public interface IAssetGUID
    7.     {
    8.         /// <summary>
    9.         /// Accessor to the asset's AssetGUID data.
    10.         /// </summary>
    11.         public AssetGUID AssetGUID { get; }
    12.     }
    13. }
    Most likely they just have simple databases of their own. I just use, you guessed it, more scriptable objects as simple databases.
  13. Kurt-Dekker


    Mar 16, 2013
    Spiney! I would never murder you! But I will say that since the filesystem imposes unique names, I'm happy to lean on that technology. Your mileage may vary. I just love code that doesn't exist because then it cannot have bugs. :)
  14. Kurt-Dekker


    Mar 16, 2013
    I did indeed base it upon that, but it could be based on any resource locator pattern you want: Resources.Load<T>(), Addressables, your own flavor, Spiney's GUID-based type, etc.
  15. DSivtsov


    Feb 20, 2019
    The current Unity architecture doesn't give a possibility to serialize any objects (instance of class) (e.g. ScriptableObject) with Type fields with reference to any Unity assets (include other ScriptableObject), especial in build of Game.
    Because the current main API to work with assets in runtime is the methods of class Resources (UnityEngine namespace) which give only possibility to load asset based on known path to the asset. And doesn't give a possibility to detect the path of the loaded asset.
    In this case to store this Type objects (in Runtime especial) must be used any workarounds described in this thread by @Kurt-Dekker and @spiney199.
    But to be completely honest, if you want store this Type objects in Editor you can use the Odin Serializer (free part software of Odin Inspector), which use the possibilities the methods of class AssetDatabase (UnityEditor namespace !!!).
    The TeamSirenix made the good products, but not very clear documentations (the procedure partially described on they GitHub TeamSirenix and partially on they tutorials External Reference Resolver, below I put the compilation from these sources, because a part of the original code contains many small weird errors):
    • The object to store is GameSettingsSO (described in my first post here).
    • The procedure of Store contains 2 steps (the Load will use the antiwise sequence of steps)):
    1. Call Save and receive the file with references to assets and List<Object> unityReferences which contains list of these assets.
    2. Call one method (here the SaveGuiDRef) from "External Reference Resolver" (the methods which realize the IExternalStringReferenceResolver or IExternalGuidReferenceResolver interface) which for every element from the list get the GuiD of asset and store it in the separate file.
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using OdinSerializer;
    4. using System.IO;
    6. public static class Example
    7. {
    8.     private const DataFormat FormatData = DataFormat.JSON;
    10.     public static void Save(GameSettingsSO data, string filePath, ref List<Object> unityReferences)
    11.     {
    12.         byte[] bytes = SerializationUtility.SerializeValue(data, FormatData, out unityReferences);
    13.         File.WriteAllBytes(filePath, bytes);
    14.     }
    16.     public static GameSettingsSO Load(string filePath, List<Object> unityReferences)
    17.     {
    18.         byte[] bytes = File.ReadAllBytes(filePath);
    19.         return SerializationUtility.DeserializeValue<GameSettingsSO>(bytes, FormatData, unityReferences);
    20.     }
    22.     static byte[] Serialize(object obj, SerializationContext context)
    23.     {
    24.         return SerializationUtility.SerializeValue(obj, FormatData, context);
    25.     }
    27.     static object Deserialize(byte[] bytes, DeserializationContext context)
    28.     {
    29.         return SerializationUtility.DeserializeValue<object>(bytes, FormatData, context);
    30.     }
    32.    public static void SaveGuiDRef(string filePath, UnityEngine.Object unityReferences)
    33.     {
    34.         var context = new SerializationContext()
    35.         {
    36.             GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    37.         };
    38.         byte[] bytes = Serialize(unityReferences, context);
    39.         File.WriteAllBytes(filePath, bytes);
    40.     }
    42.     public static Object LoadGuiDRef(string filePath)
    43.     {
    44.         byte[] bytes = File.ReadAllBytes(filePath);
    45.         var context = new DeserializationContext()
    46.         {
    47.             GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    48.         };
    49.         return (Object)Deserialize(bytes, context);
    50.     }
    51. }
    I removed little errors and refactored the ScriptableObjectGuidReferenceResolver w/o the "ToString("N")" it is not work initially.
    Didn't test the ScriptableObjectStringReferenceResolver, but with the same refactoring it will also works
    Code (CSharp):
    1. public class ScriptableObjectGuidReferenceResolver : IExternalGuidReferenceResolver
    2. {
    3.     public IExternalGuidReferenceResolver NextResolver { get; set; }
    5.     public bool CanReference(object value, out Guid id)
    6.     {
    7.         if (value is ScriptableObject)
    8.         {
    9.             id = new Guid(AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject)));
    10.             return true;
    11.         }
    13.         id = default(Guid);
    14.         return false;
    15.     }
    17.     public bool TryResolveReference(Guid id, out object value)
    18.     {
    19.         value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id.ToString("N")));
    20.         return value != null;
    21.     }
    22. }
    As you can see, it uses methods from the AssetDatabase class, so it won't work in the Build.
    Last edited: Sep 1, 2022
  16. DSivtsov


    Feb 20, 2019
    To make the output of these scripts more visual, I replaced the GameSettingSO on this class (It's a plain c# class with one field with ref to asset - ComplexitySO, don't forget the output Odin JSON little differ from std Unity JSON):
    Code (CSharp):
    1. public class GameSettings
    2. {
    3.     [SerializeField] private ComplexitySO _complexityGame;
    4.     [SerializeField] private PlayMode _usedPlayMode;
    5.     [SerializeField] private bool _notCopyToGlobal;
    6.     [SerializeField] private bool _globalDefaultTopList;
    7.     [SerializeField] private float _masterVolume;
    8.     [SerializeField] private float _musicVolume;
    9.     [SerializeField] private float _effectVolume;
    10.     [SerializeField] private SequenceType _musicSequenceType;
    11. }
    and put the result of output after first step:
    Code (CSharp):
    1. {
    2.     "$id": 0,
    3.     "$type": "0|GameSettings, Assembly-CSharp",
    4.     "_complexityGame": $eref:0,
    5.     "_usedPlayMode": 2,
    6.     "_notCopyToGlobal": true,
    7.     "_globalDefaultTopList": true,
    8.     "_masterVolume": 2,
    9.     "_musicVolume": 3,
    10.     "_effectVolume": 4,
    11.     "_musicSequenceType": 1
    12. }
    and after Second:
    Code (CSharp):
    1. $guidref:a385e0b8-1915-59e4-5bd9-a781a116b3ba
  17. spiney199


    Feb 11, 2021
    Still sounds like you're still at square one, honestly. As Bunny said, you still have to produce your own lookup system to convert these GUID's back to asset references.

    I've been working on just that myself, but in a manner which should hopefully be project agnostic. It'll also use the Odin Serialiser's external reference resolvers, but swap out assets with various types of substitutes and restore them from a lookup table system.
    Last edited: Sep 1, 2022
  18. DSivtsov


    Feb 20, 2019
    :) I simple love make some summary, which i can think may be useful for others (and I two days made a rest - was helped to Agamemnon to defeat Troy (ToW))
    ... And I know why on forum used the the Surrogate term to discuss the solutions for these serialization issues.
    Last edited: Sep 1, 2022
  19. Bunny83


    Oct 18, 2010
    I guess this was just a quickly written example, because this would not be valid json and could not be read by any json parser :) You probably wanted to quote that value
    , right?

    Though as Spiney has reiterated, Unity doesn't really provide any kind of id that can be used for read / write operations. So you have to come up with your own system for giving objects ids and a way to search for them / look them up.
  20. DSivtsov


    Feb 20, 2019
    No, It's a real output from code (copy/past) which I received when I tested the Save/Load the Object with ref to asset in Editor (and show in this post).... and it work correctly (it could be Odin's internal JSON format).
    Because the every value placed between <:> <,>, it's not created problem for parsing.
    And i think if it will be demands exist the option which can change the format JSON to standard, but in this format it more comfortable to read by human (in the standard so many quotes)
    Last edited: Sep 1, 2022
  21. DSivtsov


    Feb 20, 2019
    in this post this was described...
    Last edited: Sep 1, 2022
  22. spiney199


    Feb 11, 2021
    You only have a way of doing it in the editor, not at run time. So you've spent days on this and still haven't solved the crux of the problem. It's useless to anyone reading this in the future as a save system that only works in the editor is pretty useless.

    To be clear, the method of serialisation is secondary to the true issue here. In fact it's almost completely ancillary. You should be putting your focus into a basic database system in which you can look up some sort of ID and use that to get an asset reference, regardless of the serialisation method.

    I'm not sure if there's a language issue at play, but you've basically ignored all the advice given so far. It's kind of infuriating.
    Last edited: Sep 2, 2022
  23. DSivtsov


    Feb 20, 2019
    I didn't understand your reaction. I had a question, you and other gave to me answers and proposals to workaround of existent issue.
    There is an Odin Serializer, the documentation of which describes (not very detail) the possibilities of use it for the same issue. I tested it and described the result of using it. It can be used to save complex Unity objects, but only in the editor (this solution will not help in the Build). It's a standard solution proposed by Odin (for Editor)
    I didn't talk what this a "final" solution for initial issue.

    As I told initially, I had two aims - store complex objects in Unity (it's not a big problem, and can be decided by 5 min through use the intermediate surrogate class, it I known initially) and how it did "elegant using the OOP and other possibilities of c#", if you studied the possibilities of Binary serialization (IFormatter, real "Surrogate class" and so on),which give a elegant possibilities to change the "standard serialization protocols", you can understand it.
    If I was a participating in Game Jam, i didn't ask here about possible solutions.
    I hope I more clear described my aims and motivations.

    Thank you for your time and information ... and take it easy.:)
    Last edited: Sep 2, 2022