A bit of guidance needed for some custom Farming code

[Just a heads-up, there’s a ton of custom code here, hence why it’s a “Talk”. If you’re still in the RPG Course or what not, you may want to hard-pass on this post]

Hello friends, hope you’re all having a great day. This one is a bit of custom code, but I am having some serious problems trying to figure this one out

Not too long ago, I started working on making crafting systems saveable when crafting tables are built and destroyed, and frankly speaking I got about 80% of all systems to work in one fix… except my own Farming variant, since Farming uses the same UI but has a lot of differences from the other systems

So, to keep it short, here’s my custom script where most of my issues still persist, and I still can’t figure it out:

using System;
using System.Collections;
using System.Linq;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using RPG.Crafting;
using UnityEngine;

namespace RPG.Farming
{
    public class PlantGrowthHandler : MonoBehaviour, ICraftingObserver, IJsonSaveable
    {
        private ICraftingEventBus eventBus;
        private float[] slicePoints;
        private CraftingQuantity craftingQuantity;

        public GameObject currentPlant;

        // TESTS - DELETE IF FAILED
        private GameObject currentInstance;
        private int currentStage;
        private string currentRecipeID; // (TEST - DELETE THIS VARIABLE ENTIRELY IF IT FAILS ITS JOB!)

        void Awake()
        {
            eventBus = GetComponentInParent<ICraftingEventBus>();
            eventBus.RegisterObserver(this);
        }

        void OnDestroy()
        {
            eventBus.DeregisterObserver(this);
        }

        public void OnCraftingStarted(Recipe recipe)
        {
            // craftingQuantity = FindObjectOfType<CraftingQuantity>(); // when you start crafting (I can't get it to link to 'NotifyCrafting' without resulting in a circular dependency, so this is my next best option)

            slicePoints = ComputeRandomGrowthSlices(recipe);
            currentRecipeID = recipe.GetRecipeID(); // TEST - DELETE IF FAILED
            StartCoroutine(GrowthRoutine(recipe, recipe.GetCraftDuration()));
        }

        public void OnCraftingResumed(Recipe recipe, float remaining)
        {
            // craftingQuantity = FindObjectOfType<CraftingQuantity>(); // when you resume crafting, from a loaded save scene (I can't get it to link to 'NotifyCrafting' without resulting in a circular dependency, so this is my next best option)

            slicePoints = ComputeRandomGrowthSlices(recipe); // Makes sure our Plants don't just grow at the exact same time simultaneously, with a little bit of Randomness
            currentRecipeID = recipe.GetRecipeID(); // TEST - DELETE IF FAILED
            StartCoroutine(GrowthRoutine(recipe, remaining));
        }

        private float[] ComputeRandomGrowthSlices(Recipe recipe)
        {
            var stages = recipe.GetStages.Length - 1; // we don't want to wait for the final product (pickup) so we don't add a slice for it
            var duration = recipe.GetCraftDuration();

            var avg = duration / stages;
            var deviations = new float[stages];
            var slices = new float[stages];

            // Seed the randomiser for consistent slices
            var currentState = UnityEngine.Random.state; // cache the current state
            UnityEngine.Random.InitState(transform.position.GetHashCode()); // seed based on the position

            // Generate deviations within 30% of the average length
            for (int i = 0; i < stages; i++)
            {
                deviations[i] = avg + (UnityEngine.Random.value * 2f - 1f) * avg * 0.3f;
            }

            // Restore the randomiser state
            UnityEngine.Random.state = currentState; // restore to the cached state

            // Normalize deviations to ensure the sum equals duration
            var totalDeviation = deviations.Sum();
            for (int i = 0; i < stages; i++)
            {
                slices[i] = deviations[i] * duration / totalDeviation;
            }

            // Adjust the slices to ensure the sum is exactly duration
            const float deviation = 0.001f;
            var sumSlices = slices.Sum();
            var difference = duration - sumSlices;

            // Distribute the difference across the slices
            for (int i = 0; i < Mathf.Abs(difference / deviation); i++)
            {
                int index = i % stages;
                if (difference > 0) slices[index] += deviation;
                else slices[index] -= deviation;
            }

            // Accumulate the slices
            var accumulator = slices[0];
            for (var i = 1; i < slices.Length; i++)
            {
                accumulator += slices[i];
                slices[i] = accumulator;
            }

            return slices;
        }

