Can't Buy/Sell More Than One Item Per Buy/Sell Click

So, a few lessons ago (prior to the introduction of buying/selling level restrictions), I had things set up nicely to buy/sell items that the trader didn’t have listings for, and have these items transfer to the seller’s buy inventory after the sale. I implemented the changes outlined here to achieve that: Sell Items that aren’t in shop stock - Unity Courses / Show - GameDev.tv

Now that we’ve further modified the system, a lot of what was written there isn’t working anymore. I’ve made some (likely bodgy) modifications that have the system ‘mostly’ working. The two remaining issues are that I can only buy/sell one item of a single quantity per transaction, and that sold items aren’t appearing in the merchant’s buy window.

Edit: Futher testing has revealed that stock that has been bought from the merchant depletes his stock correctly, and then when sold back to him will raise his stock level correctly. Additional items of the same type found and sold to him will NOT increase his set initial stock level. (So, he starts with 5 foot cream and I buy two, he’ll have three left. I sell him two and his stock is back to five. I find another foot cream and sell that to him. His available stock remains at five.)

I’ll post the Shop.cs below. A lot of it probably needs fixing, as I’ve duct-taped old and new functions together to achieve a semi-working system that allows you to sell items to a merchant that doesn’t sell those items.

using System;
using System.Collections.Generic;
using UnityEngine;
using GameDevTV.Inventories;
using RPG.Control;
using RPG.Inventories;
using RPG.Movement;
using RPG.Saving;
using RPG.Stats;

namespace RPG.Shops
{
    public class Shop : MonoBehaviour, IRaycastable, ISaveable
    {
        [SerializeField] string shopName;
        [Range(0, 100)]
        [SerializeField] float sellingPercentage = 50f;

        [SerializeField] List<StockItemConfig> stockConfig = new List<StockItemConfig>();
        List<InventoryItem> sellingList = new List<InventoryItem>();

        [System.Serializable]
        class StockItemConfig
        {
            public StockItemConfig(InventoryItem item, int initialStock)
            {
                this.item = item;
                this.initialStock = initialStock;
                buyingDiscountPercentage = 0;
            }

            public InventoryItem item;
            public int initialStock;
            [Range(0,100)]
            public float buyingDiscountPercentage;
            public int levelToUnlock = 0;
        }

        Dictionary<InventoryItem, int> transaction = new Dictionary<InventoryItem, int>();
        Dictionary<InventoryItem, int> stockSold = new Dictionary<InventoryItem, int>();

        Shopper currentShopper = null;

        bool isBuyingMode = true;

        ItemCategory filter = ItemCategory.None;

        public event Action onChange;

        public void SetShopper(Shopper shopper)
        {
            currentShopper = shopper;
        }

        public IEnumerable<ShopItem> GetAllItems()
        {
            sellingList.Clear();
            Dictionary<InventoryItem, float> prices = GetPrices();
            Dictionary<InventoryItem, int> availabilities = GetAvailabilities();
            if (isBuyingMode)
            {
                foreach (InventoryItem item in availabilities.Keys)
                {
                    if (availabilities[item] <= 0) { continue; }

                    float price = prices[item];
                    int quantityInTransaction = 0;
                    transaction.TryGetValue(item, out quantityInTransaction);
                    int availibility = availabilities[item];
                    yield return new ShopItem(item, availibility, price, quantityInTransaction);
                }
            }
            else
            {
                Inventory shopperInventory = currentShopper.GetComponent<Inventory>();
                for (int i = 0; i < shopperInventory.GetSize(); i++)
                {
                    InventoryItem item = shopperInventory.GetItemInSlot(i);
                    if (item != null)
                    {
                        int quantityInTransaction = 0;
                        transaction.TryGetValue(item, out quantityInTransaction);
                        float price = 0;
                        if (stockSold.ContainsKey(item))
                        {
                            foreach (StockItemConfig config in stockConfig)
                            {
                                if (config.item == item)
                                {
                                    price = item.GetPrice();
                                }
                            }
                        }
                        else
                        {
                            price = item.GetPrice() * 0.3f;
                        }
                        if (!sellingList.Contains(item))
                        {
                            sellingList.Add(item);
                            var getAvailability = CountItemsInInventory(item);
                            yield return new ShopItem(item, getAvailability, price, quantityInTransaction);
                        }
                    }
                }
            }
        }

