[REQUEST] Improvement of loading and saving nested items

WeiYiHua

Member
Hello, due to functional requirements, I need to nest multiple levels of items when creating, saving, and loading items.

Upon reviewing the code related to saving nested items, I noticed that it only supports the ItemAmounts type and one level of nesting. There is significant room for improvement.

I believe that the extensibility of reading and writing nested items can be enhanced through an interface.
Here is the interface and the types that inherit from it:

Code:
/// <summary>
/// Interface for containers that hold nested items.
/// </summary>
public interface INestedItemsContainer
{
    /// <summary>
    /// Gets the nested items.
    /// </summary>
    /// <param name="nestedItems">The list to populate with nested items.</param>
    /// <returns>The count of nested items.</returns>
    int GetNestedItems(List<Item> nestedItems);

    /// <summary>
    /// Loads the nested items.
    /// </summary>
    /// <param name="registeredItems">The list of registered items to load.</param>
    void LoadNestedItems(List<Item> registeredItems);
}
Code:
/// <summary>
/// Specifies the amount of each item.
/// </summary>
[Serializable]
public struct ItemAmount : IEquatable<ItemAmount>, IObjectAmount<Item>, INestedItemsContainer
{

...

/// <summary>
/// Gets the nested items.
/// </summary>
/// <param name="nestedItems">The list to populate with nested items.</param>
/// <returns>The count of nested items.</returns>
public int GetNestedItems(List<Item> nestedItems)
{
    if (Item == null || Item.ItemDefinition == null) { return 0; }

    nestedItems.Add(Item);
    return 1;
}

/// <summary>
/// Loads the nested items.
/// </summary>
/// <param name="registeredItems">The list of registered items to load.</param>
public void LoadNestedItems(List<Item> registeredItems)
{
    if (Item == null || Item.ItemDefinition == null) { return; }

    for (int j = 0; j < registeredItems.Count; j++)
    {
        var registeredItem = registeredItems[j];
        if (Item.ID == registeredItem.ID)
        {
            m_Item = registeredItem;
            break;
        }
    }
}

...

}

Code:
/// <summary>
/// Item Amounts is an array of item amounts.
/// </summary>
[System.Serializable]
public class ItemAmounts : ObjectAmounts<Item, ItemAmount>, INestedItemsContainer
{

...

/// <summary>
/// Gets the nested items.
/// </summary>
/// <param name="nestedItems">The list to populate with nested items.</param>
/// <returns>The count of nested items.</returns>
public int GetNestedItems(List<Item> nestedItems)
{
    int count = 0;
    for (int i = 0; i < Count; i++)
    {
        var nestedItem = Array[i].Item;
        if ((nestedItem == null || nestedItem.ItemDefinition == null)) { continue; }

        bool find = false;
        foreach (var item in nestedItems)
        {
            if (nestedItem.ID == item.ID)
            {
                find = true;
                break;
            }
        }
        if (find) { continue; }

        nestedItems.Add(nestedItem);
        count++;
    }
    return count;
}

/// <summary>
/// Loads the nested items.
/// </summary>
/// <param name="registeredItems">The list of registered items to load.</param>
public void LoadNestedItems(List<Item> registeredItems)
{
    for (int i = 0; i < Count; i++)
    {
        var nestedItem = Array[i].Item;
        if (nestedItem == null || nestedItem.ItemDefinition == null) { continue; }

        for (int j = 0; j < registeredItems.Count; j++)
        {
            var registeredItem = registeredItems[j];
            if (nestedItem.ID == registeredItem.ID)
            {
                Array[i] = new ItemAmount(registeredItem, Array[i].Amount);
                break;
            }
        }
    }
}

...

}

To obtain the nested items to be saved during the saving process, recursive searching of item attributes can be implemented:

Code:
/// <summary>
/// Recursively retrieves nested items from the specified item.
/// </summary>
/// <param name="item">The item to retrieve nested items from.</param>
/// <param name="nestedItems">The list to populate with nested items.</param>
public void GetNestedItemsRecursive(Item item, List<Item> nestedItems)
{
    for (int i = 0; i < item.ItemAttributeCollection.Count; i++)
    {
        var attribute = item.ItemAttributeCollection[i];

        // Don't get if it inherits.
        if (attribute.VariantType == VariantType.Inherit) { continue; }

        if (!typeof(INestedItemsContainer).IsAssignableFrom(attribute.GetValueType())) { continue; }
        var nestedItemsContainer = (INestedItemsContainer)attribute.GetValueAsObject();

        var containerNestedItems = new List<Item>();
        if (nestedItemsContainer.GetNestedItems(containerNestedItems) == 0) { continue; }

        for (int j = containerNestedItems.Count - 1; j >= 0; j--)
        {
            foreach (var nestedItem in nestedItems)
            {
                if (nestedItem.ID == containerNestedItems[j].ID)
                {
                    containerNestedItems.RemoveAt(j);
                    break;
                }
            }
        }

        nestedItems.AddRange(containerNestedItems);

        foreach (var containerNestedItem in containerNestedItems)
        {
            GetNestedItemsRecursive(containerNestedItem, nestedItems);
        }
    }
}