        private IEnumerator GrowthRoutine(Recipe recipe, float duration)
        {
            // Determine where to start the timer
            var totalDuration = recipe.GetCraftDuration();
            var startTime = totalDuration - duration;

            // Determine the current stage, based on the elapsed time (if any)
            // var currentStage = DetermineStage(startTime); // TEST BELOW
            currentStage = DetermineStage(startTime);

            // GameObject currentInstance = null; // TEST BELOW
            currentInstance = null;

            if (currentPlant != null)
            {
                // Instantiate the first phase, only for plants that are currently being grown (empty slots get nothing)
                currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
            }

            for (var timer = startTime; timer / totalDuration <= 1f; timer += Time.unscaledDeltaTime /* <-- using 'unscaledDeltaTime' instead of 'deltaTime' because our notification, crafting and all systems work based on real time, so it has to match */)
            {
                var newStage = DetermineStage(timer);
                if (currentStage != newStage)
                {
                    // Update the current stage index
                    currentStage = newStage;
                    // Destroy the old instance
                    Destroy(currentInstance);

                    if (currentPlant != null)
                    {
                        // Instantiate next phase, only for plants that are currently being grown (empty slots get nothing)
                        currentInstance = Instantiate(recipe.GetStages[currentStage], transform.position, Quaternion.identity);
                    }
                }

                yield return null;
            }

            // Destroy the plant
            Destroy(currentInstance);

            if (currentPlant != null)
            {
                // Instantiate the final phase, only for plants that are being grown (empty slots get nothing)
                recipe.GetResult().Item.SpawnPickup(transform.position, 1 /* <-- one output per farming slot */);
                currentPlant = null;
                craftingQuantity = FindObjectOfType<CraftingQuantity>(true); // Regardless of whether you're crafting or not (hence the word 'true' for when it's inactive), you'll need to refind that value and re-update it when crafting is done, otherwise you'll have a mathematical bug in the quantity UI the first time after the previous farm is done
                craftingQuantity.SetMaxQuantity(craftingQuantity.GetMaxQuantity() + 1 /* <-- each script holder can only output one pickup, hence +1 only */);
            }
        }

        private int DetermineStage(float time)
        {
            var stage = Array.BinarySearch(slicePoints, time); // Blazingly-fast solution of determine the growth stage of the Farming Plant
            if (stage < 0)
            {
                stage = ~stage;
            }

            return stage;
        }

        public GameObject GetCurrentPlant()
        {
            return currentPlant;
        }

        public void SetCurrentPlant(GameObject currentPlant)
        {
            this.currentPlant = currentPlant;
        }

        public JToken CaptureAsJToken()
        {
            JToken saveData = JToken.FromObject(new SaveData
            {
                CurrentPlantName = currentPlant != null ? currentPlant.name : null, // no plant, no name
                CurrentStage = currentStage,
                RecipeID = currentRecipeID
            });

            Debug.Log($"Captured values: {saveData}");

            return saveData;
        }

        public void RestoreFromJToken(JToken state)
        {
            var data = state.ToObject<SaveData>();

            currentStage = data.CurrentStage;
            currentRecipeID = data.RecipeID;

            Debug.Log($"Restored data: Current Plant Name = {data.CurrentPlantName}, Current Stage = {currentStage}, RecipeID = {data.RecipeID}");

            if (!string.IsNullOrEmpty(data.CurrentPlantName))
            {
                GameObject plantPrefab = LoadPlantPrefabByName(data.CurrentPlantName);
                if (plantPrefab != null)
                {
                    Debug.Log($"Restore from JToken: Loaded Plant Prefab = {plantPrefab.name}");

                    currentPlant = plantPrefab;
                    currentInstance = Instantiate(currentPlant, transform.position, Quaternion.identity);

                    Debug.Log($"Restore from JToken: Instantiated Plant Prefab = {currentInstance.name}");
                }
                else
                {
                    Debug.Log($"RestoreFromJToken failed to load a plant prefab with name {data.CurrentPlantName}");
                }
            }
            else
            {
                Debug.Log($"RestoreFromJToken: No plant name provided");
            }
        }

