Loading through LoadInto in our use case broke after latest update

Discussion and help for Easy Save 3
Post Reply
User avatar
TauCetiLabs
Posts: 11
Joined: Mon Dec 28, 2020 2:31 pm

Loading through LoadInto in our use case broke after latest update

Post by TauCetiLabs »

Overall, using ES3 has been great. However, I have just run into an issue after updating ES3 (which I needed to do to fix another bug after updating Netcode for Gameobjects).

We are using ES3 with a custom script that allows any script to add an 'ISaveLoadComponent' within the hierarchy of a 'SaveLoadEntity'. This had been working great before, but now while it saves everything, the 'LoadInto' code is called but does nothing and throws no error. Other data, such as the 'SiteOres', are loaded correctly, but while eadh 'SaveLoadEntity' gets instantiated, none of the variables within the 'ISaveLoadComponent's are changed (public or with the [ES3Serializable] tag). I have been debugging this for hours now, with no progress.

I'll cut down the code to just the relevant bits, the whole code is provided at the very end.

Here is the save code (works just fine)

Code: Select all

        List<SaveLoadEntity> entities = m_Site.SaveLoadEntities;
        ES3.Save("SiteEntityCount", entities.Count, settings);
        for (int j = 0; j < entities.Count; j++)
        {
            SaveLoadEntity sle = entities[j];
            ES3.Save("SaveLoadEntity_" + j + "ID", sle.UniqueID, settings);

            for (int k = 0; k < sle.Components.Length; k++)
            {
                for (int l = 0; l < sle.Components.Length; l++)
                {
                    if (k != l && sle.Components[k].SLC_UniqueName == sle.Components[l].SLC_UniqueName)
                    {
                        GameObject tracePoint = sle.Components[k].SavedComponent.gameObject;

                        Debug.LogError("Saving component with non-unique uniqueID : " + sle.gameObject.name + " : " + sle.UniqueID + " : " + sle.Components[k].SLC_UniqueName + " : " + DebugHelper.HierarchyTrace(tracePoint));
                        break;
                    }
                }

                if (sle.Components[k] == null) { Debug.LogError("Tried saving a null reference : " + sle.gameObject.name + " : " + sle.UniqueID); break; }
                ES3.Save("Component_" + j + "_" + sle.Components[k].SLC_UniqueName, sle.Components[k].SavedComponent, settings);
            }
        }
This creates entries like

Code: Select all

                "Component_0_GridOccupant" : {
		"__type" : "GridOccupant,SiteCore",
		"value" : {
		"_ES3Ref" : "3797481660449551490",
		"goID" : "1084078376051653123",
		"_IsRooted_Offline" : true,
		"IsRooted" : true,
		"GridIndex" : 3,
		"LocalPose" : {
			"Point" : {
				"m_P" : -10,
				"m_Q" : -6
			},
			"Height" : -0.3107605,
			"Rotation" : 0
		}
	}
	},


And here is the equivalent load section (does not load any data into the components)

Code: Select all

