[SOLVED: FEEL FREE TO IGNORE]
[THIS IS A BIT OF A LENGTHY COMMENT, BUT PLEASE BEAR WITH ME. I MANAGED TO TRACK THE SOURCE DOWN, I JUST DON’T KNOW HOW TO SOLVE IT]
@Brian_Trotter (sorry for dragging you into this) Quick hot update:
After a little bit of research, I came to the conclusion that ‘ItemDropper.cs’ has the saving system that is responsible for the drops done by the NPC (and it makes sense, because the RandomDropper, which does the dropping, inherits from it)
and after a little bit of further debugging, I noticed two major flaws in my ‘RestoreFromJToken’, which is this function:
public void RestoreFromJToken(JToken state)
{
if (state is JArray stateArray) {
int currentScene = SceneManager.GetActiveScene().buildIndex;
IList<JToken> stateList = stateArray;
ClearExistingDrops();
foreach (var entry in stateList) {
if (entry is JObject dropState) {
IDictionary<string, JToken> dropStateDict = dropState;
int scene = dropStateDict["scene"].ToObject<int>();
InventoryItem item = InventoryItem.GetFromID(dropStateDict["id"].ToObject<string>());
int number = dropStateDict["number"].ToObject<int>();
Vector3 location = dropStateDict["location"].ToVector3();
Debug.Log($"(ItemDropper) Restoring item: {item.name}, number: {number}, scene: {scene}, location: {location}");
if (scene == currentScene) {
SpawnPickup(item, location, number);
}
else {
var otherDrop = new otherSceneDropRecord();
otherDrop.id = item.GetItemID();
otherDrop.number = number;
otherDrop.location = location;
otherDrop.scene = scene;
otherSceneDrops.Add(otherDrop);
}
}
}
}
}
-
The “(ItemDropper)” Debug I threw into Restoring gets called 5 times (assuming it gets called, that is), although the items in the end appear on the ground get called once. I’m not sure why yet, but it’s there. Any idea what this indicates to…?!
-
Not every NPC gets that restore called, and I have absolutely zero clue why (and the ones that do, it may or may not happen). I also threw a debug in capturing, and I believe it does its job just fine. It’s the restoring that has some issues
If it helps, I do also have to mention that this ONLY occurs with Enemy NPCs, not with the player
I read a little further from the link @Cat_Hill provided earlier, and I noticed that there could be a race condition introduced somewhere, responsible for this. I’m honestly not very sure though
From a glimpse, is this a bug from the course code, or did I probably do something dumb elsewhere down the line? Because looking at it, especially that the restore only happens on a JObject called ‘dropState’, which only has been named this way in this script, out of my entire codebase), I don’t think I ever touched that code ever again down the line to be honest
Edit 1: 3 hours later, and here’s what I noticed. This is exactly where the code fails:
foreach (var entry in stateList)
the foreach loop, at the code above, is exactly where the restoring may or may not work, and I have absolutely no idea why!
Edit 2: the foreach loop doesn’t get entered because my ‘stateArray’ is empty, before it even gets fed to the ‘stateList’ JToken List. What does this mean, and how do I solve it?
Edit 3: I did a little further research, and threw in some debugs in my ‘CaptureAsJToken()’:
public JToken CaptureAsJToken() {
RemoveDestroyedDrops();
var drops = MergeDroppedItemsWithOtherSceneDrops();
JArray state = new JArray();
IList<JToken> stateList = state;
Debug.Log($"(ItemDropper) Number of drops to capture: {drops.Count}");
foreach (var drop in drops) {
JObject dropState = new JObject();
IDictionary<string, JToken> dropStateDict = dropState;
dropStateDict["id"] = JToken.FromObject(drop.id);
dropStateDict["number"] = drop.number;
dropStateDict["location"] = drop.location.ToToken();
dropStateDict["scene"] = drop.scene;
stateList.Add(dropState);
Debug.Log($"(ItemDropper) Capturing Item: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
}
Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
return state;
}
and, as it turns out, this Debug line (right before returning the state):
Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
return state;
returns as an empty array, which means it does not even get saved to begin with, eventually leading to the disasters down the line…
Edit 4: I went to the core of the problem (I think so), and it turns out that ‘MergeDroppedItemsWithOtherSceneDrops()’ does add the results, as seen in this function:
List<otherSceneDropRecord> MergeDroppedItemsWithOtherSceneDrops() {
List<otherSceneDropRecord> result = new List<otherSceneDropRecord>();
result.AddRange(otherSceneDrops);
foreach (var item in droppedItems) {
otherSceneDropRecord drop = new otherSceneDropRecord();
drop.id = item.GetItem().GetItemID();
drop.number = item.GetNumber();
drop.location = item.transform.position;
drop.scene = SceneManager.GetActiveScene().buildIndex;
result.Add(drop);
Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
}
Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
return result;
}
through this debug:
Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
(This debug works perfectly fine)
HOWEVER, This debug, at the end of the function:
Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
return result;
returns a total drop count of zero
SO… the ‘Merge’ function has a false result, which leads to a false result in capturing, eventually leading to a false result in restoring
and that’s where my problem is currently at… How do I fix this? (I wish I knew about the ‘new System.Diagnostics.StackTrace()’ function earlier…!)
If needed, here is my entire ‘ItemDropper.cs’ script:
using System.Collections.Generic;
using UnityEngine;
using GameDevTV.Saving;
using UnityEngine.SceneManagement;
using Newtonsoft.Json.Linq;
using Unity.VisualScripting;
namespace GameDevTV.Inventories
{
/// <summary>
/// To be placed on anything that wishes to drop pickups into the world.
/// Tracks the drops for saving and restoring.
/// </summary>
public class ItemDropper : MonoBehaviour, IJsonSaveable //, ISaveable
{
// STATE
private List<Pickup> droppedItems = new List<Pickup>();
private List<DropRecord> otherSceneDroppedItems = new List<DropRecord>();
// PUBLIC
/// <summary>
/// Create a pickup at the current position.
/// </summary>
/// <param name="item">The item type for the pickup.</param>
/// <param name="number">
/// The number of items contained in the pickup. Only used if the item
/// is stackable.
/// </param>
public void DropItem(InventoryItem item, int number)
{
SpawnPickup(item, GetDropLocation(), number);
Debug.Log($"DropItem being called from {(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name}");
}
/// <summary>
/// Create a pickup at the current position.
/// </summary>
/// <param name="item">The item type for the pickup.</param>
public void DropItem(InventoryItem item)
{
SpawnPickup(item, GetDropLocation(), 1);
Debug.Log($"DropItem being called from {(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name}");
}
// PROTECTED
/// <summary>
/// Override to set a custom method for locating a drop.
/// </summary>
/// <returns>The location the drop should be spawned.</returns>
protected virtual Vector3 GetDropLocation()
{
return transform.position;
}
// PRIVATE
public void SpawnPickup(InventoryItem item, Vector3 spawnLocation, int number)
{
var pickup = item.SpawnPickup(spawnLocation, number);
droppedItems.Add(pickup);
}
[System.Serializable]
private struct DropRecord
{
public string itemID;
public SerializableVector3 position;
public int number;
public int sceneBuildIndex;
}
/* object ISaveable.CaptureState()
{
RemoveDestroyedDrops();
var droppedItemsList = new List<DropRecord>();
int buildIndex = SceneManager.GetActiveScene().buildIndex;
foreach (Pickup pickup in droppedItems)
{
var droppedItem = new DropRecord();
droppedItem.itemID = pickup.GetItem().GetItemID();
droppedItem.position = new SerializableVector3(pickup.transform.position);
droppedItem.number = pickup.GetNumber();
droppedItem.sceneBuildIndex = buildIndex;
droppedItemsList.Add(droppedItem);
}
droppedItemsList.AddRange(otherSceneDroppedItems);
return droppedItemsList;
}
void ISaveable.RestoreState(object state)
{
var droppedItemsList = (List<DropRecord>)state;
int buildIndex = SceneManager.GetActiveScene().buildIndex;
otherSceneDroppedItems.Clear();
foreach (var item in droppedItemsList)
{
if (item.sceneBuildIndex != buildIndex)
{
otherSceneDroppedItems.Add(item);
continue;
}
var pickupItem = InventoryItem.GetFromID(item.itemID);
Vector3 position = item.position.ToVector();
int number = item.number;
SpawnPickup(pickupItem, position, number);
}
} */
class otherSceneDropRecord {
public string id;
public int number;
public Vector3 location;
public int scene;
}
private List<otherSceneDropRecord> otherSceneDrops = new List<otherSceneDropRecord>();
List<otherSceneDropRecord> MergeDroppedItemsWithOtherSceneDrops() {
List<otherSceneDropRecord> result = new List<otherSceneDropRecord>();
result.AddRange(otherSceneDrops);
foreach (var item in droppedItems)
{
otherSceneDropRecord drop = new otherSceneDropRecord();
drop.id = item.GetItem().GetItemID();
drop.number = item.GetNumber();
drop.location = item.transform.position;
drop.scene = SceneManager.GetActiveScene().buildIndex;
result.Add(drop);
Debug.Log($"(ItemDropper) Adding Drop: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
}
Debug.Log($"(ItemDropper) Total Drops: {result.Count}");
return result;
}
public JToken CaptureAsJToken() {
RemoveDestroyedDrops();
var drops = MergeDroppedItemsWithOtherSceneDrops();
JArray state = new JArray();
IList<JToken> stateList = state;
Debug.Log($"(ItemDropper) Number of drops to capture: {drops.Count}");
foreach (var drop in drops) {
JObject dropState = new JObject();
IDictionary<string, JToken> dropStateDict = dropState;
dropStateDict["id"] = JToken.FromObject(drop.id);
dropStateDict["number"] = drop.number;
dropStateDict["location"] = drop.location.ToToken();
dropStateDict["scene"] = drop.scene;
stateList.Add(dropState);
Debug.Log($"(ItemDropper) Capturing Item: {drop.id}, number: {drop.number}, scene: {drop.scene}, location: {drop.location}");
}
Debug.Log($"(ItemDropper) Captured State: {state.ToString()}");
return state;
}
private void ClearExistingDrops() {
foreach (var oldDrop in droppedItems) {
if (oldDrop != null) Destroy(oldDrop.gameObject);
}
otherSceneDrops.Clear();
}
public void RestoreFromJToken(JToken state)
{
Debug.Log($"(ItemDropper) Restoring State started"); // SUCCESS
if (state == null)
{
Debug.Log($"(ItemDropper) state is null");
return;
}
if (state is JArray stateArray)
{
Debug.Log($"(ItemDropper) stateArray entered with {stateArray.Count} entries"); // SUCCESS
int currentScene = SceneManager.GetActiveScene().buildIndex;
IList<JToken> stateList = stateArray;
Debug.Log($"(ItemDropper) stateArray: {stateArray.ToString()}");
ClearExistingDrops();
if (stateArray.Count == 0)
{
Debug.Log($"(ItemDropper) stateArray is empty"); // THIS IS THE REASON WHY THE STATELIST IS EMPTY, AND NO RESTORING OCCURS
return;
}
foreach (var entry in stateList) {
Debug.Log($"(ItemDropper) stateList entry: {entry.ToString()}"); // FAILED (it's why the Restore of the pickups fails)
if (entry is JObject dropState) {
IDictionary<string, JToken> dropStateDict = dropState;
int scene = dropStateDict["scene"].ToObject<int>();
InventoryItem item = InventoryItem.GetFromID(dropStateDict["id"].ToObject<string>());
int number = dropStateDict["number"].ToObject<int>();
Vector3 location = dropStateDict["location"].ToVector3();
Debug.Log($"(ItemDropper) Restoring item: {item.name}, number: {number}, scene: {scene}, location: {location}");
if (scene == currentScene) {
SpawnPickup(item, location, number);
}
else
{
var otherDrop = new otherSceneDropRecord();
otherDrop.id = item.GetItemID();
otherDrop.number = number;
otherDrop.location = location;
otherDrop.scene = scene;
otherSceneDrops.Add(otherDrop);
}
}
else Debug.Log($"(ItemDropper) Unexpected entry type: {entry.GetType().ToString()}");
}
}
}
/// <summary>
/// Remove any drops in the world that have subsequently been picked up.
/// </summary>
private void RemoveDestroyedDrops()
{
var newList = new List<Pickup>();
foreach (var item in droppedItems)
{
if (item != null)
{
newList.Add(item);
}
}
droppedItems = newList;
}
}
}