        private GameObject LoadPlantPrefabByName(string name)
        {
            return Resources.Load<GameObject>($"FarmingRecipes/{name}");
        }

        [Serializable]
        private struct SaveData 
        {
            public string CurrentPlantName;
            public int CurrentStage;
            public string RecipeID;
        }
    }
}

This is my ‘PlantGrowthHandler.cs’ script, and through other changes it manages to plant plants on the field based on what you want to plant, the current instance, how much time has passed and so on and so forth.

But the saving system for this is an absolute mess, and the values simply do not recover as expected… at all, and that’s where I’ll need help

So I’m trying to save (and load) the following variables, and for the following reasons:

  1. ‘currentPlant’. I want to save and restore the current plant so that the script holder can indicate that this slot is taken, because that’s how the farming system knows whether this slot is taken or available to farm at, from ‘CraftingTable.cs’:
                for (int i = 0; i < craftingQuantity.GetCurrentQuantity(); i++)
                {
                    // Plant quantity based on what the 'GetCurrentQuantity()' has available, 
                    // which is controlled in 'CraftingSystem.OnCraftingInteraction()', and the 
                    // value is also updated below, after you hit the 'Craft' button on the UI
                    var availableSlots = GetComponentsInChildren<PlantGrowthHandler>().Where(handler => handler.GetCurrentPlant() == null);
                    var firstAvailableSlot = availableSlots.First();
                    firstAvailableSlot.SetCurrentPlant(recipe.GetResult().Item.GetPickup().gameObject); // Just a way to indicate that this slot is occupied
                }
  1. ‘CurrentInstance’ (or ‘currentStage’, preferably currentStage), because this is what tells this slot which instance (i.e: stage) it was at, before the saving happened, indicating where the plant’s growth level was at, so it can instantiate the correct stage. For reference, it gets the stage prefabs from ‘Recipe.cs’, mainly these two lines of code:
        // FARMING (used in 'PlantGrowthHandler.cs')
        [SerializeField] GameObject[] stagePrefabs;
        public GameObject[] GetStages => stagePrefabs;
  1. This is a bit of an extremely odd problem, but when the plants move on to the next stage, they all (again, not all points are meant to be there in my testing…) vanish off the farming field, and I can’t explain why. Again, the stages are accessed from the two lines of code beneath point 2 above, these two lines:
        // FARMING (used in 'PlantGrowthHandler.cs')
        [SerializeField] GameObject[] stagePrefabs;
        public GameObject[] GetStages => stagePrefabs;

So far, what I’ve tried to do, is create a serialized class to store the data, and save and restore that. The big problem I have, and I can’t explain why, is that the capturing captures the correct data, but the restore returns everything as null. It returns no “CurrentPlantName” (it comes back empty, literally), and the recipe id returns as null (so I can’t access the stage prefabs now, which means I can’t tell the plant what stage to move on next to), and the current stage value returns as zero, regardless of whether it’s at stage 0 or not (so regardless of how far they’re in the planting, they look like infant plants. No good!).

And the worst part is, even the slots that are carrying the ‘PlantGrowthHandler.cs’ script, but have no ‘currentPlant’ on them (i.e: they are free to use) somehow get the same values as the plant slot that is occupied, and now everyone who was free has the same plant (and since I’m currently testing for only one plant variant, I can’t tell yet how multiple plant variants will affect this). Point is, I only want to restore the correct data for the slot that was occupied, not all of them

