Skip to content
RustPluginGenerator

How to make a Rust entity owner check plugin (raycast admin tool)

An admin chat command that raycasts from the player's eyes and reports the hit entity's prefab, owner name and SteamID, plus everyone authorized on the tool cupboard protecting it. It reads OwnerID and the cupboard authorization list directly and needs no Oxide hooks beyond startup.

When something on your server is griefing, duped or placed by a player you cannot identify, the usual tools are the server console and save-file inspection. Neither answers the practical question fast: who owns this, and whose tool cupboard protects it? This plugin adds /owner, a chat command for admins that fires a physics raycast from the player's eyes, resolves the hit entity and prints a compact report to chat.

It uses a single Physics.Raycast from player.eyes.position along eyes.HeadForward(), reads OwnerID from the hit entity, resolves the owner name with BasePlayer.FindAwakeOrSleeping, and looks up the covering privilege with GetBuildingPrivilege(). Authorized players are listed with name and SteamID. Output goes through the language file so you can translate it, and all lookups are null-checked.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Entity Owner Check", "RustPluginGenerator", "1.0.0")]
    [Description("Adds an admin /owner command that raycasts and reports entity ownership and covering cupboard authorizations.")]
    public class EntityOwnerCheck : RustPlugin
    {
        private const string PermUse = "entityownercheck.use";

        private PluginConfig config;

        private class PluginConfig
        {
            [JsonProperty("Raycast Distance")]
            public float RaycastDistance = 25f;

            [JsonProperty("Show Authorized Players")]
            public bool ShowAuthorizedPlayers = true;

            [JsonProperty("Show Grade")]
            public bool ShowGrade = false;
        }

        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>
            {
                ["NoPermission"] = "You do not have permission to use this command.",
                ["NoHit"] = "No entity found in front of you within {0} metres.",
                ["ServerOwned"] = "Owner: server (OwnerID 0)",
                ["UnknownOwner"] = "Owner: unknown player (SteamID {0})",
                ["Report"] = "Entity: <color=#e6c15c>{0}</color> | Owner: <color=#e6c15c>{1}</color> ({2})",
                ["Grade"] = "Grade: {0}",
                ["NoCupboard"] = "Authorization: not inside any tool cupboard zone.",
                ["Cupboard"] = "Tool cupboard at {0} | Authorized ({1}):",
                ["CupboardEmpty"] = "Tool cupboard found but nobody is authorized.",
                ["AuthLine"] = "  - {0} ({1})"
            }, 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(PermUse, this);
        }

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

            RaycastHit hit;
            if (!Physics.Raycast(player.eyes.position, player.eyes.HeadForward(), out hit, config.RaycastDistance))
            {
                SendReply(player, Msg("NoHit", player.UserIDString, config.RaycastDistance));
                return;
            }

            var entity = hit.collider != null ? hit.collider.ToBaseEntity() : null;
            if (entity == null)
            {
                SendReply(player, Msg("NoHit", player.UserIDString, config.RaycastDistance));
                return;
            }

            string prefab = entity.ShortPrefabName ?? "unknown";
            string ownerName = "server";
            string ownerId = "0";

            if (entity.OwnerID != 0)
            {
                ownerId = entity.OwnerID.ToString();
                var owner = BasePlayer.FindAwakeOrSleeping(ownerId);
                if (owner != null)
                {
                    ownerName = owner.displayName;
                }
                else
                {
                    ownerName = "unknown";
                }
            }

            SendReply(player, Msg("Report", player.UserIDString, prefab, ownerName, ownerId));

            if (config.ShowGrade)
            {
                var block = entity as BuildingBlock;
                if (block != null)
                {
                    SendReply(player, Msg("Grade", player.UserIDString, block.grade));
                }
            }

            var privilege = entity.GetBuildingPrivilege();
            if (privilege == null)
            {
                SendReply(player, Msg("NoCupboard", player.UserIDString));
                return;
            }

            if (!config.ShowAuthorizedPlayers) return;

            var list = privilege.authorizedPlayers;
            if (list == null || list.Count == 0)
            {
                SendReply(player, Msg("CupboardEmpty", player.UserIDString));
                return;
            }

            var pos = privilege.transform.position;
            SendReply(player, Msg("Cupboard", player.UserIDString, string.Format("{0:F0},{1:F0},{2:F0}", pos.x, pos.y, pos.z), list.Count));
            foreach (var entry in list)
            {
                ulong id = entry;
                var authed = BasePlayer.FindAwakeOrSleeping(id.ToString());
                string name = authed != null && !string.IsNullOrEmpty(authed.displayName) ? authed.displayName : "unknown";
                SendReply(player, Msg("AuthLine", player.UserIDString, name, id));
            }
        }
    }
}

Config file

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

