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
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.
{
"Raycast Distance": 25,
"Show Authorized Players": true,
"Show Grade": false
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/owner | Raycast 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 |
| Permission | Grants |
|---|---|
entityownercheck.use | Grants access to the /owner admin lookup command. |
How the code works
Raycast from the player's eyes
The command starts a
Physics.Raycastfromplayer.eyes.positionin the direction ofplayer.eyes.HeadForward(), limited to the configRaycast Distance, storing the result in aRaycastHit. The hit collider is converted to a game entity withToBaseEntity(). If the raycast misses or the conversion returns null, the plugin sends theNoHitmessage and stops, so pointing at nothing or at terrain with no entity gives a clean answer.Resolve the owner identity
The hit entity exposes
OwnerIDas a ulong. If it is 0 the plugin reports it through theReportkey with the nameserver. Otherwise it callsBasePlayer.FindAwakeOrSleeping(ownerId.ToString()), which searches online and sleeping players so offline owners still resolve. A null result marks the owner asunknownbut the raw SteamID is still shown. The display name, if found, goes intoownerNameand is printed by theReportlanguage key together with the short prefab name fromShortPrefabName.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 sendsNoCupboard. Otherwise, providedShow Authorized Playersis enabled in config, it readsprivilege.authorizedPlayers, a list of ulong entries. Empty or null lists produceCupboardEmpty; otherwise the cupboard position and count are shown, then oneAuthLineper entry, with each name resolved viaBasePlayer.FindAwakeOrSleeping.Format the chat output
All text lives in
LoadDefaultMessagesunder keysNoPermission,NoHit,ServerOwned,UnknownOwner,Report,Grade,NoCupboard,CupboardEmpty,CupboardandAuthLine, read via theMsghelper and sent withSendReply. 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.Gate the command with a permission
The
[ChatCommand("owner")]method takesBasePlayer player, string command, string[] argsand checkspermission.UserHasPermission(player.UserIDString, PermUse)before any raycast runs. The constantentityownercheck.usefollows the lowercase class-name prefix Oxide expects and is registered inInitwithpermission.RegisterPermission. Players without it receive theNoPermissionmessage.Optional config for range and detail
The config class exposes
Raycast Distance(float, default 25),Show Authorized Players(bool, default true) andShow Grade(bool, default false). The distance is passed directly to the raycast so you can line up distant objects without recompiling. WithShow Gradeenabled, the hit entity is cast toBuildingBlockand itsgradevalue is printed through theGradekey.
Install it on your server
Save EntityOwnerCheck.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/EntityOwnerCheck.json(Carbon:carbon/configs/EntityOwnerCheck.json). After editing, reload it:oxide.reload EntityOwnerCheck c.reload EntityOwnerCheck # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionLog lookups to a data file
Records an audit trail so admins can review who checked what.
Generate this versionOwner-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More admin tools plugins
- Admin command loggerRecords every console command executed by a player with admin rights into an oxide/data file, optionally echoes it to the server console, and showsโฆOnServerInitializedOnServerCommand
- Admin entity removerAdds an admin-only /kill command that raycast-targets the entity you are looking at and removes it, plus /killall <shortprefabname> which asks for aโฆOnServerInitialized
- Admin radarA permission-gated admin tool that lists every player within a configurable radius with distance, compass direction and sleep state, then drawsโฆ
Last updated Oct 26, 2026.
Customize entity owner lookup
Customize