Here’s the code I’m talking about, in ‘PlantGrowthHandler.cs’:

        public JToken CaptureAsJToken()
        {
            JToken saveData = JToken.FromObject(new SaveData
            {
                CurrentPlantName = currentPlant != null ? currentPlant.name : null, // no plant, no name
                CurrentStage = currentStage,
                RecipeID = currentRecipeID
            });

            Debug.Log($"Captured values: {saveData}");

            return saveData;
        }

        public void RestoreFromJToken(JToken state)
        {
            var data = state.ToObject<SaveData>();

            currentStage = data.CurrentStage;
            currentRecipeID = data.RecipeID;

            Debug.Log($"Restored data: Current Plant Name = {data.CurrentPlantName}, Current Stage = {currentStage}, RecipeID = {data.RecipeID}");

            if (!string.IsNullOrEmpty(data.CurrentPlantName))
            {
                GameObject plantPrefab = LoadPlantPrefabByName(data.CurrentPlantName);
                if (plantPrefab != null)
                {
                    Debug.Log($"Restore from JToken: Loaded Plant Prefab = {plantPrefab.name}");

                    currentPlant = plantPrefab;
                    currentInstance = Instantiate(currentPlant, transform.position, Quaternion.identity);

                    Debug.Log($"Restore from JToken: Instantiated Plant Prefab = {currentInstance.name}");
                }
                else
                {
                    Debug.Log($"RestoreFromJToken failed to load a plant prefab with name {data.CurrentPlantName}");
                }
            }
            else
            {
                Debug.Log($"RestoreFromJToken: No plant name provided");
            }
        }

        private GameObject LoadPlantPrefabByName(string name)
        {
            return Resources.Load<GameObject>($"FarmingRecipes/{name}");
        }

        [Serializable]
        private struct SaveData 
        {
            public string CurrentPlantName;
            public int CurrentStage;
            public string RecipeID;
        }

@Brian_Trotter (when you’re available) or @bixarrio (when you’re available) or anyone else, if anyone has any inputs on this or can help me in anyway, please let me know. I know this is a little way too off topic, but I don’t know what else to do or who to ask, and it’s been bothering me for a bit now. In the meanwhile if I get anything working, I’ll update this post

And that’s why you test everything early on… I was not expecting that kind of chaos at all!

The stuff we did on Friday is actually not going to work well with the PlantGrowthHandler and that’s my fault. Remember I told you that this does not work the same as JsonSaveableEntity.
image
The problem is that it does work like the JsonSaveableEntity in some ways meaning - like with the JsonSaveableEntity - data is saved and keyed using the type name of the component. You now have 12 copies of the same component so when you save, you are only saving the last slot. This is because each slot after the first one finds the first one (because the key is the same) and overwrites the data. If the last slot is empty, all the slots will be filled with empty data. Why all the slots? Because all the slots are being loaded with the same data because they all match the key.

One way to solve it would be to provide a custom key instead of using the type name. What you could do is to use a bit of composition to supply custom keys, and add it to the slots

  • Create an interface for custom keys
public interface IHaveCustomKey
{
    string CustomKey { get; }
}
  • Implement this custom key on PlantGrowthHandler (iirc, each slot has one of these)
public class PlantGrowthHandler : MonoBehaviour, ICraftingObserver, IJsonSaveable, IHaveCustomKey
{
    [field: SerializeField] public string CustomKey { get; private set; }
}
  • Provide a unique key (at least unique within the scope of the building part) for each slot. I’m letting you do this manually. You could always look at the JsonSaveableEntity (or SaveableEntity from the course) to see how you can keep a dictionary that would ensure that the key is truly unique.
  • In the RPGCourseBuildingSaver (or the new one), you can now check if the interface is implemented and use the custom key or - if not - use the type name as before
private JToken CaptureChildrenAsToken(GameObject source) 
{
    JObject state = new();
    foreach (var saveable in source.GetComponentsInChildren<IJsonSaveable>()) 
    {
        var key = saveable.GetType().ToString();
        if (saveable is IHaveCustomKey custom)
        {
            key = custom.CustomKey;
        }
        state[key] = saveable.CaptureAsJToken();
    }
    return state;
}

