Skip to content
RustPluginGenerator

OnItemStacked

OnItemStacked is called after an item is merged into an existing stack inside a container, right before the source item's remaining amount is adjusted.

Signature
void OnItemStacked(Item stack, Item item, ItemContainer container, int amount)

Parameters

Hook parameters
ParameterTypeDescription
stackItemThe existing item in the destination container that received the incoming amount. Read its amount, skin or info to identify the stack.
itemItemThe item that was moved and is being merged into the stack. At this point its own amount has not yet been reduced by the merged amount.
containerItemContainerThe ItemContainer the stack lives in, e.g. a player's main inventory, a box inventory or a belt container.
amountintHow many units were moved from the incoming item into the stack.

Return value

Returning nothing or null has no effect; any value is ignored. The method continues, subtracting the merged amount from the incoming item and removing it from the world when its amount reaches zero.

How it works

The hook fires from Item.MoveToContainer, on the branch where the destination slot already holds a compatible item and the move is resolved as a stack merge rather than a new slot. At that point the destination stack has already had its amount increased and onItemAddedToStack has already run, so the merge is effectively done.

Use stack to read the final quantity of the destination item, item for the incoming item (its amount still reflects the pre-merge value), container to know where the stack lives, and amount for the merged quantity. This is useful for tracking how resources accumulate players, for recognizing stacks that cross a threshold, or for reacting when a specific item type is consolidated.

You cannot cancel or alter the move here: the hook's return value is ignored, and the code that follows adjusts the incoming item's amount and removes it if nothing is left. Because it fires for any merge, including quick-loot, transfers between boxes, pickups into the inventory and crafting output moves, declare parameters narrowly only where it helps; declaring Item and ItemContainer as given is required by the signature. Carbon dispatches the same hook, so the plugin behaves identically there.

Example plugin: Announce stacks of high-value resources

StackAnnouncer.cs
using System.Linq;
using UnityEngine;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Stack Announcer", "RustPluginGenerator", "1.0.0")]
    [Description("Announces when a player merges a watched resource into a large stack")]
    public class StackAnnouncer : RustPlugin
    {
        private const string PermNotify = "stackannouncer.notify";

        private class PluginConfig
        {
            [JsonProperty("Watched shortnames")]
            public List<string> Shortnames = new List<string> { "sulfur", "scrap" };

            [JsonProperty("Notify at stack amount")]
            public int Threshold = 1000;

            [JsonProperty("Per player cooldown in seconds")]
            public float Cooldown = 30f;
        }

        private PluginConfig config;
        private readonly Dictionary<ulong, float> lastNotify = new Dictionary<ulong, float>();

        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>
            {
                ["LargeStack"] = "You now hold {0} {1} in one stack.",
                ["Notify"] = "{0} merged a stack of {1} {2}."
            }, 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(PermNotify, this);
        }

        private void OnItemStacked(Item stack, Item item, ItemContainer container, int amount)
        {
            if (stack?.info == null || container == null) return;
            if (!config.Shortnames.Contains(stack.info.shortname)) return;

            int total = stack.amount;
            if (total < config.Threshold) return;

            BasePlayer owner = item?.GetOwnerPlayer() ?? container.playerOwner;
            if (owner == null || owner.IsNpc) return;

            float now = UnityEngine.Time.realtimeSinceStartup;
            float last;
            if (lastNotify.TryGetValue(owner.userID, out last) && now - last < config.Cooldown) return;
            lastNotify[owner.userID] = now;

            string display = stack.info.displayName.english;
            owner.ChatMessage(Msg("LargeStack", owner.UserIDString, total, display));

            foreach (BasePlayer admin in BasePlayer.activePlayerList)
            {
                if (admin == owner || !admin.IsAdmin) continue;
                if (!permission.UserHasPermission(admin.UserIDString, PermNotify)) continue;
                admin.ChatMessage(Msg("Notify", admin.UserIDString, owner.displayName, total, display));
            }
        }

        private void Unload()
        {
            lastNotify.Clear();
        }
    }
}

The plugin watches stack merges and reports when a player consolidates a watched resource into a large stack. OnItemStacked looks at the shortname of the destination stack and ignores anything not listed in the config. When the stack's new amount reaches the configured threshold it resolves the owner through item.GetOwnerPlayer() (falling back to container.playerOwner), then sends a localized chat line and an optional console message. A short per-player cooldown, kept in a static-free timer reference, prevents chat spam when a player quick-loots a whole container of the same resource. Config holds the watched shortnames, the threshold and the message cooldown.

Common use cases

  • Resource tracking dashboards

    Record how many units of sulfur or scrap a player has merged into a stack and feed that into a stats plugin.

  • Large-stack notifications

    Notify a player or admins when a single stack of a chosen item reaches a configured size.

  • Conditional rewards

    Grant a bonus once a player consolidates a full stack of a resource, checking amount and the destination stack size.

Pitfalls

  • The move already happened

    The destination stack's amount is updated before the hook runs and the return value is ignored. Adjusting stack.amount here fights the game's own bookkeeping instead of preventing the merge.

  • Fires for every merge

    Transfers, quick-loot, pickups and crafting moves all reach this path. Never log or print inside the hook on busy servers; gate with a cheap shortname check first.

  • Owner may be unknown

    item.GetOwnerPlayer() is null for items without a player owner and container.playerOwner can also be null, so handle both before sending messages.

Build a plugin with OnItemStacked

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Resource tracking dashboards using the OnItemStacked hook, with a config file and a permission
Generate this plugin

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnItemStacked

Generate