EntityOwnerCheck.json
{
  "Raycast Distance": 25,
  "Show Authorized Players": true,
  "Show Grade": false
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/ownerRaycast from your eyes and report the hit entity's prefab, owner name and SteamID, plus the authorized players on the tool cupboard protecting it.entityownercheck.use
Permissions
PermissionGrants
entityownercheck.useGrants access to the /owner admin lookup command.

How the code works

  1. Raycast from the player's eyes

    The command starts a Physics.Raycast from player.eyes.position in the direction of player.eyes.HeadForward(), limited to the config Raycast Distance, storing the result in a RaycastHit. The hit collider is converted to a game entity with ToBaseEntity(). If the raycast misses or the conversion returns null, the plugin sends the NoHit message and stops, so pointing at nothing or at terrain with no entity gives a clean answer.

  2. Resolve the owner identity

    The hit entity exposes OwnerID as a ulong. If it is 0 the plugin reports it through the Report key with the name server. Otherwise it calls BasePlayer.FindAwakeOrSleeping(ownerId.ToString()), which searches online and sleeping players so offline owners still resolve. A null result marks the owner as unknown but the raw SteamID is still shown. The display name, if found, goes into ownerName and is printed by the Report language key together with the short prefab name from ShortPrefabName.

  3. Find the covering tool cupboard

    entity.GetBuildingPrivilege() returns the cupboard entity whose protected zone contains the hit entity's position, or null outside any privilege. When null the plugin sends NoCupboard. Otherwise, provided Show Authorized Players is enabled in config, it reads privilege.authorizedPlayers, a list of ulong entries. Empty or null lists produce CupboardEmpty; otherwise the cupboard position and count are shown, then one AuthLine per entry, with each name resolved via BasePlayer.FindAwakeOrSleeping.

  4. Format the chat output

    All text lives in LoadDefaultMessages under keys NoPermission, NoHit, ServerOwned, UnknownOwner, Report, Grade, NoCupboard, CupboardEmpty, Cupboard and AuthLine, read via the Msg helper and sent with SendReply. The authorized list length varies, so the plugin sends a header line with the cupboard's position formatted to zero decimals and a count, then one reply per authorized player. Admins see results on screen without touching the console.

  5. Gate the command with a permission

    The [ChatCommand("owner")] method takes BasePlayer player, string command, string[] args and checks permission.UserHasPermission(player.UserIDString, PermUse) before any raycast runs. The constant entityownercheck.use follows the lowercase class-name prefix Oxide expects and is registered in Init with permission.RegisterPermission. Players without it receive the NoPermission message.

  6. Optional config for range and detail

    The config class exposes Raycast Distance (float, default 25), Show Authorized Players (bool, default true) and Show Grade (bool, default false). The distance is passed directly to the raycast so you can line up distant objects without recompiling. With Show Grade enabled, the hit entity is cast to BuildingBlock and its grade value is printed through the Grade key.

Install it on your server

  1. Save EntityOwnerCheck.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/EntityOwnerCheck.json (Carbon: carbon/configs/EntityOwnerCheck.json). After editing, reload it:oxide.reload EntityOwnerCheck c.reload EntityOwnerCheck # Carbon
  4. Grant the permission

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

Variations

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

  • Console variant for RCON

    Adds a console command that takes a SteamID so you can inspect from RCON.

    Generate this version
  • Log lookups to a data file

    Records an audit trail so admins can review who checked what.

    Generate this version
  • Owner-only cleanup

    Turn the lookup into an action that removes entities owned by a given SteamID.

    Generate this version

FAQ

Why does /owner report the wrong entity?

The raycast hits colliders, and some entity types are made of several colliders, so you can hit a child part. Point at the centre of the object and stand a little further away. If you need exact results, lower the config Raycast Distance and aim carefully.

Why is OwnerID 0 shown as server-owned?

Entities spawned by the server such as map monuments, barrels, airdrops and NPCs usually have OwnerID 0. The plugin reports these with the name server and ID 0 instead of trying to resolve a player name, so you are not chasing phantom SteamIDs.

How do I install it on Oxide or Carbon?

Drop the compiled plugin into oxide/plugins, which Carbon also reads. The plugin registers entityownercheck.use on load. Grant it with oxide.grant user <steamid> entityownercheck.use or oxide.grant group admin entityownercheck.use. Reload with oxide.reload EntityOwnerCheck after changing the config.

The covering cupboard is missing from the report. Why?

GetBuildingPrivilege returns null when the entity is not inside a protected zone, for example crates in the open or a base whose cupboard was destroyed. In that case there is no authorization list to show, so the plugin states the entity is not inside any tool cupboard zone.

Does this work on sleeping players' bases?

Yes. Name resolution uses BasePlayer.FindAwakeOrSleeping, so offline owners resolve to their last known display name. The authorized list comes from the cupboard entity itself and does not need anyone online.

Customize it for your server

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

Prompt: EntityOwnerCheck plugin for Rust: /owner permission entityownercheck.use, raycast from eyes with configurable distance 25, report prefab, owner name, SteamID, and authorized players on the covering tool cupboard, with optional building grade.
Customize for my server

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

Last updated Oct 26, 2026.

Customize entity owner lookup

Customize