// Restore all children of the registered building part (useful for dynamic crafting tables)
private void RestoreChildrenAsToken(GameObject target, JToken state) 
{
    if (state is JObject stateDict) 
    {
        foreach (var saveable in target.GetComponentsInChildren<IJsonSaveable>()) 
        {
            var key = saveable.GetType().ToString();
            if (saveable is IHaveCustomKey custom)
            {
                key = custom.CustomKey;
            }
            if (stateDict.TryGetValue(key, out var token)) 
            {
                saveable.RestoreFromJToken(token);
            }
        }
    }
}

This should solve that problem.

Note I’m not 100% sure the pattern will match because - as usual - none of this was tested. The saveable is a component, but it’s an IJsonSaveable at that specific point. If it doesn’t work, let me know. It means we’ll have to get components and cast to IJsonSaveable as well.

Believe me when I say this, this drove me to pure insanity since Friday, and I tried everything from my limited knowledge to try and fix this problem. Next comment I’ll update you on what happened :slight_smile:

Moving forward, I’ll do full tests, as close as humanly possible to the final version, before moving to the next concept. I don’t want to keep skipping between topics for long, it makes debugging in the future quite hard

I could be a little wrong. I think it may actually save all 12 entries, but when it loads it only loads the first one over and over because that’s the first key that matches all 12 PlantGrowthHandlers. I’m not sure if JObject actually bother with unique keys. It probably does, which means my first assumption was correct

There are more issues, though, but let’s solve this one first

For this one, I’m not sure if what I’m about to say is correct or not, but here’s what I did for unique Identifiers (if you recall a while ago, my pickup save and restore failed, because my JSON Unique IDs were all the same, and this was a major headache for me back then, so I created this back then):

// in 'JSONSaveableEntity.cs'

        // TEST (Delete if failed)
        void OnValidate() 
        {
            if (string.IsNullOrEmpty(uniqueIdentifier) || usedIdentifiers.Contains(uniqueIdentifier))
            {
                uniqueIdentifier = Guid.NewGuid().ToString();
                usedIdentifiers.Add(uniqueIdentifier);
                Debug.Log($"New Unique Identifier generated");
            }
        }

I did notice this algorithm though keeps creating and destroying the values pretty frequently, in a pattern I wasn’t able to notice. Is this correct, though? Can I copy-paste this in ‘PlantGrowthHandler.cs’?

That’s broken, don’t use it.

It will generate a new key each time OnValidate() runs because it finds its own, previously valid, key in the list and generates a new one, adding it to the list. That list is going to grow and grow and grow over time…

Change the list to a dictionary, use the identifier as the key and the component as the value. When OnValidate() runs, check if the key is there and if it is, check if the component is this component. If not, generate a new key. This is all the stuff that the course’s SaveableEntity does.

But you have 12 slots. You can just manually give each one a key like Slot1, Slot2, Slot3, etc. They only need to be unique within the scope of the building part

Ehh I just figured being consistent would be helpful, so why not just fix this problem since it’s in the way anyway? That way on the long term, I can properly revise what I was doing and learn from it :smiley:

Something like this?:

        // TEST (Delete if failed)
        private static Dictionary<string, float> usedIdentifiers = new Dictionary<string, float>();

Look at SaveableEntity. It does this.

private bool IsUnique(string candidate)
{
    if (!globalLookup.ContainsKey(candidate)) return true;

    if (globalLookup[candidate] == this) return true;

    if (globalLookup[candidate] == null)
    {
        globalLookup.Remove(candidate);
        return true;
    }

    if (globalLookup[candidate].GetUniqueIdentifier() != candidate)
    {
        globalLookup.Remove(candidate);
        return true;
    }

    return false;
}

Also consider that there must be a reason Sam chose to do this in the Update of a component that runs in editor as opposed to OnValidate()