        public IEnumerable<ShopItem> GetFilteredItems()
        {
            foreach (ShopItem shopItem in GetAllItems())
            {
                InventoryItem item = shopItem.GetInventoryItem();
                if (filter == ItemCategory.None || item.GetCategory() == filter)
                {
                    yield return shopItem;
                }
            }
        }

        public void SelectFilter(ItemCategory category)
        {
            filter = category;
            if (onChange != null)
            {
                onChange();
            }
        }

        public ItemCategory GetFilter()
        {
            return filter;
        }

        public void SelectMode(bool isBuying)
        {
            transaction.Clear();
            isBuyingMode = isBuying;
            if (onChange != null)
            {
                onChange();
            }
        }

        public bool IsBuyingMode()
        {
            return isBuyingMode;
        }

        public void AddToTransaction(InventoryItem item, int quantity)
        {
            if (!transaction.ContainsKey(item))
            {
                transaction[item] = 0;
            }

            var availabilities = GetAvailabilities();
            if (!availabilities.ContainsKey(item))
            {
                availabilities.Add(item, quantity);
            }
            int availability = availabilities[item];
            {
                transaction[item] = 0;
            }
            if (transaction[item] + quantity > availability)
            {
                transaction[item] = availability;
            }
            else
            {
                transaction[item] += quantity;
            }

            if (transaction[item] <= 0)
            {
                transaction.Remove(item);
            }

            if (onChange != null)
            {
                onChange();
            }
        }

        public string GetShopName()
        {
            return shopName;
        }

        public bool CanTransact()
        {
            if (IsTransactionEmpty()) { return false; }
            if (!HasSufficientFunds()) { return false; }
            if (!HasInventorySpace()) { return false; }
            return true;
        }

        public bool IsTransactionEmpty()
        {
            return transaction.Count == 0;
        }

        public bool HasSufficientFunds()
        {
            if (!isBuyingMode) { return true; }

            Purse purse = currentShopper.GetComponent<Purse>();
            if (purse == null) { return false; }
            return purse.GetBalance() > TransactionTotal();
        }

        public bool HasInventorySpace()
        {
            if (!isBuyingMode) { return true; }

            Inventory shopperInventory = currentShopper.GetComponent<Inventory>();
            if (shopperInventory == null) { return false; }

            List<InventoryItem> flatItems = new List<InventoryItem>();
            foreach (ShopItem shopItem in GetAllItems())
            {
                InventoryItem item = shopItem.GetInventoryItem();
                int quantity = shopItem.GetQuantityInTransaction();
                for (int i = 0; i < quantity; i++)
                {
                    flatItems.Add(item);
                }
            }

            return shopperInventory.HasSpaceFor(flatItems);
        }

        public void ConfirmTransaction()
        {
            Inventory shopperInventory = currentShopper.GetComponent<Inventory>();
            Purse shopperPurse = currentShopper.GetComponent<Purse>();
            if (shopperInventory == null || shopperPurse == null) { return; }

            foreach (ShopItem shopItem in GetAllItems())
            {
                InventoryItem item = shopItem.GetInventoryItem();
                int quantity = shopItem.GetQuantityInTransaction();
                float price = shopItem.GetPrice();
                for (int i = 0; i < quantity; i++)
                {
                    if (isBuyingMode)
                    {
                        BuyItem(shopperInventory, shopperPurse, item, price);
                    }
                    else
                    {
                        SellItem(shopperInventory, shopperPurse, item, price);
                    }
                }
            }
            if (onChange != null)
            {
                onChange();
            }
        }

        public float TransactionTotal()
        {
            float total = 0;
            foreach (ShopItem item in GetAllItems())
            {
                total += item.GetPrice() * item.GetQuantityInTransaction();
            }
            return total;
        }

        public CursorType GetCursorType()
        {
            return CursorType.Shop;
        }

