C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
// #define DEBUG_POOLING
// #define DEBUG_GATHER
// #define FEATURE_BELT_ACTIVATION
// #define FEATURE_EXTRA_POCKETS_CONFIG
// #define CARBON
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using System.ComponentModel;
using System.Reflection;
using System.Text.RegularExpressions;
using static BaseEntity;
using static PlayerInventory;
using Time = UnityEngine.Time;
namespace Oxide.Plugins {
[Info("Bag of Holding", "BlackLightning", "1.8.3")]
[Description("Adds portable bags for carrying loot.")]
internal class BagOfHolding : CovalencePlugin {
#region Fields
[PluginReference]
private Plugin Backpacks, Economics, ItemRetriever, Kits, Loottable, ServerRewards, SkillTree, StackModifier;
private Configuration _config;
private const string PermissionListBags = "bagofholding.listbags";
private const string PermissionListContainers = "bagofholding.listcontainers";
private const string PermissionGiveBag = "bagofholding.givebag";
private const string PermissionGiveKitBag = "bagofholding.givekitbag";
private const string PermissionConfig = "bagofholding.config";
private const string PermissionStats = "bagofholding.stats";
private const string PermissionManageLoot = "bagofholding.manageloot";
private const int BagItemId = 479292118;
private const int SaddleBagItemId = 1400460850;
private const int ParachuteItemId = 602628465;
private const int SmallBackpackItemId = 2068884361;
private const int LargeBackpackItemId = -907422733;
private const int VisualBackpackItemId = LargeBackpackItemId;
private const string GenericResizableLootPanelName = "generic_resizable";
private const Item.Flag FlagHasItems = Item.Flag.Cooking;
private const Item.Flag FlagGather = Item.Flag.OnFire;
private const Item.Flag FlagGatherExisting = (Item.Flag)(1 << 20);
private const Item.Flag UnsearchableItemFlag = (Item.Flag)(1 << 24);
private const ItemDefinition.Flag SearchableItemDefinitionFlag = (ItemDefinition.Flag)(1 << 24);
private const string PrefabPlayerCorpse = "assets/prefabs/player/player_corpse.prefab";
private const string PrefabStorageBox = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab";
private static readonly object True = true;
private static readonly object False = false;
private static readonly object CannotAccept = ItemContainer.CanAcceptResult.CannotAccept;
private UIBuilder _containerUIBuilder = new(15000);
private BaseEntity _sharedLootEntity;
private LootableCorpse _sharedCorpseEntity;
private ProtectionProperties _immortalProtection;
private BackpacksAdapter _backpacksAdapter;
private SkillTreeAdapter _skillTreeAdapter;
private ItemRetrieverAdapter _itemRetrieverAdapter;
private LimitManager _limitManager;
private BagManager _bagManager;
private UIUpdateManager _uiUpdateManager;
private ItemModBagOfHolding _bagItemMod;
private ItemModWearableBag _wearableBagItemMod;
private ItemModContainerDummy _containerDummyMod;
private ApiInstance _api;
private PaymentProviderResolver _paymentProviderResolver;
private CustomItemManager _customItemManager;
private BeltUIRenderer _beltUIRenderer;
#if FEATURE_BELT_ACTIVATION
private BeltTracker _beltTracker;
#endif
private ItemDefinition _bagItemDefinition;
private bool _preInitialized;
private bool _initialized;
private bool _subscribedToOnItemSplit;
private Item[] _lockerItemBuffer = new Item[Locker.setSize];
private string[] _delayedHooks;
private string[] _conditionalHooks;
private object[] _objectArray1 = new object[1];
private object[] _objectArray2 = new object[2];
private string[] _networkingHooks = {
nameof(OnEntitySaved), nameof(OnInventoryNetworkUpdate),
}
;
private readonly ProtoBuf.Item _visualBackpackItemData = new() {
ShouldPool = false, itemid = VisualBackpackItemId, amount = 1,
}
;
public BagOfHolding() {
_backpacksAdapter = new BackpacksAdapter(this);
_skillTreeAdapter = new SkillTreeAdapter(this);
_itemRetrieverAdapter = new ItemRetrieverAdapter(this);
_uiUpdateManager = new UIUpdateManager(this);
_paymentProviderResolver = new PaymentProviderResolver(this);
_beltUIRenderer = new BeltUIRenderer(this);
#if FEATURE_BELT_ACTIVATION
_beltTracker = new BeltTracker(this);
#endif
_delayedHooks = new[] {
nameof(OnPlayerLootEnd), nameof(OnItemAction), nameof(CanAcceptItem), nameof(CanMoveItem), nameof(OnLockerSwap),
}
;
_conditionalHooks = new[] {
#if FEATURE_BELT_ACTIVATION
nameof(OnActiveItemChanged),
#endif
nameof(CanWearItem), nameof(CanLockerAcceptItem), nameof(OnPlayerSleepEnded), nameof(CanStackItem), nameof(CanCombineDroppedItem), nameof(OnDroppedItemCombined), nameof(OnItemSplit), nameof(OnItemPickup), nameof(CanBeRecycled), nameof(CanRecycle), nameof(OnItemRecycle), nameof(OnMaxStackable)
}
.Concat(_networkingHooks).ToArray();
}
#endregion
#region Hooks
private void Init() {
permission.RegisterPermission(PermissionListBags, this);
permission.RegisterPermission(PermissionListContainers, this);
permission.RegisterPermission(PermissionGiveBag, this);
permission.RegisterPermission(PermissionGiveKitBag, this);
permission.RegisterPermission(PermissionConfig, this);
permission.RegisterPermission(PermissionStats, this);
permission.RegisterPermission(PermissionManageLoot, this);
CustomPool.Reset<BagInfo>(512);
CustomPool.Reset<List<BagInfo>>(512);
CustomPool.Reset<DisposableList<BagInfo>>(8);
CustomPool.Reset<ContainerSupervisor>(512);
CustomPool.Reset<List<Item>>(8);
CustomPool.Reset<ItemContainer>(8);
_limitManager = new LimitManager(this, _config);
_bagManager = new BagManager(this, _config, _limitManager);
_api = new ApiInstance(this);
_customItemManager = new CustomItemManager(_config);
_config.Init(this);
foreach (var hookName in _delayedHooks.Concat(_conditionalHooks)) {
Unsubscribe(hookName);
}
if (!_config.LootConfig.Enabled) {
// OnLootSpawn is intentionally not included in the list of hooks that are always subscribed,
// so that it is subscribed during server boot.Unsubscribe(nameof(OnLootSpawn));
}
}
private void OnServerInitialize() {
_preInitialized = true;
_config.OnServerInitialize();
}
private void OnServerInitialized() {
if (!_preInitialized) {
OnServerInitialize();
}
_config.OnServerInitialized(this);
_bagItemDefinition = ItemManager.FindItemDefinition(BagItemId);
if (_bagItemDefinition == null) {
LogError($"Unable to initialize plugin because item {BagItemId} was not found.");
return;
}
_bagItemDefinition.flags |= SearchableItemDefinitionFlag;
var oldBagItemMod = _bagItemDefinition.itemMods.FirstOrDefault(itemMod => itemMod.GetType().Name == nameof(ItemModBagOfHolding));
if (oldBagItemMod != null) {
oldBagItemMod.GetType().GetMethod(nameof(ItemModBagOfHolding.Unload))?.Invoke(oldBagItemMod, null);
}
_immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
_immortalProtection.name = "BagOfHoldingProtection";
_immortalProtection.Add(1);
_sharedLootEntity = CreateLootEntity(_immortalProtection);
if (_sharedLootEntity == null || _sharedLootEntity.IsDestroyed) {
LogError($"Failed to create the shared lootable entity.This plugin will not work correctly.There is likely a plugin conflict.");
return;
}
_sharedCorpseEntity = CreateCorpseEntity(_immortalProtection);
if (_sharedCorpseEntity == null || _sharedCorpseEntity.IsDestroyed) {
LogError($"Failed to create the shared lootable corpse entity.This plugin will not work correctly.There is likely a plugin conflict.");
return;
}
if (_config.Wearable.Enabled) {
_wearableBagItemMod = ItemModWearableBag.AddToItemDefinition(_bagItemDefinition);
}
_bagItemMod = ItemModBagOfHolding.AddToItemDefinition(_bagItemDefinition, this);
_containerDummyMod = ItemModContainerDummy.AddToItemDefinition(_bagItemDefinition);
// Setup container dependencies before discovering items.Most important for backpacks._skillTreeAdapter.HandleLoadedChanged();
_backpacksAdapter.HandleLoadedChanged();
_itemRetrieverAdapter.HandleLoadedChanged();
// Enable networking hooks before sending network updates.if (_config.VisualBackpack.Enabled)
{
foreach (var hookName in _networkingHooks) {
Subscribe(hookName);
}
}
// Best effort attempt to update all bags.foreach (var entity in BaseNetworkable.serverEntities)
{
var ridableAnimal = entity as BaseRidableAnimal;
if ((object)ridableAnimal != null) {
DiscoverBags(ridableAnimal.storageInventory);
DiscoverBags(ridableAnimal.equipmentInventory);
continue;
}
if (entity is IItemContainerEntity containerEntity) {
DiscoverBags(containerEntity.inventory);
var shopFront = containerEntity as ShopFront;
if ((object)shopFront != null) {
DiscoverBags(shopFront.customerInventory);
}
continue;
}
var droppedItemContainer = entity as DroppedItemContainer;
if ((object)droppedItemContainer != null) {
DiscoverBags(droppedItemContainer.inventory);
continue;
}
var worldItem = entity as WorldItem;
if ((object)worldItem != null) {
DiscoverBags(worldItem.item);
var droppedItem = entity as DroppedItem;
if ((object)droppedItem != null && droppedItem.item != null && IsSkinnedBag(droppedItem.item)) {
// Reset removal time.Most important for server restarts._bagItemMod.HandleBagMovedToWorld(droppedItem.item, delayRemovalTimeReset: true);
}
continue;
}
var lootableCorpse = entity as LootableCorpse;
if ((object)lootableCorpse != null) {
DiscoverBags(lootableCorpse.containers);
continue;
}
var basePlayer = entity as BasePlayer;
if (basePlayer != null) {
var inventory = basePlayer.inventory;
if (inventory == null) continue;
DiscoverBags(basePlayer.inventory.containerMain);
DiscoverBags(basePlayer.inventory.containerBelt);
DiscoverBags(basePlayer.inventory.containerWear);
if (_config.VisualBackpack.Enabled && ShouldDisplayVisualBackpack(basePlayer)) {
inventory.SendUpdatedInventory(PlayerInventory.Type.Wear, inventory.containerWear, bSendInventoryToEveryone : true);
}
continue;
}
}
foreach (var hookName in _delayedHooks) {
Subscribe(hookName);
}
if (_config.Wearable.Enabled) {
Subscribe(nameof(CanWearItem));
Subscribe(nameof(CanLockerAcceptItem));
}
if (_config.DroppedBagSettings.EnableOpeningDroppedBags || (_config.Wearable.Enabled && _config.DroppedBagSettings.AutoEquipDroppedBags)) {
Subscribe(nameof(OnItemPickup));
}
if (_config.ContainsRecyclableBags) {
Subscribe(nameof(CanBeRecycled));
Subscribe(nameof(CanRecycle));
Subscribe(nameof(OnItemRecycle));
}
#if FEATURE_BELT_ACTIVATION
if (_config.BeltActivation.Enabled) {
Subscribe(nameof(OnActiveItemChanged));
}
#endif
if (_config.UISettings.BeltButton.Enabled) {
Subscribe(nameof(OnPlayerSleepEnded));
}
if (_config.ContainsStackProfileItemOverrides) {
Subscribe(nameof(OnMaxStackable));
}
RegisterCustomItemsWithLootTable();
_initialized = true;
// Give stack plugins one frame to update stack sizes.NextTick(() =>
{
// Always subscribe to CanStackItem, since StackModifier only handles it in player inventories.Subscribe(nameof(CanStackItem));
RefreshStackHooks();
if (_bagItemDefinition.stackable > 1) {
TestOnItemSplit();
}
}
);
}
private void Unload() {
foreach (var hookName in _networkingHooks) {
Unsubscribe(hookName);
}
if (_config.VisualBackpack.Enabled || _config.Wearable.Enabled) {
foreach (var entity in BaseNetworkable.serverEntities) {
if (_config.VisualBackpack.Enabled) {
var basePlayer = entity as BasePlayer;
if ((object)basePlayer != null) {
var inventory = basePlayer.inventory;
if (inventory == null) continue;
if (ShouldDisplayVisualBackpack(basePlayer)) {
inventory.SendUpdatedInventory(PlayerInventory.Type.Wear, inventory.containerWear, bSendInventoryToEveryone : true);
}
continue;
}
}
}
}
_wearableBagItemMod?.Destroy();
_containerDummyMod.Destroy();
_customItemManager.Unload();
_bagManager.Unload();
_limitManager.Unload();
CustomPool.Reset<BagInfo>(0);
CustomPool.Reset<List<BagInfo>>(0);
CustomPool.Reset<DisposableList<BagInfo>>(0);
CustomPool.Reset<ContainerSupervisor>(0);
CustomPool.Reset<List<Item>>(0);
CustomPool.Reset<ItemContainer>(0);
}
private void OnPluginLoaded(Plugin plugin) {
if (!_initialized) return;
switch (plugin.Name) {
case nameof(Backpacks) : _backpacksAdapter.HandleLoadedChanged();
return;
case nameof(ItemRetriever) : _itemRetrieverAdapter.HandleLoadedChanged();
return;
case nameof(Loottable) : RefreshStackHooks();
RegisterCustomItemsWithLootTable();
return;
case nameof(SkillTree) : _skillTreeAdapter.HandleLoadedChanged();
return;
case nameof(StackModifier) : RefreshStackHooks();
return;
}
}
private void OnPluginUnloaded(Plugin plugin) {
if (!_initialized) return;
switch (plugin.Name) {
case nameof(Backpacks) : _backpacksAdapter.HandleLoadedChanged();
return;
case nameof(ItemRetriever) : _itemRetrieverAdapter.HandleLoadedChanged();
return;
case nameof(Loottable) : RefreshStackHooks();
return;
case nameof(SkillTree) : _skillTreeAdapter.HandleLoadedChanged();
return;
case nameof(StackModifier) : RefreshStackHooks();
return;
}
}
// Subscribed only while wearable bags are enabled.private object CanWearItem(PlayerInventory playerInventory, Item item, int targetSlot)
{
if (item.info.itemid != BagItemId) return null;
// Disallow wearing vanilla loot bags, necessary because of the ItemModWearable component.if (!_bagManager.IsBag(item))
return False;
if (targetSlot == Locker.backpackSlotIndex) {
if (_config.Wearable.AllowInBackpackSlot) return True;
}
else if (_config.Wearable.AllowInNonBackpackSlot) {
return True;
}
// Must return false instead of null, due to the existence of the ItemModWearable component.return False;
}
// Subscribed only while wearable bags are enabled.private object CanLockerAcceptItem(Locker locker, Item item, int targetSlot)
{
if (!_bagManager.IsBag(item)) return null;
if (locker.IsBackpackSlot(targetSlot)) return _config.Wearable.AllowInBackpackSlot ? True : null;
if (locker.GetRowType(targetSlot) == Locker.RowType.Clothing) return _config.Wearable.AllowInNonBackpackSlot ? True : null;
return null;
}
// Always subscribed to avoid bag limit issues when switching non-empty bags.// Also subscribed to allow items to be swapped in clothing slots.private object OnLockerSwap(Locker locker, int outfitIndex, BasePlayer player)
{
if (locker.IsEquipping()) return null;
var startIndex = outfitIndex * 14;
var wantsToTransferBag = false;
if (_config.Wearable.Enabled) {
// Override locker swap if any bag is being worn.for (var i = 0; i < Locker.attireSize; i++)
{
var lockerItem = locker.inventory.GetSlot(startIndex + i);
if (lockerItem != null && _bagManager.IsBag(lockerItem)) {
wantsToTransferBag = true;
break;
}
var playerItem = player.inventory.containerWear.GetSlot(i);
if (playerItem != null && _bagManager.IsBag(playerItem)) {
wantsToTransferBag = true;
break;
}
}
}
if (!wantsToTransferBag) {
// Override locker swap if any non-empty bag is being transferred.for (var i = 0; i < Locker.beltSize; i++)
{
var lockerItem = locker.inventory.GetSlot(startIndex + i);
if (lockerItem != null && _bagManager.IsNonEmptyBag(lockerItem)) {
wantsToTransferBag = true;
break;
}
var playerItem = player.inventory.containerBelt.GetSlot(i);
if (playerItem != null && _bagManager.IsNonEmptyBag(playerItem)) {
wantsToTransferBag = true;
break;
}
}
}
if (!wantsToTransferBag) return null;
var didSwapAnything = false;
// Move attire and belt items to limbo to avoid bag limit issues.for (var i = 0; i < Locker.attireSize; i++)
{
var playerItem = player.inventory.containerWear.GetSlot(i);
if (playerItem == null) continue;
playerItem.RemoveFromContainer();
_lockerItemBuffer[i] = playerItem;
}
for (var i = 0; i < Locker.beltSize; i++) {
var playerItem = player.inventory.containerBelt.GetSlot(i);
if (playerItem == null) continue;
playerItem.RemoveFromContainer();
_lockerItemBuffer[i + Locker.attireSize] = playerItem;
}
// Move items from locker to player inventory.for (var i = 0; i < Locker.attireSize; i++)
{
var lockerItemIndex = startIndex + i;
var lockerItem = locker.inventory.GetSlot(lockerItemIndex);
if (lockerItem == null) continue;
didSwapAnything = true;
if (lockerItem.info.category != ItemCategory.Attire && !_bagManager.IsBag(lockerItem) || !lockerItem.MoveToContainer(player.inventory.containerWear, i)) {
lockerItem.Drop(locker.GetDropPosition(), locker.GetDropVelocity());
}
}
for (var i = 0; i < Locker.beltSize; i++) {
var lockerItemIndex = startIndex + Locker.attireSize + i;
var lockerItem = locker.inventory.GetSlot(lockerItemIndex);
if (lockerItem == null) continue;
didSwapAnything = true;
if (!lockerItem.MoveToContainer(player.inventory.containerBelt, i)) {
lockerItem.Drop(locker.GetDropPosition(), locker.GetDropVelocity());
}
}
// Move items from limbo to locker.for (var i = 0; i < Locker.attireSize; i++)
{
var lockerItemIndex = startIndex + i;
var playerItem = _lockerItemBuffer[i];
if (playerItem == null) continue;
didSwapAnything = true;
if (playerItem.info.category != ItemCategory.Attire && !_bagManager.IsBag(playerItem) || !playerItem.MoveToContainer(locker.inventory, lockerItemIndex)) {
playerItem.Drop(locker.GetDropPosition(), locker.GetDropVelocity());
}
_lockerItemBuffer[i] = null;
}
for (var i = 0; i < Locker.beltSize; i++) {
var bufferIndex = Locker.attireSize + i;
var lockerItem = _lockerItemBuffer[bufferIndex];
if (lockerItem == null) continue;
var lockerItemIndex = startIndex + Locker.attireSize + i;
didSwapAnything = true;
if (!lockerItem.MoveToContainer(locker.inventory, lockerItemIndex)) {
lockerItem.Drop(locker.GetDropPosition(), locker.GetDropVelocity());
}
_lockerItemBuffer[bufferIndex] = null;
}
if (didSwapAnything) {
Effect.server.Run(locker.equipSound.resourcePath, player, StringPool.Get("spine3"), Vector3.zero, Vector3.zero);
locker.SetFlag(Locker.LockerFlags.IsEquipping, true);
locker.Invoke(locker.ClearEquipping, 1.5f);
}
return False;
}
// Subscribed only while the visual backpack is enabled.private void OnEntitySaved(BasePlayer player, BaseNetworkable.SaveInfo saveInfo)
{
if (!ShouldDisplayVisualBackpack(player)) return;
AddVisualBackpackItemToContainer(saveInfo.msg.basePlayer.inventory.invWear);
}
// Subscribed only while the visual backpack is enabled.private void OnInventoryNetworkUpdate(PlayerInventory playerInventory, ItemContainer container,
ProtoBuf.UpdateItemContainer payload, PlayerInventory.Type inventoryType) {
if (inventoryType != PlayerInventory.Type.Wear || !ShouldDisplayVisualBackpack(playerInventory.baseEntity)) return;
AddVisualBackpackItemToContainer(payload.container[0]);
}
// Possible improvement: Dynamically subscribe only while a player is looting a bag.// Possible improvement: Attach components with `PlayerStoppedLooting(BasePlayer)` on-demand and keep them until plugin unload.private void OnPlayerLootEnd(PlayerLoot playerLoot)
{
var player = playerLoot.baseEntity;
if (player == null) return;
if (!IsLootingBag(player, out var bagInfo)) return;
_bagManager.HandlePlayerStoppedLooting(player, bagInfo);
}
#if FEATURE_BELT_ACTIVATION
// Subscribed only while belt activation is enabled.private void OnActiveItemChanged(BasePlayer player, Item previousItem, Item nextItem)
{
if (!IsRealPlayer(player)) return;
if (nextItem != null) {
var nextBagInfo = _bagManager.EnsureBagInfoIfEligible(nextItem);
if (nextBagInfo != null) {
if (nextItem == previousItem) {
StopLooting(player);
}
else {
if (player.inventory.loot.IsLooting()) {
AttemptOpenBag(player, nextBagInfo);
}
else {
// Player might be scrolling through their items, so don't open immediately._beltTracker.AddPlayer(player, nextBagInfo);
}
// var originalPosition = nextItem.position;
// timer.Once(0.1f, () =>
// {
// var activeItem = player.GetActiveItem();
// if (nextItem == activeItem && activeItem.position == originalPosition)
// {
// AttemptOpenBag(player, nextBagInfo);
// }
// });
}
}
return;
}
if (previousItem == null) return;
var previousBagInfo = _bagManager.GetBagInfo(previousItem);
if (previousBagInfo == null) return;
_beltTracker.RemovePlayer(player);
var lootContainer = player.inventory.loot.containers.FirstOrDefault();
if (lootContainer == null || lootContainer != previousItem.contents) return;
// The following code must be in a block to ensure the delegate doesn't get created for code paths that don't reach this.{
var originalContainer = previousItem.parent;
var originalPosition = previousItem.position;
var originalAmount = previousItem.amount;
NextTick(() => {
if (originalContainer != previousItem.parent || originalPosition != previousItem.position || originalAmount != previousItem.amount) return;
ClosePlayerInventory(player);
}
);
}
}
#endif
// Subscribed only while the belt button is enabled.private void OnPlayerSleepEnded(BasePlayer player)
{
if (ShouldDisplayBeltUI(player)) {
_beltUIRenderer.CreateUIIfMissing(player);
}
}
// Always subscribed due to vanilla issues that need to be mitigated, most importantly for dropping locked bags.private object OnItemAction(Item item, string action, BasePlayer player)
{
switch (action) {
// Unlock the bag before it's dropped since otherwise vanilla won't allow the action.case "drop":
{
if (!item.IsLocked()) return null;
var bagInfo = _bagManager.GetBagInfo(item);
if (bagInfo == null) return null;
// Set the item unlocked so it can be dropped.bagInfo.SetLocked(false);
if (bagInfo.Item.amount > 1) {
var parentSupervisor = bagInfo.ParentSupervisor;
if (parentSupervisor != null) {
NextTick(() => parentSupervisor.RefreshBags());
}
}
return null;
}
// Fix issue where wrapping a gift can delete the contents.case "wrap":
{
if (item.amount != 1) return null;
if (!ItemUtils.HasItemMod(item.info, out ItemModWrap itemModWrap)) return null;
if (!IsInsideBag(item, out var parentContainer)) return null;
if (!CanPlayerMoveItem(player, item)) return null;
var childItem = item.contents.GetSlot(0);
if (childItem == null) return null;
var position = item.position;
item.RemoveFromContainer();
var wrappedItem = ItemManager.Create(itemModWrap.wrappedDefinition);
if (!childItem.MoveToContainer(wrappedItem.contents)) {
wrappedItem.Remove();
player.GiveItem(childItem);
return null;
}
if (!wrappedItem.MoveToContainer(parentContainer, position)) {
player.GiveItem(wrappedItem);
}
item.Remove();
if (itemModWrap.successEffect.isValid) {
Effect.server.Run(itemModWrap.successEffect.resourcePath, player.eyes.position);
}
return False;
}
// Fix issue where unwrapping a gift can delete the contents.case "open":
{
if (item.amount != 1) return null;
if (!ItemUtils.HasItemMod(item.info, out ItemModOpenWrapped itemModOpenWrapped)) return null;
if (!IsInsideBag(item, out var parentContainer)) return null;
if (!CanPlayerMoveItem(player, item)) return null;
var childItem = item.contents.GetSlot(0);
if (childItem == null) return null;
var position = item.position;
item.RemoveFromContainer();
if (!childItem.MoveToContainer(parentContainer, position)) {
player.GiveItem(childItem);
}
item.Remove();
if (itemModOpenWrapped.successEffect.isValid) {
Effect.server.Run(itemModOpenWrapped.successEffect.resourcePath, player.eyes.position);
}
return False;
}
default : return null;
}
}
// Always subscribed to prevent containers from accepting bags when bag limit is reached.private object CanAcceptItem(ItemContainer container, Item item, int targetPosition)
{
// Allow bags to move around in the same inventory.if (item.parent == container)
return null;
if (!EnforceBagLimitsInContainer(container)) return null;
var bagInfo = _bagManager.EnsureBagInfoIfEligible(item);
if (bagInfo == null || !bagInfo.HasItems) return null;
var supervisor = _limitManager.GetSupervisor(container);
if (supervisor != null) {
return supervisor.CanAddBag(bagInfo, container) ? null : CannotAccept;
}
// If there is no supervisor, assume the container does not have any bags yet.var limitProfile = _limitManager.GetLimitProfile(container);
if (limitProfile.MaxTotalBags == 0 || limitProfile.GetMaxBagsOfCategory(bagInfo.BagProfile.CategoryNumber) == 0) return CannotAccept;
return null;
}
// Subscribed only while multiple bag profiles share a skin ID, and while no suitable stack plugin is loaded.private Item OnItemSplit(Item sourceItem, int amount)
{
var bagInfo = _bagManager.EnsureBagInfoIfEligible(sourceItem);
if (bagInfo == null) return null;
sourceItem.amount -= amount;
sourceItem.MarkDirty();
return CreateBag(bagInfo.BagProfile, amount);
}
// Subscribed only while stack protection is enabled.private object CanStackItem(Item hostItem, Item movedItem)
{
if (hostItem.info.stackable <= 1) return null;
var bagInfo = _bagManager.EnsureBagInfoIfEligible(hostItem);
if (bagInfo == null) return null;
var otherBagInfo = _bagManager.EnsureBagInfoIfEligible(movedItem);
if (otherBagInfo == null) return null;
if (bagInfo.BagProfile != otherBagInfo.BagProfile) return False;
if (ItemUtils.HasChildren(movedItem)) return False;
return null;
}
// Subscribed only while no suitable stack plugin is loaded.private object CanCombineDroppedItem(DroppedItem itemEntity, DroppedItem otherItemEntity)
{
var bagInfo = _bagManager.EnsureBagInfoIfEligible(itemEntity.item);
if (bagInfo == null) return null;
var otherBagInfo = _bagManager.EnsureBagInfoIfEligible(otherItemEntity.item);
if (otherBagInfo == null) return null;
if (bagInfo.BagProfile != otherBagInfo.BagProfile) return False;
if (ItemUtils.HasChildren(itemEntity.item) || ItemUtils.HasChildren(otherItemEntity.item)) return False;
return null;
}
// Always subscribed to mitigate an issue where quick-looting an item from a bag in your inventory would move it back into the bag.// Possible improvement: Dynamically subscribe only while a player is looting a bag in their inventory, and while stack improvements is enabled.private object CanMoveItem(Item item, PlayerInventory playerInventory, ItemContainerId targetContainerId, int targetSlot, int amount)
{
if (targetContainerId.Value == 0 && targetSlot == -1 && IsInsideBag(item, out var parentContainer) && parentContainer.playerOwner == playerInventory.baseEntity) {
// Player is quick moving an item from a bag in their inventory into their inventory.// This needs to be overriden since vanilla logic would move the item back into the bag.if (!playerInventory.GiveItem(item))
{
playerInventory.baseEntity.ChatMessage("GiveItem failed!");
}
return False;
}
// When attempting to move a bag into another bag, try to stack it if the destination bag is closed.if (_config.EnableStackImprovements)
{
var bagInfo = _bagManager.GetBagInfo(item);
if (bagInfo == null) return null;
// If the bag contains items, only allow moving a partial stack from it onto another bag.if (bagInfo.HasItems && (item.amount == 1 || amount >= item.amount))
return null;
// Target container must have a parent item (must be a bag).var targetContainer = playerInventory.FindContainer(targetContainerId);
if (targetContainer?.parent == null) return null;
// Target container must be a bag of the same profile as the bag being moved.var destinationBagInfo = _bagManager.GetBagInfo(targetContainer.parent);
if (destinationBagInfo == null || destinationBagInfo.BagProfile != bagInfo.BagProfile) return null;
// Don't stack if looting the destination bag (move the bag into it instead).if (playerInventory.loot.containers.FirstOrDefault() == targetContainer)
return null;
// Don't allow stacking onto a bag item that doesn't accept items (e.g., that bag is in a loot container).if (targetContainer.PlayerItemInputBlocked())
return null;
// Vanilla checks to prohibit taking items in various situations (e.g., from a locked container).if (item.parent.entityOwner is ICanMoveFrom canMoveFrom
&& !canMoveFrom.CanMoveFrom(playerInventory.baseEntity, item)) return null;
// If amount is negative, the player is trying to move the whole stack.if (amount <= 0)
{
amount = item.amount;
}
amount = Mathf.Clamp(amount, 1, destinationBagInfo.Item.MaxStackable() - destinationBagInfo.Item.amount);
if (amount <= 0) return null;
destinationBagInfo.Item.amount += amount;
destinationBagInfo.Item.MarkDirty();
item.amount -= amount;
if (item.amount > 0) {
item.MarkDirty();
}
else {
item.RemoveFromContainer();
item.Remove();
}
return False;
}
return null;
}
// Subscribed only while looting or equipping dropped bags is enabled.private object OnItemPickup(Item item, BasePlayer player)
{
var bagInfo = _bagManager.EnsureBagInfoIfEligible(item);
if (bagInfo == null) return null;
if (CanAcceptItem(player.inventory.containerMain, item, -1) == null) return null;
if (_config.Wearable.Enabled && _config.WearableBagLimits.Enabled && _config.DroppedBagSettings.AutoEquipDroppedBags && item.MoveToContainer(player.inventory.containerWear)) return False;
if (!_config.DroppedBagSettings.EnableOpeningDroppedBags) return null;
var droppedItem = item.GetWorldEntity() as DroppedItem;
if (droppedItem == null || droppedItem.IsDestroyed) return null;
// Reset the removal time to reduce the likelihood of the bag item being removed while it's being looted.ResetRemovalTime(droppedItem);
AttemptOpenBag(player, bagInfo);
return False;
}
// Subscribed only while loot spawns are enabled.private void OnLootSpawn(LootContainer lootContainer)
{
if (lootContainer.OwnerID != 0 || lootContainer.skinID != 0) return;
if (!_config.LootConfig.SpawnChanceByPrefabId.TryGetValue(lootContainer.prefabID, out var spawnChanceByProfile)) return;
ScheduleLootSpawn(lootContainer, spawnChanceByProfile);
}
// Subscribed only while a bag profile has recycling enabled.private object CanBeRecycled(Item item, Recycler recycler)
{
if (item == null) return null;
var bagInfo = _bagManager.GetBagInfo(item);
if (bagInfo == null) return null;
return ObjectCache.Get(bagInfo.BagProfile.CanBeRecycled && !bagInfo.HasItems);
}
// Subscribed only while a bag profile has recycling enabled.private object CanRecycle(Recycler recycler, Item item)
{
return CanBeRecycled(item, recycler);
}
// Subscribed only while a bag profile has recycling enabled.private object OnItemRecycle(Item item, Recycler recycler)
{
var recycleInfo = _bagManager.GetBagInfo(item)?.BagProfile.RecycleInfo;
if (recycleInfo == null || !recycleInfo.Enabled) return null;
if (recycleInfo.Ingredients == null) return null;
var recycleAmount = Mathf.CeilToInt(Mathf.Min(item.amount, item.info.stackable * 0.1f));
item.UseItem(recycleAmount);
foreach (var ingredientInfo in recycleInfo.Ingredients) {
if (ingredientInfo.Amount <= 0 || ingredientInfo.ItemDefinition == null) continue;
var ingredientItem = ItemManager.Create(ingredientInfo.ItemDefinition, ingredientInfo.Amount * recycleAmount, ingredientInfo.SkinId);
if (ingredientItem == null) continue;
if (!string.IsNullOrWhiteSpace(ingredientInfo.DisplayName)) {
ingredientItem.name = ingredientInfo.DisplayName;
}
recycler.MoveItemToOutput(ingredientItem);
}
return False;
}
// Subscribed only while no suitable stack plugin is loaded.private void OnDroppedItemCombined(DroppedItem droppedItem)
{
var item = droppedItem.item;
if (item == null) return;
var bagInfo = _bagManager.GetBagInfo(item);
if (bagInfo == null) return;
// Delay in case another plugin (such as DespawnConfig) is generically resetting despawn time.ResetRemovalTime(droppedItem, delayed: true);
}
// Only subscribed while at least one bag profile exists that uses a Stack profile that has item-specific or skin-specific overrides.private object OnMaxStackable(Item item)
{
var stackSizeOverride = GetParentBag(item)?.BagProfile?.StackProfile?.GetStackSizeOverride(item);
if (stackSizeOverride == null) return null;
var stackSizeInt = (int)stackSizeOverride;
if (stackSizeInt >= item.info.stackable) {
// Don't allow increasing max stack size, since that creates some complications.return null;
}
if (item.parent != null && item.parent.maxStackSize != 0 && stackSizeInt >= item.parent.maxStackSize) {
// Don't allow increasing max stack size, since that creates some complications.return null;
}
return stackSizeOverride;
}
private void OnUserPermissionGranted(string userIdString, string perm) {
if (perm.StartsWith(PlayerLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshPlayerLimitProfile(userIdString);
}
else if (perm.StartsWith(WearableLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshWearableLimitProfile(userIdString);
}
else if (perm.StartsWith(BackpackLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshBackpackLimitProfile(userIdString);
}
else if (perm.StartsWith(ContainerLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshContainerLimitProfileForPlayer(userIdString);
}
}
private void OnUserPermissionRevoked(string userIdString, string perm) {
OnUserPermissionGranted(userIdString, perm);
}
private void OnGroupPermissionGranted(string groupName, string perm) {
if (perm.StartsWith(PlayerLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshPlayerLimitProfilesForGroup(groupName);
}
else if (perm.StartsWith(WearableLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshWearableLimitProfilesForGroup(groupName);
}
else if (perm.StartsWith(BackpackLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshBackpackLimitProfilesForGroup(groupName);
}
else if (perm.StartsWith(ContainerLimitProfile.LimitPrefix, StringComparison.OrdinalIgnoreCase)) {
_limitManager.RefreshContainerLimitProfiles();
}
}
private void OnGroupPermissionRevoked(string groupName, string perm) {
OnGroupPermissionGranted(groupName, perm);
}
private void OnUserGroupAdded(string userIdString, string groupName) {
_limitManager.RefreshPlayerLimitProfile(userIdString);
if (_config.WearableBagLimits.Enabled) {
_limitManager.RefreshWearableLimitProfile(userIdString);
}
_limitManager.RefreshBackpackLimitProfile(userIdString);
_limitManager.RefreshContainerLimitProfileForPlayer(userIdString);
}
private void OnUserGroupRemoved(string userIdString, string groupName) {
OnUserGroupAdded(userIdString, groupName);
}
#endregion
#region API
private class ApiInstance {
public Dictionary<string, object> _apiWrapper {
get;
}
private BagOfHolding _plugin;
private Configuration _config => _plugin._config;
private BagManager _bagManager => _plugin._bagManager;
private LimitManager _limitManager => _plugin._limitManager;
public ApiInstance(BagOfHolding plugin) {
_plugin = plugin;
_apiWrapper = new Dictionary<string, object> {
[nameof(IsBag)]
= new Func<Item, bool>(IsBag), [nameof(IsLootingBag)] = new Func<BasePlayer, bool>(IsLootingBag), [nameof(GetBagProfileName)] = new Func<Item, string>(GetBagProfileName), [nameof(OpenBag)] = new Func<BasePlayer, Item, bool>(OpenBag), [nameof(CreateBag)] = new Func<string, int, Item>(CreateBag), [nameof(GiveBag)] = new Func<BasePlayer, string, int, Item>(GiveBag), [nameof(CreateKitBag)] = new Func<string, string, BasePlayer, Item>(CreateKitBag), [nameof(GiveKitBag)] = new Func<BasePlayer, string, string, Item>(GiveKitBag), [nameof(ChangeBagProfile)] = new Func<Item, string, BasePlayer, bool>(ChangeBagProfile), [nameof(UpgradeBag)] = new Func<Item, BasePlayer, bool>(UpgradeBag), [nameof(FindUnlockedBag)] = new Func<ItemContainer, string, int, int, Item>(FindUnlockedBag), [nameof(DiscoverBags)] = new Action<ItemContainer>(DiscoverBags), [nameof(CreateLimitProfile)] = new Func<string, object>(CreateLimitProfile), [nameof(SetLimitProfile)] = new Func<ItemContainer, object, bool>(SetLimitProfile), [nameof(RemoveLimitProfile)] = new Action<ItemContainer>(RemoveLimitProfile),
}
;
}
public bool IsBag(Item item) {
return _bagManager.EnsureBagInfoIfEligible(item) != null;
}
public bool IsLootingBag(BasePlayer player) {
return _plugin.IsLootingBag(player);
}
public string GetBagProfileName(Item item) {
return _bagManager.EnsureBagInfoIfEligible(item)?.BagProfile.Name;
}
public bool OpenBag(BasePlayer player, Item item) {
if (!VerifyValidBag(nameof(API_OpenBag), item, out var bagInfo)) return false;
return _plugin.AttemptOpenBag(player, bagInfo);
}
public Item CreateBag(string profileName, int amount = 1) {
if (amount <= 0) return null;
if (!VerifyValidBagProfile(nameof(API_CreateBag), profileName, out var bagProfile)) return null;
return _plugin.CreateBag(bagProfile, amount);
}
public Item GiveBag(BasePlayer player, string profileName, int amount = 1) {
if (amount <= 0) return null;
if (!VerifyValidBagProfile(nameof(API_GiveBag), profileName, out var bagProfile)) return null;
var bagItem = _plugin.CreateBag(bagProfile, amount);
if (bagItem == null) return null;
player.GiveItem(bagItem);
return bagItem;
}
public Item CreateKitBag(string profileName, string kitName, BasePlayer initiator = null) {
if (!VerifyValidBagProfile(nameof(API_CreateKitBag), profileName, out var bagProfile)) return null;
return _plugin.CreateKitBag(bagProfile, kitName, initiator);
}
public Item GiveKitBag(BasePlayer player, string profileName, string kitName) {
if (!VerifyValidBagProfile(nameof(API_GiveBag), profileName, out var bagProfile)) return null;
if (!VerifyValidKit(nameof(API_GiveBag), kitName)) return null;
var bagItem = _plugin.CreateKitBag(bagProfile, kitName, player);
if (bagItem == null) return null;
player.GiveItem(bagItem);
return bagItem;
}
public bool ChangeBagProfile(Item item, st
// ... [Code preview truncated due to length. Full source in attached file] ...