Skip to content
RustPluginGenerator

OnNetworkSubscriptionsGather

OnNetworkSubscriptionsGather is called at the start of NetworkVisibilityGrid.GetVisibleFrom, before the grid populates the visible-group set for a network group. Returning a non-null value skips the game's own visibility gathering for that query.

Signature
object OnNetworkSubscriptionsGather(NetworkVisibilityGrid networkVisibilityGrid, Network.Visibility.Group group, ListHashSet<Network.Visibility.Group> groups, int radius)

Parameters

Hook parameters
ParameterTypeDescription
networkVisibilityGridNetworkVisibilityGridThe NetworkVisibilityGrid instance that is computing visibility (the object GetVisibleFrom was called on). Useful mainly for identification; you cannot meaningfully mutate it.
groupNetwork.Visibility.GroupThe Network.Visibility.Group whose visible neighbours are being gathered (usually the group of a connecting or moving player).
groupsListHashSet<Network.Visibility.Group>The ListHashSet of groups the grid is about to add the visible groups into. When you return non-null before the game fills it, it is still the caller-provided set, so treat it as read-only input.
radiusintThe grid radius (in grid cells) being used for this visibility query.

Return value

Returning null (or nothing) lets GetVisibleFrom continue and populate the visible-group set as normal. Returning any non-null object returns from GetVisibleFrom immediately, so no visible groups are gathered for that query and the caller's set is left as it was.

How it works

This hook fires inside NetworkVisibilityGrid.GetVisibleFrom(Group group, ListHashSet<Group> groups, int radius), which is the routine the server uses to work out which network visibility groups a client should be subscribed to. It runs when the server refreshes a player's visibility subscriptions, typically on connect, respawn, teleport and as players move between grid cells. The call happens before the game calls its own GetGlobalNetworkGroups routine, so it fires once per query.

The parameters identify the grid instance, the group being resolved, the set that will receive the visible groups, and the radius in grid cells. Because the call sits at the top of the method, the groups set is not yet populated when your hook runs, so it is not a reliable source of the resulting subscriptions.

Returning non-null stops the original method before it adds any groups. That means the target group gets no visibility neighbours from this query, which is a heavy-handed action and can break client entity streaming for affected players. Returning null lets normal gathering proceed. There is no parameter type that usefully narrows this call: every invocation has the same four types, so filtering has to be done in code by inspecting group or radius.

Example plugin: Report visibility grid queries for diagnostics

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

namespace Oxide.Plugins
{
    [Info("Visibility Grid Monitor", "RustPluginGenerator", "1.0.0")]
    [Description("Counts network visibility grid queries and reports stats on demand.")]
    public class VisibilityGridMonitor : RustPlugin
    {
        private const string PermUse = "visibilitygridmonitor.use";

        private PluginConfig config;
        private long queryCount;
        private int maxRadiusSeen;
        private int queriesOutsideRange;
        private Timer reportTimer;

        private class PluginConfig
        {
            [JsonProperty("Minimum expected radius")]
            public int MinRadius = 1;

            [JsonProperty("Maximum expected radius")]
            public int MaxRadius = 6;

            [JsonProperty("Report every N seconds (0 disables)")]
            public int ReportIntervalSeconds = 60;
        }

        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>
            {
                ["Stats"] = "Visibility queries: {0}. Largest radius seen: {1}. Queries outside range [{2}, {3}]: {4}.",
                ["Reset"] = "Visibility grid counters reset.",
                ["NoPermission"] = "You lack 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()
        {
            if (config.ReportIntervalSeconds > 0)
            {
                reportTimer = timer.Every(config.ReportIntervalSeconds, ReportToConsole);
            }
        }

        private object OnNetworkSubscriptionsGather(NetworkVisibilityGrid grid, Network.Visibility.Group group, ListHashSet<Network.Visibility.Group> groups, int radius)
        {
            queryCount++;
            if (radius > maxRadiusSeen)
            {
                maxRadiusSeen = radius;
            }

            if (radius < config.MinRadius || radius > config.MaxRadius)
            {
                queriesOutsideRange++;
            }

            return null;
        }

        private void ReportToConsole()
        {
            Puts(Msg("Stats", null, queryCount, maxRadiusSeen, config.MinRadius, config.MaxRadius, queriesOutsideRange));
        }

        [ConsoleCommand("visibilitygrid.stats")]
        private void CmdStats(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse) && !arg.IsAdmin)
            {
                arg.ReplyWith(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (arg.GetString(0, "") == "reset")
            {
                queryCount = 0;
                maxRadiusSeen = 0;
                queriesOutsideRange = 0;
                arg.ReplyWith(Msg("Reset", player?.UserIDString));
                return;
            }

            arg.ReplyWith(Msg("Stats", player?.UserIDString, queryCount, maxRadiusSeen, config.MinRadius, config.MaxRadius, queriesOutsideRange));
        }

        private void Unload()
        {
            reportTimer?.Destroy();
            reportTimer = null;
        }
    }
}

The plugin listens on OnNetworkSubscriptionsGather and counts every visibility query it sees rather than altering them. It reads the radius parameter and compares it against a configured baseline, and records the number of distinct queries and the largest radius observed. Every 60 seconds a timer prints a short summary line to the server console, and the same summary can be pulled on demand with the visibilitygrid.stats console command, which works from server console, RCON or a player. A permission gate (visibilitygridmonitor.use) restricts the console command to admins. All hook work is a couple of field increments and a comparison, so the cost per query is negligible; the plugin never returns non-null, so client streaming is unaffected.

Common use cases

  • Diagnose streaming problems

    Log which groups and radii are queried when players report missing entities, to see whether the visibility grid is running the queries you expect.

  • Track grid load on busy servers

    Count visibility queries over time to gauge how much churn movement and massive player counts cause in the networking layer.

  • Detect unexpected radius values

    Warn when a query uses a radius outside the range you consider normal, which can hint at plugins or configs altering network culling.

Pitfalls

  • Returning non-null breaks streaming

    Any non-null return skips the game's own gathering, so the group gets no visible neighbours and clients may stop receiving nearby entities. Only do this if you fully understand the consequences.

  • The groups set is empty at this point

    The hook fires before GetGlobalNetworkGroups runs, so inspecting or modifying groups here does not reflect or replace the final subscriptions.

  • It fires very often

    Visibility queries run per player movement across grid cells, so avoid logging, string formatting or heavy work inside the hook; buffer counts and report elsewhere.

Build a plugin with OnNetworkSubscriptionsGather

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

Prompt: Diagnose streaming problems using the OnNetworkSubscriptionsGather 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 OnNetworkSubscriptionsGather

Generate