Forum rules - please read before posting.

Decimal Separator Issue in Serialization with Steam Cloud Across Different Regional Settings

edited April 2023 in Engine development

Hello!

After facing a persistent issue with decimal separators when saving and loading game data using Steam Cloud I found out the problem was because of regional settings. I have successfully identified the root cause of the decimal separator error and would like to share a possible solution with you all.

Issue:
When saving the game's Score - for example - to Steam Cloud and loading it on a different computer, the decimal separator was incorrectly parsed. This issue occurs with various serialization formats, including XML, binary, and JSON. A float of 14.72 turns into 1472.00.

Cause:
The problem was related to handling decimal numbers inconsistently across different regional settings during serialization and deserialization.

Resolution:
To resolve this issue, I temporarily changed the current thread's culture to CultureInfo.InvariantCulture during serialization and deserialization processes. This solution can be applied to various serialization formats, ensuring that decimal separators are consistently parsed across different regional settings when using Steam Cloud.

public virtual string SerializeObject <T> (object dataObject)
{
    System.Globalization.CultureInfo oldCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
    System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;

    MemoryStream memoryStream = new MemoryStream(); 
    XmlSerializer xs = new XmlSerializer (typeof (T)); 
    XmlTextWriter xmlTextWriter = new XmlTextWriter (memoryStream, Encoding.UTF8); 

    xs.Serialize (xmlTextWriter, dataObject); 
    memoryStream = (MemoryStream) xmlTextWriter.BaseStream;

    System.Threading.Thread.CurrentThread.CurrentCulture = oldCulture;
    return UTF8ByteArrayToString (memoryStream.ToArray());
}


public virtual T DeserializeObject <T> (string dataString)
{
    System.Globalization.CultureInfo oldCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
    System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;

    // ... rest of the method ...

    System.Threading.Thread.CurrentThread.CurrentCulture = oldCulture;
    return default (T);
}

Anyway, that's all!

Comments

  • Thanks very much for sharing such detailed findings.

    Looks good. From what you've seen, though, do you think this could cause issues when loading existing save data?

  • I've done some digging, and I'm not 100% sure this'd cover all situations. The SerializeObject function won't necessarily handle all string conversions - variables, for example, are converted to strings beforehand.

    An alternative option would be to hook into save/load custom events, to have it alter the culture before and after the operation. It's the same principle, just covering the entire process:

    using System.Globalization;
    using System.Threading;
    using UnityEngine;
    using AC;
    
    public class InvariantCultureSaves : MonoBehaviour
    {
    
        private CultureInfo backupCulture;
    
        private void OnEnable ()
        {
            EventManager.OnBeforeLoading += OnBeforeLoading;
            EventManager.OnBeforeSaving += OnBeforeSaving;
            EventManager.OnFailLoading += OnFailLoading;
            EventManager.OnFinishLoading += OnFinishLoading;
            EventManager.OnFailSaving += OnFailSaving;
            EventManager.OnFinishSaving += OnFinishSaving;
        }
    
        private void OnDisable ()
        {
            EventManager.OnBeforeLoading -= OnBeforeLoading;
            EventManager.OnBeforeSaving -= OnBeforeSaving;
            EventManager.OnFailLoading -= OnFailLoading;
            EventManager.OnFinishLoading -= OnFinishLoading;
            EventManager.OnFailSaving -= OnFailSaving;
            EventManager.OnFinishSaving -= OnFinishSaving;
        }
    
        private void OnFinishLoading () { RestoreBackup (); }
        private void OnFinishSaving (SaveFile saveFile) { RestoreBackup (); }
        private void OnFailSaving (int saveID) { RestoreBackup (); }
        private void OnFailLoading (int saveID) { RestoreBackup (); }
        private void OnBeforeSaving (int saveID) { SetInvariant (); }
        private void OnBeforeLoading (SaveFile saveFile) { SetInvariant (); }
    
        private void SetInvariant ()
        {
            backupCulture = Thread.CurrentThread.CurrentCulture;
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
        }
    
        private void RestoreBackup ()
        {
            Thread.CurrentThread.CurrentCulture = backupCulture;
        }
    
    }
    
Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.