Getting Saving system to work with WebGL

I finished the full course (all 4 parts) and recently built my game for WebGL to share early prototypes for feedback and play testing. I solved a bunch of Build/WebGL related bugs along the way (and posted solutions in applicable tagged threads).

One problem escapes me. Persistent storage between Web GL sessions. Browsers have ~10MB of localstorage for purposes like this. There are lots of snippets of documentation on how Unity WebGL builds can take advantage of local storage but the threads are high level I don’t have the technical know how to fill in the gaps. I will want to put my prototype on itch.io

Anyone succeed with this? I will post relevant links I found below

Also when I finish building for WebGL Unity gives me this warning but very little useful content online to know what to do with it. Perhaps this is a solved problem for somebody here.

Saving has no effect. Your class ‘UnityEditor.WebGL.HttpServerEditorWrapper’ is missing the FilePathAttribute. Use this attribute to specify where to save your ScriptableSingleton.
Only call Save() and use this attribute if you want your state to survive between sessions of Unity.
UnityEditor.BuildPlayerWindow:BuildPlayerAndRun ()

The stackoverflow link you provided seems like the way to go. I’ve only ever used the jslib thing once to determine if my webgl game is running in someone’s mobile browser (as opposed to a full pc browser) so that I can hide/show the on-screen mobile controller, so I can’t say how reliable it is but I haven’t heard any complaints.

I simply had a file called mobileBrowserDetector.jslib in a Plugins folder under Assets

mergeInto(LibraryManager.library, {

    IsMobileBrowser: function() {
        return (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent));
    },

});

To call it, I had this in a persistent class

#region External Stuff

[DllImport("__Internal")]
private static extern bool IsMobileBrowser(); // This matches the function in the jslib

#endregion

and called it in Awake()

private void Awake()
{
    if (IsMobileBrowser())
    {
        DoSomething();
    }
}

So, based on this I suspect you may be able to pass your json save data to a function in the jslib that can save it to the js localStorage as well as retrieve it again from there - as presented in the stackoverflow post.

Thank you. I am experimenting with this to see if I can put something together.

For our saving system, would you put something like the following

#region External Stuff

[DllImport("__Internal")]
private static extern bool SaveToLocalBrowserStorage(); 
private static extern bool LoadFromLocalBrowserStorage(); 
private static extern bool IsPlayingInBrowser(); 
#endregion

in the (Json)SavingSystem?

And then something like this as a modification to the JSonSavingSystem.cs ?

public IEnumerator LoadLastScene(string saveFile)
{
    if (IsPlayingInBrowser()) 
    {
         JObject state  = LoadFromBrowserStorage(saveFile);  //??
    }
    else
    {
         JObject state = LoadJsonFromFile(saveFile);
    }
}

Yeah, looks right - except that your imports will be different

#region External Stuff

[DllImport("__Internal")]
private static extern bool SaveToLocalBrowserStorage(string filename, string data);
private static extern string LoadFromLocalBrowserStorage(string filename);

#endregion

I don’t know js that well, but the jslib may look something like this (adapted from the SO post)

mergeInto(LibraryManager.library, {
    LoadFromLocalBrowserStorage: function(filename){
        if(typeof loadData  !== 'undefined'){
            return loadData(Pointer_stringify(filename));
        } else {
            console.log("Javacript function not available...");
            return "";
        }
    },
    SaveToLocalBrowserStorage: function(filename, data){
        if(typeof saveData !== 'undefined'){
            saveData(Pointer_stringify(filename), Pointer_stringify(data));
            return true;
        } else {
            console.log("Javacript function not available...");
            return false;
        }
    }
});

I left out the IsPlayingInBrowser() because WebGL will always be played in the browser. For the Editor you could use the compiler directives

public IEnumerator LoadLastScene(string saveFile)
{
#if UNITY_EDITOR
    JObject state = LoadJsonFromFile(saveFile);
#else
    JObject state = LoadFromLocalBrowserStorage(saveFile);
#endif
}

These are just thoughts. Things I would’ve tried. I don’t know if any of this is sufficient

1 Like

Thanks. I have a contact locally here who is helping me out with this. I will post as soon as I can get something that integrates our JSonSavingSystem and JSonSavingWrapper with this external system.

I am getting close to having it working!

I am only doing for JSon variants since I think it’s not OK to save/load binaries for WebGL.

