C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("FriendlySAM", "wGw", "1.0.1")]
[Description("SAM sites ignore players and air vehicles that are authorised on the covering Tool Cupboard. Pure event-driven, zero per-tick.")]
public class FriendlySAM : RustPlugin
{
#region Fields
private Configuration _config;
// TC net.ID → currently authorised player userIDs
private readonly Dictionary<ulong, HashSet<ulong>> _tcAuthCache = new Dictionary<ulong, HashSet<ulong>>();
// Air vehicles we protect (mini, scrap heli, attack heli)
private static readonly HashSet<string> AirVehiclePrefabs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"minicopter.entity",
"scraptransporthelicopter",
"attackhelicopter.entity"
};
private const string PermAdmin = "friendlysam.admin";
#endregion
#region Config
private class Configuration
{
[JsonProperty("Version (do not edit)")]
public VersionNumber Version { get; set; } = new VersionNumber(1, 0, 1);
[JsonProperty("Enabled")]
public bool Enabled { get; set; } = true;
[JsonProperty("Debug (console spam)")]
public bool Debug { get; set; } = false;
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
throw new Exception("Config is null");
if (_config.Version < new VersionNumber(1, 0, 1))
{
_config.Version = new VersionNumber(1, 0, 1);
}
}
catch
{
PrintWarning("Config corrupt or missing — generating defaults.");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
#endregion
#region Oxide Hooks - Lifecycle
private void Init()
{
permission.RegisterPermission(PermAdmin, this);
}
private void OnServerInitialized()
{
// Delay slightly so all TCs are fully loaded
timer.Once(2f, RebuildAllTcCache);
}
private void Unload()
{
_tcAuthCache.Clear();
}
#endregion
#region Hooks - SAM Targeting
private object OnSamSiteTarget(SamSite samSite, SamSite.ISamSiteTarget target)
{
if (!_config.Enabled || samSite == null || target == null)
return null;
// Monument / static / orphan SAMs stay fully aggressive
BuildingPrivlidge priv = samSite.GetBuildingPrivilege();
if (priv == null)
return null;
// --- Player on foot ---
BasePlayer player = target as BasePlayer;
if (player != null)
{
if (IsAuthedCached(priv, player.userID))
{
if (_config.Debug)
Puts($"[FriendlySAM] Blocked player {player.displayName} ({player.userID}) on TC {priv.net.ID}");
return false;
}
return null;
}
// --- Air vehicle ---
BaseEntity entity = target as BaseEntity;
if (entity == null || !AirVehiclePrefabs.Contains(entity.ShortPrefabName))
return null;
BaseVehicle vehicle = entity as BaseVehicle;
if (vehicle == null)
return null;
// If any seated player is authorised on this TC, the whole vehicle is friendly
if (vehicle.mountPoints != null)
{
for (int i = 0; i < vehicle.mountPoints.Count; i++)
{
var mount = vehicle.mountPoints[i];
if (mount?.mountable == null)
continue;
BasePlayer occupant = mount.mountable.GetMounted();
if (occupant != null && IsAuthedCached(priv, occupant.userID))
{
if (_config.Debug)
Puts($"[FriendlySAM] Blocked air vehicle {entity.ShortPrefabName} (occupant {occupant.displayName}) on TC {priv.net.ID}");
return false;
}
}
}
return null;
}
#endregion
#region Hooks - Cupboard Auth Changes
private void OnCupboardAuthorize(BuildingPrivlidge priv, BasePlayer player)
{
InvalidateTc(priv);
}
private void OnCupboardDeauthorize(BuildingPrivlidge priv, BasePlayer player)
{
InvalidateTc(priv);
}
private void OnCupboardClearList(BuildingPrivlidge priv, BasePlayer player)
{
InvalidateTc(priv);
}
private void OnEntityKill(BuildingPrivlidge priv)
{
InvalidateTc(priv);
}
#endregion
#region Cache
private bool IsAuthedCached(BuildingPrivlidge priv, ulong userId)
{
if (priv == null || priv.net == null)
return false;
ulong tcId = priv.net.ID.Value;
if (!_tcAuthCache.TryGetValue(tcId, out HashSet<ulong> set))
{
// Cache miss — build on demand
set = BuildAuthSet(priv);
_tcAuthCache[tcId] = set;
}
return set.Contains(userId);
}
private HashSet<ulong> BuildAuthSet(BuildingPrivlidge priv)
{
var set = new HashSet<ulong>();
if (priv?.authorizedPlayers == null)
return set;
// In current Rust, authorizedPlayers is a collection of ulong userIDs
foreach (ulong userId in priv.authorizedPlayers)
{
set.Add(userId);
}
return set;
}
private void InvalidateTc(BuildingPrivlidge priv)
{
if (priv?.net == null)
return;
_tcAuthCache.Remove(priv.net.ID.Value);
if (_config.Debug)
Puts($"[FriendlySAM] Invalidated TC cache for {priv.net.ID}");
}
private void RebuildAllTcCache()
{
_tcAuthCache.Clear();
foreach (var priv in UnityEngine.Object.FindObjectsOfType<BuildingPrivlidge>())
{
if (priv == null || priv.net == null || priv.IsDestroyed)
continue;
_tcAuthCache[priv.net.ID.Value] = BuildAuthSet(priv);
}
Puts($"[FriendlySAM] Rebuilt TC auth cache — {_tcAuthCache.Count} cupboards");
}
#endregion
#region Commands
[ChatCommand("friendlysam.reload")]
[ConsoleCommand("friendlysam.reload")]
private void CmdReload(BasePlayer player, string command, string[] args)
{
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage("You do not have permission to use this command.");
return;
}
LoadConfig();
RebuildAllTcCache();
string msg = $"[FriendlySAM] Reloaded. Enabled={_config.Enabled} | Cached TCs={_tcAuthCache.Count}";
if (player != null)
player.ChatMessage(msg);
else
Puts(msg);
}
#endregion
}
}