The JSON Saveable Entity does it as well. All changes I did were in the ‘JSONSaveableEntity.cs’ script. I asked ChatGPT for a little help, and here’s what I came up with:

        [SerializeField] string uniqueIdentifier = "";

        // TEST (Delete if failed)
        private static Dictionary<string, JSONSaveableEntity> usedIdentifiers = new Dictionary<string, JSONSaveableEntity>();


        private void OnValidate() 
        {
            if (string.IsNullOrEmpty(uniqueIdentifier) || !IsIdentifierValid()) 
            {
                GenerateNewIdentifier();
            }
            else 
            {
                usedIdentifiers[uniqueIdentifier] = this;
            }
        }

        private bool IsIdentifierValid() 
        {
            if (usedIdentifiers.TryGetValue(uniqueIdentifier, out JSONSaveableEntity existingComponent)) 
            {
                return existingComponent == this;
            }

            return false;
        }

        private void GenerateNewIdentifier() 
        {
            uniqueIdentifier = Guid.NewGuid().ToString();
            usedIdentifiers[uniqueIdentifier] = this;
        }

This should be fine, right?

I am aware of that. The only reason why I’m adding an OnValidate as well is for cases where I forget to manually do something about it :slight_smile:

Yeah, looks ok. The one from the course just cleans up the dictionary, too, but I doubt it matters much. Every time you restart Unity, that dictionary is going to get rebuilt and will start clean

That’s great to know!

I also tried writing it in ‘PlantGrowthHandler.cs’. Does this looks correct?:

        // --------------------- Custom Identifier Code below, for the saving system of Dynamic Farming Patches, connected in 'NewRPGCourseBuildingSaver.cs', to work properly -----------------------

        [field: SerializeField] public string CustomKey { get; private set; }
        static Dictionary<string, PlantGrowthHandler> usedIdentifiers = new Dictionary<string, PlantGrowthHandler>();

        private void OnValidate() 
        {
            if (string.IsNullOrEmpty(CustomKey) || !IsIdentifierValid()) 
            {
                GenerateNewIdentifier();
            }
            else 
            {
                usedIdentifiers[CustomKey] = this;
            }
        }

        private bool IsIdentifierValid() 
        {
            if (usedIdentifiers.TryGetValue(CustomKey, out PlantGrowthHandler existingComponent)) 
            {
                return existingComponent == this;
            }

            return false;
        }

        private void GenerateNewIdentifier() 
        {
            CustomKey = Guid.NewGuid().ToString();
            usedIdentifiers[CustomKey] = this;
        }

        // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Considering how frequently I have to restart unity for garbage cleaning, I think it matters, but we’ll take care of that later :slight_smile:

Yeah, looks ok. You’ve veered off the path now. We’re trying to solve your saving.

Something is 100% wrong here, because whatever we have done means that everytime I restore my game it’s like I’m starting from a brand new scene, literally. Let’s review the changes I did:

  1. I renamed your interface a little bit to something I thought might be well suited for what we’re doing
// This interface is developed so that each plant growth
// handler can have it's own unique identifier, so that the
// Capturing and Restoring of individual farming slots does
// not iterate over the last plant growth handler slot
// over and over again

