Skip to content
RustPluginGenerator

OnInventoryNetworkUpdate

OnInventoryNetworkUpdate is called inside PlayerInventory.SendUpdatedInventoryInternal just before the player's container update is written to the network layer. Returning a non-null value aborts the send, keeping the stale state on the client.

Signature
object OnInventoryNetworkUpdate(PlayerInventory playerInventory, ItemContainer container, ProtoBuf.UpdateItemContainer updateItemContainer, PlayerInventory.Type type, PlayerInventory.NetworkInventoryMode mode)

Parameters

Hook parameters
ParameterTypeDescription
playerInventoryPlayerInventoryThe PlayerInventory instance that is building this network update; use its playerOwner to reach the BasePlayer.
containerItemContainerThe ItemContainer being serialized into the outgoing message (main, belt, wear, or a separate container such as a lootable).
updateItemContainerProtoBuf.UpdateItemContainerThe ProtoBuf.UpdateItemContainer message being prepared for the client, including the already-saved container list.
typePlayerInventory.TypePlayerInventory.Type indicating which of the player's own containers this update covers (Main, Belt, Wear, Backpack).
modePlayerInventory.NetworkInventoryModePlayerInventory.NetworkInventoryMode controlling how much of the container is serialized, e.g. LocalPlayer, Eyes, Slots, or LocalPlayerUpdate.

Return value

Return null to let the update be sent as normal. Returning any non-null object short-circuits the method through the injection guard, so the prepared update is never passed on and the client keeps its previous view of the container.

How it works

The hook fires every time the server is about to push a container snapshot to a client, inside PlayerInventory.SendUpdatedInventoryInternal. That method runs for the player's own main, belt and wear containers, and also when container contents need resending after changes. The container has already been marked clean and serialized into updateItemContainer.container when the hook runs, and mode decides which slots are included (for example wear containers are only fully serialized for the local player).

Use playerInventory.playerOwner to identify the player and container to inspect the item list. Because this is a per-client network path, it fires frequently for active players and during inventory churn, so keep the body cheap and never print to console here. Declaring playerInventory more specifically is not possible since the type is exact. You can read type and mode to scope logic to a single container without scanning every slot.

Returning non-null stops the original send. That means the client keeps whatever it previously received, so the container appears frozen until a later update is allowed through. Use this only for deliberate cases such as masking a container from a viewer.

Example plugin: Block wear-container network updates for masked players

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

namespace Oxide.Plugins
{
    [Info("InventoryUpdateMask", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks wear-container network updates for players added to a staff-managed mask list")]
    public class InventoryUpdateMask : RustPlugin
    {
        private const string PermUse = "inventoryupdatemask.use";

        private PluginConfig config;
        private HashSet<string> masked;

        private class PluginConfig
        {
            [JsonProperty("Maximum masked players")]
            public int MaxMasked = 5;
        }

        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>
            {
                ["Masked"] = "Container updates for {0} are now blocked.",
                ["Unmasked"] = "Container updates for {0} are restored.",
                ["Full"] = "Mask list is full. Unmask a player first.",
                ["Unknown"] = "No player found for '{0}'.",
                ["List"] = "Masked players: {0}"
            }, 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);
            masked = Interface.Oxide.DataFileSystem.ReadObject<HashSet<string>>(Name) ?? new HashSet<string>();
        }

        private void Unload()
        {
            Interface.Oxide.DataFileSystem.WriteObject(Name, masked);
        }

        [ChatCommand("inventorymask")]
        private void CmdMask(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsAdmin) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse)) return;

            if (args == null || args.Length == 0)
            {
                string list = masked.Count == 0 ? "none" : string.Join(", ", masked);
                SendReply(player, Msg("List", player.UserIDString, list));
                return;
            }

            BasePlayer target = BasePlayer.FindAwakeOrSleeping(args[0]);
            if (target == null)
            {
                SendReply(player, Msg("Unknown", player.UserIDString, args[0]));
                return;
            }

            string id = target.UserIDString;
            if (masked.Remove(id))
            {
                SendReply(player, Msg("Unmasked", player.UserIDString, target.displayName));
            }
            else
            {
                if (masked.Count >= config.MaxMasked)
                {
                    SendReply(player, Msg("Full", player.UserIDString));
                    return;
                }
                masked.Add(id);
                SendReply(player, Msg("Masked", player.UserIDString, target.displayName));
            }
            Interface.Oxide.DataFileSystem.WriteObject(Name, masked);
        }

        private object OnInventoryNetworkUpdate(PlayerInventory playerInventory, ItemContainer container, ProtoBuf.UpdateItemContainer updateItemContainer, PlayerInventory.Type type, PlayerInventory.NetworkInventoryMode mode)
        {
            if (playerInventory == null || container == null || masked == null) return null;
            if (type != PlayerInventory.Type.Wear) return null;

            BasePlayer owner = playerInventory.baseEntity as BasePlayer;
            if (owner == null || !owner.IsConnected) return null;
            if (!masked.Contains(owner.UserIDString)) return null;

            return true;
        }
    }
}

The plugin keeps a set of user IDs whose player-inventory network updates are dropped, managed with the admin-only inventorymask chat command that also requires the inventoryupdatemask.use permission. On each OnInventoryNetworkUpdate call it checks the owner from playerInventory.baseEntity; if the owner is connected and on the list, it returns a non-null value to stop the send. Only PlayerInventory.Type.Wear updates are blocked, so other container updates continue normally. Running the command with a name or Steam ID toggles the entry: an existing entry is removed and restored, otherwise the player is added up to the Maximum masked players config value. The set is saved to the InventoryUpdateMask data file when it changes and on plugin unload.

Common use cases

  • Mask container contents for spectators

    Stop specific players from receiving updates for a given container so their client keeps showing the previous state while staff review a scene.

  • Audit high-frequency inventory churn

    Count how often a container is reserialized per player for a short diagnostic window, keeping the logic allocation-free inside the hook.

  • Gate sends by inventory mode

    Only allow updates for NetworkInventoryMode.LocalPlayer on a container to keep other modes from being delivered, useful when testing custom inventory handling.

Pitfalls

  • Runs on a hot network path

    SendUpdatedInventoryInternal fires repeatedly for every active player. Do not log, allocate lists, or iterate every item unless you have measured the cost on a populated server.

  • Client state goes stale, not empty

    Returning non-null skips the send but does not clear the client cache. The viewer continues to see the last snapshot, so blocked updates can look like a frozen container rather than an empty one.

  • Container is already serialized

    By the time the hook runs the container has been saved into the protobuf message and marked clean. Mutating items at this point will not be reflected in that message.

Build a plugin with OnInventoryNetworkUpdate

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

Prompt: Mask container contents for spectators using the OnInventoryNetworkUpdate 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 OnInventoryNetworkUpdate

Generate