Spam console: Item.SetParent caused removeError

[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
[FancyDrop] SupplySignal thrown by 'john Maclister' at: (958.05, 2.16, -1827.55)
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen
Item.SetParent caused remove - this shouldn't ever happen

keeps going so I reset back to previous version for now.

This happens when?

it comes after reloading plugin and first time entering ANY personal backpack from plugin. Only first time tho. So if 1 person triggers it it wont come for anybody in the console again. Unless I reload it, then it will come again on first trigger.

Merged post

I removed custom-items from backpack and it never triggered the spam. So it has with custom items to do.  It hit me now I did my own version for this before your update, not perfect anywhere but it worked for me

// #define DEBUG_DROP_ON_DEATH
// #define DEBUG_POOLING
// #define DEBUG_BACKPACK_LIFECYCLE

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Facepunch;
using Network;
using Newtonsoft.Json.Converters;
using Oxide.Core.Configuration;
using Oxide.Core.Libraries;
using Rust;
using UnityEngine;
using UnityEngine.UI;
using Time = UnityEngine.Time;

namespace Oxide.Plugins
{
    [Info("Backpacks", "WhiteThunder", "3.15.5")]
    [Description("Allows players to have a Backpack which provides them extra inventory space.")]
    internal class Backpacks : CovalencePlugin
    {
        // Init gate to prevent early command use before capacity is ready
        private static bool _initReady = false;

        // FIXPOINT: Gate Backpacks until custom items stack is ready
        [PluginReference] private Plugin CustomItemDefinitions;
        [PluginReference] private Plugin CustomizableProtection;
        [PluginReference] private Plugin CustomizableWeapons;
        private bool DepsReady() => CustomItemDefinitions != null && CustomizableProtection != null && CustomizableWeapons != null;

        // FIXPOINT: Ensure Init completes before ServerInitialized logic runs
        private bool _lateInitDone = false;
        private void EnsureReady(Action after)
        {
            if (!DepsReady() || !_lateInitDone)
            {
                timer.Once(1f, () => EnsureReady(after));
                return;
            }
            after?.Invoke();
        }
        
        #region Fields

        private static int _maxCapacityPerPage = 48;

        private const int MinRows = 1;
        private const int MaxRows = 8;
        private const int MinContainerCapacity = 1;
        private const int MaxContainerCapacity = 48;
        private const int SlotsPerRow = 6;
        private const float StandardLootDelay = 0.1f;
        private const Item.Flag SearchableItemFlag = (Item.Flag)(1 << 24);
        private const Item.Flag UnsearchableItemFlag = (Item.Flag)(1 << 25);
        private const ItemDefinition.Flag SearchableItemDefinitionFlag = (ItemDefinition.Flag)(1 << 24);

        private const string UsagePermission = "backpacks.use";
        private const string SizePermission = "backpacks.size";
        private const string GUIPermission = "backpacks.gui";
        private const string FetchPermission = "backpacks.fetch";
        private const string GatherPermission = "backpacks.gather";
        private const string RetrievePermission = "backpacks.retrieve";
        private const string AdminPermission = "backpacks.admin";
        private const string AdminProtectedPermission = "backpacks.admin.protected";
        private const string CapacityProfilePermission = "backpacks.size.profile";
        private const string KeepOnDeathPermission = "backpacks.keepondeath";
        private const string LegacyKeepOnWipePermission = "backpacks.keeponwipe";
        private const string LegacyNoBlacklistPermission = "backpacks.noblacklist";

        private const string CoffinPrefab = "assets/prefabs/misc/halloween/coffin/coffinstorage.prefab";
        private const string DroppedBackpackPrefab = "assets/prefabs/misc/item drop/item_drop_backpack.prefab";
        private const string ResizableLootPanelName = "generic_resizable";

        private const int SaddleBagItemId = 1400460850;

        private readonly CapacityManager _capacityManager;
        private readonly BackpackManager _backpackManager;
        private readonly SubscriberManager _subscriberManager = new();

        private ProtectionProperties _immortalProtection;
        private Effect _reusableEffect = new();
        private string _cachedButtonUi;

        private readonly ApiInstance _api;
        private Configuration _config;
        private PreferencesData _preferencesData;
        private CapacityData _capacityData;
        private readonly HashSet<ulong> _uiViewers = new();
        private Coroutine _saveRoutine;

        [PluginReference]
        private readonly Plugin Arena, BackpackButton, EventManager, ItemRetriever;

        public Backpacks()
        {
            _backpackManager = new BackpackManager(this);
            _capacityManager = new CapacityManager(this, _backpackManager);
            _api = new ApiInstance(this);
        }

        #endregion

        #region Hooks

        
        // FIXPOINT: Gate Init() until dependencies are present
        private void Init()
        {
            if (!DepsReady())
            {
                timer.Once(1f, Init);
                return;
            }
            LateInit();
        }
        
        private void LateInit()
        {
            permission.RegisterPermission(UsagePermission, this);
                        permission.RegisterPermission(GUIPermission, this);
                        permission.RegisterPermission(FetchPermission, this);
                        permission.RegisterPermission(GatherPermission, this);
                        permission.RegisterPermission(RetrievePermission, this);
                        permission.RegisterPermission(AdminPermission, this);
                        permission.RegisterPermission(AdminProtectedPermission, this);
                        permission.RegisterPermission(KeepOnDeathPermission, this);

                        _config.Init(this);

                        _maxCapacityPerPage = Mathf.Clamp(_config.BackpackSize.MaxCapacityPerPage, MinContainerCapacity, MaxContainerCapacity);

                        PoolUtils.ResizePools();

                        _preferencesData = PreferencesData.Load();
                        _capacityData = CapacityData.Exists() ? CapacityData.Load() : new CapacityData();
                        _capacityManager.Init(_config, _capacityData);

                        Unsubscribe(nameof(OnPlayerSleep));
                        Unsubscribe(nameof(OnPlayerSleepEnded));

                        if (_config.GUI.Enabled)
                        {
                            AddCovalenceCommand("backpackgui", nameof(ToggleBackpackGUICommand));
                        }
                        else
                        {
                            Unsubscribe(nameof(OnPlayerConnected));
                            Unsubscribe(nameof(OnNpcConversationStart));
                            Unsubscribe(nameof(OnNpcConversationEnded));
                        }
            _lateInitDone = true; // FIXPOINT: signal that init is complete
        }


        
        // FIXPOINT: Run ServerInitialized after deps+init
       private void OnServerInitialized()
{
    EnsureReady(() =>
    {
        LateServerInit();
        _initReady = true;
    });
}
        
        private void LateServerInit()
        {
            _immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
                        _immortalProtection.name = "BackpacksProtection";
                        _immortalProtection.Add(1);

                        CheckBackpackButtonPlugin();
                        RegisterAsItemSupplier();

                        if (_config.GUI.Enabled)
                        {
                            Subscribe(nameof(OnPlayerSleep));
                            Subscribe(nameof(OnPlayerSleepEnded));
                            Subscribe(nameof(OnPlayerConnected));
                            Subscribe(nameof(OnNpcConversationStart));
                            Subscribe(nameof(OnNpcConversationEnded));

                            foreach (var player in BasePlayer.activePlayerList)
                            {
                                MaybeCreateButtonUi(player);
                            }
                        }
        }


        private void Unload()
        {
            UnityEngine.Object.Destroy(_immortalProtection);

            RestartSaveRoutine(async: false, keepInUseBackpacks: false);

            BackpackNetworkController.ResetNetworkGroupId();

            foreach (var player in BasePlayer.activePlayerList)
            {
                DestroyButtonUi(player);
            }

            PoolUtils.ResizePools(empty: true);
        }

        private void OnNewSave(string filename)
        {
            if (_config.BackpackSize.DynamicSize is { Enabled: true, CapacityResetOptions.Enabled: true })
            {
                _capacityData.Clear();
                if (_capacityData.SaveIfChanged())
                {
                    LogWarning("Dynamic size data has been reset.");
                }
            }

            if (_config.ClearOnWipe.Enabled)
            {
                _backpackManager.ClearCache();

                IEnumerable<string> backpackFileNameList;
                try
                {
                    backpackFileNameList = Interface.Oxide.DataFileSystem.GetFiles(Name)
                        .Select(fn => fn.Split(Path.DirectorySeparatorChar).Last()
                            .Replace(".json", string.Empty));
                }
                catch (DirectoryNotFoundException)
                {
                    // No backpacks to clear.
                    return;
                }

                var retainedDueToContents = 0;
                var retainedDueToPreferences = 0;
                var deletedBackpackFiles = 0;

                foreach (var backpackFileName in backpackFileNameList)
                {
                    if (!ulong.TryParse(backpackFileName, out var userId))
                        continue;

                    var backpack = _backpackManager.GetBackpackIfExists(userId);
                    if (backpack == null)
                        continue;

                    backpack.EraseContents(_config.ClearOnWipe.GetForPlayer(backpackFileName));

                    // Only delete the backpack data file if it's empty and has no saved preferences.
                    if (backpack.HasItems || backpack.HasPreferences)
                    {
                        backpack.SaveIfChanged();

                        if (backpack.HasItems)
                        {
                            retainedDueToContents++;
                        }
                        else if (backpack.HasPreferences)
                        {
                            retainedDueToPreferences++;
                        }
                    }
                    else
                    {
                        _backpackManager.DeleteBackpackFile(userId);
                        deletedBackpackFiles++;
                    }
                }

                _backpackManager.ClearCache();

                var logMessage =
                    "New save created. Backpacks were wiped according to the config and player permissions.";
                if (deletedBackpackFiles > 0)
                {
                    logMessage +=
                        $"\n- {deletedBackpackFiles} file(s) were deleted because those backpacks are now empty.";
                }

                if (retainedDueToContents > 0)
                {
                    logMessage +=
                        $"\n- {retainedDueToContents} file(s) were retained because those backpacks were not empty after applying the player's wipe ruleset.";
                }

                if (retainedDueToPreferences > 0)
                {
                    logMessage +=
                        $"\n- {retainedDueToPreferences} file(s) were retained even though those backpacks are empty because they contain player gather/retrieve preferences.";
                }

                LogWarning(logMessage);
            }
        }

        private void OnServerSave()
        {
            RestartSaveRoutine(async: true, keepInUseBackpacks: true);
        }

        private void OnPluginLoaded(Plugin plugin)
        {
            switch (plugin.Name)
            {
                case nameof(BackpackButton):
                    CheckBackpackButtonPlugin();
                    break;
                case nameof(ItemRetriever):
                    RegisterAsItemSupplier();
                    break;
            }
        }

        private void OnPluginUnloaded(Plugin plugin)
        {
            _subscriberManager.RemoveSubscriber(plugin);
        }

        private void OnPlayerDisconnected(BasePlayer player)
        {
            _capacityManager.ForgetCachedCapacity(player.userID);
            _backpackManager.GetBackpackIfCached(player.userID)?.NetworkController?.Unsubscribe(player);
        }

        // Handle player death by normal means.
        private void OnEntityDeath(BasePlayer player, HitInfo info) =>
            OnEntityKill(player);

        // Handle player death while sleeping in a safe zone.
        private void OnEntityKill(BasePlayer player)
        {
            if (player.IsNpc)
                return;

            DestroyButtonUi(player);

            if (!_backpackManager.HasBackpack(player.userID))
                return;

            if (permission.UserHasPermission(player.UserIDString, KeepOnDeathPermission))
            {
                #if DEBUG_DROP_ON_DEATH
                LogWarning($"[DEBUG_DROP_ON_DEATH] [Player {player.UserIDString}] Backpack not dropped because the player has the {KeepOnDeathPermission} permission.");
                #endif
                return;
            }

            if (_config.EraseOnDeath)
            {
                _backpackManager.TryEraseForPlayer(player.userID);
            }
            else if (_config.DropOnDeath)
            {
                _backpackManager.Drop(player.userID, player.transform.position);
            }
            else
            {
                #if DEBUG_DROP_ON_DEATH
                LogWarning($"[DEBUG_DROP_ON_DEATH] [Player {player.UserIDString}] Backpack not dropped because \"Drop on Death (true/false)\" is set to false in the config.");
                #endif
            }
        }

        private void OnGroupPermissionGranted(string groupName, string perm)
        {
            if (!perm.StartsWith("backpacks"))
                return;

            if (perm.StartsWith(SizePermission) || perm.StartsWith(UsagePermission) || perm.StartsWith(CapacityProfilePermission))
            {
                _backpackManager.HandleCapacityPermissionChangedForGroup(groupName);
            }
            else if (perm.StartsWith(RestrictionRuleset.FullPermissionPrefix) || perm.Equals(LegacyNoBlacklistPermission))
            {
                _backpackManager.HandleRestrictionPermissionChangedForGroup(groupName);
            }
            else if (perm.Equals(GatherPermission))
            {
                _backpackManager.HandleGatherPermissionChangedForGroup(groupName);
            }
            else if (perm.Equals(RetrievePermission))
            {
                _backpackManager.HandleRetrievePermissionChangedForGroup(groupName);
            }
            else if (_config.GUI.Enabled && perm.Equals(GUIPermission))
            {
                var groupName2 = groupName;
                foreach (var player in BasePlayer.activePlayerList.Where(p => permission.UserHasGroup(p.UserIDString, groupName2)))
                {
                    CreateOrDestroyButtonUi(player);
                }
            }
        }

        private void OnGroupPermissionRevoked(string groupName, string perm)
        {
            OnGroupPermissionGranted(groupName, perm);
        }

        private void OnUserPermissionGranted(string userId, string perm)
        {
            if (!perm.StartsWith("backpacks"))
                return;

            if (perm.StartsWith(SizePermission) || perm.StartsWith(UsagePermission) || perm.StartsWith(CapacityProfilePermission))
            {
                _backpackManager.HandleCapacityPermissionChangedForUser(userId);
            }
            else if (perm.StartsWith(RestrictionRuleset.FullPermissionPrefix) || perm.Equals(LegacyNoBlacklistPermission))
            {
                _backpackManager.HandleRestrictionPermissionChangedForUser(userId);
            }
            else if (perm.Equals(GatherPermission))
            {
                _backpackManager.HandleGatherPermissionChangedForUser(userId);
            }
            else if (perm.Equals(RetrievePermission))
            {
                _backpackManager.HandleRetrievePermissionChangedForUser(userId);
            }
            else if (_config.GUI.Enabled && perm.Equals(GUIPermission))
            {
                var player = BasePlayer.Find(userId);
                if (player != null)
                {
                    CreateOrDestroyButtonUi(player);
                }
            }
        }

        private void OnUserPermissionRevoked(string userId, string perm)
        {
            OnUserPermissionGranted(userId, perm);
        }

        private void OnUserGroupAdded(string userId, string groupName)
        {
            _backpackManager.HandleGroupChangeForUser(userId);
        }

        private void OnUserGroupRemoved(string userId, string groupName)
        {
            _backpackManager.HandleGroupChangeForUser(userId);
        }

        // Only subscribed while the GUI button is enabled.
        private void OnPlayerConnected(BasePlayer player) => MaybeCreateButtonUi(player);

        private void OnPlayerRespawned(BasePlayer player)
        {
            // People reported NRE on Carbon framework, meaning the player is somehow null, unknown root cause.
            if (player == null)
                return;

            MaybeCreateButtonUi(player);
            _backpackManager.GetBackpackIfCached(player.userID)?.PauseGatherMode(1f);
        }

        // Only subscribed while the GUI button is enabled.
        private void OnPlayerSleepEnded(BasePlayer player) => OnPlayerRespawned(player);

        // Only subscribed while the GUI button is enabled.
        private void OnPlayerSleep(BasePlayer player) => DestroyButtonUi(player);

        // Only subscribed while the GUI button is enabled.
        private void OnNpcConversationStart(NPCTalking npcTalking, BasePlayer player, ConversationData conversationData)
        {
            // This delay can be removed in the future if an OnNpcConversationStarted hook is created.
            NextTick(() =>
            {
                // Verify the conversation started, since another plugin may have blocked it.
                if (!npcTalking.conversingPlayers.Contains(player))
                    return;

                DestroyButtonUi(player);
            });
        }

        // Only subscribed while the GUI button is enabled.
        private void OnNpcConversationEnded(NPCTalking npcTalking, BasePlayer player) => MaybeCreateButtonUi(player);

        private void OnNetworkSubscriptionsUpdate(Networkable networkable, List<Network.Visibility.Group> groupsToAdd, List<Network.Visibility.Group> groupsToRemove)
        {
            if (groupsToRemove == null)
                return;

            for (var i = groupsToRemove.Count - 1; i >= 0; i--)
            {
                var group = groupsToRemove[i];
                if (BackpackNetworkController.IsBackpackNetworkGroup(group))
                {
                    // Prevent automatically unsubscribing from backpack network groups.
                    // This allows the subscriptions to persist while players move around.
                    groupsToRemove.Remove(group);
                }
            }
        }

        #endregion

        #region API

        private class ApiInstance
        {
            public readonly Dictionary<string, object> ApiWrapper;

            private readonly Backpacks _plugin;
            private BackpackManager _backpackManager => _plugin._backpackManager;

            public ApiInstance(Backpacks plugin)
            {
                _plugin = plugin;

                ApiWrapper = new Dictionary<string, object>
                {
                    [nameof(AddSubscriber)] = new Action<Plugin, Dictionary<string, object>>(AddSubscriber),
                    [nameof(RemoveSubscriber)] = new Action<Plugin>(RemoveSubscriber),
                    [nameof(GetExistingBackpacks)] = new Func<Dictionary<ulong, ItemContainer>>(GetExistingBackpacks),
                    [nameof(EraseBackpack)] = new Action<ulong>(EraseBackpack),
                    [nameof(DropBackpack)] = new Func<BasePlayer, List<DroppedItemContainer>, DroppedItemContainer>(DropBackpack),
                    [nameof(GetBackpackOwnerId)] = new Func<ItemContainer, ulong>(GetBackpackOwnerId),
                    [nameof(IsBackpackLoaded)] = new Func<BasePlayer, bool>(IsBackpackLoaded),
                    [nameof(IsDynamicCapacityEnabled)] = new Func<bool>(IsDynamicCapacityEnabled),
                    [nameof(GetBackpackCapacity)] = new Func<BasePlayer, int>(GetBackpackCapacity),
                    [nameof(GetBackpackCapacityById)] = new Func<ulong, string, int>(GetBackpackCapacityById),
                    [nameof(GetBackpackInitialCapacity)] = new Func<BasePlayer, int>(GetBackpackInitialCapacity),
                    [nameof(GetBackpackMaxCapacity)] = new Func<BasePlayer, int>(GetBackpackMaxCapacity),
                    [nameof(AddBackpackCapacity)] = new Func<BasePlayer, int, int>(AddBackpackCapacity),
                    [nameof(SetBackpackCapacity)] = new Func<BasePlayer, int, int>(SetBackpackCapacity),
                    [nameof(IsBackpackGathering)] = new Func<BasePlayer, bool>(IsBackpackGathering),
                    [nameof(IsBackpackRetrieving)] = new Func<BasePlayer, bool>(IsBackpackRetrieving),
                    [nameof(GetBackpackContainer)] = new Func<ulong, ItemContainer>(GetBackpackContainer),
                    [nameof(GetBackpackItemAmount)] = new Func<ulong, int, ulong, int>(GetBackpackItemAmount),
                    [nameof(TryOpenBackpack)] = new Func<BasePlayer, ulong, bool>(TryOpenBackpack),
                    [nameof(TryOpenBackpackContainer)] = new Func<BasePlayer, ulong, ItemContainer, bool>(TryOpenBackpackContainer),
                    [nameof(TryOpenBackpackPage)] = new Func<BasePlayer, ulong, int, bool>(TryOpenBackpackPage),
                    [nameof(SumBackpackItems)] = new Func<ulong, Dictionary<string, object>, int>(SumBackpackItems),
                    [nameof(CountBackpackItems)] = new Func<ulong, Dictionary<string, object>, int>(CountBackpackItems),
                    [nameof(TakeBackpackItems)] = new Func<ulong, Dictionary<string, object>, int, List<Item>, int>(TakeBackpackItems),
                    [nameof(MutateBackpackItems)] = new Func<ulong, Dictionary<string, object>, Dictionary<string, object>, int>(MutateBackpackItems),
                    [nameof(TryDepositBackpackItem)] = new Func<ulong, Item, bool>(TryDepositBackpackItem),
                    [nameof(WriteBackpackContentsFromJson)] = new Action<ulong, string>(WriteBackpackContentsFromJson),
                    [nameof(ReadBackpackContentsAsJson)] = new Func<ulong, string>(ReadBackpackContentsAsJson),
                };
            }

            public void AddSubscriber(Plugin plugin, Dictionary<string, object> spec)
            {
                if (plugin == null)
                    throw new ArgumentNullException(nameof(plugin));

                if (spec == null)
                    throw new ArgumentNullException(nameof(spec));

                _plugin._subscriberManager.AddSubscriber(plugin, spec);
            }

            public void RemoveSubscriber(Plugin plugin)
            {
                if (plugin == null)
                    throw new ArgumentNullException(nameof(plugin));

                _plugin._subscriberManager.RemoveSubscriber(plugin);
            }

            public Dictionary<ulong, ItemContainer> GetExistingBackpacks()
            {
                return _backpackManager.GetAllCachedContainers();
            }

            public void EraseBackpack(ulong userId)
            {
                _backpackManager.TryEraseForPlayer(userId);
            }

            public DroppedItemContainer DropBackpack(BasePlayer player, List<DroppedItemContainer> collect)
            {
                var backpack = _backpackManager.GetBackpackIfExists(player.userID);
                if (backpack == null)
                    return null;

                return _backpackManager.Drop(player.userID, player.transform.position, collect);
            }

            public ulong GetBackpackOwnerId(ItemContainer container)
            {
                return _backpackManager.GetCachedBackpackForContainer(container)?.OwnerId ?? 0;
            }

            public bool IsBackpackLoaded(BasePlayer player)
            {
                return _backpackManager.GetBackpackIfCached(player.userID) != null;
            }

            public bool IsDynamicCapacityEnabled()
            {
                return _plugin._config.BackpackSize.DynamicSize.Enabled;
            }

            public int GetBackpackCapacity(BasePlayer player)
            {
                return _plugin._capacityManager.GetCapacity(player.userID, player.UserIDString);
            }
            
            public int GetBackpackCapacityById(ulong playerID, string playerIDString)
            {
                return _plugin._capacityManager.GetCapacity(playerID, playerIDString);
            }

            public int GetBackpackInitialCapacity(BasePlayer player)
            {
                return _plugin._capacityManager.GetInitialCapacity(player.userID, player.UserIDString);
            }

            public int GetBackpackMaxCapacity(BasePlayer player)
            {
                return _plugin._capacityManager.GetMaxCapacity(player.userID, player.UserIDString);
            }

            public int AddBackpackCapacity(BasePlayer player, int amount)
            {
                return _plugin._capacityManager.AddCapacity(player, amount);
            }

            public int SetBackpackCapacity(BasePlayer player, int capacity)
            {
                return _plugin._capacityManager.SetCapacity(player, capacity);
            }

            public bool IsBackpackGathering(BasePlayer player)
            {
                return _backpackManager.GetBackpackIfCached(player.userID)?.IsGathering ?? false;
            }

            public bool IsBackpackRetrieving(BasePlayer player)
            {
                return _backpackManager.GetBackpackIfCached(player.userID)?.IsRetrieving ?? false;
            }

            public ItemContainer GetBackpackContainer(ulong ownerId)
            {
                return _backpackManager.GetBackpackIfExists(ownerId)?.GetContainer(ensureContainer: true);
            }

            public int GetBackpackItemAmount(ulong ownerId, int itemId, ulong skinId)
            {
                var itemQuery = new ItemQuery { ItemId = itemId, SkinId = skinId };
                return _backpackManager.GetBackpackIfExists(ownerId)?.SumItems(ref itemQuery) ?? 0;
            }

            public bool TryOpenBackpack(BasePlayer player, ulong ownerId)
            {
                return _backpackManager.TryOpenBackpack(player, ownerId);
            }

            public bool TryOpenBackpackContainer(BasePlayer player, ulong ownerId, ItemContainer container)
            {
                return _backpackManager.TryOpenBackpackContainer(player, ownerId, container);
            }

            public bool TryOpenBackpackPage(BasePlayer player, ulong ownerId, int page)
            {
                return _backpackManager.TryOpenBackpackPage(player, ownerId, page);
            }

            public int SumBackpackItems(ulong ownerId, Dictionary<string, object> dict)
            {
                var itemQuery = ItemQuery.Parse(dict);
                return _backpackManager.GetBackpackIfExists(ownerId)?.SumItems(ref itemQuery) ?? 0;
            }

            public int CountBackpackItems(ulong ownerId, Dictionary<string, object> dict)
            {
                var backpack = _backpackManager.GetBackpackIfExists(ownerId);
                if (backpack == null)
                    return 0;

                if (dict == null)
                    return backpack.ItemCount;

                var itemQuery = ItemQuery.Parse(dict);
                return backpack.CountItems(ref itemQuery);
            }

            public int TakeBackpackItems(ulong ownerId, Dictionary<string, object> dict, int amount, List<Item> collect)
            {
                var itemQuery = ItemQuery.Parse(dict);
                return _backpackManager.GetBackpackIfExists(ownerId)?.TakeItems(ref itemQuery, amount, collect) ?? 0;
            }

            public int MutateBackpackItems(ulong ownerId, Dictionary<string, object> itemQueryDict, Dictionary<string, object> mutationRequestDict)
            {
                var itemQuery = ItemQuery.Parse(itemQueryDict);
                var mutationRequest = MutationRequest.Parse(mutationRequestDict);
                return _backpackManager.GetBackpackIfExists(ownerId)?.MutateItems(ref itemQuery, ref mutationRequest) ?? 0;
            }

            public bool TryDepositBackpackItem(ulong ownerId, Item item)
            {
                return _backpackManager.GetBackpack(ownerId).TryDepositItem(item);
            }

            public void WriteBackpackContentsFromJson(ulong ownerId, string json)
            {
                _backpackManager.GetBackpack(ownerId).WriteContentsFromJson(json);
            }

            public string ReadBackpackContentsAsJson(ulong ownerId)
            {
                return _backpackManager.GetBackpackIfExists(ownerId)?.SerializeContentsAsJson();
            }
        }

        [HookMethod(nameof(API_GetApi))]
        public Dictionary<string, object> API_GetApi()
        {
            return _api.ApiWrapper;
        }

        [HookMethod(nameof(API_AddSubscriber))]
        public void API_AddSubscriber(Plugin plugin, Dictionary<string, object> spec)
        {
            _api.AddSubscriber(plugin, spec);
        }

        [HookMethod(nameof(API_RemoveSubscriber))]
        public void API_RemoveSubscriber(Plugin plugin)
        {
            _api.RemoveSubscriber(plugin);
        }

        // Deprecated, only returns container for first page. Use higher level APIs instead.
        [HookMethod(nameof(API_GetExistingBackpacks))]
        public Dictionary<ulong, ItemContainer> API_GetExistingBackpacks()
        {
            return _api.GetExistingBackpacks();
        }

        [HookMethod(nameof(API_EraseBackpack))]
        public void API_EraseBackpack(ulong userId)
        {
            _api.EraseBackpack(userId);
        }
        [HookMethod(nameof(API_EraseBackpack))]
        public void API_EraseBackpack(EncryptedValue<ulong> userId)
        {
            _api.EraseBackpack(userId);
        }

        [HookMethod(nameof(API_DropBackpack))]
        public DroppedItemContainer API_DropBackpack(BasePlayer player, List<DroppedItemContainer> collect = null)
        {
            return _api.DropBackpack(player, collect);
        }

        [HookMethod(nameof(API_GetBackpackOwnerId))]
        public object API_GetBackpackOwnerId(ItemContainer container)
        {
            return ObjectCache.Get(_api.GetBackpackOwnerId(container));
        }

        [HookMethod(nameof(API_IsBackpackLoaded))]
        public object API_IsBackpackLoaded(BasePlayer player)
        {
            return ObjectCache.Get(_api.IsBackpackLoaded(player));
        }

        [HookMethod(nameof(API_IsDynamicCapacityEnabled))]
        public object API_IsDynamicCapacityEnabled(BasePlayer player)
        {
            return ObjectCache.Get(_api.IsDynamicCapacityEnabled());
        }

        [HookMethod(nameof(API_GetBackpackCapacity))]
        public object API_GetBackpackCapacity(BasePlayer player)
        {
            return ObjectCache.Get(_api.GetBackpackCapacity(player));
        }
        
        [HookMethod(nameof(API_GetBackpackCapacityById))]
        public object API_GetBackpackCapacityById(ulong playerID, string playerIDString)
        {
            return ObjectCache.Get(_api.GetBackpackCapacityById(playerID, playerIDString));
        }
        [HookMethod(nameof(API_GetBackpackCapacityById))]
        public object API_GetBackpackCapacityById(EncryptedValue<ulong> playerID, string playerIDString)
        {
            return ObjectCache.Get(_api.GetBackpackCapacityById(playerID, playerIDString));
        }

        [HookMethod(nameof(API_GetBackpackInitialCapacity))]
        public object API_GetBackpackInitialCapacity(BasePlayer player)
        {
            return ObjectCache.Get(_api.GetBackpackInitialCapacity(player));
        }

        [HookMethod(nameof(API_GetBackpackMaxCapacity))]
        public object API_GetBackpackMaxCapacity(BasePlayer player)
        {
            return ObjectCache.Get(_api.GetBackpackMaxCapacity(player));
        }

        [HookMethod(nameof(API_AddBackpackCapacity))]
        public object API_AddBackpackCapacity(BasePlayer player, int amount)
        {
            return ObjectCache.Get(_api.AddBackpackCapacity(player, amount));
        }

        [HookMethod(nameof(API_SetBackpackCapacity))]
        public object API_SetBackpackCapacity(BasePlayer player, int capacity)
        {
            return ObjectCache.Get(_api.SetBackpackCapacity(player, capacity));
        }

        [HookMethod(nameof(API_IsBackpackGathering))]
        public object API_IsBackpackGathering(BasePlayer player)
        {
            return ObjectCache.Get(_api.IsBackpackGathering(player));
        }

        [HookMethod(nameof(API_IsBackpackRetrieving))]
        public object API_IsBackpackRetrieving(BasePlayer player)
        {
            return ObjectCache.Get(_api.IsBackpackRetrieving(player));
        }

        // Deprecated, only returns container for first page. Use higher level APIs instead.
        [HookMethod(nameof(API_GetBackpackContainer))]
        public ItemContainer API_GetBackpackContainer(ulong ownerId)
        {
            return _api.GetBackpackContainer(ownerId);
        }
        [HookMethod(nameof(API_GetBackpackContainer))]
        public ItemContainer API_GetBackpackContainer(EncryptedValue<ulong> ownerId)
        {
            return _api.GetBackpackContainer(ownerId);
        }

        [HookMethod(nameof(API_GetBackpackItemAmount))]
        public int API_GetBackpackItemAmount(ulong ownerId, int itemId, ulong skinId = 0)
        {
            return _api.GetBackpackItemAmount(ownerId, itemId, skinId);
        }
        [HookMethod(nameof(API_GetBackpackItemAmount))]
        public int API_GetBackpackItemAmount(EncryptedValue<ulong> ownerId, int itemId, ulong skinId = 0)
        {
            return _api.GetBackpackItemAmount(ownerId, itemId, skinId);
        }

        [HookMethod(nameof(API_TryOpenBackpack))]
        public object API_TryOpenBackpack(BasePlayer player, ulong ownerId = 0)
        {
            return ObjectCache.Get(_api.TryOpenBackpack(player, ownerId));
        }
        [HookMethod(nameof(API_TryOpenBackpack))]
        public object API_TryOpenBackpack(BasePlayer player, EncryptedValue<ulong> ownerId = default)
        {
            return ObjectCache.Get(_api.TryOpenBackpack(player, ownerId));
        }

        [HookMethod(nameof(API_TryOpenBackpackContainer))]
        public object API_TryOpenBackpackContainer(BasePlayer player, ulong ownerId, ItemContainer container)
        {
            return ObjectCache.Get(_api.TryOpenBackpackContainer(player, ownerId, container));
        }
        [HookMethod(nameof(API_TryOpenBackpackContainer))]
        public object API_TryOpenBackpackContainer(BasePlayer player, EncryptedValue<ulong> ownerId, ItemContainer container)
        {
            return ObjectCache.Get(_api.TryOpenBackpackContainer(player, ownerId, container));
        }

        [HookMethod(nameof(API_TryOpenBackpackPage))]
        public object API_TryOpenBackpackPage(BasePlayer player, ulong ownerId = 0, int page = 0)
        {
            return ObjectCache.Get(_api.TryOpenBackpackPage(player, ownerId, page));
        }
        [HookMethod(nameof(API_TryOpenBackpackPage))]
        public object API_TryOpenBackpackPage(BasePlayer player, EncryptedValue<ulong> ownerId = default, int page = 0)
        {
            return ObjectCache.Get(_api.TryOpenBackpackPage(player, ownerId, page));
        }

        [HookMethod(nameof(API_SumBackpackItems))]
        public object API_SumBackpackItems(ulong ownerId, Dictionary<string, object> dict)
        {
            return ObjectCache.Get(_api.SumBackpackItems(ownerId, dict));
        }
        [HookMethod(nameof(API_SumBackpackItems))]
        public object API_SumBackpackItems(EncryptedValue<ulong> ownerId, Dictionary<string, object> dict)
        {
            return ObjectCache.Get(_api.SumBackpackItems(ownerId, dict));
        }

        [HookMethod(nameof(API_CountBackpackItems))]
        public object API_CountBackpackItems(ulong ownerId, Dictionary<string, object> dict)
        {
            return ObjectCache.Get(_api.CountBackpackItems(ownerId, dict));
        }
        [HookMethod(nameof(API_CountBackpackItems))]
        public object API_CountBackpackItems(EncryptedValue<ulong> ownerId, Dictionary<string, object> dict)
        {
            return ObjectCache.Get(_api.CountBackpackItems(ownerId, dict));
        }

        [HookMethod(nameof(API_TakeBackpackItems))]
        public object API_TakeBackpackItems(ulong ownerId, Dictionary<string, object> dict, int amount, List<Item> collect)
        {
            return ObjectCache.Get(_api.TakeBackpackItems(ownerId, dict, amount, collect));
        }
        [HookMethod(nameof(API_TakeBackpackItems))]
        public object API_TakeBackpackItems(EncryptedValue<ulong> ownerId, Dictionary<string, object> dict, int amount, List<Item> collect)
        {
            return ObjectCache.Get(_api.TakeBackpackItems(ownerId, dict, amount, collect));
        }

        [HookMethod(nameof(API_MutateBackpackItems))]
        public object API_MutateBackpackItems(ulong ownerId, Dictionary<string, object> itemQueryDict, Dictionary<string, object> mutationRequestDict)
        {
            return ObjectCache.Get(_api.MutateBackpackItems(ownerId, itemQueryDict, mutationRequestDict));
        }
        [HookMethod(nameof(API_MutateBackpackItems))]
        public object API_MutateBackpackItems(EncryptedValue<ulong> ownerId, Dictionary<string, object> itemQueryDict, Dictionary<string, object> mutationRequestDict)
        {
            return ObjectCache.Get(_api.MutateBackpackItems(ownerId, itemQueryDict, mutationRequestDict));
        }

        [HookMethod(nameof(API_TryDepositBackpackItem))]
        public object API_TryDepositBackpackItem(ulong ownerId, Item item)
        {
            return ObjectCache.Get(_api.TryDepositBackpackItem(ownerId, item));
        }
        [HookMethod(nameof(API_TryDepositBackpackItem))]
        public object API_TryDepositBackpackItem(EncryptedValue<ulong> ownerId, Item item)
        {
            return ObjectCache.Get(_api.TryDepositBackpackItem(ownerId, item));
        }

        [HookMethod(nameof(API_WriteBackpackContentsFromJson))]
        public void API_WriteBackpackContentsFromJson(ulong ownerId, string json)
        {
            _api.WriteBackpackContentsFromJson(ownerId, json);
        }
        [HookMethod(nameof(API_WriteBackpackContentsFromJson))]
        public void API_WriteBackpackContentsFromJson(EncryptedValue<ulong> ownerId, string json)
        {
            _api.WriteBackpackContentsFromJson(ownerId, json);
        }

        [HookMethod(nameof(API_ReadBackpackContentsAsJson))]
        public object API_ReadBackpackContentsAsJson(ulong ownerId)
        {
            return _api.ReadBackpackContentsAsJson(ownerId);
        }
        [HookMethod(nameof(API_ReadBackpackContentsAsJson))]
        public object API_ReadBackpackContentsAsJson(EncryptedValue<ulong> ownerId)
        {
            return _api.ReadBackpackContentsAsJson(ownerId);
        }

        #endregion

        #region Exposed Hooks

        private static class ExposedHooks
        {
            public static object CanOpenBackpack(BasePlayer looter, ulong ownerId)
            {
                return Interface.CallHook("CanOpenBackpack", looter, ObjectCache.Get(ownerId));
            }

            public static void OnBackpackClosed(BasePlayer looter, ulong ownerId, ItemContainer container)
            {
                Interface.CallHook("OnBackpackClosed", looter, ObjectCache.Get(ownerId), container);
            }

            public static void OnBackpackOpened(BasePlayer looter, ulong ownerId, ItemContainer container)
            {
                Interface.CallHook("OnBackpackOpened", looter, ObjectCache.Get(ownerId), container);
            }

            public static object CanDropBackpack(ulong ownerId, Vector3 position)
            {
                return Interface.CallHook("CanDropBackpack", ObjectCache.Get(ownerId), position);
            }

            public static void OnBackpackDropped(ulong ownerId, List<DroppedItemContainer> droppedBackpackList)
            {
                Interface.CallHook("OnBackpackDropped", ownerId, droppedBackpackList);
            }

            public static object CanEraseBackpack(ulong ownerId)
            {
                return Interface.CallHook("CanEraseBackpack", ObjectCache.Get(ownerId));
            }

            public static object CanBackpackAcceptItem(ulong ownerId, ItemContainer container, Item item)
            {
                return Interface.CallHook("CanBackpackAcceptItem", ObjectCache.Get(ownerId), container, item);
            }
        }

        #endregion

        #region Commands

        [Command("backpack", "backpack.open")]
        private void BackpackOpenCommand(IPlayer player, string cmd, string[] args)
        {
            if (!_initReady)
            {
                player.Reply("Backpacks is still initializing. Try again in a moment.");
                return;
            }

            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, UsagePermission))
                return;

            OpenBackpack(
                basePlayer,
                IsKeyBindArg(args.LastOrDefault()),
                ParsePageArg(args.FirstOrDefault()),
                wrapAround: false
            );
        }

        [Command("backpack.next")]
        private void BackpackNextCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, UsagePermission))
                return;

            OpenBackpack(
                basePlayer,
                IsKeyBindArg(args.LastOrDefault())
            );
        }

        [Command("backpack.previous", "backpack.prev")]
        private void BackpackPreviousCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, UsagePermission))
                return;

            OpenBackpack(
                basePlayer,
                IsKeyBindArg(args.LastOrDefault()),
                forward: false
            );
        }

        [Command("backpack.fetch")]
        private void BackpackFetchCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, FetchPermission))
                return;

            if (args.Length < 2)
            {
                ReplyToPlayer(player, LangEntry.BackpackFetchSyntax);
                return;
            }

            if (!VerifyCanOpenBackpack(basePlayer, basePlayer.userID))
                return;

            if (!VerifyValidItem(player, args[0], out var itemDefinition))
                return;

            if (!int.TryParse(args[1], out var desiredAmount) || desiredAmount < 1)
            {
                ReplyToPlayer(player, LangEntry.InvalidItemAmount);
                return;
            }

            var itemLocalizedName = itemDefinition.displayName.translated;
            var backpack = _backpackManager.GetBackpack(basePlayer.userID);

            var itemQuery = new ItemQuery { ItemDefinition = itemDefinition };

            var quantityInBackpack = backpack.SumItems(ref itemQuery);
            if (quantityInBackpack == 0)
            {
                ReplyToPlayer(player, LangEntry.ItemNotInBackpack, itemLocalizedName);
                return;
            }

            if (desiredAmount > quantityInBackpack)
            {
                desiredAmount = quantityInBackpack;
            }

            var amountTransferred = backpack.FetchItems(basePlayer, ref itemQuery, desiredAmount);
            if (amountTransferred <= 0)
            {
                ReplyToPlayer(player, LangEntry.FetchFailed, itemLocalizedName);
                return;
            }

            ReplyToPlayer(player, LangEntry.ItemsFetched, amountTransferred, itemLocalizedName);
        }

        [Command("backpack.erase")]
        private void EraseBackpackCommand(IPlayer player, string cmd, string[] args)
        {
            if (!player.IsServer)
                return;

            if (args.Length < 1 || !ulong.TryParse(args[0], out var userId))
            {
                player.Reply($"Syntax: {cmd} <id>");
                return;
            }

            if (!_backpackManager.TryEraseForPlayer(userId))
            {
                LogWarning($"Player {userId.ToString()} has no backpack to erase.");
                return;
            }

            LogWarning($"Erased backpack for player {userId.ToString()}.");
        }

        [Command("viewbackpack")]
        private void ViewBackpackCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, AdminPermission))
                return;

            if (args.Length < 1)
            {
                ReplyToPlayer(player, LangEntry.ViewBackpackSyntax);
                return;
            }

            if (!VerifyTargetPlayer(player, args[0], out var targetPlayerId, out var targetPlayerIdString))
                return;

            if (permission.UserHasPermission(targetPlayerIdString, AdminProtectedPermission))
            {
                ReplyToPlayer(player, LangEntry.ViewBackpackProtected);
                return;
            }

            OpenBackpack(
                basePlayer,
                IsKeyBindArg(args.LastOrDefault()),
                ParsePageArg(args.ElementAtOrDefault(1)),
                desiredOwnerId: targetPlayerId
            );
        }

        // Alias for older versions of Player Administration (which should ideally not be calling this method directly).
        private void ViewBackpack(BasePlayer player, string cmd, string[] args) =>
            ViewBackpackCommand(player.IPlayer, cmd, args);

        [Command("backpack.addsize")]
        private void AddBackpackCapacityCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyHasPermission(player, AdminPermission))
                return;

            if (args.Length < 2 || !int.TryParse(args[1], out var amount))
            {
                ReplyToPlayer(player, LangEntry.BackpackCapacitySyntax, cmd);
                return;
            }

            if (!VerifyTargetPlayer(player, args[0], out var targetPlayerId, out var targetPlayerIdString)
                || !VerifyDynamicCapacityEnabled(player))
                return;

            var newCapacity = _capacityManager.AddCapacity(targetPlayerId, targetPlayerIdString, amount);
            ReplyToPlayer(player, LangEntry.ChangeCapacitySuccess, targetPlayerId, newCapacity);
        }

        [Command("backpack.setsize")]
        private void SetBackpackCapacityCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyHasPermission(player, AdminPermission))
                return;

            if (args.Length < 2 || !int.TryParse(args[1], out var amount))
            {
                ReplyToPlayer(player, LangEntry.BackpackCapacitySyntax, cmd);
                return;
            }

            if (!VerifyTargetPlayer(player, args[0], out var targetPlayerId, out var targetPlayerIdString)
                || !VerifyDynamicCapacityEnabled(player))
                return;

            var newCapacity = _capacityManager.SetCapacity(targetPlayerId, targetPlayerIdString, amount);
            ReplyToPlayer(player, LangEntry.ChangeCapacitySuccess, targetPlayerId, newCapacity);
        }

        private void ToggleBackpackGUICommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyPlayer(player, out var basePlayer)
                || !VerifyHasPermission(player, GUIPermission))
                return;

            var enabledNow = _preferencesData.ToggleGuiButtonPreference(basePlayer.userID, _config.GUI.EnabledByDefault);
            if (enabledNow)
            {
                MaybeCreateButtonUi(basePlayer);
            }
            else
            {
                DestroyButtonUi(basePlayer);
            }

            ReplyToPlayer(player, LangEntry.ToggledBackpackGUI);
        }

        [Command("backpack.setgathermode")]
        private void SetGatherCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyCanInteract(player, out var basePlayer)
                || !VerifyHasPermission(player, UsagePermission)
                || !VerifyHasPermission(player, GatherPermission))
                return;

            var backpack = _backpackManager.GetBackpack(basePlayer.userID);
            if (!backpack.CanGather)
                return;

            if (args.Length < 1 || !TryParseGatherMode(basePlayer, args[0], out var gatherMode))
            {
                ReplyToPlayer(player, LangEntry.SetGatherSyntax, cmd, GetGatherModeDisplayOptions(basePlayer));
                return;
            }

            var oneBasedPageIndex = 1;
            if (args.Length >= 2 && !IsKeyBindArg(args[1]) && !int.TryParse(args[1], out oneBasedPageIndex))
            {
                ReplyToPlayer(player, LangEntry.SetGatherSyntax, cmd, GetGatherModeDisplayOptions(basePlayer));
                return;
            }

            if (oneBasedPageIndex < 1 || oneBasedPageIndex > backpack.PageCount)
            {
                ReplyToPlayer(player, LangEntry.PageOutOfRange, backpack.PageCount);
                return;
            }

            var pageIndex = oneBasedPageIndex - 1;
            if (backpack.GetGatherModeForPage(pageIndex) == gatherMode)
                return;

            backpack.SetGatherModeForPage(basePlayer, pageIndex, gatherMode);
            ReplyToPlayer(player, LangEntry.SetGatherModeSuccess, oneBasedPageIndex, GetGatherModeDisplayString(basePlayer, gatherMode));
        }

        [Command("backpack.ui.togglegather")]
        private void ToggleGatherUICommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyPlayer(player, out var basePlayer))
                return;

            var lootingContainer = basePlayer.inventory.loot.containers.FirstOrDefault();
            if (lootingContainer == null)
                return;

            if (!_backpackManager.IsBackpack(lootingContainer, out var backpack, out var pageIndex)
                || backpack.OwnerId != basePlayer.userID
                || !backpack.CanGather)
                return;

            backpack.ToggleGatherMode(basePlayer, pageIndex);
        }

        [Command("backpack.ui.toggleretrieve")]
        private void ToggleRetrieveUICommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyPlayer(player, out var basePlayer))
                return;

            var lootingContainer = basePlayer.inventory.loot.containers.FirstOrDefault();
            if (lootingContainer == null)
                return;

            if (!_backpackManager.IsBackpack(lootingContainer, out var backpack, out var pageIndex)
                || pageIndex > 31
                || backpack.OwnerId != basePlayer.userID
                || !backpack.CanRetrieve)
                return;

            backpack.ToggleRetrieve(basePlayer, pageIndex);
        }

        [Command("backpack.debug.gather")]
        private void DebugGatherCommand(IPlayer player, string cmd, string[] args)
        {
            if (!VerifyHasPermission(player, AdminPermission))
                return;

            if (args.Length < 1)
            {
                player.Reply($"Syntax: {cmd} [player]");
                return;
            }

            if (!VerifyTargetPlayer(player, args[0], out var targetPlayerId, out var targetPlayerIdString))
                return;

            var targetBackpack = _backpackManager.GetBackpackIfCached(targetPlayerId);
            if (targetBackpack == null)
            {
                player.Reply($"Player {targetPlayerIdString} backpack is not initialized.");
                return;
            }

            var sb = new StringBuilder();
            sb.Append("Player: ").AppendLine(targetPlayerIdString);
            sb.Append("NumPagesGatherEnabled: ").Append(targetBackpack.NumPagesGathering).AppendLine();
            sb.Append("IsGathering: ").Append(targetBackpack.IsGathering).AppendLine();
            sb.Append("CanGather: ").Append(targetBackpack.CanGather).AppendLine();

            var targetPlayer = BasePlayer.FindByID(targetPlayerId);
            if (targetPlayer != null)
            {
                var watcher = targetPlayer.GetComponent<InventoryWatcher>();
                if (watcher != null)
                {
                    sb.Append("DetailedStats: ").AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherSucceeded)).Append(": ").Append(watcher.Stats.GatherSucceeded).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_PlayerIneligible)).Append(": ").Append(watcher.Stats.GatherFailed_PlayerIneligible).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_PlayerReceivingSnapshot)).Append(": ").Append(watcher.Stats.GatherFailed_PlayerReceivingSnapshot).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_LootingIneligibleContainer)).Append(": ").Append(watcher.Stats.GatherFailed_LootingIneligibleContainer).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_ParentContainerIneligible)).Append(": ").Append(watcher.Stats.GatherFailed_ParentContainerIneligible).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_GatherPaused)).Append(": ").Append(watcher.Stats.GatherFailed_GatherPaused).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_NotAllowedToMoveItems)).Append(": ").Append(watcher.Stats.GatherFailed_NotAllowedToMoveItems).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_FoundMatchingStackInInventory)).Append(": ").Append(watcher.Stats.GatherFailed_FoundMatchingStackInInventory).AppendLine();
                    sb.Append("- ").Append(nameof(GatherModeStats.GatherFailed_BackpackRejectedOrDidNotCareAboutItem)).Append(": ").Append(watcher.Stats.GatherFailed_BackpackRejectedOrDidNotCareAboutItem).AppendLine();
                }
            }

            player.Reply(sb.ToString());
        }

        #endregion

        #region Helper Methods

        public static void LogDebug(string message) => Interface.Oxide.LogDebug($"[Backpacks] {message}");
        public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Backpacks] {message}");
        public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Backpacks] {message}");
        public static void LogError(string message) => Interface.Oxide.LogError($"[Backpacks] {message}");

        private static T[] ParseEnumList<T>(string[] list, string errorFormat) where T : struct
        {
            var valueList = new List<T>(list?.Length ?? 0);

            if (list != null)
            {
                foreach (var itemName in list)
                {
                    if (Enum.TryParse(itemName, ignoreCase: true, result: out T result))
                    {
                        valueList.Add(result);
                    }
                    else
                    {
                        LogError(string.Format(errorFormat, itemName));
                    }
                }
            }

            return valueList.ToArray();
        }

        private static bool IsKeyBindArg(string arg)
        {
            return arg == "True";
        }

        private static int ParsePageArg(string arg)
        {
            if (arg == null)
                return -1;

            return int.TryParse(arg, out var pageIndex)
                ? Math.Max(0, pageIndex - 1)
                : -1;
        }

        private static bool HasItemMod<T>(ItemDefinition itemDefinition, out T itemModOfType) where T : ItemMod
        {
            foreach (var itemMod in itemDefinition.itemMods)
            {
                itemModOfType = itemMod as T;
                if (itemModOfType is not null)
                    return true;
            }

            itemModOfType = null;
            return false;
        }

        private static string DetermineLootPanelName(ItemContainer container)
        {
            return (container.entityOwner as StorageContainer)?.panelName
                   ?? (container.entityOwner as ContainerIOEntity)?.lootPanelName
                   ?? (container.entityOwner as LootableCorpse)?.lootPanelName
                   ?? (container.entityOwner as DroppedItemContainer)?.lootPanelName
                   ?? (container.entityOwner as RidableHorse)?.lootPanelName
                   ?? ResizableLootPanelName;
        }

        private static void ClosePlayerInventory(BasePlayer player)
        {
            player.ClientRPCPlayer(null, player, "OnRespawnInformation");
        }

        private static float CalculateOpenDelay(ItemContainer currentContainer, int nextContainerCapacity, bool isKeyBind = false)
        {
            if (currentContainer != null)
            {
                // Can instantly switch to a smaller container.
                if (nextContainerCapacity <= currentContainer.capacity)
                    return 0;

                // Can instantly switch to a generic resizable loot panel from a different loot panel.
                if (DetermineLootPanelName(currentContainer) != ResizableLootPanelName)
                    return 0;

                // Need a short delay so the generic_resizable loot panel can be redrawn properly.
                return StandardLootDelay;
            }

            // Can open instantly since not looting and chat is assumed to be closed.
            if (isKeyBind)
                return 0;

            // Not opening via key bind, so the chat window may be open.
            // Must delay in case the chat is still closing or else the loot panel may close instantly.
            return StandardLootDelay;
        }

        private static void StartLooting(BasePlayer player, ItemContainer container, StorageContainer entitySource)
        {
            if (player.CanInteract()
                && Interface.CallHook("CanLootEntity", player, entitySource) == null
                && player.inventory.loot.StartLootingEntity(entitySource, doPositionChecks: false))
            {
                player.inventory.loot.AddContainer(container);
                player.inventory.loot.SendImmediate();
                player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", entitySource.panelName);
            }
        }

        private static ItemContainer GetRootContainer(Item item)
        {
            var container = item.parent;
            if (container == null)
                return null;

            while (container.parent?.parent != null && container.parent != item)
            {
                container = container.parent.parent;
            }

            return container;
        }

        private bool TryParseGatherMode(BasePlayer player, string arg, out GatherMode gatherMode)
        {
            foreach (var enumValue in typeof(GatherMode).GetEnumValues())
            {
                var name = Enum.GetName(typeof(GatherMode), enumValue);
                var gatherModeValue = (GatherMode)enumValue;

                if (StringUtils.EqualsCaseInsensitive(arg, name))
                {
                    gatherMode = gatherModeValue;
                    return true;
                }

                var localizedName = GetGatherModeDisplayString(player, gatherModeValue);
                if (StringUtils.EqualsCaseInsensitive(arg, localizedName, StringComparison.InvariantCultureIgnoreCase))
                {
                    gatherMode = gatherModeValue;
                    return true;
                }
            }

            gatherMode = GatherMode.None;
            return false;
        }

        private string GetGatherModeDisplayOptions(BasePlayer player)
        {
            return string.Join("|", new List<string>
            {
                GetGatherModeDisplayString(player, GatherMode.All),
                GetGatherModeDisplayString(player, GatherMode.Existing),
                GetGatherModeDisplayString(player, GatherMode.None),
            });
        }

        private void SendEffect(BasePlayer player, string effectPrefab)
        {
            if (string.IsNullOrWhiteSpace(effectPrefab))
                return;

            _reusableEffect.Init(Effect.Type.Generic, player, 0, Vector3.zero, Vector3.forward);
            _reusableEffect.pooledString = effectPrefab;
            EffectNetwork.Send(_reusableEffect, player.net.connection);
        }

        private void CheckBackpackButtonPlugin()
        {
            if (BackpackButton == null
                || _config.UsingDefaults
                || !_config.GUI.Enabled)
                return;

            foreach (var player in BasePlaye

Looks like that got cut off because it was too long.

The latest update replaced item.MoveToContainer calls with item.SetParent calls in order to skip certain checks that were causing the item to be rejected (e.g., attachments being rejected from weapons that normally don't allow them). In theory, the fix for that issue shouldn't have been in Backpacks at all, as the plugin which manages the custom item could have intercepted the appropriate hook and allowed the item in the first place.

Even though item.SetParent is skipping some checks, it's possible that the items could still be rejected for other reasons when calling item.SetParent, such as when the target container (e.g., the weapon's inventory) is full. Previously, the item would probably have been deleted without any notice, and now you will see this error instead. The container being full shouldn't really happen because the Backpacks plugin stores the container capacity and ensures the container will be at least that size.

I could bypass even item.SetParent, by just re-implementing its logic without the checks, but I would need to evaluate if there are other potential consequences of this.

The most efficient way to proceed would be for you to provide some detailed reproduction steps (e.g., some player's backpack file, and the plugin/configuration that provides the custom item).

It's paid plugins so don't want to add them here, can I add you on discord?

Yes, my discord username is WhiteThunder. You can also find me in the uMod discord.