diff --git a/README.md b/README.md index 026f1cb..b2c4711 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,42 @@ * Stacks can easily be modified globally, by category or individually in the configuration file. * Several quality of life commands allowing you to **search all items** and list categories. * Item search displays vanilla stack rate as well as custom stack rate after multipliers. +* Stateful/container items (water containers, etc.) are protected from resizing so they don't dupe or lose their contents. ## Quick Important Notes * Running the plugin once generates items in configuration for IndividualItemStackSize from vanilla defaults. This list is automatically updated when new items are detected, and a notification is put in the console. -* Multipliers multiply IndividualItemStackSize definitions not vanilla stack size. Individual multipliers take priority over category stack multipliers. -* Datafiles are no longer used. Editing vanilla-defaults does nothing but screw up stack sizes when unloading the plugin. +* Multipliers multiply IndividualItemStackSize definitions not vanilla stack size. Individual multipliers take priority over category stack multipliers. +* This fork uses a local **vanilla defaults datafile** (`oxide/data/StackSizeController_vanilla-defaults.json`) as the authoritative source of vanilla stack sizes. By default (`UpdateVanillaDefaultsFromRepo: false`) this file is **not** overwritten from the GitHub repo on load — the repo copy is stale and missing items added by newer Rust updates, and overwriting it caused those items to lose their vanilla anchor. The repo is only fetched once to bootstrap when no local datafile exists, or on every load if you set `UpdateVanillaDefaultsFromRepo: true`. +* Some items are **never resized** regardless of multipliers (see "Protected Items" below). * Stacking an item over 2,147,483,647 will cause an error when loaded and will not stack the item at that number. 2,147,483,647 is the max for stack sizes for all stack size plugins as it is a hardcoded limitation of Rust. +## Protected Items (never resized) +Certain items hold variable sub-state (a liquid amount, condition/attachments, etc.). Stacking them merges or duplicates that state, causing item dupes or content loss, so they are always kept at their vanilla stack size: + +* `water`, `water.salt` — the liquid unit items. +* `waterjug`, `botabag`, `smallwaterbottle`, `bucket.water`, `gun.water`, `pistol.water` — liquid **containers** that hold a variable amount of liquid. +* `cardtable`, `hat.bunnyhat`, `rustige_egg_e` — misc items that misbehave when resized. + +In addition, if `AllowStackingItemsWithDurability` is `false`, every item with durability (weapons, armor, tools — anything with condition/attachment sub-state) is forced back to its vanilla stack size. Set this to `false` if you are seeing armor/weapons dupe their attachments or slotted items when stacked. + + ## Installation Instructions * Put plugin in oxide/plugins. * Start server and wait for StackSizeController to be loaded. * Open the configuration and modify settings as needed. Setting individual stack sizes is done in the configuration NOT datafile in value IndividualItemStackSize which is generated on plugin load. * Run o.reload StackSizeController in console to set configured stack sizes. +### Keeping vanilla defaults current after a Rust update +Because the repo defaults file is stale, the recommended way to pick up items added by a game update is to regenerate the local datafile from your own server: + +1. Temporarily set `GlobalStackMultiplier` and every `CategoryStackMultipliers` entry to `1` (so nothing is modified away from vanilla), then reload the plugin. +2. Run `stacksizecontroller.vd` in the console. This reverts stacks to vanilla, writes every current item definition's stack size to `StackSizeController_vanilla-defaults.json`, then restores your custom sizes. +3. Restore your multipliers. + +With `UpdateVanillaDefaultsFromRepo` left at `false`, that generated file stays authoritative and won't be overwritten. Re-run after each wipe/game update to capture newly added items. + ## Console Commands ### **stacksizecontroller.itemsearch** @@ -83,6 +104,15 @@ ##### Updates configuration file, changing every category to the defined multiplier. +---- + +### **stacksizecontroller.vd** +##### **Permission:** `stacksizecontroller.vd` +##### **Usage:** `stacksizecontroller.vd` +##### **Parameters:** `No Parameters` + +##### Regenerates the local `StackSizeController_vanilla-defaults.json` datafile from the current server's item definitions, then restores your custom stack sizes. Run it (with all multipliers temporarily set to `1`) after a Rust update to capture newly added items. See "Keeping vanilla defaults current after a Rust update" above. + ## Configuration @@ -92,6 +122,7 @@ "RevertStackSizesToVanillaOnUnload": true, "AllowStackingItemsWithDurability": true, "HidePrefixWithPluginNameInMessages": false, + "UpdateVanillaDefaultsFromRepo": false, "GlobalStackMultiplier": 1.0, "CategoryStackMultipliers": { "Weapon": 1.0, @@ -117,8 +148,8 @@ "IndividualItemStackSize": {}, "VersionNumber": { "Major": 4, - "Minor": 0, - "Patch": 0 + "Minor": 1, + "Patch": 4 } } ``` @@ -129,6 +160,7 @@ "RevertStackSizesToVanillaOnUnload": true, "AllowStackingItemsWithDurability": true, "HidePrefixWithPluginNameInMessages": false, + "UpdateVanillaDefaultsFromRepo": false, "GlobalStackMultiplier": 1.0, "CategoryStackMultipliers": { "Weapon": 1.0, @@ -172,15 +204,16 @@ }, "VersionNumber": { "Major": 4, - "Minor": 0, - "Patch": 0 + "Minor": 1, + "Patch": 4 } } ``` - `RevertStackSizesToVanillaOnUnload` - If true; item stacksizes are returned to vanilla defaults on plugin unload. -- `AllowStackingItemsWithDurability` - If enabled, items with durability such as weapons can be stacked if they are at full durability. If disabled items with durability can't be stacked at all. (Contents, attachments and ammo are all returned to the player) +- `AllowStackingItemsWithDurability` - If enabled, items with durability such as weapons can be stacked if they are at full durability. If disabled items with durability can't be stacked at all. (Contents, attachments and ammo are all returned to the player.) Set to `false` if armor/weapons are duping slotted items or attachments when stacked. - `HidePrefixWithPluginNameInMessages` - Currently does nothing. Future version will hide the prefix from chat messages in-game. +- `UpdateVanillaDefaultsFromRepo` - If `false` (default), the local `StackSizeController_vanilla-defaults.json` datafile is authoritative and is not overwritten from the GitHub repo on load (the repo copy is stale). The repo is still fetched once to bootstrap if no local datafile exists. Set to `true` to restore the old always-download-from-repo behavior. - `GlobalStackMultiplier` - Multiplies all item stacks by this value. - `CategoryStackMultipliers` - Each category will multiply stacks for those items by the defined amount. - `IndividualItemStackMultipliers` - Accepts "item_id": multiplier. Use stacksizecontroller.itemsearch to find the item id easily. @@ -196,4 +229,11 @@ ```csharp bool? OnVendorHeliFuelAdjust(MiniCopter heli) -``` \ No newline at end of file +``` + +## Changelog + +### 4.1.4 +- **Protected liquid containers from resizing.** Added `waterjug`, `botabag`, `smallwaterbottle`, `bucket.water`, `gun.water`, and `pistol.water` to the ignore list. Previously only the liquid unit items (`water`, `water.salt`) were protected, so the containers holding those liquids were still being resized and could dupe or lose their contents when stacked. +- **Stopped the stale repo defaults from clobbering local edits.** The local `StackSizeController_vanilla-defaults.json` datafile is now authoritative and is no longer overwritten from GitHub on every load. Added the `UpdateVanillaDefaultsFromRepo` config option (default `false`). This fixes newer items (e.g. `hazmatsuit.pilot`) that were missing from the stale repo file losing their vanilla anchor, falling back to their live (already-modified) stack size, and compounding on reload. +- **Fixed `itemsearch` / `listcategoryitems` crashing.** These commands indexed the defaults dictionary directly and threw `KeyNotFoundException` for any item missing from the file; they now use `GetVanillaStackSize()`. Incorporates [PR #27](https://github.com/AnExiledDev/StackSizeController/pull/27) by IsaiahPetrichor: null-guards on `displayName`/`displayDescription` in `itemsearch` (null on some item definitions) and a `ContainsKey` guard on the category multiplier lookup in `listcategoryitems`. diff --git a/StackSizeController.cs b/StackSizeController.cs index c676b4d..f88aab3 100644 --- a/StackSizeController.cs +++ b/StackSizeController.cs @@ -8,9 +8,39 @@ using Oxide.Core.Plugins; using UnityEngine; +// --------------------------------------------------------------------------- +// Patch notes (4.1.4) +// Fixes for stateful items being made stackable and for items missing from +// the (stale) repo vanilla-defaults file being mis-anchored / crashing: +// +// 1. _ignoreList now also protects liquid CONTAINERS (waterjug, botabag, +// smallwaterbottle, bucket.water, gun.water, pistol.water). Previously +// only the liquid unit items ("water", "water.salt") were protected, so +// the containers holding those liquids were still being resized and could +// dupe/lose their contents when stacked. +// +// 2. DownloadVanillaDefaults() now prefers the locally-maintained +// "_vanilla-defaults" datafile instead of overwriting it from the +// repo on every load. The repo file is stale and missing items added by +// newer Rust updates (e.g. hazmatsuit.pilot); overwriting it meant those +// items had no fixed vanilla anchor and fell back to their live (already +// modified) stack size, which then compounded on reload. The repo is only +// fetched on first install (no local file) or when the new config option +// UpdateVanillaDefaultsFromRepo is set to true. +// +// 3. ItemSearchCommand / ListCategoryItemsCommand now use GetVanillaStackSize() +// instead of indexing _vanillaDefaults directly, which threw +// KeyNotFoundException for any item missing from the defaults file. +// +// 4. Additional null checks for those two commands, incorporating PR #27 by +// IsaiahPetrichor: guard displayName/displayDescription (null on some item +// definitions) in ItemSearchCommand, and guard the CategoryStackMultipliers +// lookup with ContainsKey in ListCategoryItemsCommand. +// --------------------------------------------------------------------------- + namespace Oxide.Plugins { - [Info("Stack Size Controller", "AnExiledDev/patched by chrome", "4.1.3")] + [Info("Stack Size Controller", "AnExiledDev/patched by chrome", "4.1.4")] [Description("Allows configuration of most items max stack size.")] class StackSizeController : CovalencePlugin { @@ -24,8 +54,18 @@ class StackSizeController : CovalencePlugin private readonly List _ignoreList = new List { + // Liquid unit items (the contents). "water", "water.salt", + // Liquid CONTAINERS - hold a variable amount of liquid as sub-state, + // so stacking them dupes/loses contents. Keep them at vanilla (1). + "waterjug", + "botabag", + "smallwaterbottle", + "bucket.water", + "gun.water", + "pistol.water", + // Misc items that misbehave when resized. "cardtable", "hat.bunnyhat", "rustige_egg_e" @@ -77,6 +117,13 @@ private class Configuration public bool AllowStackingItemsWithDurability = true; public bool HidePrefixWithPluginNameInMessages; + // Patch (4.1.4): When false (default), the locally-maintained + // vanilla-defaults datafile is used and never overwritten by the + // repo copy, so manually-added newer items are preserved. The repo + // is still fetched once on first install (when no local file exists). + // Set to true to restore the old always-download-from-repo behavior. + public bool UpdateVanillaDefaultsFromRepo = false; + public float GlobalStackMultiplier = 1; public Dictionary CategoryStackMultipliers = GetCategoriesAndDefaults(1) .ToDictionary(k => k.Key, @@ -395,9 +442,12 @@ private void ItemSearchCommand(IPlayer player, string command, string[] args) string.Format(GetMessage("NotEnoughArguments", player.Id), 1)); } + // Patch (4.1.4, from PR #27 by IsaiahPetrichor): null-guard displayName / + // displayDescription, which are null on some item definitions and were + // throwing NullReferenceException and making this command unusable. List itemDefinitions = ItemManager.itemList.Where(itemDefinition => - itemDefinition.displayName.english.Contains(args[0]) || - itemDefinition.displayDescription.english.Contains(args[0]) || + (itemDefinition.displayName?.english != null && itemDefinition.displayName.english.Contains(args[0])) || + (itemDefinition.displayDescription?.english != null && itemDefinition.displayDescription.english.Contains(args[0])) || itemDefinition.shortname.Equals(args[0]) || itemDefinition.shortname.Contains(args[0])) .ToList(); @@ -407,8 +457,10 @@ private void ItemSearchCommand(IPlayer player, string command, string[] args) foreach (ItemDefinition itemDefinition in itemDefinitions) { + // Patch (4.1.4): use GetVanillaStackSize() so items missing from the + // defaults file don't throw KeyNotFoundException. output.AddRow(itemDefinition.itemid.ToString(), itemDefinition.shortname, - itemDefinition.category.ToString(), _vanillaDefaults[itemDefinition.shortname].ToString("N0"), + itemDefinition.category.ToString(), GetVanillaStackSize(itemDefinition).ToString("N0"), Mathf.Clamp(GetStackSize(itemDefinition), 0, int.MaxValue).ToString("N0")); } @@ -443,10 +495,17 @@ private void ListCategoryItemsCommand(IPlayer player, string command, string[] a foreach (ItemDefinition itemDefinition in ItemManager.GetItemDefinitions() .Where(itemDefinition => itemDefinition.category == itemCategory)) { + // Patch (4.1.4): use GetVanillaStackSize() so items missing from the + // defaults file don't throw KeyNotFoundException. + // Category-multiplier ContainsKey guard from PR #27 by IsaiahPetrichor. + float categoryMultiplier = _config.CategoryStackMultipliers.ContainsKey(itemDefinition.category.ToString()) + ? _config.CategoryStackMultipliers[itemDefinition.category.ToString()] + : 1f; + output.AddRow(itemDefinition.itemid.ToString(), itemDefinition.shortname, - itemDefinition.category.ToString(), _vanillaDefaults[itemDefinition.shortname].ToString("N0"), + itemDefinition.category.ToString(), GetVanillaStackSize(itemDefinition).ToString("N0"), Mathf.Clamp(GetStackSize(itemDefinition), 0, int.MaxValue).ToString("N0"), - _config.CategoryStackMultipliers[itemDefinition.category.ToString()].ToString()); + categoryMultiplier.ToString()); } player.Reply(output.ToString()); @@ -486,6 +545,38 @@ private void GenerateVanillaStackSizeFile() private void DownloadVanillaDefaults() { + // Patch (4.1.4): Prefer the locally-maintained vanilla defaults datafile so + // manual additions of newer items (items added by Rust updates that the stale + // repo file is missing, e.g. hazmatsuit.pilot) are not overwritten on every + // load. Only fetch from the repo when explicitly opted in via config, or when + // no local datafile exists yet (first install, to bootstrap). + if (!_config.UpdateVanillaDefaultsFromRepo) + { + Dictionary localDefaults = null; + + try + { + localDefaults = Interface.Oxide.DataFileSystem.ReadObject>( + nameof(StackSizeController) + "_vanilla-defaults"); + } + catch (Exception ex) + { + LogWarning($"Could not read local vanilla defaults datafile, will fetch from repo instead. {ex.Message}"); + } + + if (localDefaults != null && localDefaults.Count > 0) + { + Log("Using locally-maintained vanilla defaults datafile (repo download skipped). " + + "Set UpdateVanillaDefaultsFromRepo to true in the config to fetch from GitHub instead."); + + SetVanillaDefaults(200, JsonConvert.SerializeObject(localDefaults)); + + return; + } + + Log("No local vanilla defaults datafile found; fetching once from the repo to bootstrap."); + } + Log($"Acquiring vanilla defaults file from official GitHub repo and overwriting; {_vanillaDefaultsUri}"); try @@ -620,4 +711,4 @@ private void RevertStackSizes() #endregion } -} \ No newline at end of file +}