I’m not sure if the system will stop you, but to quote Lilly Allen, It’s really not ok. BinaryFormatter is it’s most dangerous in environments like WebGL. I mentioned in the other thread that WebGL is sandboxed, and how great that is, but a hacker worth his salt can use BF to inject his way right through the sandbox walls of HTML5 or WebGL apps.

Ok folks here it is. I got it 95% of the way there. Special thanks to Matt Brelsford who helped me a ton with this sample code https://github.com/giantlight-matt/unity-webgl-itch-playerprefs/

Gaps

  1. I couldn’t get the list saves to work. There’s a bug in my code and I’m not sure exactly how to operate on lists / arrays in JS. With this bug it is not possible to load a prior save other than with the continue button.

  2. The code is ugly. I haven’t cleaned it up. Ideally I think what would make this neater is to put all persistence access (be it local file system, browser local storage, or player prefs) in a single class. You can then get rid of #if in our SavingSystem and SavingWrapper and just keep it in one class that does the appropriate thing based on the build.

Help needed: If anyone with a little more experience than me can look at the above two and try to address them that would be awesome!

Instructions on how to use.

  1. Create a Plugins folder under assets. Put the JS file there.
  2. Put UserDataManager somewhere. I used a UserDataManager namespace. It could possibly go in a GameDevTV library once properly refactored.
  3. Modify JsonSavingSystem and JsonSavingLibrary as shown.

P.S. Also apologies… I couldn’t figure out the correct markdown to get the expandable/collapsible text boxes and also have it render as code.

jsLibrary.jslib
mergeInto(LibraryManager.library, {
    loadData: function(yourkey){
        var returnStr = "";

        if(localStorage.getItem(UTF8ToString(yourkey)) !==null)
        {
            returnStr = localStorage.getItem(UTF8ToString(yourkey));
        }

        var bufferSize = lengthBytesUTF8(returnStr) + 1;
        var buffer = _malloc(bufferSize);
        stringToUTF8(returnStr, buffer, bufferSize);
        return buffer;
    },
    saveData: function(yourkey, yourdata){
        localStorage.setItem(UTF8ToString(yourkey), UTF8ToString(yourdata));
    },
    deleteKey: function(yourkey){
        localStorage.removeItem(UTF8ToString(yourkey));
    },
    deleteAllKeys: function(prefix){
        for ( var i = 0, len = localStorage.length; i < len; ++i ) {
            var key = localStorage.key(i);
            if(key != null && key.startsWith(prefix)){
                localStorage.removeItem(key);
            }
        }
    },
    listAllKeys: function(prefix){
        var keyList = [];    
        for ( var i = 0, len = localStorage.length; i < len; ++i ) {
            var key = localStorage.key(i);
            if(key != null && key.startsWith(prefix)){
                key = key.substring(prefix.length);
                keyList.push(key);
            }
        }
    return keyList;
    }
});
UserDataManager.cs
using System.Runtime.InteropServices;
using UnityEngine;

public static class UserDataManager
{
    private const string WebGLSavePath = "save_files/";
    private const string WebGLPrefPath = "player_pref/";

    private static string GetPathForKey(string key)
    {
        return WebGLSavePath + key;
    }

    private static string GetPathForPreferenceKey(string key)
    {
        return WebGLPrefPath + key;
    }

    public static void SetString(string key, string data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        saveData(GetPathForKey(key), data);
#else
        //TODO: 
#endif
    }

    public static void SetPreferenceString(string key, string data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        saveData(GetPathForPreferenceKey(key), data);
#else
        PlayerPrefs.SetString(key, data);
#endif
    }

    public static string GetString(string key, string defaultValue = "")
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        return loadData(GetPathForKey(key));
#else
        //TODO:
        return "";
#endif
    }

    public static string GetPreferenceString(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        return loadData(GetPathForPreferenceKey(key));
#else
        return PlayerPrefs.GetString(key);
#endif
    }

    public static bool HasKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        var data = loadData(GetPathForKey(key));
        return data != string.Empty;
#else
        //TODO:
        return false;
#endif
    }

    public static bool HasPreferenceKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        var data = loadData(GetPathForPreferenceKey(key));
        return data != string.Empty;
#else
        return PlayerPrefs.HasKey(key);
#endif
    }

    public static void DeleteKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        deleteKey(GetPathForKey(key));
