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.
object OnNetworkSubscriptionsGather(NetworkVisibilityGrid networkVisibilityGrid, Network.Visibility.Group group, ListHashSet<Network.Visibility.Group> groups, int radius)Parameters
| Parameter | Type | Description |
|---|---|---|
networkVisibilityGrid | NetworkVisibilityGrid | The NetworkVisibilityGrid instance that is computing visibility (the object GetVisibleFrom was called on). Useful mainly for identification; you cannot meaningfully mutate it. |
group | Network.Visibility.Group | The Network.Visibility.Group whose visible neighbours are being gathered (usually the group of a connecting or moving player). |
groups | ListHashSet<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. |
radius | int | The 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
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
GetGlobalNetworkGroupsruns, so inspecting or modifyinggroupshere 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.
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