        public bool HandleRaycast(PlayerController callingController)
        {
            if (Input.GetMouseButtonDown(0))
            {
                if (Vector3.Distance(callingController.transform.position, this.transform.position) > 3)
                {
                    callingController.GetComponent<Mover>().StartMoveAction(this.transform.position, 1);
                }
                else
                {
                    callingController.GetComponent<Shopper>().SetActiveShop(this);
                }
            }
            return true;
        }

        private int CountItemsInInventory(InventoryItem item)
        {
            Inventory inventory = currentShopper.GetComponent<Inventory>();
            if (inventory == null) { return 0; }

            int total = 0;
            for (int i = 0; i < inventory.GetSize(); i++)
            {
                if (inventory.GetItemInSlot(i) == item)
                {
                    total += inventory.GetNumberInSlot(i);
                }
            }
            return total;
        }

        private Dictionary<InventoryItem, int> GetAvailabilities()
        {
            Dictionary<InventoryItem, int> availabilities = new Dictionary<InventoryItem, int>();

            foreach (var config in GetAvailableConfigs())
            {
                if (isBuyingMode)
                {
                    if (!availabilities.ContainsKey(config.item))
                    {
                        int sold = 0;
                        stockSold.TryGetValue(config.item, out sold);
                        availabilities[config.item] = -sold;
                    }
                    availabilities[config.item] += config.initialStock;
                }
                else
                {
                    availabilities[config.item] = CountItemsInInventory(config.item);
                }
            }

            return availabilities;
        }

        private Dictionary<InventoryItem, float> GetPrices()
        {
            Dictionary<InventoryItem, float> prices = new Dictionary<InventoryItem, float>();

            foreach (var config in GetAvailableConfigs())
            {
                if (isBuyingMode)
                {
                    if (!prices.ContainsKey(config.item))
                    {
                        prices[config.item] = config.item.GetPrice();
                    }
                    prices[config.item] *= (1 - config.buyingDiscountPercentage / 100);
                }
                else
                {
                    prices[config.item] = config.item.GetPrice() * (sellingPercentage / 100);
                }
            }

            return prices;
        }

        private IEnumerable<StockItemConfig> GetAvailableConfigs()
        {
            int shopperLevel = GetShopperLevel();
            foreach (var config in stockConfig)
            {
                if (config.levelToUnlock > shopperLevel) { continue; }
                yield return config;
            }
        }

        private void BuyItem(Inventory shopperInventory, Purse shopperPurse, InventoryItem item, float price)
        {
            if (shopperPurse.GetBalance() < price) { return; }

            bool success = shopperInventory.AddToFirstEmptySlot(item, 1);
            if (success)
            {
                AddToTransaction(item, -1);
                if (!stockSold.ContainsKey(item))
                {
                    stockSold[item] = 0;
                }
                stockSold[item]++;
                shopperPurse.UpdateBalance(-price);
            }
        }

        private void SellItem(Inventory shopperInventory, Purse shopperPurse, InventoryItem item, float price)
        {
            int slot = FindFirstItemSlot(shopperInventory, item);
            if (slot == -1) return;
            AddToTransaction(item, -1);
            shopperInventory.RemoveFromSlot(slot, 1);
            if (stockSold.ContainsKey(item))
            {
                stockSold[item]--;
            }
            else
            {
                stockConfig.Add(new StockItemConfig(item, 1));
                stockSold[item] = 1;
            }
            shopperPurse.UpdateBalance(price);
        }

        private int FindFirstItemSlot(Inventory shopperInventory, InventoryItem item)
        {
            for (int i = 0; i < shopperInventory.GetSize(); i++)
            {
                if (shopperInventory.GetItemInSlot(i) == item)
                {
                    return i;
                }
            }
            return -1;
        }

        private int GetShopperLevel()
        {
            BaseStats stats = currentShopper.GetComponent<BaseStats>();
            if (stats == null) { return 0; }

            return stats.GetLevel();
        }

        public object CaptureState()
        {
            Dictionary<string, int> saveObject = new Dictionary<string, int>();

            foreach (var pair in stockSold)
            {
                saveObject[pair.Key.GetItemID()] = pair.Value;
            }
            return saveObject;
        }

