OnBookmarkControlEnded
OnBookmarkControlEnded is called when a player stops controlling a remote controllable (CCTV, turret, drone, etc.) through a ComputerStation, either by releasing control or by switching to a new bookmark.
void OnBookmarkControlEnded(ComputerStation computerStation, BasePlayer ply, BaseEntity entity)Parameters
| Parameter | Type | Description |
|---|---|---|
computerStation | ComputerStation | The ComputerStation the player was controlling bookmarks from. Use it to identify which station lost control and to check its flags or owning entity. |
ply | BasePlayer | The BasePlayer whose control session ended. It is the player being detached from the remote controllable, and can be null in theory, so guard before use. |
entity | BaseEntity | The entity that was being controlled, either as a BaseEntity (from StopControl) or an IRemoteControllable component (from BeginControllingBookmark). Cast or use as is depending on the call site. |
Return value
The return value is ignored at both injection sites. Returning null or any value does not stop the player from losing control and does not change the station's state.
How it works
The hook fires from two places inside ComputerStation. First, StopControl calls it after the player fully stops controlling an entity, once the station clears its controlling flag, updates the network, resends bookmarks and cancels its control check. Second, BeginControllingBookmark calls it when the player switches to another bookmark while already controlling something: the old controllable's StopControl is invoked, then the hook fires before the station starts controlling the new entity. In the first case the third argument is the BaseEntity that was controlled; in the second case it is the IRemoteControllable component, so treat both overloads carefully if you subscribe with one method. The most common use is tracking who is watching which camera, or cleaning up per-viewer state (HUD, cached entity, subscriptions) when a session ends. Because the hook is not filtered by entity type, you can check entity is CCTV or cast to narrow your logic. Returning anything has no effect: the game code ignores the result and the control release always proceeds.
Other signatures
Oxide also calls OnBookmarkControlEnded from other places in the game with these parameter types. Declare the overload you need, or several:
void OnBookmarkControlEnded(ComputerStation computerStation, BasePlayer player, IRemoteControllable component)
Example plugin: Log and announce CCTV control sessions ending
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Cctv Session Log", "RustPluginGenerator", "1.0.0")]
[Description("Notifies players and logs when they stop controlling a CCTV through a ComputerStation.")]
public class CctvSessionLog : RustPlugin
{
private const string PermView = "cctvsessionlog.view";
private PluginConfig config;
private readonly Dictionary<ulong, float> lastNotify = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Notify player on release")]
public bool NotifyPlayer { get; set; } = true;
[JsonProperty("Log control ends to server console")]
public bool LogToConsole { get; set; } = true;
[JsonProperty("Message cooldown seconds")]
public float Cooldown { get; set; } = 4f;
}
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>
{
["Stopped"] = "You stopped controlling {0}.",
["ConsoleLog"] = "{0} ({1}) stopped controlling {2} in {3}."
}, 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(PermView, this);
}
private void Unload()
{
lastNotify.Clear();
}
private void OnBookmarkControlEnded(ComputerStation computerStation, BasePlayer ply, BaseEntity entity)
{
if (ply == null || !ply.IsConnected) return;
string label = ResolveLabel(entity);
if (label == null) return;
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (lastNotify.TryGetValue(ply.userID.Get(), out last) && now - last < config.Cooldown) return;
lastNotify[ply.userID.Get()] = now;
if (config.NotifyPlayer && permission.UserHasPermission(ply.UserIDString, PermView))
ply.ChatMessage(Msg("Stopped", ply.UserIDString, label));
if (config.LogToConsole && computerStation != null)
Puts(Msg("ConsoleLog", null, ply.displayName, ply.UserIDString, label,
computerStation.transform.position.ToString("F1")));
}
private static string ResolveLabel(BaseEntity entity)
{
if (entity == null) return null;
if (entity.ShortPrefabName != null && entity.ShortPrefabName.Contains("cctv")) return "the camera feed";
return entity.ShortPrefabName;
}
}
}The plugin responds when a player stops controlling an entity through a ComputerStation via the OnBookmarkControlEnded hook. It reads the controlled entity's ShortPrefabName; if it contains cctv, the label the camera feed is used, otherwise the short prefab name is used. When the cctvsessionlog.view permission is granted and the chat option is enabled, the player receives a localized message. If console logging is enabled, a line with the player's name, user ID, label and ComputerStation position is written to the server console. A four-second per-player cooldown throttles both the message and the log entry. The config exposes toggles for player chat notifications and console logging, plus the cooldown duration.
Common use cases
Per-player HUD cleanup
Destroy any CUI elements you showed while the player was watching a camera, so the overlay does not stick after release.
Session tracking
Maintain a dictionary of player to controlled entity and remove the entry here to know who is currently viewing which device.
Announce camera events
Notify teammates or admins that someone stopped watching a CCTV or turret feed, useful for monitoring base access.
Pitfalls
Two possible third argument types
The same hook name is called with a
BaseEntityfromStopControland anIRemoteControllablefromBeginControllingBookmark. DeclareBaseEntityin your method and handle the component case viaas, or you may get a null cast for one of the paths.Cooldown needed on bookmark switches
Players flipping through bookmarks fire this hook for every switch, so sending chat messages without cooldown spams the player. Track last event time per user.
Not a permission gate
The hook is informational; the control release already happened. Do not build anti-cheat logic on it — use
CanUseLockedEntityor similar hooks that can block actions.
Build a plugin with OnBookmarkControlEnded
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 OnBookmarkControlEnded
Generate