Skip to content
RustPluginGenerator

Unload

Unload is called by Oxide when a plugin is being removed from the plugin manager, so it runs on every reload, unload and server shutdown. Use it to tear down everything the plugin created outside its own managed state.

Signature
void Unload()

Return value

The hook returns void and the return value is ignored; nothing you return changes how Oxide removes the plugin. Work is done purely through the side effects of the method body.

How it works

Oxide calls Unload from CSharpPlugin.HandleRemovedFromManager right before the plugin object is detached from the manager, only while the plugin still reports itself as loaded. That covers oxide.reload <name>, oxide.unload <name>, plugins that fail during compilation after being live, and the plugin teardown that happens on server shutdown. The hook takes no arguments and gives you no indication of why the plugin is going away, so cleanup logic must be safe to run in all of those cases.

This is the counterpart of Init and Loaded. Anything that lives outside your plugin instance and would survive a reload must be released here: CUI elements added with CuiHelper.AddUi, timers created with timer.Once / timer.Every / timer.Repeat, and data you keep in memory that is not written to oxide/data after every change. Permanent side effects - permission registrations, lang entries, config files, data files - do not need to be undone and should not be, since they are meant to survive restarts.

There is nothing to narrow here: the signature has no parameters, so Unload always fires with no arguments. Keep the body short and defensive; a throw inside Unload leaves the cleanup half-finished.

Example plugin: Clean shutdown for a timed supply-drop announcer

SupplyDropAnnouncer.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Supply Drop Announcer", "RustPluginGenerator", "1.0.0")]
    [Description("Announces the next timed supply drop and shows a countdown panel to all players")]
    public class SupplyDropAnnouncer : RustPlugin
    {
        private const string PanelName = "SupplyDropAnnouncer.Panel";
        private const string PermAdmin = "supplydropannouncer.admin";

        private PluginConfig config;
        private Timer dropTimer;
        private Timer panelTimer;
        private float nextDropAt;

        private class PluginConfig
        {
            [JsonProperty("Interval between drops (seconds)")]
            public float IntervalSeconds = 900f;

            [JsonProperty("Required online players")]
            public int RequiredPlayers = 3;

            [JsonProperty("Spawn a real airdrop when the timer ends")]
            public bool SpawnAirdrop = true;
        }

        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>
            {
                ["NextDrop"] = "Next supply drop in {0}.",
                ["DropNow"] = "A supply drop is on its way.",
                ["NotEnough"] = "Not enough players online for a scheduled drop.",
                ["Panel"] = "Supply drop in {0}",
                ["Reloaded"] = "Supply Drop Announcer reset its timers."
            }, 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()
        {
            ScheduleDrop();
            panelTimer = timer.Every(1f, UpdatePanels);
        }

        private void ScheduleDrop()
        {
            nextDropAt = UnityEngine.Time.realtimeSinceStartup + config.IntervalSeconds;

            if (dropTimer != null) dropTimer.Destroy();
            dropTimer = timer.Once(config.IntervalSeconds, OnDropTimerElapsed);
        }

        private void OnDropTimerElapsed()
        {
            if (BasePlayer.activePlayerList.Count >= config.RequiredPlayers)
            {
                if (config.SpawnAirdrop) SpawnFallingAirdrop();
                Server.Broadcast(Msg("DropNow"));
            }

            ScheduleDrop();
        }

        private void SpawnFallingAirdrop()
        {
            var prefab = "assets/prefabs/npc/cargo plane/cargo_plane.prefab";
            var entity = GameManager.server.CreateEntity(prefab, new Vector3(0f, 200f, 0f), Quaternion.identity);
            entity?.Spawn();
        }

        private void UpdatePanels()
        {
            int remaining = Mathf.Max(0, Mathf.CeilToInt(nextDropAt - UnityEngine.Time.realtimeSinceStartup));
            string text = Msg("Panel", null, FormatTime(remaining));

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected) continue;

                CuiHelper.DestroyUi(player, PanelName);
                var container = new CuiElementContainer();
                container.Add(new CuiPanel
                {
                    Image = { Color = "0.05 0.05 0.05 0.6" },
                    RectTransform = { AnchorMin = "0.42 0.92", AnchorMax = "0.58 0.955" }
                }, "Hud", PanelName);
                container.Add(new CuiLabel
                {
                    Text = { Text = text, FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "0.9 0.9 0.9 1" },
                    RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
                }, PanelName);
                CuiHelper.AddUi(player, container);
            }
        }

        private static string FormatTime(int seconds)
        {
            return string.Format("{0:00}:{1:00}", seconds / 60, seconds % 60);
        }

        private void Unload()
        {
            if (dropTimer != null) { dropTimer.Destroy(); dropTimer = null; }
            if (panelTimer != null) { panelTimer.Destroy(); panelTimer = null; }

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null) continue;
                CuiHelper.DestroyUi(player, PanelName);
            }
        }
    }
}

The plugin announces upcoming airdrops in chat and shows a countdown panel to every connected player. OnServerInitialized calls ScheduleDrop to start the drop timer and starts a repeating timer that refreshes the panel via UpdatePanels using CUI. Because the timers and the UI both outlive a normal plugin reload if nothing removes them, Unload destroys both the drop and panel timers and loops BasePlayer.activePlayerList calling CuiHelper.DestroyUi for the panel name. The plugin has no persistent gameplay data, but its configuration is saved through SaveConfig. Reloading the plugin repeatedly therefore never stacks extra timers or leaves an orphaned overlay on screen.

Common use cases

  • Remove CUI overlays

    Destroy every panel, HUD or menu element the plugin added so a reload does not leave dead UI on screen or let a deleted command string linger in a button.

  • Stop timers

    Keep Timer fields for timer.Once and timer.Every and call Destroy() on each, otherwise the callbacks keep firing against a removed plugin instance.

  • Flush in-memory state

    Write player caches or statistics to Interface.Oxide.DataFileSystem so data collected since the last save is not lost on reload or shutdown.

Pitfalls

  • Timer callbacks reference the old instance

    An undisposed timer keeps a reference to the unloaded plugin and will run its lambda after reload, usually causing duplicate messages or errors about a missing plugin.

  • Undoing permanent registrations

    Unregistering permissions or clearing lang entries in Unload makes them vanish on every reload. Leave those to exist for the lifetime of the server.

  • Assuming players are connected

    On shutdown or a late unload the player list may be empty or the server may be stopping. Guard loops and never rely on network calls succeeding inside Unload.

Build a plugin with Unload

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

Prompt: Remove CUI overlays using the Unload 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 Unload

Generate