int entitiesCount = ES3.Load<int>("SiteEntityCount", settings);
        SaveLoadEntity[] entities = new SaveLoadEntity[entitiesCount];
        for (int j = 0; j < entities.Length; j++)
        {
            string uniqueID = ES3.Load<string>("SaveLoadEntity_" + j + "ID", settings);
            SaveLoadEntity sleFound = null;
            foreach (EquipmentType sle in EquipmentType.AllTypes)
            {
                SaveLoadEntity entity = sle.SimulationPrefab.GetComponent<SaveLoadEntity>();
                if (entity != null && entity.UniqueID == uniqueID)
                {
                    sleFound = entity;
                    break;
                }
            }

            Debug.Log("SaveLoadSite: Loading entity - " + sleFound.ToString());

            if (sleFound)
            {
                entities[j] = Instantiate(sleFound, Site.transform);

                if (Application.isPlaying)
                {
                    //get the instances NetworkObject and Spawn
                    NetworkObject newCharNO = entities[j].GetComponent<NetworkObject>();
                    newCharNO.Spawn();
                    newCharNO.TrySetParent(Site.transform);
                }
                else
                {
                    entities[j].transform.SetParent(Site.transform);
                }

                //load in all the entity components that do not reference other components
                for (int k = 0; k < entities[j].Components.Length; k++)
                {
                    if (ES3.KeyExists("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, settings))
                    {
                        Debug.Log("SaveLoadSite: Loading component - " + entities[j].UniqueID + " - " + entities[j].Components[k].SLC_UniqueName + " - " + entities[j].Components[k].SavedComponent);
                        ES3.LoadInto("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, entities[j].Components[k].SavedComponent, settings);
                    }
                }
            }
            else
            {
                Debug.LogError("SaveLoadSite: Entity not found when loading: " + uniqueID, gameObject);
            }

            yield return new WaitForSeconds(0.1f);
        }
In the loading code, the log messages right before the LoadInto call is showing up, and it does iterate over all of the components without any errors being thrown. For example:

Code: Select all

SaveLoadSite: Loading component - LaunchComplex - GridOccupant - Launch Complex Simulation(Clone) (GridOccupant)
UnityEngine.Debug:Log (object)
SaveLoadSite/<StartLoad>d__12:MoveNext () (at Assets/Scripts/SaveLoad/SaveLoadSite.cs:236)
UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)

Thank you in advance for any help or pointers in the right direction!

Code: Select all

public class SaveLoadSite : MonoBehaviour, ISaveLoadHandler
{
    public string UniqueID => m_Site.UniqueID;

    private string FileName = "_Site.lbs";

    public Site Site => m_Site;
    [SerializeField] private Site m_Site;

    [SerializeField] private List<Component> m_SaveLoadComponents;

    public bool SaveLoadInProgress => m_SaveLoadInProgress;
    private bool m_SaveLoadInProgress;

    public IEnumerator StartNewGame()
    {
        m_SaveLoadInProgress = true;

        yield return new WaitForSeconds(0.1f);

        m_SaveLoadInProgress = false;
    }

    public IEnumerator StartSave(string folder)
    {
        //make sure there is a save folder
        if (!ES3.DirectoryExists(folder))
        {
            yield break;
        }

        //make sure there is no core folder yet
        if (ES3.FileExists(folder + UniqueID + FileName))
        {
#if UNITY_EDITOR
            if (EditorUtility.DisplayDialog("Overwrite Site File", $"A {UniqueID + FileName} site file already exists, do you want to overwrite it?", "Yes", "No"))
            {
                ES3.DeleteFile(folder + UniqueID + FileName);
            }
            else
            {
                yield break;
            }
#else
            Debug.LogError($"Tried to save when a {FileName} Site file already exists : " + folder);
            yield break;
#endif
        }

        m_SaveLoadInProgress = true;

        ES3Settings settings = new ES3Settings(ES3.Location.Cache);
        settings.path = folder + UniqueID + FileName;

        //save game version
        ES3.Save("Version", Application.version, settings);

        //save site information
        ES3.Save("Site", m_Site, settings);
        ES3.Save("SiteID", m_Site.UniqueID, settings);
        ES3.Save("SiteBody", m_Site.Location.SOIName, settings);

        //save the ores
        ES3.Save("SiteOres", m_Site.GetComponentInChildren<OreHandler>().CeramicOreData, settings);

        //make sure the site transform is included in the references and save the reference
        ES3Internal.ES3ReferenceMgrBase.Current.Add(m_Site.transform);
        ES3.Save("SiteTransformRef", ES3Internal.ES3ReferenceMgrBase.Current.Get(m_Site.transform), settings);

        List<SaveLoadEntity> entities = m_Site.SaveLoadEntities;
        ES3.Save("SiteEntityCount", entities.Count, settings);
        for (int j = 0; j < entities.Count; j++)
        {
            SaveLoadEntity sle = entities[j];
            ES3.Save("SaveLoadEntity_" + j + "ID", sle.UniqueID, settings);

            for (int k = 0; k < sle.Components.Length; k++)
            {
                for (int l = 0; l < sle.Components.Length; l++)
                {
                    if (k != l && sle.Components[k].SLC_UniqueName == sle.Components[l].SLC_UniqueName)
                    {
                        GameObject tracePoint = sle.Components[k].SavedComponent.gameObject;

                        Debug.LogError("Saving component with non-unique uniqueID : " + sle.gameObject.name + " : " + sle.UniqueID + " : " + sle.Components[k].SLC_UniqueName + " : " + DebugHelper.HierarchyTrace(tracePoint));
                        break;
                    }
                }

                if (sle.Components[k] == null) { Debug.LogError("Tried saving a null reference : " + sle.gameObject.name + " : " + sle.UniqueID); break; }
                ES3.Save("Component_" + j + "_" + sle.Components[k].SLC_UniqueName, sle.Components[k].SavedComponent, settings);
            }
        }

        //Save the sites SaveLoadComponents
        for (int i = 0; i < m_SaveLoadComponents.Count; i++)
        {
            ISaveLoadComponent slc = (ISaveLoadComponent)m_SaveLoadComponents[i];
            if (slc == null) { Debug.LogError("SaveLoadSite: Missing ISaveLoadComponent on " + m_SaveLoadComponents[i].name); continue; }
            ES3.Save(slc.SLC_UniqueName, slc, settings);
        }

        //load the parts information
        ES3.Save("SiteParts", PartRoutingController.GetControllerForSite(m_Site), settings);

        ES3.StoreCachedFile(settings);
        yield return new WaitForSeconds(0.1f);

        //clear file from cache
        ES3.DeleteFile(settings);
        yield return new WaitForSeconds(0.1f);

        m_SaveLoadInProgress = false;
    }

    public IEnumerator StartLoad(string folder)
    {
        while (string.IsNullOrEmpty(UniqueID))
        {
            //wait for Sites file to finish loading
            Debug.Log("SaveLoadSite: Waiting for UniqueID to be loaded");
            yield return new WaitForSeconds(1);
        }

        if (!ES3.FileExists(folder + UniqueID + FileName))
        {
            //incomplete path or no file found, assume this is a new game
            Debug.Log("SaveLoadSite: File not found - " + folder + UniqueID + FileName);
            yield break;
        }

        m_SaveLoadInProgress = true;

        
        //Wait until we are sure the network is ready to handle loading
        while (Application.isPlaying && !NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
        {
            Debug.Log("SaveLoadSite: Waiting for network approval");
            yield return new WaitForSeconds(5);
        }

        if (Application.isPlaying && !NetworkManager.Singleton.IsServer)
        {
            //only the server should load anything
            Debug.Log("SaveLoadSite: Only the server can load save data");
            yield break;
        }

        yield return new WaitForSeconds(0.1f);

        ES3Settings settings = new ES3Settings(ES3.Location.Cache);
        settings.path = folder + UniqueID + FileName;
        ES3.CacheFile(folder + UniqueID + FileName);

        ES3.LoadInto("Site", m_Site, settings);

        OreHandler handler = m_Site.GetComponentInChildren<OreHandler>();
        OreHandler.CeramicData[] data = ES3.Load("SiteOres", new OreHandler.CeramicData[0], settings);
        handler.Clear();
        handler.Generate(data);

        Debug.Log("SaveLoadSite: Loaded Site Ores - " + data.ToString());

        yield return new WaitForSeconds(0.1f);

        //set the site transform as a reference
        ES3Internal.ES3ReferenceMgrBase.Current.Add(m_Site.transform, ES3.Load<long>("SiteTransformRef", settings));

        //find and generate each moon surface entity
        int entitiesCount = ES3.Load<int>("SiteEntityCount", settings);
        SaveLoadEntity[] entities = new SaveLoadEntity[entitiesCount];
        for (int j = 0; j < entities.Length; j++)
        {
            string uniqueID = ES3.Load<string>("SaveLoadEntity_" + j + "ID", settings);
            SaveLoadEntity sleFound = null;
            foreach (EquipmentType sle in EquipmentType.AllTypes)
            {
                SaveLoadEntity entity = sle.SimulationPrefab.GetComponent<SaveLoadEntity>();
                if (entity != null && entity.UniqueID == uniqueID)
                {
                    sleFound = entity;
                    break;
                }
            }

            Debug.Log("SaveLoadSite: Loading entity - " + sleFound.ToString());

            if (sleFound)
            {
                entities[j] = Instantiate(sleFound, Site.transform);

                if (Application.isPlaying)
                {
                    //get the instances NetworkObject and Spawn
                    NetworkObject newCharNO = entities[j].GetComponent<NetworkObject>();
                    newCharNO.Spawn();
                    newCharNO.TrySetParent(Site.transform);
                }
                else
                {
                    entities[j].transform.SetParent(Site.transform);
                }

                //load in all the entity components that do not reference other components
                for (int k = 0; k < entities[j].Components.Length; k++)
                {
                    if (ES3.KeyExists("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, settings))
                    {
                        Debug.Log("SaveLoadSite: Loading component - " + entities[j].UniqueID + " - " + entities[j].Components[k].SLC_UniqueName + " - " + entities[j].Components[k].SavedComponent);
                        ES3.LoadInto("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, entities[j].Components[k].SavedComponent, settings);
                    }
                }
            }
            else
            {
                Debug.LogError("SaveLoadSite: Entity not found when loading: " + uniqueID, gameObject);
            }

            yield return new WaitForSeconds(0.1f);
        }


        for (int j = 0; j < entities.Length; j++)
        {
            if (entities[j])
            {
                //load in all the entity components that may reference another component a second time to ensure all references exist
                for (int k = 0; k < entities[j].Components.Length; k++)
                {
                    if (entities[j].Components[k].SLC_IncludesReferences)
                    {
                        if (ES3.KeyExists("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, settings))
                        {
                            Debug.Log("SaveLoadSite: Loading refrencing component - " + entities[j].UniqueID + " - " + entities[j].Components[k].SLC_UniqueName);
                            ES3.LoadInto("Component_" + j + "_" + entities[j].Components[k].SLC_UniqueName, entities[j].Components[k].SavedComponent, settings);
                        }
                    }
                }
                yield return new WaitForSeconds(0.1f);
            }
        }

        string version = ES3.Load<string>("Version", settings);
        int buildVersion = int.Parse(version.Split('.')[1]);

        //load again for the site anchors
        ES3.LoadInto("Site", m_Site, settings);

        //Load the sites SaveLoadComponents
        for (int i = 0; i < m_SaveLoadComponents.Count; i++)
        {
            ISaveLoadComponent slc = (ISaveLoadComponent)m_SaveLoadComponents[i];
            if (slc == null) { Debug.LogError("SaveLoadSite: Missing ISaveLoadComponent on " + m_SaveLoadComponents[i].name); continue; }
            if (ES3.KeyExists(slc.SLC_UniqueName, settings))
            {
                Debug.Log("SaveLoadSite: Loading component - " + slc.SLC_UniqueName);
                ES3.LoadInto(slc.SLC_UniqueName, slc, settings);
            }
        }

        //load the parts information
        ES3.LoadInto("SiteParts", PartRoutingController.GetControllerForSite(m_Site), settings);
        yield return new WaitForSeconds(0.1f);
        
        //clear file from cache
        ES3.DeleteFile(settings);
        yield return new WaitForSeconds(0.1f);

        m_SaveLoadInProgress = false;
    }

    private void Awake()
    {
        SaveLoadManager.RegisterHandler(this);
    }

    private void OnDestroy()
    {
        SaveLoadManager.UnregisterHandler(this);
    }
}
User avatar
Joel
Moodkie Staff
Posts: 4871
Joined: Wed Nov 07, 2012 10:32 pm

Re: Loading through LoadInto in our use case broke after latest update

Post by Joel »

Hi there,

Unfortunately it wouldn't be possible to debug this just by looking at it. Please could you send us a new project via the form at moodkie.com/contact with a simple scene which replicates this with the minimum amount of logic required so that we can debug the code (don't send the project here as this is a publically-viewable forum).

As you can see the object you're loading in the save data (and it's key), the easiest way to replicate this would be to have a scene with a script which loads that key into the object and send us your save file along with your project. I.e.

Code: Select all

public class Test : MonoBehaviour
{
	public Component objectTheSaveDataRefersTo;

	public void Start()
	{
		ES3.LoadInto("keyFromTheSaveFile", objectTheSaveDataRefersTo);
	}
}
This will then isolate the issue to Easy Save rather than your project's logic.

All the best,
Joel
Joel @ Moodkie Interactive
Purchase Easy Save | Contact | Guides | Docs | Getting started
User avatar
TauCetiLabs
Posts: 11
Joined: Mon Dec 28, 2020 2:31 pm

Re: Loading through LoadInto in our use case broke after latest update

Post by TauCetiLabs »

I have found a janky work around, that might help shed some light on what is going on.

The issue seems to stem from the fact that the base 'Component' class is being used instead of the more specific class where the data exists. So to make this flexible, we use this 'ISaveLoadComponent' interface and have any scripts with data on them implement this interface:

Code: Select all

public class SaveLoadEntity : MonoBehaviour
{
    [ES3NonSerializable][SerializeField] private string m_UniqueID;
    public string UniqueID => m_UniqueID;

    [ES3NonSerializable] public ISaveLoadComponent[] Components => GetComponentsInChildren<ISaveLoadComponent>();
}

public interface ISaveLoadComponent
{
    public string SLC_UniqueName { get; }

    public bool SLC_IncludesReferences { get; }

    public Component SavedComponent => (Component)this;
}
However, when calling 'LoadInto', it seems to be handling this as the 'Component' class instead of something like the 'GridOccupant' class mentioned earlier. So to summarize, the commented out code below does not work, and the uncommented code does. However, this would need an if statement for each class that implements the ISaveLoadComponent to fully function:

Code: Select all

//ES3.LoadInto($"Component_{j}_{entities[j].Components[k].SLC_UniqueName}", entities[j].Components[k].SavedComponent, settings);

if (entities[j].Components[k].SavedComponent.GetType() == typeof(GridOccupant))
{
       ES3.LoadInto($"Component_{j}_{entities[j].Components[k].SLC_UniqueName}", entities[j].Components[k].SavedComponent as GridOccupant, settings);
}
else if ...
This is workable, so I will continue unless there is a more general/better way to handle this. I do find it a bit strange that it worked differently before, and now requires to be cast as the specific class being loaded into. I'm curious what changed.
User avatar
TauCetiLabs
Posts: 11
Joined: Mon Dec 28, 2020 2:31 pm

Re: Loading through LoadInto in our use case broke after latest update

Post by TauCetiLabs »

As a quick follow up, I have migrated the LoadInto to a function that figures out what class should be used for loading. It isn't pretty, but it works:

Code: Select all

private static void LoadIntoEntity(string key, ISaveLoadComponent component, ES3Settings settings)
    {
        if (ES3.KeyExists(key, settings))
        {
            Debug.Log($"SaveLoadSite: Loading component - {component.SLC_UniqueName} - {key}");

            switch (component.SavedComponent)
            {
                case Builder castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case TempPartGen castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case Construct castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case EquipmentList castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case MinerCollectOre castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case IngotHandler castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case IngotProducer castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case Mixer castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case Hauler castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PartProducer castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case BatteryArray castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case ElectricGrid castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case ElectricNode castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case ProgressionState castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case Sifter castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case ChemicalContainer castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case IngotPalet castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PowderInput castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PowderOutput castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case MappedPartBuffer castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PartDynamicHolderSync castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PartHandler castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PartPoseSaveLoad castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case PartStructureSync castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case NameableEntity castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case AnchorToSite castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case GridOccupant castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case RoverNavigator castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
                case SurfaceNavigationPose castedComponent: ES3.LoadInto(key, castedComponent, settings); break;
            }
        }
    }
Post Reply