using System; using System.Collections.Generic; using System.Linq; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using UnityEngine; namespace Oxide.Plugins { [Info("Stack Size Controller", "AnExiledGod", "3.3.1")] [Description("Allows configuration of most items max stack size.")] class StackSizeController : CovalencePlugin { private ConfigData _config; private ItemIndex _data; private Dictionary _vanillaDefaults; private readonly List _ignoreList = new List { "water", "water.salt", "cardtable", "hat.bunnyhat", "rustige_egg_e" }; private void Init() { _config = Config.ReadObject(); _data = Interface.Oxide.DataFileSystem.ReadObject(nameof(StackSizeController)); _vanillaDefaults = Interface.Oxide.DataFileSystem.ReadObject>(nameof(StackSizeController) + "_vanilla-defaults"); if (_config.IsNull()) { Puts("Generating Default Config File."); LoadDefaultConfig(); } EnsureConfigIntegrity(); if (_data.IsUnityNull() || _data.ItemCategories.IsUnityNull()) { Puts("Generating Data File."); CreateItemIndex(); _data.VersionNumber = Version; SaveData(); } // Data File Migrations - TODO: Setup full implementation if (_data.VersionNumber <= new VersionNumber(3, 1, 2)) { Puts("Datafile version number is less than or equal to v3.1.2. Migration necessary. " + "Backing up old datafile..."); Interface.Oxide.DataFileSystem.WriteObject(nameof(StackSizeController) + "_backup", _data); if (Interface.Oxide.DataFileSystem.ExistsDatafile(nameof(StackSizeController))) { foreach (KeyValuePair> items in _data.ItemCategories) { foreach (ItemInfo itemInfo in items.Value) { if (itemInfo.VanillaStackSize == itemInfo.CustomStackSize) { itemInfo.CustomStackSize = 0; } } } _data.VersionNumber = Version; SaveData(); Puts("Datafile migration complete. Notice: Backup files must be manually deleted."); } else { Puts("Datafile backup failed. Migration failed, report to developer."); } } AddCovalenceCommand("stacksizecontroller.regendatafile", nameof(RegenerateDataFileCommand), "stacksizecontroller.regendatafile"); AddCovalenceCommand("stacksizecontroller.setstack", nameof(SetStackCommand), "stacksizecontroller.setstack"); AddCovalenceCommand("stacksizecontroller.setstackcat", nameof(SetStackCategoryCommand), "stacksizecontroller.setstackcat"); AddCovalenceCommand("stacksizecontroller.setallstacks", nameof(SetAllStacksCommand), "stacksizecontroller.setallstacks"); AddCovalenceCommand("stacksizecontroller.itemsearch", nameof(ItemSearchCommand), "stacksizecontroller.itemsearch"); AddCovalenceCommand("stacksizecontroller.listcategories", nameof(ListCategoriesCommand), "stacksizecontroller.listcategories"); AddCovalenceCommand("stacksizecontroller.listcategoryitems", nameof(ListCategoryItemsCommand), "stacksizecontroller.listcategoryitems"); } private void OnServerInitialized() { if (_vanillaDefaults.IsUnityNull() || _vanillaDefaults.Count == 0) { MaintainVanillaStackSizes(); } SetStackSizes(); } private void OnTerrainInitialized() { Puts("Ensuring VanillaStackSize integrity."); MaintainVanillaStackSizes(true); UpdateItemIndex(); } private void Unloaded() { if (_config.RevertStackSizesToVanillaOnUnload) { RevertStackSizes(); } } #region Configuration private class ConfigData { public bool RevertStackSizesToVanillaOnUnload = true; public bool AllowStackingItemsWithDurability = true; public bool PreventStackingDifferentSkins; public bool HidePrefixWithPluginNameInMessages; public bool DisableDupeFixAndLeaveWeaponMagsAlone; public float GlobalStackMultiplier = 1; public Dictionary CategoryStackMultipliers = GetCategoriesAndDefaults(1) .ToDictionary(k => k.Key, k => Convert.ToSingle(k.Value)); public Dictionary IndividualItemStackMultipliers = new Dictionary(); public Dictionary CategoryStackHardLimits = GetCategoriesAndDefaults(0) .ToDictionary(x => x.Key, x => Convert.ToInt32(x.Value)); public Dictionary IndividualItemStackHardLimits = new Dictionary(); public VersionNumber VersionNumber; } protected override void SaveConfig() { Config.WriteObject(_config); } protected override void LoadDefaultConfig() { ConfigData defaultConfig = GetDefaultConfig(); defaultConfig.VersionNumber = Version; Config.WriteObject(defaultConfig); _config = Config.ReadObject(); } private void EnsureConfigIntegrity() { ConfigData configDefault = new ConfigData(); if (_config.RevertStackSizesToVanillaOnUnload.IsNull()) { _config.RevertStackSizesToVanillaOnUnload = configDefault.RevertStackSizesToVanillaOnUnload; } if (_config.AllowStackingItemsWithDurability.IsNull()) { _config.AllowStackingItemsWithDurability = configDefault.AllowStackingItemsWithDurability; } if (_config.PreventStackingDifferentSkins.IsNull()) { _config.PreventStackingDifferentSkins = configDefault.PreventStackingDifferentSkins; } if (_config.HidePrefixWithPluginNameInMessages.IsNull()) { _config.HidePrefixWithPluginNameInMessages = configDefault.HidePrefixWithPluginNameInMessages; } if (_config.DisableDupeFixAndLeaveWeaponMagsAlone.IsNull()) { _config.DisableDupeFixAndLeaveWeaponMagsAlone = configDefault.DisableDupeFixAndLeaveWeaponMagsAlone; } if (_config.GlobalStackMultiplier.IsNull()) { _config.GlobalStackMultiplier = configDefault.GlobalStackMultiplier; } if (_config.CategoryStackMultipliers.IsNull>()) { _config.CategoryStackMultipliers = configDefault.CategoryStackMultipliers; } if (_config.IndividualItemStackMultipliers.IsNull>()) { _config.IndividualItemStackMultipliers = configDefault.IndividualItemStackMultipliers; } if (_config.CategoryStackHardLimits.IsNull>()) { _config.IndividualItemStackHardLimits = configDefault.IndividualItemStackHardLimits; } if (_config.IndividualItemStackHardLimits.IsNull>()) { _config.IndividualItemStackHardLimits = configDefault.IndividualItemStackHardLimits; } _config.VersionNumber = Version; SaveConfig(); } private ConfigData GetDefaultConfig() { return new ConfigData(); } private void UpdateIndividualItemStackMultiplier(int itemId, float multiplier) { if (_config.IndividualItemStackMultipliers.ContainsKey(itemId.ToString())) { _config.IndividualItemStackMultipliers[itemId.ToString()] = multiplier; SaveConfig(); return; } _config.IndividualItemStackMultipliers.Add(GetIndexedItem(itemId).Shortname, multiplier); SaveConfig(); } private void UpdateIndividualItemStackMultiplier(string shortname, float multiplier) { if (_config.IndividualItemStackMultipliers.ContainsKey(shortname)) { _config.IndividualItemStackMultipliers[shortname] = multiplier; SaveConfig(); return; } _config.IndividualItemStackMultipliers.Add(shortname, multiplier); SaveConfig(); } private void UpdateIndividualItemHardLimit(int itemId, int stackLimit) { if (_config.IndividualItemStackHardLimits.ContainsKey(itemId.ToString())) { _config.IndividualItemStackHardLimits[itemId.ToString()] = stackLimit; SaveConfig(); return; } _config.IndividualItemStackHardLimits.Add(GetIndexedItem(itemId).Shortname, stackLimit); SaveConfig(); } private void UpdateIndividualItemHardLimit(string shortname, int stackLimit) { if (_config.IndividualItemStackHardLimits.ContainsKey(shortname)) { _config.IndividualItemStackHardLimits[shortname] = stackLimit; SaveConfig(); return; } _config.IndividualItemStackHardLimits.Add(shortname, stackLimit); SaveConfig(); } #endregion #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["NotEnoughArguments"] = "This command requires {0} arguments.", ["InvalidItemShortnameOrId"] = "Item shortname or id is incorrect. Try stacksizecontroller.itemsearch [partial item name]", ["InvalidCategory"] = "Category not found. Try stacksizecontroller.listcategories", ["OperationSuccessful"] = "Operation completed successfully.", }, this); } private string GetMessage(string key, string playerId) { if (_config.HidePrefixWithPluginNameInMessages || playerId == "server_console") { return lang.GetMessage(key, this, playerId); } return $"[{nameof(StackSizeController)}] " + lang.GetMessage(key, this, playerId); } #endregion #region Data Handling private class ItemIndex { public Dictionary> ItemCategories; public VersionNumber VersionNumber; } private class ItemInfo { public int ItemId; public string Shortname; public bool HasDurability; public int VanillaStackSize; public int CustomStackSize; } private void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(nameof(StackSizeController), _data); } private void CreateItemIndex() { _data = new ItemIndex { ItemCategories = new Dictionary>() }; // Create categories foreach (string category in Enum.GetNames(typeof(ItemCategory))) { if (category == "All") { continue; } _data.ItemCategories.Add(category, new List()); } // Iterate and categorize items foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions()) { _data.ItemCategories[itemDefinition.category.ToString()].Add( new ItemInfo { ItemId = itemDefinition.itemid, Shortname = itemDefinition.shortname, HasDurability = itemDefinition.condition.enabled, VanillaStackSize = GetVanillaStackSize(itemDefinition), CustomStackSize = 0 }); } _data.VersionNumber = Version; SaveData(); } private void UpdateItemIndex() { foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions()) { if (!_data.ItemCategories[itemDefinition.category.ToString()] .Exists(itemInfo => itemInfo.ItemId == itemDefinition.itemid)) { _data.ItemCategories[itemDefinition.category.ToString()].Add( new ItemInfo { ItemId = itemDefinition.itemid, Shortname = itemDefinition.shortname, HasDurability = itemDefinition.condition.enabled, VanillaStackSize = GetVanillaStackSize(itemDefinition), CustomStackSize = 0 }); } } SaveData(); } private ItemInfo AddItemToIndex(int itemId) { ItemDefinition itemDefinition = ItemManager.FindItemDefinition(itemId); ItemInfo item = new ItemInfo { ItemId = itemId, Shortname = itemDefinition.shortname, HasDurability = itemDefinition.condition.enabled, VanillaStackSize = GetVanillaStackSize(itemDefinition), CustomStackSize = 0 }; _data.ItemCategories[itemDefinition.category.ToString()].Add(item); SaveData(); return item; } private ItemInfo GetIndexedItem(int itemId) { ItemInfo indexedItem = null; foreach (List itemInfo in _data.ItemCategories.Values) { indexedItem = itemInfo.Find(x => x.ItemId == itemId); if (indexedItem != null) { break; } } return indexedItem; } private ItemInfo GetIndexedItem(ItemCategory itemCategory, int itemId) { ItemInfo itemInfo = _data.ItemCategories[itemCategory.ToString()].First(item => item.ItemId == itemId) ?? AddItemToIndex(itemId); return itemInfo; } private void MaintainVanillaStackSizes(bool refreshDataFile = false) { SortedDictionary vanillaStackSizes = new SortedDictionary(); foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions()) { vanillaStackSizes.Add(itemDefinition.shortname, itemDefinition.stackable); if (refreshDataFile) { ItemInfo existingItemInfo = _data.ItemCategories[itemDefinition.category.ToString()] .Find(itemInfo => itemInfo.ItemId == itemDefinition.itemid); existingItemInfo.VanillaStackSize = GetVanillaStackSize(itemDefinition); SaveData(); } } Interface.Oxide.DataFileSystem.WriteObject(nameof(StackSizeController) + "_vanilla-defaults", vanillaStackSizes); _vanillaDefaults = new Dictionary(vanillaStackSizes); } #endregion #region Commands /* * dumpitemlist command */ private void RegenerateDataFileCommand(IPlayer player, string command, string[] args) { CreateItemIndex(); player.Reply(GetMessage("OperationSuccessful", player.Id)); } private void SetStackCommand(IPlayer player, string command, string[] args) { if (args.Length != 2) { player.Reply( string.Format(GetMessage("NotEnoughArguments", player.Id), 2)); return; } ItemDefinition itemDefinition = ItemManager.FindItemDefinition(args[0]); string stackSizeString = args[1]; if (itemDefinition == null) { player.Reply(GetMessage("InvalidItemShortnameOrId", player.Id)); return; } if (stackSizeString.Substring(stackSizeString.Length - 1) == "x") { UpdateIndividualItemStackMultiplier(itemDefinition.itemid, Convert.ToSingle(stackSizeString.TrimEnd('x'))); SetStackSizes(); player.Reply(GetMessage("OperationSuccessful", player.Id)); return; } UpdateIndividualItemHardLimit(itemDefinition.shortname, Convert.ToInt32(stackSizeString.TrimEnd('x'))); SetStackSizes(); player.Reply(GetMessage("OperationSuccessful", player.Id)); } private void SetAllStacksCommand(IPlayer player, string command, string[] args) { if (args.Length != 1) { player.Reply( string.Format(GetMessage("NotEnoughArguments", player.Id), 1)); } foreach (string category in _config.CategoryStackMultipliers.Keys.ToList()) { _config.CategoryStackMultipliers[category] = Convert.ToInt32(args[0]); } SaveConfig(); SetStackSizes(); player.Reply(GetMessage("OperationSuccessful", player.Id)); } private void SetStackCategoryCommand(IPlayer player, string command, string[] args) { if (args.Length != 2) { player.Reply( string.Format(GetMessage("NotEnoughArguments", player.Id), 2)); } ItemCategory itemCategory = (ItemCategory) Enum.Parse(typeof(ItemCategory), args[0], true); if (itemCategory.IsNull()) { player.Reply(GetMessage("InvalidCategory", player.Id)); } _config.CategoryStackMultipliers[itemCategory.ToString()] = Convert.ToInt32(args[1].TrimEnd('x')); SaveConfig(); SetStackSizes(); player.Reply(GetMessage("OperationSuccessful", player.Id)); } private void ItemSearchCommand(IPlayer player, string command, string[] args) { if (args.Length != 1) { player.Reply( string.Format(GetMessage("NotEnoughArguments", player.Id), 1)); } List itemDefinitions = ItemManager.itemList.Where(itemDefinition => itemDefinition.displayName.english.Contains(args[0]) || itemDefinition.displayDescription.english.Contains(args[0]) || itemDefinition.shortname.Equals(args[0]) || itemDefinition.shortname.Contains(args[0])) .ToList(); TextTable output = new TextTable(); output.AddColumns("Unique Id", "Shortname", "Category", "Vanilla Stack", "Custom Stack"); foreach (ItemDefinition itemDefinition in itemDefinitions) { ItemInfo itemInfo = GetIndexedItem(itemDefinition.category, itemDefinition.itemid); output.AddRow(itemDefinition.itemid.ToString(), itemDefinition.shortname, itemDefinition.category.ToString(), itemInfo.VanillaStackSize.ToString("N0"), Mathf.Clamp(GetStackSize(itemDefinition), 0, int.MaxValue).ToString("N0")); } player.Reply(output.ToString()); } private void ListCategoriesCommand(IPlayer player, string command, string[] args) { TextTable output = new TextTable(); output.AddColumns("Category Name", "Items In Category"); foreach (string category in Enum.GetNames(typeof(ItemCategory))) { output.AddRow(category, _data.ItemCategories[category].Count.ToString()); } player.Reply(output.ToString()); } private void ListCategoryItemsCommand(IPlayer player, string command, string[] args) { if (args.Length != 1) { player.Reply(string.Format(GetMessage("NotEnoughArguments", player.Id), 1)); } ItemCategory itemCategory = (ItemCategory) Enum.Parse(typeof(ItemCategory), args[0]); if (itemCategory.IsNull()) { player.Reply(GetMessage("InvalidCategory", player.Id)); } TextTable output = new TextTable(); output.AddColumns("Unique Id", "Shortname", "Category", "Vanilla Stack", "Custom Stack", "Multiplier"); foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions() .Where(itemDefinition => itemDefinition.category == itemCategory)) { ItemInfo itemInfo = GetIndexedItem(itemDefinition.category, itemDefinition.itemid); output.AddRow(itemDefinition.itemid.ToString(), itemDefinition.shortname, itemDefinition.category.ToString(), itemInfo.VanillaStackSize.ToString("N0"), Mathf.Clamp(GetStackSize(itemDefinition), 0, int.MaxValue).ToString("N0"), _config.CategoryStackMultipliers[itemDefinition.category.ToString()].ToString()); } player.Reply(output.ToString()); } #endregion #region Hooks // TODO: Investigate merging CanStackItem into CanMoveItem and potential performance issues object CanMoveItem(Item item, PlayerInventory playerLoot, uint targetContainer, int targetSlot, int amount) { if (_config.DisableDupeFixAndLeaveWeaponMagsAlone) { return null; } if (item.contents?.itemList.Count > 0) { foreach (Item containedItem in item.contents.itemList) { item.parent.AddItem(containedItem.info, containedItem.amount, containedItem.skin); } item.contents.Clear(); } Item targetItem = item.parent.GetSlot(targetSlot); // Return contents if (targetItem?.contents?.itemList.Count > 0) { foreach (Item containedItem in targetItem.contents.itemList) { targetItem.parent.AddItem(containedItem.info, containedItem.amount, containedItem.skin); } targetItem.contents.Clear(); } return null; } private object CanStackItem(Item item, Item targetItem) { if (_config.DisableDupeFixAndLeaveWeaponMagsAlone || (item.GetOwnerPlayer().IsUnityNull() && targetItem.GetOwnerPlayer().IsUnityNull()) ) { return null; } // Duplicating all game checks since we're overriding them by returning true if ( item == targetItem || item.info.stackable <= 1 || targetItem.info.stackable <= 1 || item.info.itemid != targetItem.info.itemid || !item.IsValid() || item.IsBlueprint() && item.blueprintTarget != targetItem.blueprintTarget || targetItem.hasCondition && (targetItem.condition < targetItem.info.condition.max - 5) || (_config.PreventStackingDifferentSkins && item.skin != targetItem.skin) ) { return false; } if (item.info.amountType == ItemDefinition.AmountType.Genetics || targetItem.info.amountType == ItemDefinition.AmountType.Genetics) { if ((item.instanceData?.dataInt ?? -1) != (targetItem.instanceData?.dataInt ?? -1)) { return false; } } BaseProjectile.Magazine itemMag = targetItem.GetHeldEntity()?.GetComponent()?.primaryMagazine; // Return ammo if (itemMag != null) { if (itemMag.contents > 0) { item.parent.AddItem(itemMag.ammoType, itemMag.contents); itemMag.contents = 0; } } if (targetItem.GetHeldEntity() is FlameThrower) { FlameThrower flameThrower = targetItem.GetHeldEntity().GetComponent(); if (flameThrower.ammo > 0) { item.parent.AddItem(flameThrower.fuelType, flameThrower.ammo); flameThrower.ammo = 0; } } if (targetItem.GetHeldEntity() is Chainsaw) { Chainsaw chainsaw = targetItem.GetHeldEntity().GetComponent(); if (chainsaw.ammo > 0) { item.parent.AddItem(chainsaw.fuelType, chainsaw.ammo); chainsaw.ammo = 0; } } return true; } private Item OnItemSplit(Item item, int amount) { if (_config.DisableDupeFixAndLeaveWeaponMagsAlone) { return null; } Item newItem = ItemManager.CreateByItemID(item.info.itemid, amount, item.skin); BaseProjectile.Magazine newItemMag = newItem.GetHeldEntity()?.GetComponent()?.primaryMagazine; if (newItem.contents?.itemList.Count == 0 && (_config.DisableDupeFixAndLeaveWeaponMagsAlone || (newItem.contents?.itemList.Count == 0 && newItemMag?.contents == 0))) { return null; } item.amount -= amount; newItem.name = item.name; newItem.skin = item.skin; if (item.IsBlueprint()) { newItem.blueprintTarget = item.blueprintTarget; } if (item.info.amountType == ItemDefinition.AmountType.Genetics && item.instanceData != null && item.instanceData.dataInt != 0) { newItem.instanceData = new ProtoBuf.Item.InstanceData() { dataInt = item.instanceData.dataInt, ShouldPool = false }; } // Remove default contents (fuel, etc) if (newItem.contents?.itemList.Count > 0) { foreach (Item containedItem in item.contents.itemList) { containedItem.Remove(); } } item.MarkDirty(); // Remove default ammo if (newItemMag != null) { newItemMag.contents = 0; } if (newItem.GetHeldEntity() is FlameThrower) { newItem.GetHeldEntity().GetComponent().ammo = 0; } if (newItem.GetHeldEntity() is Chainsaw) { newItem.GetHeldEntity().GetComponent().ammo = 0; } return newItem; } #endregion #region Helpers private int GetStackSize(int itemId) { return GetStackSize(ItemManager.FindItemDefinition(itemId)); } private int GetStackSize(ItemDefinition itemDefinition) { ItemInfo customStackInfo = _data.ItemCategories[itemDefinition.category.ToString()] .Find(itemInfo => itemInfo.ItemId == itemDefinition.itemid); if (customStackInfo.IsNull()) { customStackInfo = AddItemToIndex(itemDefinition.itemid); } if (_ignoreList.Contains(itemDefinition.shortname)) { return GetVanillaStackSize(itemDefinition); } // Individual Limit set by shortname if (_config.IndividualItemStackHardLimits.ContainsKey(itemDefinition.shortname)) { return _config.IndividualItemStackHardLimits[itemDefinition.shortname]; } // Individual Limit set by item id if (_config.IndividualItemStackHardLimits.ContainsKey(itemDefinition.itemid.ToString())) { return _config.IndividualItemStackHardLimits[itemDefinition.itemid.ToString()]; } // Custom stack exists if (customStackInfo.CustomStackSize > 0) { return Mathf.RoundToInt(customStackInfo.CustomStackSize * _config.GlobalStackMultiplier); } // Individual Multiplier set by shortname int stackable = _vanillaDefaults.ContainsKey(itemDefinition.shortname) ? _vanillaDefaults[itemDefinition.shortname] : itemDefinition.stackable; if (_config.IndividualItemStackMultipliers.ContainsKey(itemDefinition.shortname)) { return Mathf.RoundToInt(stackable * _config.IndividualItemStackMultipliers[itemDefinition.shortname]); } // Individual Multiplier set by item id if (_config.IndividualItemStackMultipliers.ContainsKey(itemDefinition.itemid.ToString())) { return Mathf.RoundToInt(stackable * _config.IndividualItemStackMultipliers[itemDefinition.itemid.ToString()]); } // Category stack limit defined if (_config.CategoryStackHardLimits.ContainsKey(itemDefinition.category.ToString()) && _config.CategoryStackHardLimits[itemDefinition.category.ToString()] > 0) { return _config.CategoryStackHardLimits[itemDefinition.category.ToString()]; } // Category stack multiplier defined if (_config.CategoryStackMultipliers.ContainsKey(itemDefinition.category.ToString()) && _config.CategoryStackMultipliers[itemDefinition.category.ToString()] > 1.0f) { return Mathf.RoundToInt( stackable * _config.CategoryStackMultipliers[itemDefinition.category.ToString()]); } return Mathf.RoundToInt(stackable * _config.GlobalStackMultiplier); } private int GetVanillaStackSize(ItemDefinition itemDefinition) { return _vanillaDefaults.ContainsKey(itemDefinition.shortname) ? _vanillaDefaults[itemDefinition.shortname] : itemDefinition.stackable; } private void SetStackSizes() { foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions()) { if (itemDefinition.condition.enabled && !_config.AllowStackingItemsWithDurability) { continue; } if (_ignoreList.Contains(itemDefinition.shortname)) { continue; } itemDefinition.stackable = Mathf.Clamp(GetStackSize(itemDefinition), 1, int.MaxValue); } } private void RevertStackSizes() { foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions()) { itemDefinition.stackable = GetVanillaStackSize(itemDefinition); } } private static Dictionary GetCategoriesAndDefaults(object defaultValue) { Dictionary categoryDefaults = new Dictionary(); foreach (string category in Enum.GetNames(typeof(ItemCategory))) { categoryDefaults.Add(category, defaultValue); } return categoryDefaults; } #endregion } }