Skip to content
RustPluginGenerator

How to make a Rust treasure hunt plugin (Oxide/Carbon)

Adds a /treasure admin command that spawns a loot crate at a random land position and announces its map grid, then announces the first player to loot it. It uses OnLootEntity to detect the winner and a timer to despawn the crate.

Treasure hunts give players a reason to leave their base and read the map. The hard part is spawning a crate somewhere that is not underwater, computing its map grid, and detecting the first player who actually opens it rather than just walks past.

This plugin registers /treasure for admins. On start it picks a random point inside the configured Map Radius, drops it to terrain height with TerrainMeta.HeightMap.GetHeight, skips points below Minimum Altitude, and spawns the configured Crate Prefab there. The same grid string is broadcast as the hint, built from Grid Cell Size and Map Radius by adding letters A to Z to the column index. OnLootEntity checks whether the looted entity is the tracked crate, announces the winner, and despawns the crate. A timer removes it silently if nobody reaches it in Hunt Duration Minutes. Everything is driven by the config file, so the crate prefab, loot table, radius and timeout are adjustable without touching code.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("TreasureHunt", "RustPluginGenerator", "1.0.0")]
    [Description("Spawns a loot crate at a random land position, announces its grid hint and rewards the first player to loot it.")]
    public class TreasureHunt : RustPlugin
    {
        private const string PermUse = "treasurehunt.use";

        private PluginConfig config;
        private Timer timeoutTimer;
        private BaseEntity treasure;

        private class LootEntry
        {
            [JsonProperty("Shortname")]
            public string Shortname = "scrap";

            [JsonProperty("Min Amount")]
            public int MinAmount = 50;

            [JsonProperty("Max Amount")]
            public int MaxAmount = 200;
        }

        private class PluginConfig
        {
            [JsonProperty("Crate Prefab")]
            public string CratePrefab = "assets/prefabs/misc/supply drop/supply_drop.prefab";

            [JsonProperty("Loot Items")]
            public List<LootEntry> LootItems = new List<LootEntry>
            {
                new LootEntry { Shortname = "scrap", MinAmount = 200, MaxAmount = 600 },
                new LootEntry { Shortname = "rifle.ak", MinAmount = 1, MaxAmount = 1 },
                new LootEntry { Shortname = "ammo.rifle", MinAmount = 60, MaxAmount = 120 },
                new LootEntry { Shortname = "metal.refined", MinAmount = 50, MaxAmount = 150 }
            };

            [JsonProperty("Map Radius")]
            public float MapRadius = 2000f;

            [JsonProperty("Minimum Altitude")]
            public float MinimumAltitude = 1f;

            [JsonProperty("Grid Cell Size")]
            public float GridCellSize = 150f;

            [JsonProperty("Max Placement Attempts")]
            public int MaxAttempts = 30;

            [JsonProperty("Hunt Duration Minutes")]
            public float DurationMinutes = 20f;
        }

        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>
            {
                ["HuntStarted"] = "A treasure crate has been dropped near grid {0}. First player to open it wins.",
                ["HuntWon"] = "{0} found the treasure crate in grid {1}!",
                ["HuntExpired"] = "The treasure crate was never found and has been removed.",
                ["HuntStopped"] = "The active treasure hunt has been cancelled.",
                ["NoHunt"] = "There is no active treasure hunt.",
                ["NoSpawnFound"] = "Could not find a valid land position. Increase Map Radius or lower Minimum Altitude.",
                ["AlreadyActive"] = "A treasure hunt is already running. Use /treasure stop to cancel it first.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

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

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

        [ChatCommand("treasure")]
        private void CmdTreasure(BasePlayer player, string command, string[] args)
        {
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args != null && args.Length > 0 && args[0].ToLower() == "stop")
            {
                if (treasure == null)
                {
                    Reply(player, Msg("NoHunt"));
                    return;
                }
                EndHunt(false);
                Reply(player, Msg("HuntStopped"));
                return;
            }

            if (treasure != null)
            {
                Reply(player, Msg("AlreadyActive"));
                return;
            }

            if (!StartHunt())
            {
                Reply(player, Msg("NoSpawnFound"));
            }
        }

        private void Reply(BasePlayer player, string message)
        {
            if (player != null) SendReply(player, message);
            else Puts(message);
        }

        private bool StartHunt()
        {
            Vector3 position = FindSpawnPosition();
            if (position == Vector3.zero) return false;

            var entity = GameManager.server.CreateEntity(config.CratePrefab, position, Quaternion.identity);
            if (entity == null) return false;

            entity.Spawn();
            treasure = entity;

            var container = entity as StorageContainer;
            if (container != null) FillLoot(container);

            string grid = GetGrid(position);
            Server.Broadcast(Msg("HuntStarted", null, grid));

            if (config.DurationMinutes > 0f)
            {
                timeoutTimer = timer.Once(config.DurationMinutes * 60f, () =>
                {
                    if (treasure != null)
                    {
                        EndHunt(false);
                        Server.Broadcast(Msg("HuntExpired"));
                    }
                });
            }

            return true;
        }

        private Vector3 FindSpawnPosition()
        {
            for (int attempt = 0; attempt < config.MaxAttempts; attempt++)
            {
                float x = UnityEngine.Random.Range(-config.MapRadius, config.MapRadius);
                float z = UnityEngine.Random.Range(-config.MapRadius, config.MapRadius);
                float y = TerrainMeta.HeightMap.GetHeight(new Vector3(x, 0f, z));
                if (y < config.MinimumAltitude) continue;
                return new Vector3(x, y + 0.5f, z);
            }
            return Vector3.zero;
        }

        private void FillLoot(StorageContainer container)
        {
            if (container.inventory == null || config.LootItems == null) return;

            foreach (var entry in config.LootItems)
            {
                if (entry == null || string.IsNullOrEmpty(entry.Shortname)) continue;
                int amount = UnityEngine.Random.Range(entry.MinAmount, entry.MaxAmount + 1);
                if (amount < 1) amount = 1;

                var item = ItemManager.CreateByName(entry.Shortname, amount);
                if (item == null) continue;

                if (!item.MoveToContainer(container.inventory))
                {
                    item.Remove();
                }
            }
            container.inventory.MarkDirty();
        }

        private string GetGrid(Vector3 position)
        {
            int col = Mathf.FloorToInt((position.x + config.MapRadius) / config.GridCellSize);
            int row = Mathf.FloorToInt((position.z + config.MapRadius) / config.GridCellSize);
            col = Mathf.Max(0, col);
            row = Mathf.Max(0, row);
            char letter = (char)('A' + (col % 26));
            return letter + row.ToString();
        }

        private void OnLootEntity(BasePlayer player, BaseEntity targetEntity)
        {
            if (treasure == null || player == null || targetEntity == null) return;
            if (player.IsNpc) return;
            if (targetEntity.net == null) return;
            if (targetEntity.net.ID.Value != treasure.net.ID.Value) return;

            string grid = GetGrid(treasure.transform.position);
            Server.Broadcast(Msg("HuntWon", null, player.displayName, grid));
            EndHunt(true);
        }

        private void EndHunt(bool destroyNow)
        {
            if (timeoutTimer != null)
            {
                timeoutTimer.Destroy();
                timeoutTimer = null;
            }

            if (treasure != null)
            {
                if (!treasure.IsDestroyed) treasure.Kill();
                treasure = null;
            }
        }

        private void Unload()
        {
            if (timeoutTimer != null)
            {
                timeoutTimer.Destroy();
                timeoutTimer = null;
            }
            if (treasure != null)
            {
                if (!treasure.IsDestroyed) treasure.Kill();
                treasure = null;
            }
        }
    }
}

