Skip to content
RustPluginGenerator

OnCounterTargetChange

OnCounterTargetChange is called when a player sets the target count on a PowerCounter (the electrical counter component) through the in-game UI. Returning a non-null value cancels the change.

Signature
object OnCounterTargetChange(PowerCounter powerCounter, BasePlayer player, int num)

Parameters

Hook parameters
ParameterTypeDescription
powerCounterPowerCounterThe PowerCounter entity the client is configuring. Check its OwnerID or GetBuildingPrivilege() if you need to gate changes by ownership.
playerBasePlayerThe player who sent the set-target request. Can be null if the RPC arrives without an attached connection, so null-check before using it.
numintThe target count the client asked to apply (clamped to 1-999 by the game). Do not assume the reset value that the game reads next is validated the same way.

Return value

Returning null lets the request proceed: the game then runs CanPlayerAdmin and, if the player passes, applies the clamped target and reset numbers. Returning any non-null object stops the method immediately, so the target number is never written and no response is sent to the client.

How it works

The hook fires inside PowerCounter.SERVER_SetTarget, which the client invokes over RPC when a player edits the counter's target number. The server reads the requested value, calls the hook, and only if the hook returns null does it check CanPlayerAdmin(player) and apply the clamped target and reset numbers.

Use powerCounter to identify the entity (its owner, building privilege, or position) and player to decide whether the request is allowed. num is the raw value the client sent before the game clamps it to 1-999, so treat it as untrusted input.

Because only PowerCounter is passed, you cannot usefully narrow the parameter type; declare it exactly as shown. The hook runs on the server thread during RPC handling, so keep the body cheap and do not call blocking work. Note the ordering: a non-null return short-circuits the call before the admin check, so blocking here is a policy decision, not just a validation one.

Example plugin: Restrict counter target changes to authorized players

CounterTargetGuard.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Counter Target Guard", "RustPluginGenerator", "1.0.0")]
    [Description("Restricts who may change the target number on a PowerCounter and flags repeated attempts")]
    public class CounterTargetGuard : RustPlugin
    {
        private const string PermBypass = "countertargetguard.bypass";

        private PluginConfig config;
        private readonly Dictionary<ulong, int> blockedAttempts = new Dictionary<ulong, int>();

        private class PluginConfig
        {
            [JsonProperty("Require building authorization")]
            public bool RequireAuth = true;

            [JsonProperty("Warn after this many blocked attempts")]
            public int WarnThreshold = 3;
        }

        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>
            {
                ["Blocked"] = "You are not allowed to change the target of this counter."
            }, this);
        }

        private void Init()
        {
            permission.RegisterPermission(PermBypass, this);
        }

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

        private object OnCounterTargetChange(PowerCounter powerCounter, BasePlayer player, int num)
        {
            if (powerCounter == null || player == null) return null;
            if (player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermBypass)) return null;
            if (powerCounter.OwnerID == player.userID.Get()) return null;

            if (config.RequireAuth)
            {
                var privilege = powerCounter.GetBuildingPrivilege();
                if (privilege == null || !privilege.IsAuthed(player)) return Reject(powerCounter, player);
            }

            return null;
        }

        private object Reject(PowerCounter powerCounter, BasePlayer player)
        {
            int count;
            blockedAttempts.TryGetValue(player.userID.Get(), out count);
            count++;
            blockedAttempts[player.userID.Get()] = count;

            player.ChatMessage(Msg("Blocked", player.UserIDString));
            if (config.WarnThreshold > 0 && count == config.WarnThreshold)
                PrintWarning($"{player.displayName} ({player.UserIDString}) hit the counter edit limit at {powerCounter.transform.position}");

            return true;
        }

        private void OnServerSave()
        {
            blockedAttempts.Clear();
        }

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

The plugin blocks anyone who is not building-authorized on the counter's privilege from changing its target, even if they can physically interact with it. OnCounterTargetChange checks the stored OwnerID first, then calls GetBuildingPrivilege() and IsAuthed. If neither check passes, it sends the player a localized message and returns a non-null object to cancel the write. Blocked attempts are counted per player, and when a configurable threshold is reached the player receives a warning and is removed from the tracking map. The count map is cleared on OnServerSave so it does not grow unbounded, and messages live in the language file.

Common use cases

  • Owner-only counters

    Only allow the entity owner or their building-authorized teammates to change a counter's target, blocking passing players who can reach the keypad.

  • Cap target values

    Reject requests where num is outside a server-defined range before the game clamps it, so unusual inputs never reach the counter.

  • Audit suspicious edits

    Increment a per-player counter of target changes and flag players who spam the RPC, without logging inside the hook itself.

Pitfalls

  • Cancelling before the admin check

    A non-null return stops the method before CanPlayerAdmin runs, so even admins are blocked. Check player.IsAdmin yourself if admins should be exempt.

  • The reset number is not in the signature

    The game reads a second value from the RPC after the target. You cannot inspect or validate it here, so a non-null return cancels the whole edit rather than rewriting one field.

  • Null player on malformed RPC

    Do not assume player is non-null. Guard it first, otherwise a permission check throws inside the RPC handler and the client sees no response.

Build a plugin with OnCounterTargetChange

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

Prompt: Owner-only counters using the OnCounterTargetChange 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 OnCounterTargetChange

Generate