#else
        //TODO:
#endif
    }

    public static void DeleteAllSaves(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        deleteAllKeys(WebGLSavePath);
#else
        //TODO:
#endif
    }

    public static string[] ListAllSaveKeys()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        Debug.Log($"Key list: {listAllKeys(WebGLSavePath)}");
        return listAllKeys(WebGLSavePath);
#else
        //TODO:
        return null;
#endif
    }

    [DllImport("__Internal")]
    private static extern void saveData(string key, string data);

    [DllImport("__Internal")]
    private static extern string loadData(string key);

    [DllImport("__Internal")]
    private static extern string deleteKey(string key);

    [DllImport("__Internal")]
    private static extern string deleteAllKeys(string prefix);

    [DllImport("__Internal")]
    private static extern string[] listAllKeys(string prefix);
}
JsonSavingSystem.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using static UserDataManager;

namespace GameDevTV.Saving
{
    public class JsonSavingSystem : MonoBehaviour
    {
        private const string extension = ".json";
        
        /// <summary>
        /// Will load the last scene that was saved and restore the state. This
        /// must be run as a coroutine.
        /// </summary>
        /// <param name="saveFile">The save file to consult for loading.</param>
        public IEnumerator LoadLastScene(string saveFile)
        {
            JObject state = LoadJsonFromFile(saveFile);
            IDictionary<string, JToken> stateDict = state; 
            int buildIndex = SceneManager.GetActiveScene().buildIndex;
            if (stateDict.ContainsKey("lastSceneBuildIndex"))
            {
                buildIndex = (int)stateDict["lastSceneBuildIndex"];
            }
            yield return SceneManager.LoadSceneAsync(buildIndex);
            RestoreFromToken(state);
        }

        /// <summary>
        /// Save the current scene to the provided save file.
        /// </summary>
        public void Save(string saveFile)
        {
            JObject state = LoadJsonFromFile(saveFile);
            CaptureAsToken(state);
            SaveFileAsJSon(saveFile, state);
        }

        /// <summary>
        /// Delete the state in the given save file.
        /// </summary>
        public void Delete(string saveFile)
        {
            
#if UNITY_WEBGL && !UNITY_EDITOR
            UserDataManager.DeleteKey(saveFile);
#else
            File.Delete(GetPathFromSaveFile(saveFile));
#endif
        }

        public void Load(string saveFile)
        {
            RestoreFromToken(LoadJsonFromFile(saveFile));
        }
        
        public bool SaveFileExists(string saveFile)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            Debug.Log($"Has Key for file: {saveFile} is status: {UserDataManager.HasKey(saveFile)}");
            return UserDataManager.HasKey(saveFile);
#else
            string path = GetPathFromSaveFile(saveFile);
            return File.Exists(path);
#endif
        }

        public IEnumerable<string> ListSaves()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            return UserDataManager.ListAllSaveKeys();
#else
            foreach (string path in Directory.EnumerateFiles(Application.persistentDataPath))
            {
                Debug.Log("Trying to get file at path: {path}");
                if (Path.GetExtension(path) == extension)
                {
                    yield return Path.GetFileNameWithoutExtension(path);
                }
            }
#endif
        }

        // PRIVATE

        private JObject LoadJsonFromFile(string saveFile)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            Debug.Log($"Save file as seen in LoadJson: {saveFile} ");
            string jsonSaveFileAsString = UserDataManager.GetString(saveFile);
            Debug.Log($"returned file contents from LoadJSonFromFile {jsonSaveFileAsString}");
            if (string.IsNullOrWhiteSpace(jsonSaveFileAsString)) return new JObject();
            return JObject.Parse(jsonSaveFileAsString);
#else
            string path = GetPathFromSaveFile(saveFile);
            if (!File.Exists(path))
            {
                return new JObject();
            }
            
            using (var textReader = File.OpenText(path))
            {
                using (var reader = new JsonTextReader(textReader))
                {
                    reader.FloatParseHandling = FloatParseHandling.Double;

                    return JObject.Load(reader);
                }
            }
#endif
        }

        private void SaveFileAsJSon(string saveFile, JObject state)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            Debug.Log($"Attempting to save file {saveFile} with data {state.ToString()} ");
            UserDataManager.SetString(saveFile, state.ToString());
