Skip to content
RustPluginGenerator

OnBookmarkControlEnd

OnBookmarkControlEnd is called when a player stops controlling a camera through a ComputerStation, before the station detaches the remote controllable entity. Returning a non-null value cancels the detach step.

Signature
object OnBookmarkControlEnd(ComputerStation computerStation, BasePlayer ply, BaseEntity entity)

Parameters

Hook parameters
ParameterTypeDescription
computerStationComputerStationThe ComputerStation entity that is releasing control. Use it to read the station's position, owner or net ID when you need to tie the event to a specific station.
plyBasePlayerThe BasePlayer who is leaving the camera view. This is the player passed into StopControl, so it is the one whose viewer is being detached.
entityBaseEntityThe remote controllable BaseEntity that was being controlled (for example a CCTV camera). Null-check it before reading components; it is the entity referenced by the station's currentlyControllingEnt.

Return value

Return null to let the station continue detaching the controllable entity as normal. Return any non-null value to abort that detach step, leaving the station's viewer logic after the injected check unexecuted; the method returns void so no value is consumed.

How it works

This hook fires inside ComputerStation.StopControl, which runs when a viewer leaves the station's camera view. The station resolves currentlyControllingEnt, and if an entity is present it calls OnBookmarkControlEnd with the station, the player and that entity. After the hook, the vanilla code calls StopControl on the entity's IRemoteControllable component using the station's viewer ID.

Use computerStation to identify the station (net ID, position, owner), ply to target the player and entity to inspect the camera or other controllable object being released. All three can be null-guarded; entity is only present when a controllable was resolved, so treat a null entity as an already-finished session.

Because the entity is typed BaseEntity, you cannot narrow this hook by declaring a more specific type for it — a CCTV camera or other controllable is still a BaseEntity and matches. Narrowing on BasePlayer for the player argument works as usual, since Oxide skips calls where the argument is not that type. The natural companion hook is OnBookmarkControlStarted, which fires when control begins, so you can pair session start and end.

Example plugin: Log and announce CCTV sessions ending

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

namespace Oxide.Plugins
{
    [Info("Bookmark Control End Logger", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks and announces when a player stops controlling a ComputerStation camera")]
    public class BookmarkControlEndLogger : RustPlugin
    {
        private class PluginConfig
        {
            [JsonProperty("Announce session end to player")]
            public bool AnnounceToPlayer = true;

            [JsonProperty("Broadcast session end to server")]
            public bool BroadcastToServer = false;
        }

        private class Session
        {
            public float StartedAt;
            public string EntityName;
        }

        private PluginConfig config;
        private readonly Dictionary<ulong, Session> sessions = new Dictionary<ulong, Session>();

        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>
            {
                ["SessionEnded"] = "You stopped viewing {0} after {1}s.",
                ["BroadcastEnded"] = "{0} stopped viewing a camera after {1}s."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
            => string.Format(lang.GetMessage(key, this, userId), args);

        private object OnBookmarkControlEnd(ComputerStation computerStation, BasePlayer ply, BaseEntity entity)
        {
            if (ply == null) return null;

            Session session;
            if (!sessions.TryGetValue(ply.userID.Get(), out session)) return null;
            sessions.Remove(ply.userID.Get());

            if (session == null) return null;

            var seconds = (int)Math.Max(0, UnityEngine.Time.realtimeSinceStartup - session.StartedAt);
            var name = session.EntityName ?? "camera";

            if (config.AnnounceToPlayer && ply.IsConnected)
                ply.ChatMessage(Msg("SessionEnded", ply.UserIDString, name, seconds));

            if (config.BroadcastToServer)
                Server.Broadcast(Msg("BroadcastEnded", null, ply.displayName, seconds));

            return null;
        }

        private object OnBookmarkControlStarted(ComputerStation computerStation, BasePlayer ply, BaseEntity entity)
        {
            if (ply == null) return null;

            var session = new Session
            {
                StartedAt = UnityEngine.Time.realtimeSinceStartup,
                EntityName = entity != null ? entity.ShortPrefabName : null
            };
            sessions[ply.userID.Get()] = session;
            return null;
        }

        private void OnPlayerDisconnected(BasePlayer player)
        {
            if (player == null) return;
            sessions.Remove(player.userID.Get());
        }

        private void Unload()
        {
            sessions.Clear();
        }
    }
}

The plugin stores each player's camera session when control starts in OnBookmarkControlStarted, recording the station and controllable net IDs. When OnBookmarkControlEnd fires, it looks up the stored session, computes how long the player watched, and removes the entry. The player receives a chat message with the entity name and elapsed seconds, and if LogSessions is enabled in the config, the session is written to chat via the message language file. Returning null keeps vanilla behavior. OnPlayerDisconnected clears leftover sessions so a disconnected player does not keep stale state. All text is routed through the language file, so admins can translate it.

Common use cases

  • Track viewing time per camera

    Record when a player starts controlling a camera and subtract timestamps in this hook to measure session length for moderation or analytics.

  • Block detach during an active scripted scene

    Return a non-null value while a plugin-driven cutscene is running so the station does not release the controllable entity prematurely.

  • Clean up per-session UI or permissions

    Destroy a camera overlay or revoke temporary access granted in OnBookmarkControlStarted when the player leaves the view.

Pitfalls

  • Entity can be null

    StopControl only calls the hook when currentlyControllingEnt resolves, but other paths or future changes may leave it empty. Guard against null before calling GetComponent or reading prefab data.

  • Cancelling the detach leaves the viewer ID set

    Returning non-null skips the IRemoteControllable.StopControl call, so the station still believes it holds a viewer. Only cancel if you follow up by detaching later yourself.

  • Pair with OnBookmarkControlStarted

    This hook gives no start time on its own. Store session state in the start hook or use a dictionary keyed by player to compute durations reliably.

Build a plugin with OnBookmarkControlEnd

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

Prompt: Track viewing time per camera using the OnBookmarkControlEnd 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 OnBookmarkControlEnd

Generate