How to make a Rust stack size plugin (global, category and per-item multipliers)
Rewrites ItemDefinition.stackable on server start using a global multiplier, per-category multipliers and per-item overrides, then restores the original values on unload. Uses OnServerInitialized and Unload.
Rust caps every item at its hard-coded stack size: 1000 for wood, 32 for rifle ammo, 1 for a rocket launcher. Admins who want bigger stacks usually edit the game files or run several plugins that fight each other. This plugin changes stack sizes in memory instead. On server start it saves each ItemDefinition's original stackable value, then writes a new value computed as original * globalMultiplier * categoryMultiplier, unless the shortname has a per-item override, which replaces the result outright. Categories map to the ItemCategory of each definition (Resources, Ammunition, Component and so on) plus an Overrides block keyed by shortname and an ExcludedCategories list for categories you want untouched. The originals are kept in memory and written back on unload, so reloading the plugin never compounds multipliers. A /stackinfo <shortname> command shows the current and original value of any item so admins can check what a config change did.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Stack Sizes", "RustPluginGenerator", "1.0.0")]
[Description("Scales item stack sizes with global, per-category and per-item settings and restores originals on unload.")]
public class StackSizes : RustPlugin
{
private const string PermUse = "stacksizes.use";
private PluginConfig config;
// itemid -> vanilla stackable value, captured before the first change
private readonly Dictionary<int, int> originals = new Dictionary<int, int>();
private class PluginConfig
{
[JsonProperty("Global Multiplier")]
public float GlobalMultiplier = 2f;
[JsonProperty("Category Multipliers")]
public Dictionary<string, float> CategoryMultipliers = new Dictionary<string, float>
{
["Resources"] = 4f,
["Ammunition"] = 2f,
["Component"] = 2f
};
[JsonProperty("Per-Item Overrides")]
public Dictionary<string, int> Overrides = new Dictionary<string, int>
{
["rifle.ammo"] = 128,
["pistol.ammo"] = 128
};
[JsonProperty("Excluded Categories")]
public List<string> ExcludedCategories = new List<string>
{
"Weapon",
"Attire"
};
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["UnknownItem"] = "No item definition found for '{0}'.",
["Info"] = "{0} ({1}): original {2}, current {3}, effective x{4}{5}",
["OverrideNote"] = " (override)",
["Denied"] = "You do not have permission to use this command."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
=> string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermUse, this);
}
private void OnServerInitialized(bool initial)
{
ApplyAll();
}
private void Unload()
{
RestoreAll();
}
private bool IsExcluded(ItemDefinition definition)
{
foreach (var name in config.ExcludedCategories)
{
ItemCategory category;
if (Enum.TryParse(name, true, out category) && category == definition.category)
return true;
}
return false;
}
private float CategoryMultiplier(ItemDefinition definition)
{
float value;
if (config.CategoryMultipliers != null && config.CategoryMultipliers.TryGetValue(definition.category.ToString(), out value))
return value;
return 1f;
}
private void ApplyAll()
{
var global = Mathf.Max(0.01f, config.GlobalMultiplier);
foreach (var definition in ItemManager.itemList)
{
if (definition == null || IsExcluded(definition))
continue;
if (!originals.ContainsKey(definition.itemid))
originals[definition.itemid] = definition.stackable;
int result;
if (config.Overrides != null && config.Overrides.TryGetValue(definition.shortname, out result))
{
definition.stackable = Mathf.Max(1, result);
continue;
}
var scaled = originals[definition.itemid] * global * CategoryMultiplier(definition);
definition.stackable = Mathf.Max(1, Mathf.RoundToInt(scaled));
}
}
private void RestoreAll()
{
if (ItemManager.itemList != null)
{
foreach (var definition in ItemManager.itemList)
{
int original;
if (definition != null && originals.TryGetValue(definition.itemid, out original))
definition.stackable = original;
}
}
originals.Clear();
}
[ChatCommand("stackinfo")]
private void CmdStackInfo(BasePlayer player, string command, string[] args)
{
if (player == null)
return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("Denied", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, "Usage: /stackinfo <shortname>");
return;
}
var definition = ItemManager.FindItemDefinition(args[0]);
if (definition == null)
{
SendReply(player, Msg("UnknownItem", player.UserIDString, args[0]));
return;
}
int original;
if (!originals.TryGetValue(definition.itemid, out original))
original = definition.stackable;
var effective = original > 0 ? (float)definition.stackable / original : 1f;
var overrideActive = config.Overrides != null && config.Overrides.ContainsKey(definition.shortname);
SendReply(player, Msg("Info", player.UserIDString, definition.displayName.english, definition.shortname,
original, definition.stackable, effective.ToString("0.##"), overrideActive ? Msg("OverrideNote", player.UserIDString) : string.Empty));
}
}
}Hooks used: OnServerInitialized
Config file
Created automatically on first load at oxide/config/StackSizes.json.
{
"Global Multiplier": 2,
"Category Multipliers": {
"Resources": 4,
"Ammunition": 2,
"Component": 2
},
"Per-Item Overrides": {
"rifle.ammo": 128,
"pistol.ammo": 128
},
"Excluded Categories": [
"Weapon",
"Attire"
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/stackinfo <shortname> | Shows the current stackable value of an item, its original value and whether an override is active. | stacksizes.use |
| Permission | Grants |
|---|---|
stacksizes.use | Grants access to the /stackinfo chat command. |
How the code works
Snapshot the original values
The plugin keeps a
Dictionary<int, int>keyed byItemDefinition.itemidthat stores the value each definition had before the plugin touched it. It only records a definition the first time it is processed, so reloading the plugin or calling the apply pass twice never overwrites the real original with an already-multiplied value. This is what makes the restore inUnloadexact.Apply in OnServerInitialized
OnServerInitialized(bool initial)runs after the item definitions are loaded, which is the earliest point whereItemManager.itemListis complete. The plugin sweeps everyItemDefinition, skips the excluded categories, computes the new value and assigns it todefinition.stackable. Nothing else writes to disk, so a plugin reload is the only way a change reaches a running server's definitions.The multiplier formula
For each definition the plugin takes the stored original, multiplies by
Global Multiplierand by the category multiplier fromCategory Multipliers(default 1.0 when the category is missing). The result is rounded and clamped to at least 1. If the shortname appears inPer-Item Overrides, that absolute value wins and the multipliers are ignored, which is the clean way to fix a single item such as ammo.Excluding categories
Excluded Categoriesis a list ofItemCategorynames such asWeaponorAttire. Definitions in those categories are never touched and keep their vanilla value. The plugin parses each string once withEnum.TryParse, to keep it readable when the category name is misspelled instead of throwing inside the loop.Restoring on unload
Unload()walks the stored map and writes every original value back to itsItemDefinition, then clears the cache. The permission registration is removed by Oxide when the plugin unloads. Because the values live only in memory, a server restart also restores vanilla. Nothing is written tooxide/data.Checking a value in game
/stackinfo <shortname>looks the definition up withItemManager.FindItemDefinition, prints the original, the current and the effective multiplier, and tells you whether an override is active. It requiresstacksizes.useand works for any connected player, which makes testing a config change on a live server quick.
Install it on your server
Save StackSizes.cs
Keep the file name exactly as the class name. Oxide matches them.Upload it to the plugins folder
Oxide:oxide/plugins/. Carbon:carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.Edit the config if you want
On first load the default config is written tooxide/config/StackSizes.json(Carbon:carbon/configs/StackSizes.json). After editing, reload it:oxide.reload StackSizes c.reload StackSizes # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default stacksizes.use c.grant group default stacksizes.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Round to the nearest vanilla multiple
Keeps stacks as whole multiples of the original value so inventory UIs look tidy.
Generate this versionBoost only for privileged players
Applies the multiplier to a player's inventory items instead of the global definitions.
Generate this versionReload command for admins
Adds a console command that re-reads the config and reapplies values without a plugin reload.
Generate this version
FAQ
Do stack sizes change for items already in containers?
No. Existing item stacks keep their amount and only the definition's limit changes, so a container with 5 stacks of 1000 wood stays as it is until it is merged or refilled. New stacks can now grow past the old cap.
Why does a `/stackinfo` result differ from the config?
The multiplier is applied to the original value, not to the value you see in game. If the original was 32 and your global multiplier is 5, the result is 160 even if you set a category multiplier as well. Check the Per-Item Overrides entry first; an override replaces the computed value.
Does this work on Carbon?
Yes. Carbon loads Oxide plugins unchanged, and the hooks used here (OnServerInitialized, Unload) are supported. Carbon's own stack-size options are separate; disable them to avoid two systems writing to the same definition.
How do I install it?
Drop the compiled DLL into oxide/plugins on Oxide or carbon/plugins on Carbon. The plugin generates oxide/config/StackSizes.json with defaults on first load; edit it and run oxide.reload StackSizes to apply.
What happens on server restart?
The plugin reapplies the config from scratch on the next OnServerInitialized. Item definitions reset to vanilla during startup, so a restart is the cleanest way to undo a bad multiplier without touching the file.
Customize it for your server
Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More quality of life plugins
- BackpacksAdds a /backpack command that opens a per-player StorageContainer sized by permission, with contents persisted to oxide/data and optionally droppedโฆOnPlayerLootEndOnPlayerDisconnectedOnPlayerDeath
- Crafting speedA plugin that speeds up crafting by scaling the duration of ItemCraftTask instances when a craft starts, using a global multiplier plus optionalโฆOnItemCraft
- Day and night controlSkips or shortens night by advancing the in-game clock to a configurable morning time, and adds /day and /night admin commands.OnServerInitialized
Last updated Oct 26, 2026.
Customize stack sizes
Customize