        public void RestoreState(object state)
        {
            Dictionary<string, int> saveObject = (Dictionary<string, int>) state;
            stockSold.Clear();
            foreach (var pair in saveObject)
            {
                stockSold[InventoryItem.GetFromID(pair.Key)] = pair.Value;
            }
        }
    }
}

Originally, I was getting Dictionary issues because those items weren’t in the seller’s dictionary. I got around that with this:

if (!availabilities.ContainsKey(item))
            {
                availabilities.Add(item, quantity);
            }

but I’m not sure if that’s part of the problem.

I appreciate any and all assistance you can provide on this one!

Thanks again,
Mark.

1 Like

Usually, I play a lot of 20 questions before getting to this, but there’s zero chance I’m going to debug this without the project in hand, I’m going to need to add a number of Debug points and try to figure out where the issue may be. (It’s a lot easier to navigate the code with a good code editor, and I have the best one out there, JetBrains Rider).
Zip up your project and upload it to https://gdev.tv/projectupload and I’ll have a look to see if I can find the bug.

2 Likes

Sent! Thank you so much for looking into this issue. Sending a project to someone else is a bit like inviting people over when the house is in a state, so please excuse the mess! :stuck_out_tongue:

1 Like

I’ll get to it later tonight. Don’t worry, it won’t be the first messy house I’ve been in. :slight_smile:

2 Likes

You’re adding an item to the stockConfig, that’s correct, but then you’re actually considering the item sold by setting stockSold[item] to 1. Changing this to 0 fixes this

stockSold[item]=0;

I’m still working on the original issue, Can’t Buy/Sell more than one item per click. More to follow.

3 Likes

So what’s happening is that no matter what, this last transaction[item]=0 will run… if you entered the method with the transaction containing a hat, once it gets to this line transaction[item]=0 (it’s in curly braces, I suspect it was once in a conditional that you moved to the top) transaction[hat] will now be zero, and then the method will add 1 to it.
Simply commenting out the transaction[item] = 0 (the one I commented, not the first one in the if block at the beginning of the method) fixed the issue.

3 Likes

I can confirm the above fixes have resolved the issues! Thank you so much. :grinning:

One other thing that may or may not be an issue. I’ve forgotten if the original system was designed this way:

So, when you select to buy items, you tally up several items and hit buy. You purchase them all at once. When you sell items, tally them all up, the system will sell one ‘item’ of set quantity per click, and to sell the rest of the items below it you have to click sell again.

So, I have a Hat (x4), a Hat [+10 Health] (x2) and a Sword (x3). I click Sell. All of the Hats are sold (x4). Remaining in the transaction are the Hat [+10 Health] (x2) and Sword (x3). The next click will sell the Hats [+10 Health], and the click after will sell the Sword (x3).

Not a major issue, could even be how the system was designed. Can’t remember, too many late nights recently. :stuck_out_tongue:

I want to thank you again for helping out with the above issues though, you’re a life saver!

1 Like

No, this is not the intended behaviour. It should sell all of the items in the transaction at once. I’ll take a look at this later this evening… I’m going to be away from my computer for probably a dozen or so hours.

1 Like

All good. Thanks again! :grinning:

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

One f the handy things of working for GameDev is I can open the topics back up. Once you choose a solution, the topic is closed 24 hours later.

So I looked again at the project and at first I couldn’t see a reason why the sale was stopping after only one item… then, on a hunch, I converted the GetAllItems to an array before iterating over the loop

