Skip to content
RustPluginGenerator

OnNetworkSubscriptionsUpdate

OnNetworkSubscriptionsUpdate is called from Network.Networkable.UpdateSubscriptions after the game has computed which visibility groups to add and remove for a networkable, and before those changes are applied.

Signature
object OnNetworkSubscriptionsUpdate(Network.Networkable networkable, List<Network.Visibility.Group> addedGroups, List<Network.Visibility.Group> removedGroups)

Parameters

Hook parameters
ParameterTypeDescription
networkableNetwork.NetworkableThe Network.Networkable whose visibility subscriptions are being recomputed, usually a BasePlayer or another networked entity. Compare it against player.net.connection to identify the owner.
addedGroupsList<Network.Visibility.Group>List of Network.Visibility.Group entries the networkable is about to subscribe to. Modifying this list changes what the client starts receiving before the game applies the delta.
removedGroupsList<Network.Visibility.Group>List of Network.Visibility.Group entries the networkable is about to drop. Removing an entry from this list prevents the unsubscribe for that group.

Return value

Returning null lets the original code continue and apply the add/remove lists as computed. Returning any non-null value (including false) aborts the original subscription update for that tick, so the player's visibility groups are not changed.

How it works

The hook fires inside Network.Networkable.UpdateSubscriptions, which the server runs periodically for every networkable to sync its visibility group subscriptions with the client. The game first gathers the desired groups with SubStrategy.GatherSubscriptions, then diffs them against the current set with ListHashSet<Group>.Compare, producing two pooled lists: groups to add and groups to remove.

The hook receives the networkable being updated and both lists. The networkable argument is typically a player's network connection wrapper - player.net.connection is a Network.Networkable - but it also runs for other networked entities. The add/remove lists are pooled and reused, so treat them as read-only unless you specifically intend to alter the subscription delta for this tick.

Because the hook fires for every networkable on every subscription update, it runs very often on populated servers. Keep the body cheap: an early identity check on the networkable, then bail. Do not log or allocate per call. You can narrow the networkable parameter to a more specific type if one fits, but Oxide's dispatch only filters when the declared type matches the runtime object - a Network.Networkable parameter matches every call.

Example plugin: Refuse subscription updates for a whitelist of players

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

namespace Oxide.Plugins
{
    [Info("Subscription Guard", "RustPluginGenerator", "1.0.0")]
    [Description("Lets admins freeze a player's network subscription updates for inspection.")]
    public class SubscriptionGuard : RustPlugin
    {
        private const string PermUse = "subscriptionguard.use";

        private PluginConfig config;
        private StoredData data;

        private class PluginConfig
        {
            [JsonProperty("Broadcast freeze to admins only")]
            public bool AdminOnlyNotice { get; set; } = true;
        }

        private class StoredData
        {
            public HashSet<string> Frozen = new HashSet<string>();
        }

        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>
            {
                ["Frozen"] = "{0} subscription updates are now frozen on {1}.",
                ["Unfrozen"] = "{0} subscription updates are now flowing on {1}.",
                ["Usage"] = "Usage: /subguard <player name>"
            }, 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()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
        }

        private void Unload()
        {
            if (data != null)
                Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        [ChatCommand("subguard")]
        private void CmdSubGuard(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("Usage", player.UserIDString));
                return;
            }

            if (args == null || args.Length < 1)
            {
                player.ChatMessage(Msg("Usage", player.UserIDString));
                return;
            }

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

            if (data.Frozen.Contains(target.UserIDString))
            {
                data.Frozen.Remove(target.UserIDString);
                player.ChatMessage(Msg("Unfrozen", player.UserIDString, target.displayName, target.UserIDString));
            }
            else
            {
                data.Frozen.Add(target.UserIDString);
                player.ChatMessage(Msg("Frozen", player.UserIDString, target.displayName, target.UserIDString));
            }

            Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        private object OnNetworkSubscriptionsUpdate(Network.Networkable networkable, List<Network.Visibility.Group> addedGroups, List<Network.Visibility.Group> removedGroups)
        {
            if (networkable == null || data == null || data.Frozen.Count == 0)
                return null;

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || player.net == null || player.net.connection == null)
                    continue;

                // Only block subscription updates that belong to a frozen player.
                if (!data.Frozen.Contains(player.UserIDString))
                    continue;

                return false;
            }

            return null;
        }
    }
}

The plugin tracks a per-player toggle stored in a JSON data file. When OnNetworkSubscriptionsUpdate fires, it scans BasePlayer.activePlayerList and, if any active player is in the frozen set, returns false. Because the check is not tied to the networkable argument, this blocks the hook whenever a frozen player is online, not only for the player whose subscriptions changed. An admin command /subguard <name> flips the flag, reports the new state through the lang file, and writes the data file immediately; it is also written on unload.

Common use cases

  • Freeze visibility for spectating admins

    Return false for a player flagged as frozen so their client keeps the current entity set and does not churn subscriptions while an admin is inspecting a base.

  • Audit subscription churn on specific entities

    When the networkable belongs to a whitelisted prefab, record the size of the added and removed lists to a rolling counter to find entities causing excessive network updates.

  • Hold subscriptions during a scripted event

    During a canned PvP event, skip subscription updates for participants so their clients only see the arena groups set at event start.

Pitfalls

  • Runs for every networkable, not just players

    The hook fires for all networked entities, so resolving a BasePlayer requires comparing against player.net.connection rather than casting the argument.

  • Lists are pooled

    The added and removed lists come from Oxide's pool and are reused after the call. Do not keep references to them or modify them unless you intend to change the delta for this tick.

  • Returning false stops the whole update

    A non-null return skips the original method entirely for that networkable, so both additions and removals are abandoned - not just the ones you wanted to block.

Build a plugin with OnNetworkSubscriptionsUpdate

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

Prompt: Freeze visibility for spectating admins using the OnNetworkSubscriptionsUpdate 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 OnNetworkSubscriptionsUpdate

Generate