#else
            string path = GetPathFromSaveFile(saveFile);
            print("Saving to " + path);
            using (var textWriter = File.CreateText(path))
            {
                using (var writer = new JsonTextWriter(textWriter))
                {
                    writer.Formatting = Formatting.Indented;
                    state.WriteTo(writer);
                }
            }
#endif
        }


        private void CaptureAsToken(JObject state)
        {
            IDictionary<string, JToken> stateDict = state;
            foreach (JsonSavableEntity savable in FindObjectsOfType<JsonSavableEntity>())
            {
                stateDict[savable.GetUniqueIdentifier()] = savable.CaptureAsJToken();
            }

            stateDict["lastSceneBuildIndex"] = SceneManager.GetActiveScene().buildIndex;
        }


        private void RestoreFromToken(JObject state)
        {
            IDictionary<string, JToken> stateDict = state;
            foreach (JsonSavableEntity savable in FindObjectsOfType<JsonSavableEntity>())
            {
                string id = savable.GetUniqueIdentifier();
                if (stateDict.ContainsKey(id))
                {
                    savable.RestoreFromJToken(stateDict[id]);
                }
            }
        }


        private string GetPathFromSaveFile(string saveFile)
        {
            return Path.Combine(Application.persistentDataPath, saveFile + extension);
        }
    }
}
SavingWrapper.cs
using System;
using GameDevTV.Saving;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace RPG.SceneManagement
{
    public class SavingWrapper : MonoBehaviour
    {
        private const string currentSaveKey = "currentSaveName";
        [SerializeField] private float fadeInTime;
        [SerializeField] private float fadeOutTime;
        [SerializeField] private int firstSceneBuildIndex = 1;
        [SerializeField] private int menuSceneBuildIndex = 0;

        public void ContinueGame()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            Debug.Log($"Checking if Player Preference Key Exists: {currentSaveKey} and value: {UserDataManager.GetPreferenceString(currentSaveKey)}");
            if (!UserDataManager.HasPreferenceKey(currentSaveKey)) return;
#else
            if (!PlayerPrefs.HasKey(currentSaveKey)) return;
#endif
            if (!GetComponent<JsonSavingSystem>().SaveFileExists(GetCurrentSave())) return;
            StartCoroutine(LoadLastScene());
        }

        public void NewGame(string saveFile)
        {
            if (String.IsNullOrEmpty(saveFile)) return;
            SetCurrentSave(saveFile);
            StartCoroutine(LoadFirstSceneandSave());
        }

        public void LoadGame(string saveFile)
        {
            SetCurrentSave(saveFile);
            ContinueGame();
        }
        
        public void LoadMenu()
        {
            StartCoroutine(LoadMenuScene());
        }

        private void SetCurrentSave(string saveFile)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            UserDataManager.SetPreferenceString(currentSaveKey, saveFile);
            Debug.Log($"Setting Player Preference Current Save Key: {currentSaveKey} and value: {UserDataManager.GetPreferenceString(currentSaveKey)}");
#else
            PlayerPrefs.SetString(currentSaveKey, saveFile);
#endif
        }

        private string GetCurrentSave()
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            Debug.Log($"Getting current save from WebGL prefs. Key: {currentSaveKey} Value: {UserDataManager.GetPreferenceString(currentSaveKey)}");
            return UserDataManager.GetPreferenceString(currentSaveKey);            
#else
            Debug.Log($"Get current save from playerprefs is: {PlayerPrefs.GetString(currentSaveKey)}");
            return PlayerPrefs.GetString(currentSaveKey);
