OnRfListenerRemoved
OnRfListenerRemoved is called after an RF object has been successfully removed as a listener from a frequency channel in RFManager.RemoveListener. It fires only when the removal actually happened, not when a plugin blocked it through OnRfListenerRemove.
void OnRfListenerRemoved(IRFObject obj, int frequency)Parameters
| Parameter | Type | Description |
|---|---|---|
obj | IRFObject | The IRFObject that was unregistered from the frequency, typically a radio, RF receiver, timed explosive or similar entity implementing RF listening. Use it to inspect the entity, its position or its owner. |
frequency | int | The frequency channel the object was listening on, already clamped to the valid RF range by RFManager. Use it to track channel usage or log which channel lost a listener. |
Return value
The return value is ignored. The game has already removed the object from the listener set and called RFSignalUpdate(false) before this hook fires, so there is no way to cancel, delay or alter the removal from OnRfListenerRemoved. To prevent unsubscribing, use the earlier OnRfListenerRemove hook and return a non-null value.
How it works
This hook fires inside RFManager.RemoveListener, right after the object has been dropped from the frequency's listener set. Because it runs only inside the if that also calls obj.RFSignalUpdate(on: false), it does not fire when the object was not subscribed (the Remove returns false) or when the earlier OnRfListenerRemove hook returned a non-null value to block the removal. Typical triggers are a radio being turned off or destroyed, a frequency change that unsubscribes the old channel first, an entity being killed or despawned, and round cleanup of RF devices. Both parameters are already validated by the game, so obj is a live RF object reference and frequency is clamped to the valid range. There is no practical need to narrow parameter types here; the signature already uses the abstract IRFObject. The hook returns void, so you cannot influence the removal from here — use OnRfListenerRemove if you need to veto. Note the hook fires after the listener set changed, so querying RFManager for the channel at this point already reflects the removal.
Example plugin: Track active RF channel usage
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Rf Channel Monitor", "RustPluginGenerator", "1.0.0")]
[Description("Tracks how many RF listeners are subscribed per frequency and warns on crowded channels.")]
public class RfChannelMonitor : RustPlugin
{
private const string PermAdmin = "rfchannelmonitor.admin";
private class PluginConfig
{
[JsonProperty("Warn threshold (listeners per channel)")]
public int WarnThreshold = 8;
[JsonProperty("Warning check interval (seconds)")]
public float CheckInterval = 60f;
}
private PluginConfig config;
private readonly Dictionary<int, int> listeners = new Dictionary<int, int>();
private Timer checkTimer;
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new System.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>
{
["Top"] = "Most used RF channels: {0}",
["Channel"] = "Channel {0} currently has {1} listener(s).",
["Empty"] = "No RF listeners are registered right now.",
["NoPerm"] = "You do not have permission to use this command.",
["Crowded"] = "RF channel {0} has {1} listeners (threshold {2})."
}, 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(PermAdmin, this);
}
private void OnServerInitialized()
{
checkTimer = timer.Every(config.CheckInterval, CheckCrowded);
}
private void Unload()
{
checkTimer?.Destroy();
}
private void OnRfListenerAdded(IRFObject obj, int frequency)
{
if (obj == null) return;
listeners[frequency] = listeners.TryGetValue(frequency, out int count) ? count + 1 : 1;
}
private void OnRfListenerRemoved(IRFObject obj, int frequency)
{
if (obj == null) return;
if (!listeners.TryGetValue(frequency, out int count)) return;
count--;
if (count <= 0) listeners.Remove(frequency);
else listeners[frequency] = count;
}
private void CheckCrowded()
{
foreach (KeyValuePair<int, int> entry in listeners)
{
if (entry.Value < config.WarnThreshold) continue;
Puts(string.Format(Msg("Crowded"), entry.Key, entry.Value, config.WarnThreshold));
}
}
[ChatCommand("rfmon.top")]
private void CmdTop(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPerm", player.UserIDString));
return;
}
if (listeners.Count == 0)
{
SendReply(player, Msg("Empty", player.UserIDString));
return;
}
var ordered = new List<KeyValuePair<int, int>>(listeners);
ordered.Sort((a, b) => b.Value.CompareTo(a.Value));
var parts = new List<string>();
for (int i = 0; i < ordered.Count && i < 5; i++)
parts.Add($"{ordered[i].Key}={ordered[i].Value}");
SendReply(player, Msg("Top", player.UserIDString, string.Join(", ", parts)));
}
[ChatCommand("rfmon.channel")]
private void CmdChannel(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPerm", player.UserIDString));
return;
}
if (!int.TryParse(args != null && args.Length > 0 ? args[0] : "", out int freq))
{
SendReply(player, "Usage: /rfmon.channel <frequency>");
return;
}
listeners.TryGetValue(freq, out int count);
SendReply(player, Msg("Channel", player.UserIDString, freq, count));
}
}
}The plugin keeps a live count of listeners per frequency so admins can see which channels are busy. OnRfListenerAdded increments the counter and OnRfListenerRemoved decrements it, guarding against negative values. The command rfmon.top prints the most-used frequencies from the in-memory map, and rfmon.channel <freq> shows a single channel count. A throttled timer logs a warning when any channel exceeds the configured limit, giving admins a signal for abusive or bugged broadcasts. The config stores the warning threshold and the log interval. Data is not persisted because the counts are rebuilt from RF events while the server runs, which keeps the plugin stateless and safe to reload.
Common use cases
Channel popularity stats
Aggregate listener counts per frequency to learn which channels are used most on your server, weighting broadcasts and events by popularity.
Warn on crowded channels
Trigger an admin warning when a channel exceeds a configured threshold of listeners, which is useful for noisy public radio spam.
Audit suspicious RF devices
Log when objects owned by a specific player unsubscribe from a frequency, helping investigate RF-based griefing or clan disputes.
Pitfalls
Hook fires only on success
If
OnRfListenerRemoveis blocked by another plugin, or the object was not registered, this hook will not fire. Do not use it as a general 'object stopped listening' signal — pair it with OnRfListenerAdd when you need matched counters.No cancellation possible
Returning a value here changes nothing; the removal already happened. Use
OnRfListenerRemovewhen you need to keep an object subscribed.Not just radios
Timed explosives, RF receivers, broadcasters and other prefabs also implement IRFObject. Avoid assuming
objis a BoomBox; check its type or convert withas BaseEntitybefore using entity-specific APIs.
Build a plugin with OnRfListenerRemoved
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 OnRfListenerRemoved
Generate