var availableItems = GetAllItems().ToArray(); //You need using System.Linq for this to work
foreach (ShopItem item in availableItems)
{
    //remainder of ConfirmTransaction

So here’s why this worked, and why things were bugging out in the first place. GetAllItems is an IEnumerable, which is really more of a recipe for a collection than an actual collection. If something changes in the collection, then the Next pointer can fail. In this case, when selling, we’re actually changing the contents of inventory, but GetAllItems() collects those items in inventory… meaning that once the items change, GetAllItems is stale.
By copying the IEnumerable into an array (.ToArray()), we get a fixed array of ShopItems that won’t change as we move through the collection. Then when we take an item out of inventory, the array isn’t reset and we can continue trolling through the collection. It didn’t come up before because our GetAvailablitities before doing All Inventory selling used the ShopConfig as a basis, not the inventory.

2 Likes

Ah, thank you once more for the help, and the interesting explanation. It works perfectly! :smiley:

1 Like

I actually ran into a similar problem with Abilities… if you make it so that the player moves within range before casting an area of effect spell, the enemies may no longer be in the area of effect, and a Debug.Log determined that the entire IEnumerable chain (filters and all) were run several times. I solved this by storing the results of the IEnumerable as an array in the User Data… it never came up in the course because we just cast it straight away.

1 Like

I think I might be experiencing a similar rare bug with item conditions. Occasionally, after loading or reloading from a save, equipping an inventory item will cause this NRE:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.Utils.Condition.Check (System.Collections.Generic.IEnumerable`1[T] evaluators) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Utils/Condition.cs:14)
GameDevTV.Inventories.EquipableItem.CanEquip (GameDevTV.Inventories.EquipLocation equipLocation, GameDevTV.Inventories.Equipment equipment) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Inventories/EquipableItem.cs:24)
GameDevTV.UI.Inventories.EquipmentSlotUI.MaxAcceptable (GameDevTV.Inventories.InventoryItem item) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/UI/Inventories/EquipmentSlotUI.cs:43)
GameDevTV.Core.UI.Dragging.DragItem`1[T].AttemptSimpleTransfer (GameDevTV.Core.UI.Dragging.IDragDestination`1[T] destination) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Utils/UI/Dragging/DragItem.cs:167)
GameDevTV.Core.UI.Dragging.DragItem`1[T].DropItemIntoContainer (GameDevTV.Core.UI.Dragging.IDragDestination`1[T] destination) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Utils/UI/Dragging/DragItem.cs:102)
GameDevTV.Core.UI.Dragging.DragItem`1[T].UnityEngine.EventSystems.IEndDragHandler.OnEndDrag (UnityEngine.EventSystems.PointerEventData eventData) (at Assets/Asset Packs/GameDev.tv Assets/Scripts/Utils/UI/Dragging/DragItem.cs:73)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IEndDragHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:85)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)

Now, line 14 encompasses this function in the Condition.cs

public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
        {
            foreach (Disjunction dis in and)
            {
                if (!dis.Check(evaluators))
                {
                    return false;
                }
            }
            return true;
        }

The items in question don’t have anything to evaluate. It doesn’t occur often, but when it does it throws the whole inventory system for a loop and the only way to fix it is a reload.

Here’s the entire Condition.cs:

using System.Collections.Generic;
using UnityEngine;

namespace GameDevTV.Utils
{
    [System.Serializable]
    public class Condition
    {
        [SerializeField]
        Disjunction[] and;

        public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
        {
            foreach (Disjunction dis in and)
            {
                if (!dis.Check(evaluators))
                {
                    return false;
                }
            }
            return true;
        }

        [System.Serializable]
        class Disjunction
        {
            [SerializeField] Predicate[] or;

            public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
            {
                foreach (Predicate pred in or)
                {
                    if (pred.Check(evaluators))
                    {
                        return true;
                    }
                }
                return false;
            }
        }
        
        [System.Serializable]
        class Predicate
        {
            [SerializeField] string predicate;
            [SerializeField] string[] parameters;
            [SerializeField] bool negate = false;

            public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
            {
                foreach (var evaluator in evaluators)
                {
                    bool? result = evaluator.Evaluate(predicate, parameters);
                    if (result == null)
                    {
                        continue;
                    }
                    if (result == negate)
                    {
                        return false;
                    }
                }
                return true;
            }
        }
    }
}
1 Like

That’s an odd bug, and technically impossible, since if you run the foreach loop, then you should only ever get existing Disjunctions.
Technically impossible doesn’t mean that it doesn’t happen… I smell a memory leak, though finding it might be next to impossible. Unity 2021.1 through 2021.2?
Try putting this small bandaid on it (as I said, it should be unneeded, but life happens… before if(!dis.Check(evaluators))

if(dis==null) continue;
3 Likes

I ran some testing this morning with this fix, and didn’t run into the issue. Thanks! :smiley:

2 Likes

Privacy & Terms