#endif
        }

        private IEnumerator LoadLastScene()
        {
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeOutTime);
            yield return GetComponent<JsonSavingSystem>().LoadLastScene(GetCurrentSave());
            yield return fader.FadeIn(fadeInTime);
        }

        private IEnumerator LoadFirstScene()
        {
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeOutTime);
            yield return SceneManager.LoadSceneAsync(firstSceneBuildIndex);
            yield return fader.FadeIn(fadeInTime);
        }
        
        private IEnumerator LoadFirstSceneandSave()
        {
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeOutTime);
            yield return SceneManager.LoadSceneAsync(firstSceneBuildIndex);
            yield return fader.FadeIn(fadeInTime);
            Save();
        }
        
        private IEnumerator LoadMenuScene()
        {
            Fader fader = FindObjectOfType<Fader>();
            yield return fader.FadeOut(fadeOutTime);
            yield return SceneManager.LoadSceneAsync(menuSceneBuildIndex);
            yield return fader.FadeIn(fadeInTime);
        }

        private void Update()
        {
            // https://community.gamedev.tv/t/followups-on-pickup-destruction-race-conditions/228964
            if (SceneManager.GetActiveScene().buildIndex == 0) return;
            
            if (Input.GetKeyDown(KeyCode.S))
            {
                Save();
            }
            
            if (Input.GetKeyDown(KeyCode.L))
            {
                StartCoroutine(LoadLastScene());
            }
            
            if (Input.GetKeyDown(KeyCode.Delete))
            {
                Delete();
            }
        }

        public void Load()
        {
            GetComponent<JsonSavingSystem>().Load(GetCurrentSave());
        }

        public void Save()
        {
            GetComponent<JsonSavingSystem>().Save(GetCurrentSave());
        }

        public void Delete()
        {
            GetComponent<JsonSavingSystem>().Delete(GetCurrentSave());
        }

        public IEnumerable<string> ListSaves()
        {
            return GetComponent<JsonSavingSystem>().ListSaves();
        }
    }
}

image

I’m going to, at least for now, hope that the wisdom of the crowd can step up on this one. JS is actually one of my few weaknesses (in addition to refined sugar and kryptonite). I would see if folks in the Discord have any ideas.

I figured out the solution, just doing some final testing. The root cause was in how strings (and even arrays of strings) need to be converted between C# and JS. Lots of land mines.

I don’t want to post the code yet, since I need to also do input validation on MainMenu.UI NewGame() It is critical that it does input validation.

Actually - @Brian_Trotter could use your input here. Should we have input validation on the characters for the saveFile in SavingWrapper and/or JSonSavingSystem? Especially as this is meant for a built game?

Something like this…

        public bool IsValidSaveFileName(string saveFile)
        {
            string allowedCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

            foreach (char c in saveFile)
            {
                if (!allowedCharacters.Contains(c.ToString()))
                {
                    Debug.LogWarning("Save File Name does not use valid characters");
                    return false;
                }
            }
            return true;
        }

If only TextMeshPro’s Input fields contained some sort of Regex validation…
image

That being said, you should still validate separately against existing save files.

1 Like

Alright. It’s ugly but listing files works. Few notes

  1. I haven’t tested deletion. That had a small bug in it also that I think I fixed. You’ll probably want deletion to get rid of old/incompatible save files and preferences.
  2. The code is UGLY. I left a lot of console print / debug statements in it, in case anyone wants to experiment with it. Methods names, the preprocessor directives, everything really deserves a good cleanup and refactoring.
  3. Make sure you validate input. Two gotchas here:
    ** JS strings and C-style strings are very different and you have to convert back and forth between them.
    ** Arrays in JS and arrays in C# are different. To get it to work, I had to return a list of delimited strings and then have the C# side convert it to an array. You want to make sure the chosen delimiter character is not part of a file name.
  4. I am using yield returns now on the C# side interfacing with JS because the conversion of an empty string from JS seems to create a C# string array of size 1 rather than one of size 0.

I’ll note that ChatGPT offered some hints but couldn’t fully solve it. ChatGPT gave me useful hints on point 3.

So yeah to summarize - if anyone wants to review and clean this up, be my guest!

