Skip to content
RustPluginGenerator

OnBookmarkControlStarted

OnBookmarkControlStarted is called after a player begins controlling a remote device through a ComputerStation bookmark, right before the station starts its periodic control validation.

Signature
void OnBookmarkControlStarted(ComputerStation computerStation, BasePlayer player, string text, IRemoteControllable remoteControllable)

Parameters

Hook parameters
ParameterTypeDescription
computerStationComputerStationThe ComputerStation entity the player is using to start controlling the bookmark. Its net.ID identifies the station for logging or per-station rules.
playerBasePlayerThe BasePlayer who is taking control of the remote device through the computer station.
textstringThe bookmark name string the player selected in the computer station UI.
remoteControllableIRemoteControllableThe controllable device being taken over; cast it to CCTV_RC, Door or other IRemoteControllable implementations to inspect what the player is now driving.

Return value

The hook fires after the control session is already set up and its return value is discarded, so returning null or anything else changes nothing. To block control you must act earlier through other hooks or plugin logic, not here.

How it works

This hook fires inside ComputerStation.BeginControllingBookmark, after the server has sent the bookmark list back to the player and, for CCTV targets on achievement-enabled servers, started the CCTV achievement repeat. It runs before ControlCheck is scheduled, so the player is already considered the controller of remoteControllable when your code executes.

Use it to react the moment control begins: verify who is driving which device, record the station and bookmark name for an audit log, notify teammates, or apply your own restrictions. computerStation and player are always present; text is the bookmark label and may be empty or player-chosen, so do not treat it as trusted input. remoteControllable is the live device interface, cast it to a concrete type such as CCTV_RC or Door before touching type-specific members, and null-check the result.

The hook fires once per control start, not per frame, but on busy servers with many CCTV and turret cameras it can still run frequently. Because the signature fixes the parameter types, you cannot narrow dispatch to a subtype here, check the device type inside the method instead. Canceling is not possible: the patched call site ignores any return value, so treat this as a notification.

Example plugin: Announce and audit CCTV control sessions

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

namespace Oxide.Plugins
{
    [Info("Bookmark Control Logger", "RustPluginGenerator", "1.0.0")]
    [Description("Announces and logs when players start controlling devices through ComputerStation bookmarks")]
    public class BookmarkControlLogger : RustPlugin
    {
        private const string PermUse = "bookmarkcontrollogger.use";

        private PluginConfig config;
        private List<string> entries = new List<string>();

        private class PluginConfig
        {
            [JsonProperty("Broadcast to all players")]
            public bool Broadcast { get; set; } = false;

            [JsonProperty("Store to data file")]
            public bool StoreToFile { get; set; } = true;

            [JsonProperty("Maximum stored entries")]
            public int MaxEntries { get; set; } = 500;
        }

        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>
            {
                ["Notice"] = "You are now controlling {0} via bookmark '{1}'.",
                ["Broadcast"] = "{0} took control of {1} ({2}).",
                ["NoPermission"] = "You are not allowed to control remote devices."
            }, 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);
            entries = Interface.Oxide.DataFileSystem.ReadObject<List<string>>(Name) ?? new List<string>();
        }

        private void OnBookmarkControlStarted(ComputerStation computerStation, BasePlayer player, string text, IRemoteControllable remoteControllable)
        {
            if (player == null || computerStation == null || remoteControllable == null) return;

            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
            }

            string device = DescribeDevice(remoteControllable);
            string safeText = string.IsNullOrEmpty(text) ? "unnamed" : text.Replace("\n", " ").Replace("\r", " ");

            player.ChatMessage(Msg("Notice", player.UserIDString, device, safeText));

            if (config.Broadcast)
            {
                Server.Broadcast(Msg("Broadcast", null, player.displayName, device, computerStation.net.ID.Value));
            }

            if (!config.StoreToFile) return;

            entries.Add(string.Format("{0:u} | {1} | station {2} | bookmark '{3}' | {4}",
                DateTime.UtcNow, player.UserIDString, computerStation.net.ID.Value, safeText, device));

            while (entries.Count > Mathf.Max(1, config.MaxEntries))
            {
                entries.RemoveAt(0);
            }

            Interface.Oxide.DataFileSystem.WriteObject(Name, entries);
        }

        private static string DescribeDevice(IRemoteControllable remoteControllable)
        {
            var cctv = remoteControllable as CCTV_RC;
            if (cctv != null) return "CCTV camera";

            var door = remoteControllable as Door;
            if (door != null) return "door";

            var ent = remoteControllable.GetEnt();
            if (ent != null && !string.IsNullOrEmpty(ent.ShortPrefabName)) return ent.ShortPrefabName;

            return "remote device";
        }

        private void Unload()
        {
            if (config != null && config.StoreToFile)
            {
                Interface.Oxide.DataFileSystem.WriteObject(Name, entries);
            }
        }
    }
}

The plugin writes a short chat notice to the controlling player and, if enabled in the config, broadcasts a server-wide message naming the player and the device they took over. When Log To File is on, it appends a line to a data file containing the player's ID, the computer station ID, the bookmark text and the device name, so admins can review who has been driving cameras and doors. It casts remoteControllable to CCTV_RC and Door to produce a readable device label, falling back to the prefab short name, and returns the first matching type check. All player-facing text goes through the language file, and the timer reference and stored list are cleaned up in Unload.

Common use cases

  • Camera audit trail

    Log which player controlled which CCTV camera from which computer station and when, giving admins a record for raid investigations on camera-heavy servers.

  • Team notifications

    Send a message to the controller's teammates when they start driving a camera or door, so a team knows an observer is at the controls.

  • Restricted areas

    Combine the device position with existing zone or privilege checks to warn admins when someone takes over a device inside a protected build.

Pitfalls

  • Return value does nothing

    The call site ignores the result, so returning a value or null cannot cancel the control session. Use other hooks or pre-emptive checks if you need to block access.

  • Untrusted bookmark text

    The text parameter comes from player-set bookmark names. Never print it raw into console output or use it as a file key without sanitizing it.

  • Type-specific device casts

    remoteControllable is an interface; casting to CCTV_RC or Door can yield null for other implementations. Always check the cast result before reading members.

Build a plugin with OnBookmarkControlStarted

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

Prompt: Camera audit trail using the OnBookmarkControlStarted 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 OnBookmarkControlStarted

Generate