OnPlayerSpectateEnd
OnPlayerSpectateEnd is called when a player's spectator mode is being stopped, before the game finishes clearing the spectating state.
object OnPlayerSpectateEnd(BasePlayer player, string spectateFilter)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer who is leaving spectator mode. Check IsConnected and IsSpectating() before relying on their state, since this fires during the transition. |
spectateFilter | string | The filter string the player was spectating with (e.g. team, players, entities). It reflects the mode the player was in and can be logged or used to branch your logic. |
Return value
Returning null (or nothing) lets StopSpectating proceed and clear the spectator state normally. Returning any non-null value makes the method return immediately, so the player stays in spectator mode; the returned value itself is discarded because the method is void.
How it works
This hook fires from BasePlayer.StopSpectating, the method the game runs when a player leaves spectator mode. Any action that ends spectating goes through it: the player pressing the spectate key again to return, disconnecting, being kicked from the team they were watching, or plugin code calling StopSpectating() directly. It fires once per stop attempt, at the start of the method.
The player argument is the player whose spectating is ending; spectateFilter is the filter string used when spectating began (for example the team or players filter). At the moment the hook runs the player is still flagged as spectating, so IsSpectating() still returns true. Because the parameter is already BasePlayer, narrowing by type does not change dispatch here.
Typical uses are restoring state a plugin applied while spectating (CUI overlays, teleport anchors, vanish flags), writing audit entries for admin spectating, or punishing exploiters who cycle in and out of spectator mode. Do not do heavy work in this hook; it can fire often if a player spams the spectate toggle.
Example plugin: Log and clear spectator overlays when leaving spectate
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Spectate End Notifier", "RustPluginGenerator", "1.0.0")]
[Description("Shows a spectate banner, then clears it and reports session length when a player stops spectating.")]
public class SpectateEndNotifier : RustPlugin
{
private const string PermUse = "spectateendnotifier.use";
private const string BannerName = "SpectateEndNotifier.Banner";
private class PluginConfig
{
[JsonProperty("Broadcast session end to admins")]
public bool AnnounceToAdmins { get; set; } = false;
}
private PluginConfig config;
private readonly Dictionary<ulong, float> sessionStart = 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>
{
["SpectateStarted"] = "Spectating with filter '{0}'. Press the spectate key again to stop.",
["SpectateEnded"] = "Spectate session ended. Filter: {0}, duration: {1}s.",
["NoPermission"] = "You do not have permission to use this.",
["AdminAnnounce"] = "{0} stopped spectating (filter {1}) after {2}s."
}, 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 OnPlayerSpectate(BasePlayer player, string spectateFilter)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PermUse)) return;
sessionStart[player.userID.Get()] = UnityEngine.Time.realtimeSinceStartup;
ShowBanner(player, spectateFilter);
player.ChatMessage(Msg("SpectateStarted", player.UserIDString, spectateFilter));
}
private object OnPlayerSpectateEnd(BasePlayer player, string spectateFilter)
{
if (player == null) return null;
CuiHelper.DestroyUi(player, BannerName);
float start;
if (sessionStart.TryGetValue(player.userID.Get(), out start))
{
sessionStart.Remove(player.userID.Get());
int seconds = Mathf.Max(0, Mathf.RoundToInt(UnityEngine.Time.realtimeSinceStartup - start));
if (player.IsConnected)
{
player.ChatMessage(Msg("SpectateEnded", player.UserIDString, spectateFilter, seconds));
}
if (config.AnnounceToAdmins)
{
foreach (var admin in BasePlayer.activePlayerList)
{
if (admin != null && admin.IsAdmin && admin != player)
{
admin.ChatMessage(Msg("AdminAnnounce", admin.UserIDString, player.displayName, spectateFilter, seconds));
}
}
}
}
return null;
}
private void ShowBanner(BasePlayer player, string spectateFilter)
{
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
Image = { Color = "0.1 0.1 0.1 0.8" },
RectTransform = { AnchorMin = "0.3 0.9", AnchorMax = "0.7 0.96" }
}, "Overlay", BannerName);
container.Add(new CuiLabel
{
Text = { Text = "Spectating: " + spectateFilter, FontSize = 14, Align = TextAnchor.MiddleCenter },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, BannerName);
CuiHelper.DestroyUi(player, BannerName);
CuiHelper.AddUi(player, container);
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player != null) CuiHelper.DestroyUi(player, BannerName);
}
sessionStart.Clear();
}
}
}The plugin shows a small CUI banner while an admin is spectating and tracks how long they watched. When StopSpectating runs, OnPlayerSpectateEnd destroys the banner, computes the session length from the stored start time, and prints a one-line chat summary to the player. Sessions are kept in a Dictionary<ulong, float> keyed by user ID; it is cleared in Unload along with any remaining UI. The example shows how to read spectateFilter, plug into both the start and end of a spectate session, and undo plugin-owned state at the right moment without ever blocking the stop.
Common use cases
Remove spectator-only UI
If your plugin draws an overlay while a player spectates, destroy it here so the HUD is clean the instant the player returns to their body.
Audit admin spectating
Record who spectated, with what filter and for how long. Combine with OnPlayerSpectate to time the session and write it to a data file.
Clean up vanish or god flags
Plugins that blank out a spectating admin's state can restore it in this hook, before the player is put back into their own body.
Pitfalls
Blocking spectate by returning a value
Returning anything non-null aborts StopSpectating and can trap the player in spectator mode. Only do this deliberately and always provide a way to end spectating later.
Player is still spectating inside the hook
The game has not cleared the spectator state yet, so IsSpectating() is still true and the player is not back on their body. Do nothing here that assumes normal gameplay position.
Hook can fire on disconnect
A disconnecting player passes through this path, so guard against null or destroyed entities and avoid UI calls for players whose connection is closing.
Build a plugin with OnPlayerSpectateEnd
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 OnPlayerSpectateEnd
Generate