public interface IUniqueIdentifierInterface 
{
    string UniqueIdentifier {get;}
}
  1. in ‘PlantGrowthHandler.cs’:
        // --------------------- Custom Identifier Code below, for the saving system of Dynamic Farming Patches, connected in 'NewRPGCourseBuildingSaver.cs', to work properly -----------------------

        [field: SerializeField] public string UniqueIdentifier { get; private set; }
        static Dictionary<string, PlantGrowthHandler> usedIdentifiers = new Dictionary<string, PlantGrowthHandler>();

        private void OnValidate() 
        {
            if (string.IsNullOrEmpty(UniqueIdentifier) || !IsIdentifierValid()) 
            {
                GenerateNewIdentifier();
            }
            else 
            {
                usedIdentifiers[UniqueIdentifier] = this;
            }
        }

        private bool IsIdentifierValid() 
        {
            if (usedIdentifiers.TryGetValue(UniqueIdentifier, out PlantGrowthHandler existingComponent)) 
            {
                return existingComponent == this;
            }

            return false;
        }

        private void GenerateNewIdentifier() 
        {
            UniqueIdentifier = Guid.NewGuid().ToString();
            usedIdentifiers[UniqueIdentifier] = this;
        }

        // -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  1. In ‘JSONSaveableEntity.cs’, which is where I suspect the problem is from:
        // ---------------------------------- TEST (DELETE IF FAILED) - New OnValidate Unique ID Code -------------------------------------

        private void OnValidate()
        {
            if (string.IsNullOrEmpty(uniqueIdentifier) || !IsIdentifierValid())
            {
                GenerateNewIdentifier();
            }
            else
            {
                usedIdentifiers[uniqueIdentifier] = this;
            }
        }

        private bool IsIdentifierValid() 
        {
            if (usedIdentifiers.TryGetValue(uniqueIdentifier, out JSONSaveableEntity existingComponent)) 
            {
                return existingComponent == this;
            }

            return false;
        }

        private void GenerateNewIdentifier() 
        {
            uniqueIdentifier = Guid.NewGuid().ToString();
            usedIdentifiers[uniqueIdentifier] = this;
        }

        // --------------------------------------------------------------------------------------------------------------------------------
  1. in ‘NewRPGCourseBuildingSaver.cs’:
    private JToken CaptureChildrenAsTokens(GameObject source) 
    {
        JObject state = new();

        foreach (var saveable in source.GetComponentsInChildren<IJsonSaveable>()) 
        {
            var key = saveable.GetType().ToString();
            if (saveable is IUniqueIdentifierInterface uniqueID) 
            {
                key = uniqueID.UniqueIdentifier;
            }

            state[key] = saveable.CaptureAsJToken();
        }

        return state;
    }

    private void RestoreChildrenAsTokens(GameObject target, JToken state) 
    {
        if (state is JObject stateDict) 
        {
            foreach (var saveable in target.GetComponentsInChildren<IJsonSaveable>()) 
            {
                var key = saveable.GetType().ToString();
                if (saveable is IUniqueIdentifierInterface uniqueID) 
                {
                    key = uniqueID.UniqueIdentifier;
                }

                if (stateDict.TryGetValue(key, out var token)) 
                {
                    saveable.RestoreFromJToken(token);
                }
            }
        }
    }

By “start from a new scene” it means nothing was ever saved and it’s like the game has just been loaded for the first time

and my suspicions are correct, it was indeed the ‘JSONSaveableEntity’'s new ‘OnValidate()’ function. I deleted that and things were working well, except that the crafting table finishes the crafting on the spot when we return to the game now

Speaking of “Finishing the Crafting On the Spot”, it’s something to do with the new CaptureTokensAsChildren and RestoreTokensAsChildren or whatever I called that in the ‘NewRPGCourseBuildingSaver.cs’ script. I reversed it, and crafting was back to normal

Oh, the code is still not right. It’s going to generate new identifiers when you start Unity and those will not be what was saved. Your IsIdentifierValid() returns false if it doesn’t find an entry in the dictionary - which it will always do the first time these are run - so it generates a new one. Rather return true at the end. It will then use the identifier that is in the inspector.

private bool IsIdentifierValid() 
{
    if (usedIdentifiers.TryGetValue(UniqueIdentifier, out PlantGrowthHandler existingComponent)) 
    {
        return existingComponent == this;
    }

    return true;
}

Nope, like I said there are more issues. But we do one thing at a time. Reverting fixes does not solve the problems

That did not fix it, something is still wrong

I revert them only to make sure that this block of code was the problem, not to be safe :smiley: - anyway, let’s tackle it one step at a time

You’ll have to delete the save and start again. You’ve now changed all the identifiers and they don’t match the save anymore. Make sure your player still has the ‘player’ identifier (or whatever you called it)

Done all of that, deleted it and restarted, still the same problem. I don’t think that was necessary, mainly because OnValidate will only check if a slot is empty or what not, which in my case did not happen to anything like the player, bank, etc

No, it checks if it’s empty and if it’s in the dictionary. If it’s not in the dictionary - which it’s not when it first runs - it will also generate a new identifier. I’m guessing the player didn’t change because it hasn’t run OnValidate() yet. Which also means this is not reliable because it will only check something when it runs OnValidate() and if you were to set one of those slots to ‘player’ it will think it’s valid

Revert the identifier stuff and name your slots manually. Let’s fix the original problem first. This jumping all over the place is driving me nuts. I don’t want to spend my whole day with this again

Privacy & Terms