UltimateInventorySystem\Scripts\SaveSystem\InventorySystemManagerItemSaver.cs
Code:
/// <summary>
/// Add nested items to the save data.
/// </summary>
/// <param name="item">The item containing nested items.</param>
/// <param name="itemsToSave">The item list.</param>
protected virtual void AddNestedItemsToSave(Item item, Stack<Item> itemsToSave)
{
    var nestedItems = new List<Item>();
    GetNestedItemsRecursive(item, nestedItems);

    foreach (var nestedItem in nestedItems)
    {
        nestedItem.Serialize();
        itemsToSave.Push(nestedItem);
    }
}

When loading the saved data, we can directly traverse and load all nested items:

Code:
/// <summary>
/// Loads nested items for the specified item.
/// </summary>
/// <param name="item">The item to load nested items for.</param>
public void LoadNestedItems(Item item)
{
    for (int i = 0; i < item.ItemAttributeCollection.Count; i++)
    {
        var attribute = item.ItemAttributeCollection[i];

        // Don't load if it inherits.
        if (attribute.VariantType == VariantType.Inherit) { continue; }

        if (!typeof(INestedItemsContainer).IsAssignableFrom(attribute.GetValueType())) { continue; }
        var nestedItemsContainer = (INestedItemsContainer)attribute.GetValueAsObject();

        var containerNestedItems = new List<Item>();
        if (nestedItemsContainer.GetNestedItems(containerNestedItems) == 0) { continue; }

        var registeredItems = new List<Item>();
        for (int j = 0; j < containerNestedItems.Count; j++)
        {
            var containerNestedItem = containerNestedItems[j];
            var registeredItem = InventorySystemManager.GetItem(containerNestedItem.ID);
            registeredItems.Add(registeredItem);
        }

        nestedItemsContainer.LoadNestedItems(registeredItems);
        attribute.SetOverrideValueAsObject(nestedItemsContainer);
    }
}

UltimateInventorySystem\Scripts\SaveSystem\InventorySystemManagerItemSaver.cs
Code:
if (m_UsingNestedItems)
{
    foreach ( var item in m_CurrentSaveData.Items)
    {
        LoadNestedItems(item);
    }
}

I have implemented an additional feature where nested items are recursively initialized and registered during item creation:

Code:
using UISItem = Opsive.UltimateInventorySystem.Core.Item;

...

/// <summary>
/// Initializes the attributes of an item.
/// </summary>
/// <param name="item">The item to initialize.</param>
public void InitializeItemAttributes(UISItem item)
{
    if (item == null || !item.IsInitialized)
    {
        return;
    }

    LoadNestedItems(item);
    
    ...
}

/// <summary>
/// Loads nested items for the specified item.
/// </summary>
/// <param name="item">The item to load nested items for.</param>
protected void LoadNestedItems(UISItem item)
{
    for (int i = 0; i < item.ItemAttributeCollection.Count; i++)
    {
        var attribute = item.ItemAttributeCollection[i];

        if (!typeof(INestedItemsContainer).IsAssignableFrom(attribute.GetValueType())) { continue; }
        var nestedItemsContainer = (INestedItemsContainer)attribute.GetValueAsObject();

        var nestedItems = new List<UISItem>();
        if (nestedItemsContainer.GetNestedItems(nestedItems) == 0) { continue; }

        var registeredItems = new List<UISItem>();
        for (int j = 0; j < nestedItems.Count; j++)
        {
            var nestedItem = nestedItems[j];
            var registeredItem = InventorySystemManager.GetItem(nestedItem.ID);

            if (registeredItem == null)
            {
                nestedItem.Initialize(false, true, true);
                InventorySystemManager.ItemRegister.Register(ref nestedItem);
                InitializeItemAttributes(nestedItem);
                registeredItems.Add(nestedItem);
            }
            else
            {
                registeredItems.Add(registeredItem);
            }
        }

        nestedItemsContainer.LoadNestedItems(registeredItems);

        attribute.SetOverrideValueAsObject(nestedItemsContainer);
    }
}
The InitializeItemAttributes method is called in the CreateItem method of the override of InventorySystemFactory

I have performed some basic tests on this code, and it is able to load and save nested items with multiple levels.

Are there any issues with this code? When writing this code, I assumed that registered items with the same ID are the same instance of the Item class, without any exceptions. Is this assumption correct? I would appreciate your guidance!
 
I am currently on holiday so I can't review the code properly.
But I really like the idea of using an interface and having more levels of nesting. It's something I had planned but it lost priority to other feature that people requested.
I will look through your code in detail when I get back. And I'll implement it if it works.

Thank you for sharing your solution
 
I missed a very important point. Even if an item is immutable and unique, it will generate a random ID when manually set in ItemAmount. In this case, when creating and initializing an item, nested items need to be set as registered items.
The INestedItemsContainer interface also needs to add methods like GetNestedContainerSize(), SetNestedItem(int index, Item item, int amount), GetNestedItem(int index, out Item item, out int amount).

If my previous mistake misled you, I apologize for that.
 
By the way, is the feature of saving and loading nested items using interfaces currently on the to-do list?
 
The past few weeks have been a bit hectic on my side, so I wasn't able to look into this at all yet.
It is in my list, but unfortunatly I don't know when I'll be able to implement all of this. It is a big change so I would need not only time to implement it but test it thoroughly in all use cases.
So I'm afraid I can't give you an ETA on it at all. I'd suggest you continue making all the required changes on your side for your project in the meantime
 
Thank you for your response. I understand that you are busy and it may not be possible for you to implement this feature immediately. Now that I know this feature is on the list, I will continue researching and testing it until you officially support it.
 
Top