/// <summary>
        /// Registers a new shop item. Check other RegisterShopItem overloads for more info.
        /// Warning: This overload skips most of the parameter checks! It's best to use the other overload instead.
        /// </summary>
        public static IDisposable RegisterShopItem(ShopInfo.Param itemParams)
        {
            if (StudioAPI.InsideStudio)
            {
                return(Disposable.Empty);
            }

            // TODO handle IDs somehow, needed for data persistance, gets used in .buyList and .buyNumTable so it has to be the same between game reloads even if other things change
            // maybe require some sort of guid and then save this data to game save? the plugin has to deal with making it unique for now.

            var existing = _customShopItems.Find(x => x.ID == itemParams.ID);

            if (existing != null)
            {
                throw new ArgumentException($"Could not add new shop item. The ID={itemParams.ID} is already being used by a custom item with Name={existing.Name}");
            }
            if (SingletonInitializer <ActionScene> .initialized && SingletonInitializer <ActionScene> .instance.shopInfoTable?.ContainsKey(itemParams.ID) == true)
            {
                KoikatuAPI.Logger.LogWarning($"Added item ID={itemParams.ID} is already being used by a base game item with Name={SingletonInitializer<ActionScene>.instance.shopInfoTable[itemParams.ID].Name}");
            }

            _customShopItems.Add(itemParams);

            StoreHooks.ApplyHooksIfNeeded();

            return(Disposable.Create(() => _customShopItems.Remove(itemParams)));
        }
            private static bool ConditionCheckPrefix(ref int __result, ShopView __instance, ShopInfo.Param param)
            {
                // If not a custom item and not set to be non-restockable, use stock game code
                if (param.ID < MinimumItemId || param.InitType != -1)
                {
                    return(true);
                }

                // If custom item is set as restockable then base game code will only allow 1 of this item
                // to be bought and then set it as sold out, so we need to patch around that here
                __instance.player.buyNumTable.TryGetValue(param.ID, out var bought);
                __result = param.Num - bought;
                return(false);
            }
        /// <summary>
        /// Registers a custom shop item.
        /// </summary>
        /// <param name="itemId">Unique ID of the shop item.
        /// This ID is used in the save file to keep track of purchases, so it has to always be the same between game starts (i.e. use a hardcoded number and never change it).
        /// Values below 1200 are reserved for the base game items. It's best to use values above 100000 just to be safe (be mindful of other mods, ID collisions are not handled).
        /// </param>
        /// <param name="itemName">Name of the item shown on the shop list.</param>
        /// <param name="explaination">Description of the item when hovering over it.</param>
        /// <param name="shopType">Which shop tab to show the item in (day/night).</param>
        /// <param name="itemBackground">Background color of the item (shows the rough type of the item: normal / unlock / lewd).</param>
        /// <param name="itemCategory">Category of the item, changes which icon is used.
        /// Default icons:
        /// 0 - Speech buble
        /// 1 - Seeds
        /// 2 - Egg in a nest
        /// 3 - Question mark
        /// 4 - Massager
        /// 5 - Vibe
        /// 6 - Onahole
        /// 7 - Perfume
        /// 8 - Drugs
        /// 9 - Book
        /// You can add custom icons by using <see cref="RegisterShopItemCategory"/>
        /// </param>
        /// <param name="stock">How many of this item are available to be bought.
        /// Number of bought items is stored in <see cref="SaveData.Player.buyNumTable"/>, and if the stored value is equal or higher than this parameter then the item can't be bought any more.</param>
        /// <param name="resetsDaily">If true, item is rectocked daily. If false, it can only be bought once.
        /// If the item is set to restock every day:
        /// 1 - It will get its owned count reset to 0 at the start of a new day when you click the "Go out" button in your room. You will be able to buy the item again since you don't own any of it any more.
        /// 2 - For the duration of that day it will be placed in the <see cref="SaveData.WorldData.shopItemEffect"/> dictionary (key = ID of the item; value = number of the items).
        /// Basically, on the day the item was bought it will be counted in <see cref="SaveData.Player.buyNumTable"/> which this method uses, and on the next day it will be counted in <see cref="SaveData.WorldData.shopItemEffect"/>, until it is completely removed on the day after that.
        /// </param>
        /// <param name="cost">How many koikatsu points this item costs to buy.</param>
        /// <param name="sort">Number used to sort items on the list. Larger number is lower on the list. Value of default items range from 0 to 200.</param>
        /// <param name="numText">Override the "You can buy x more of this item today" text shown below the name of the item. The string has to contain `{0}` in it (gets replaced with the number left). If null, the default game string is used (differs based on the value of resetsDaily).</param>
        /// <param name="onBought">Action to run right after the item was bought by the player (similar to how it pops up the "new topic" message right after buying topics in shop). You can use it to apply effects, or you can check if the item was bought later with <see cref="GetItemAmountBought"/>. This is fired for each item bought (can't buy multiple items at once).</param>
        /// <returns>Dispose of the return value to remove the item. Warning: This is intended only for development use! This might not remove the item immediately or fully, and you might need to go back to title menu and load the game again for the changes to take effect. Disposing won't clear the item from inventory lists.</returns>
        public static IDisposable RegisterShopItem(
            int itemId, string itemName, string explaination,
            ShopType shopType, ShopBackground itemBackground, int itemCategory,
            int stock, bool resetsDaily, int cost, int sort = 300,
            string numText = null, Action <ShopItem> onBought = null)
        {
            if (StudioAPI.InsideStudio)
            {
                return(Disposable.Empty);
            }

            if (itemId < MinimumItemId)
            {
                throw new ArgumentOutOfRangeException(nameof(itemId), itemId, "Values below 1200 are reserved for the base game items");
            }
            if (itemName == null)
            {
                throw new ArgumentNullException(nameof(itemName));
            }
            if (explaination == null)
            {
                throw new ArgumentNullException(nameof(explaination));
            }
            if (!Enum.IsDefined(typeof(ShopType), shopType))
            {
                throw new ArgumentOutOfRangeException(nameof(shopType), shopType, "Invalid ShopType");
            }
            if (!Enum.IsDefined(typeof(ShopBackground), itemBackground))
            {
                throw new ArgumentOutOfRangeException(nameof(itemBackground), itemBackground, "Invalid ShopBackground");
            }
            if (stock < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(stock), stock, "Value can't be negative");
            }
            if (cost < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(cost), cost, "Value can't be negative");
            }
            if (itemCategory < 0 || (itemCategory > 9 && itemCategory < CustomCategoryIndexOffset) || itemCategory >= CustomCategoryIndexOffset + _customShopCategoryIcons.Count)
            {
                throw new ArgumentOutOfRangeException(nameof(itemCategory), itemCategory, "itemCategory has to be in range of 0-9 for default categories, or it has to use an index returned from RegisterShopItemCategory.");
            }

            if (numText == null)
            {
                // Use the same strings as stock game items so they get translated as needed.
                // {0} items left for today : {0} left in stock
                numText = resetsDaily ? "本日残り{0}回" : "残り{0}個";
            }

            var param = new ShopInfo.Param
            {
                ID         = itemId,
                Name       = itemName,
                Explan     = explaination,
                Type       = (int)shopType,
                BackGround = (int)itemBackground,
                Category   = itemCategory,
                Num        = stock,
                NumText    = numText,
                // todo make sure it's correct, maybe set it a default together with some other settings
                InitType = resetsDaily ? 0 : -1,
                Pt       = cost,
                Sort     = sort,
            };

            var token = RegisterShopItem(param);

            if (onBought != null)
            {
                _customShopBuyActions.Add(itemId, onBought);
            }
            return(token);
        }