jsLibrary.jslib
mergeInto(LibraryManager.library, {
    loadData: function(yourkey){
        var returnStr = "";

        if(localStorage.getItem(UTF8ToString(yourkey)) !==null)
        {
            returnStr = localStorage.getItem(UTF8ToString(yourkey));
        }

        var bufferSize = lengthBytesUTF8(returnStr) + 1;
        var buffer = _malloc(bufferSize);
        stringToUTF8(returnStr, buffer, bufferSize);
        return buffer;
    },
    saveData: function(yourkey, yourdata){
        localStorage.setItem(UTF8ToString(yourkey), UTF8ToString(yourdata));
    },
    deleteKey: function(yourkey){
        localStorage.removeItem(UTF8ToString(yourkey));
    },
    deleteAllKeys: function(prefix){
        prefix = UTF8ToString(prefix);
        
        for ( var i = 0, len = localStorage.length; i < len; ++i ) {
            var key = localStorage.key(i);
            if(key != null && key.startsWith(prefix)){
                localStorage.removeItem(key);
            }
        }
    },
    listAllKeys: function(prefix){
        var keyList = [];
        console.log("Local Storage Length:", localStorage.length);
        prefix = UTF8ToString(prefix);
        console.log("Prefix", prefix);
        for ( var i = 0, len = localStorage.length; i < len; ++i ) {
            var key = localStorage.key(i);
            console.log("Prefixed Key:", key);
            var hasPrefix = key.startsWith(prefix);
            console.log("Has Prefix of: ", prefix, ": ", hasPrefix);
            if(key != null && hasPrefix){
                key = key.substring(prefix.length);
                console.log("    Key:", key);
                keyList.push(key);
            }
        }
        console.log("KeyList Length:", keyList.length);
        console.log("    KeyList in JS:", keyList);
        var result = keyList.join(',');
        console.log("    KeyList in JS after join:", result);
        var bufferSize = lengthBytesUTF8(result) + 1;
        var buffer = _malloc(bufferSize);
        stringToUTF8(result, buffer, bufferSize);
        return buffer;
    }
});
UserDataManager.cs
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;

public static class UserDataManager
{
    private const string WebGLSavePath = "save_files/";
    private const string WebGLPrefPath = "player_pref/";

    private const string allowedCharacters =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz0123456789.";


    public static bool IsValidSaveFileName(string saveFile)
    {
        if (String.IsNullOrEmpty(saveFile))
        {
            Debug.LogWarning("Save File Name was null or empty");
            return false;
        }

        foreach (char c in saveFile)
        {
            if (!allowedCharacters.Contains(c.ToString()))
            {
                Debug.LogWarning("Save File Name does not use valid characters");
                return false;
            }
        }

        return true;
    }

    private static string GetPathForKey(string key)
    {
        return WebGLSavePath + key;
    }

    private static string GetPathForPreferenceKey(string key)
    {
        return WebGLPrefPath + key;
    }

    public static void SetString(string key, string data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        saveData(GetPathForKey(key), data);
#else
        //TODO: 
#endif
    }

    public static void SetPreferenceString(string key, string data)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        saveData(GetPathForPreferenceKey(key), data);
#else
        PlayerPrefs.SetString(key, data);
#endif
    }

    public static string GetString(string key, string defaultValue = "")
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        return loadData(GetPathForKey(key));
#else
        //TODO:
        return "";
#endif
    }

    public static string GetPreferenceString(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        return loadData(GetPathForPreferenceKey(key));
#else
        return PlayerPrefs.GetString(key);
#endif
    }

    public static bool HasKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        var data = loadData(GetPathForKey(key));
        return data != string.Empty;
#else
        //TODO:
        return false;
#endif
    }

    public static bool HasPreferenceKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        var data = loadData(GetPathForPreferenceKey(key));
        return data != string.Empty;
#else
        return PlayerPrefs.HasKey(key);
#endif
    }

    public static void DeleteKey(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        deleteKey(GetPathForKey(key));
#else
        //TODO:
#endif
    }

    public static void DeleteAllSaves(string key)
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        deleteAllKeys(WebGLSavePath);
#else
        //TODO:
#endif
    }

    public static IEnumerable<string> ListAllSaveKeys()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        string keysString = listAllKeys(WebGLSavePath);
        Debug.Log($"Unprocessed Array is: {keysString}");
        string[] keysArray = keysString.Split(',');
        Debug.Log($"KeyArray Length: {keysArray.Length} and contents: {string.Join(", ", keysArray)}");
        int i = 0;
        foreach (var key in keysArray)
        {
            if (IsValidSaveFileName(key)) {
                Debug.LogWarning($"Key {i}: {key}");
                i++;
                yield return key;
            }
            else
            {
                Debug.LogWarning($"Key {i}: {key} is not valid save file");
            }
        }
#else
        //TODO:
        return null;
#endif
    }

    [DllImport("__Internal")]
    private static extern void saveData(string key, string data);

    [DllImport("__Internal")]
    private static extern string loadData(string key);

    [DllImport("__Internal")]
    private static extern string deleteKey(string key);

    [DllImport("__Internal")]
    private static extern string deleteAllKeys(string prefix);

    [DllImport("__Internal")]
    private static extern string listAllKeys(string prefix);
}

Privacy & Terms