Hooks used: OnLootEntity

Config file

Created automatically on first load at oxide/config/TreasureHunt.json.

TreasureHunt.json
{
  "Crate Prefab": "assets/prefabs/misc/supply drop/supply_drop.prefab",
  "Loot Items": [
    {
      "Shortname": "scrap",
      "Min Amount": 200,
      "Max Amount": 600
    },
    {
      "Shortname": "rifle.ak",
      "Min Amount": 1,
      "Max Amount": 1
    },
    {
      "Shortname": "ammo.rifle",
      "Min Amount": 60,
      "Max Amount": 120
    },
    {
      "Shortname": "metal.refined",
      "Min Amount": 50,
      "Max Amount": 150
    }
  ],
  "Map Radius": 2000,
  "Minimum Altitude": 1,
  "Grid Cell Size": 150,
  "Max Placement Attempts": 30,
  "Hunt Duration Minutes": 20
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/treasureSpawns a treasure crate at a random land position and broadcasts its map grid to all players.treasurehunt.use
/treasure stopRemoves the active treasure crate without announcing a winner.treasurehunt.use
Permissions
PermissionGrants
treasurehunt.useGrants access to the /treasure start and stop commands.

How the code works

  1. Config: crate prefab, loot and timeout

    The nested PluginConfig class holds Crate Prefab, Loot Items, Map Radius, Minimum Altitude, Grid Cell Size, Max Placement Attempts and Hunt Duration Minutes. Each loot entry is a nested LootEntry with Shortname, Min Amount and Max Amount. LoadConfig falls back to defaults on a parse error and SaveConfig writes the file back. Because the prefab path is configurable, admins can swap the supply drop for a hackable crate or another storage container without recompiling.

  2. Picking a valid land position

    FindSpawnPosition loops up to Max Placement Attempts, generating UnityEngine.Random.Range(-MapRadius, MapRadius) on X and Z. It resolves the ground height with TerrainMeta.HeightMap.GetHeight, rejects points below Minimum Altitude (this filters ocean and lakes), and returns the first passing Vector3 raised by 0.5 units. If every attempt fails it returns a zeroed vector, and the command replies with the NoSpawnFound message so the admin can widen the radius.

  3. Spawning the crate and filling loot

    StartHunt calls GameManager.server.CreateEntity with config.CratePrefab and the chosen position, then calls Spawn() and stores the entity in the treasure field. If it is a StorageContainer, FillLoot walks config.LootItems, picks a random amount between Min Amount and Max Amount, creates the item with ItemManager.CreateByName, and moves it into container.inventory with MoveToContainer. Items that do not fit are removed so the crate does not leak items.

  4. Building the grid hint

    GetGrid divides position.x and position.z (offset by Map Radius) by Grid Cell Size and floors both values with Mathf.FloorToInt. The column is wrapped into the range 0-25 and converted to a letter with (char)('A' + col % 26); the row is appended as a number. The result like G12 is passed into the HuntStarted lang key and broadcast with Server.Broadcast. No exact coordinates are leaked, only the grid square.

  5. Detecting the winner with OnLootEntity

    private void OnLootEntity(BasePlayer player, BaseEntity targetEntity) fires when a player opens a loot container. The hook checks treasure is not null, player is not an NPC, and targetEntity.net.ID.Value matches the tracked crate's network ID. Since Oxide only calls the hook when both arguments are instances of the declared types, the winner check is a single ID comparison. The winner's display name and grid are broadcast through the HuntWon lang key, then EndHunt destroys the crate.

  6. Timeout and cleanup

    StartHunt starts a Timer with timer.Once(config.DurationMinutes * 60f, ...). When it fires it checks treasure is still set, calls EndHunt(false) and broadcasts the HuntExpired message. The reference lives in timeoutTimer. Unload destroys the timer and kills any surviving crate so a reload never leaves a stray crate on the map. /treasure stop runs the same EndHunt path without a broadcast to players.

Install it on your server

  1. Save TreasureHunt.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/TreasureHunt.json (Carbon: carbon/configs/TreasureHunt.json). After editing, reload it:oxide.reload TreasureHunt c.reload TreasureHunt # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default treasurehunt.use c.grant group default treasurehunt.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Team cooldown

    Only allow one team to win per hunt and add a per-team cooldown before they can win again.

    Generate this version
  • Timed schedule

    Start a treasure hunt automatically every N minutes instead of requiring the admin command.

    Generate this version
  • Reward via Economics

    Give the winner a currency reward through the Economics plugin instead of only broadcast text.

    Generate this version

FAQ

Where does the crate spawn and can I use a different prefab?

Inside the configured Map Radius around the world origin, on any point above Minimum Altitude. Change Crate Prefab to assets/prefabs/misc/supply drop/supply_drop.prefab, a hackable crate prefab, or any other storage container prefab to change the look and difficulty.

Why is the grid hint sometimes off from the crate?

GetGrid derives the grid from Grid Cell Size and Map Radius. Set Grid Cell Size to the size of the grid squares your map actually uses so the hint matches; a wrong value shifts every hint by a consistent offset rather than breaking the plugin.

Does the crate drop its loot if nobody finds it?

No. On timeout EndHunt calls Kill() on the crate. If you want the loot to drop on the ground for late arrivals, call your own drop routine on the crate before the Kill call inside the timeout callback.

How do I install it on Oxide or Carbon?

Place TreasureHunt.cs into oxide/plugins/ (Carbon uses the same folder under its root). It compiles on the next reload and writes oxide/config/TreasureHunt.json and oxide/lang/en/TreasureHunt.json. Grant treasurehunt.use to admins or a group before running /treasure.

Can players trigger a hunt themselves?

Only players with treasurehunt.use can run /treasure. For player-driven hunts, add a second command or a chat keyword that checks a separate permission and uses the same StartHunt code path with a longer timeout and an optional fee before spawning.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create a Rust Oxide plugin called TreasureHunt where /treasure spawns a supply drop crate with configured loot at a random land position, broadcasts its grid hint, and announces the first player to loot it, despawning after the configured minutes.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize treasure hunt

Customize