OnDestroyUI
OnDestroyUI is called by Oxide whenever CUIHelper.DestroyUi is invoked for a player, right before the DestroyUI RPC is sent to that player's client. It fires for every plugin-initiated UI destroy, not for client-side closes.
void OnDestroyUI(BasePlayer player, string elem)Parameters
| Parameter | Type | Description |
|---|---|---|
player | BasePlayer | The BasePlayer whose client is receiving the UI destroy message. Check for null and a live net connection before acting on it. |
elem | string | The name (string) of the CUI element being destroyed, matching the name used when the element was created with CuiHelper.AddUi. |
Return value
The return value is ignored. Returning null, true, or anything else does not prevent the DestroyUI RPC from reaching the client; the original method always sends it and returns true when the player has a net connection.
How it works
The hook fires inside CUIHelper.DestroyUi(BasePlayer player, string elem). Any plugin calling CuiHelper.DestroyUi(player, name) โ or CuiHelper.DestroyUi(player, name, ...) overloads that route through it โ triggers OnDestroyUI once per call, with the player and the element name. The original method still runs afterwards, so returning from your hook does not stop the RPC.
Use it to track which UI panels exist per player: maintain a dictionary of player ID to destroyed element names, clean up plugin-side state (timers, cached data, sound handles) tied to a panel, or forward the destroy to another plugin that mirrors your UI. Because elem is exactly the name string passed in, you can filter by prefix to react only to your own panels.
The parameters are already concrete (BasePlayer, string). There is no useful narrowing here; just null-check player. Note that destroying an element that never existed still fires the hook โ the game sends the RPC regardless.
Example plugin: Track CUI panels per player and clean up state
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("UI Destroy Tracker", "RustPluginGenerator", "1.0.0")]
[Description("Tracks CUI panels destroyed per player and reports them to admins")]
public class UiDestroyTracker : RustPlugin
{
private const string PermStatus = "uidestroytracker.status";
private PluginConfig config;
private readonly Dictionary<string, HashSet<string>> destroyed = new Dictionary<string, HashSet<string>>();
private class PluginConfig
{
[JsonProperty("Maximum tracked names per player")]
public int MaxPerPlayer = 50;
}
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>
{
["NoEntry"] = "No destroyed UI recorded for {0}.",
["Entry"] = "Destroyed UI for {0}: {1}",
["Cleared"] = "Cleared tracked UI data.",
["Perm"] = "You do not have permission to use this command."
}, 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(PermStatus, this);
}
private void OnDestroyUI(BasePlayer player, string elem)
{
if (player == null || string.IsNullOrEmpty(elem)) return;
HashSet<string> set;
if (!destroyed.TryGetValue(player.UserIDString, out set))
{
set = new HashSet<string>();
destroyed[player.UserIDString] = set;
}
if (set.Count >= config.MaxPerPlayer) return;
set.Add(elem);
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
destroyed.Remove(player.UserIDString);
}
[ChatCommand("uistatus")]
private void CmdUiStatus(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermStatus))
{
player.ChatMessage(Msg("Perm", player.UserIDString));
return;
}
string targetId = player.UserIDString;
string targetName = player.displayName;
if (args != null && args.Length > 0)
{
BasePlayer target = BasePlayer.FindAwakeOrSleeping(args[0]);
if (target == null)
{
player.ChatMessage(Msg("NoEntry", player.UserIDString, args[0]));
return;
}
targetId = target.UserIDString;
targetName = target.displayName;
}
HashSet<string> set;
if (!destroyed.TryGetValue(targetId, out set) || set.Count == 0)
{
player.ChatMessage(Msg("NoEntry", player.UserIDString, targetName));
return;
}
player.ChatMessage(Msg("Entry", player.UserIDString, targetName, string.Join(", ", set)));
}
private void Unload()
{
destroyed.Clear();
}
}
}The plugin keeps a per-player set of CUI panel names in a dictionary keyed by UserIDString. OnDestroyUI records each destroyed name and drops empty entries. A chat command /uistatus reports whether the player still has tracked panels open. OnPlayerDisconnected wipes the entry so the dictionary does not grow forever, and Unload clears it. Because the hook fires for every CuiHelper.DestroyUi call from any plugin, the example is useful for admins who want visibility into which plugins leave UI dangling after a player closes menus.
Common use cases
Release resources tied to a panel
When your plugin creates a timer, sound loop, or cached model that is only meaningful while a specific CUI panel is open, destroy those resources inside OnDestroyUI when the matching element name is seen.
Mirror UI across plugins
A plugin that mirrors another plugin's interface can listen for OnDestroyUI and remove its shadow copy when the source element is closed, avoiding stale overlapping panels.
Admin diagnostics
Record each destroyed element name per player and expose totals through a chat command so admins can see which plugin is generating excessive CUI churn.
Pitfalls
Fires for nonexistent elements
The hook runs before the RPC is sent, so it fires even when the element name was never added. Do not assume the panel existed just because OnDestroyUI was observed.
Does not fire on client close
If the player closes a UI by pressing a close button that you wired to a console command calling CuiHelper.DestroyUi, the hook fires. If the client closes the panel via the mouse without a server-side destroy call, there is no hook. Do not rely on it for end-of-session tracking.
Called from every plugin
The hook is global; it triggers for any plugin's DestroyUi call. Filter by element name prefix if your plugin must react only to its own panels.
Build a plugin with OnDestroyUI
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 OnDestroyUI
Generate