C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using Rust;
using Rust.Ai.Gen2;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Wipe Raid Timer", "Sigilo", "2.0.0")]
[Description("Protects cupboard-covered buildings and deployables from raiding for a configurable time after wipe")]
public class WipeRaidTimer : RustPlugin
{
private const string BypassPermission = "wiperaidtimer.bypass";
private const string UiTimerBarName = "wiperaidtimer.timerbar";
private const string StatusBarBackgroundColor = "0.1 0.1 0.1 0.7";
private const string StatusBarBorderColor = "0.3 0.3 0.3 0.7";
private const string StatusBarFillColor = "0.7 0.2 0.2 0.9";
private const string StatusBarTextColor = "1 1 1 0.9";
private const string StatusBarAnchorMin = "0.10 0.03";
private const string StatusBarAnchorMax = "0.28 0.05";
private readonly HashSet<string> deployablePrefabPaths = new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<ulong, float> notificationTimes = new Dictionary<ulong, float>();
private readonly Dictionary<uint, BuildingQualificationCacheEntry> buildingQualificationCache =
new Dictionary<uint, BuildingQualificationCacheEntry>();
private ConfigData configData;
private Timer blockExpiryTimer;
private Timer uiUpdateTimer;
private bool blockActive;
private bool serverInitialized;
#region Oxide Hooks
private void Init()
{
permission.RegisterPermission(BypassPermission, this);
// Damage handling is enabled only while protection is active.
Unsubscribe(nameof(OnEntityTakeDamage));
}
private void OnServerInitialized()
{
serverInitialized = true;
BuildDeployablePrefabCache();
RefreshBlockState();
Puts($"Loaded with {deployablePrefabPaths.Count} player-deployable prefabs cached. " +
$"Raid protection is {(blockActive ? "active" : "inactive")}.");
}
private void OnNewSave(string filename)
{
if (!serverInitialized)
{
return;
}
NextTick(RefreshBlockState);
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null)
{
return;
}
NextTick(() => UpdateStatusBar(player));
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player == null)
{
return;
}
CuiHelper.DestroyUi(player, UiTimerBarName);
notificationTimes.Remove(player.userID);
}
private object OnEntityTakeDamage(BaseEntity entity, HitInfo info)
{
if (!blockActive || entity == null || entity.IsDestroyed || info?.damageTypes == null)
{
return null;
}
float totalDamage = info.damageTypes.Total();
if (totalDamage <= 0f || IsAllowedDecayDamage(info, totalDamage) || !IsProtectedTarget(entity))
{
return null;
}
BuildingPrivlidge privilege = FindCoveringPrivilege(entity);
if (privilege == null || privilege.IsDestroyed)
{
return null;
}
if (!IsQualifiedProtectedBuilding(privilege))
{
return null;
}
if (configData.Protection.RequireDeployableOwnerAuthorization &&
IsGenericPlayerDeployable(entity) &&
!IsDeployableOwnerAuthorized(entity, privilege))
{
return null;
}
BasePlayer attacker;
ulong attackerId = ResolveResponsiblePlayer(info, out attacker);
if (attackerId.IsSteamId())
{
if (HasBypassPermission(attackerId, attacker))
{
return null;
}
if (configData.Protection.AllowCupboardAuthorizedDamage &&
((configData.Protection.AllowCupboardOwnerDamage && privilege.OwnerID == attackerId) ||
privilege.IsAuthed(attackerId)))
{
return null;
}
}
else if (!configData.Protection.BlockUnattributedDamage)
{
return null;
}
info.damageTypes.Clear();
info.DidHit = false;
info.DoHitEffects = false;
NotifyBlockedAttacker(attacker, entity, privilege, totalDamage);
return true;
}
private void Unload()
{
blockActive = false;
Unsubscribe(nameof(OnEntityTakeDamage));
blockExpiryTimer?.Destroy();
blockExpiryTimer = null;
uiUpdateTimer?.Destroy();
uiUpdateTimer = null;
notificationTimes.Clear();
buildingQualificationCache.Clear();
DestroyAllStatusBars();
}
#endregion
#region Protection Lifecycle
private void RefreshBlockState()
{
int remainingSeconds = GetRemainingBlockTime();
if (remainingSeconds > 0)
{
ActivateProtection(remainingSeconds);
}
else
{
DeactivateProtection();
}
}
private void ActivateProtection(int remainingSeconds)
{
bool wasActive = blockActive;
blockActive = true;
Subscribe(nameof(OnEntityTakeDamage));
blockExpiryTimer?.Destroy();
blockExpiryTimer = timer.Once(Math.Max(1f, remainingSeconds + 0.25f), RefreshBlockState);
if (!wasActive)
{
StartUiUpdates();
}
UpdateAllStatusBars();
}
private void DeactivateProtection()
{
bool wasActive = blockActive;
blockActive = false;
Unsubscribe(nameof(OnEntityTakeDamage));
blockExpiryTimer?.Destroy();
blockExpiryTimer = null;
uiUpdateTimer?.Destroy();
uiUpdateTimer = null;
notificationTimes.Clear();
buildingQualificationCache.Clear();
DestroyAllStatusBars();
if (wasActive)
{
Puts("Wipe raid protection has expired.");
}
}
private void StartUiUpdates()
{
uiUpdateTimer?.Destroy();
uiUpdateTimer = null;
if (!configData.StatusBar.Enabled)
{
return;
}
uiUpdateTimer = timer.Every(configData.StatusBar.RefreshIntervalSeconds, UpdateAllStatusBars);
}
#endregion
#region Target Classification and Privilege
private void BuildDeployablePrefabCache()
{
deployablePrefabPaths.Clear();
foreach (ItemDefinition definition in ItemManager.GetItemDefinitions())
{
if (!definition.TryGetComponent(out ItemModDeployable deployable) || deployable?.entityPrefab == null)
{
continue;
}
string prefabPath = deployable.entityPrefab.resourcePath;
if (!string.IsNullOrEmpty(prefabPath))
{
deployablePrefabPaths.Add(prefabPath);
}
}
}
private bool IsProtectedTarget(BaseEntity entity)
{
if (entity is BasePlayer || entity.IsNpc || entity is BaseNpc || entity is BaseNPC2 ||
entity is ResourceEntity || entity is BaseVehicle || entity is RidableHorse ||
HasVehicleParent(entity))
{
return false;
}
if (entity is BuildingBlock buildingBlock)
{
if (buildingBlock.grade == BuildingGrade.Enum.Twigs)
{
return configData.Protection.ProtectTwigBuildingBlocks;
}
return configData.Protection.ProtectBuildingBlocks;
}
if (entity is Door)
{
return configData.Protection.ProtectDoors;
}
if (entity is BaseLock)
{
return configData.Protection.ProtectDoors || configData.Protection.ProtectPlayerDeployables;
}
if (entity is BuildingPrivlidge)
{
return configData.Protection.ProtectPlayerDeployables;
}
return configData.Protection.ProtectPlayerDeployables && IsGenericPlayerDeployable(entity);
}
private bool IsGenericPlayerDeployable(BaseEntity entity)
{
return !string.IsNullOrEmpty(entity.PrefabName) &&
deployablePrefabPaths.Contains(entity.PrefabName);
}
private bool IsDeployableOwnerAuthorized(BaseEntity entity, BuildingPrivlidge privilege)
{
BaseEntity current = entity;
for (int depth = 0; current != null && depth < 4; depth++)
{
ulong ownerId = current.OwnerID;
if (ownerId.IsSteamId())
{
return IsOwnerAuthorized(ownerId, privilege);
}
current = current.GetParentEntity();
}
return false;
}
private bool IsQualifiedProtectedBuilding(BuildingPrivlidge privilege)
{
BuildingManager.Building building = privilege.GetBuilding();
if (building == null)
{
return false;
}
float now = Time.realtimeSinceStartup;
if (buildingQualificationCache.TryGetValue(building.ID, out BuildingQualificationCacheEntry cached) &&
cached.ExpiresAt > now)
{
return cached.IsQualified;
}
bool isQualified = BuildingMeetsMinimumRequirements(building, privilege);
if (buildingQualificationCache.Count >= 2048)
{
buildingQualificationCache.Clear();
}
buildingQualificationCache[building.ID] = new BuildingQualificationCacheEntry
{
IsQualified = isQualified,
ExpiresAt = now + configData.BaseQualification.CacheSeconds
};
return isQualified;
}
private bool BuildingMeetsMinimumRequirements(
BuildingManager.Building building,
BuildingPrivlidge privilege)
{
int foundationCount = 0;
foreach (BuildingBlock block in building.buildingBlocks)
{
if (block == null || block.IsDestroyed || block.grade == BuildingGrade.Enum.Twigs ||
!IsOwnerAuthorized(block.OwnerID, privilege) ||
!block.ShortPrefabName.StartsWith("foundation", StringComparison.Ordinal))
{
continue;
}
if (++foundationCount >= configData.BaseQualification.MinimumNonTwigFoundations)
{
break;
}
}
if (foundationCount < configData.BaseQualification.MinimumNonTwigFoundations)
{
return false;
}
int doorCount = 0;
foreach (Door door in building.doors)
{
if (door == null || door.IsDestroyed || !IsOwnerAuthorized(door.OwnerID, privilege))
{
continue;
}
if (++doorCount >= configData.BaseQualification.MinimumDoors)
{
return true;
}
}
return doorCount >= configData.BaseQualification.MinimumDoors;
}
private bool IsOwnerAuthorized(ulong ownerId, BuildingPrivlidge privilege)
{
return ownerId.IsSteamId() &&
(privilege.OwnerID == ownerId || privilege.IsAuthed(ownerId));
}
private class BuildingQualificationCacheEntry
{
public bool IsQualified;
public float ExpiresAt;
}
private bool HasVehicleParent(BaseEntity entity)
{
BaseEntity parent = entity.GetParentEntity();
for (int depth = 0; parent != null && depth < 4; depth++)
{
if (parent is BaseVehicle || parent is RidableHorse)
{
return true;
}
parent = parent.GetParentEntity();
}
return false;
}
private BuildingPrivlidge FindCoveringPrivilege(BaseEntity entity)
{
if (entity is BuildingPrivlidge cupboard && !cupboard.IsDestroyed)
{
return cupboard;
}
if (entity is DecayEntity decayEntity)
{
BuildingManager.Building building = decayEntity.GetBuilding();
BuildingPrivlidge dominatingPrivilege = building?.GetDominatingBuildingPrivilege();
if (dominatingPrivilege != null && !dominatingPrivilege.IsDestroyed)
{
return dominatingPrivilege;
}
}
BaseEntity boundsEntity = entity;
if (entity is BaseLock)
{
BaseEntity parent = entity.GetParentEntity();
if (parent != null)
{
boundsEntity = parent;
}
}
return boundsEntity.GetBuildingPrivilege(
boundsEntity.WorldSpaceBounds(),
true,
configData.Protection.PrivilegeLookupCacheSeconds
);
}
private bool IsAllowedDecayDamage(HitInfo info, float totalDamage)
{
if (!configData.Protection.AllowDecayDamage)
{
return false;
}
float decayDamage = info.damageTypes.Get(DamageType.Decay);
return decayDamage > 0f && decayDamage >= totalDamage - 0.01f;
}
#endregion
#region Damage Attribution
private ulong ResolveResponsiblePlayer(HitInfo info, out BasePlayer player)
{
player = info.InitiatorPlayer;
if (IsHumanPlayer(player))
{
return player.userID;
}
ulong userId = ResolvePlayerFromEntity(info.Initiator, out player);
if (userId.IsSteamId())
{
return userId;
}
userId = ResolvePlayerFromEntity(info.Weapon, out player);
if (userId.IsSteamId())
{
return userId;
}
return ResolvePlayerFromEntity(info.WeaponPrefab, out player);
}
private ulong ResolvePlayerFromEntity(BaseEntity source, out BasePlayer player)
{
player = null;
BaseEntity current = source;
for (int depth = 0; current != null && depth < 6; depth++)
{
if (current is BasePlayer directPlayer && IsHumanPlayer(directPlayer))
{
player = directPlayer;
return directPlayer.userID;
}
if (current is BaseVehicle vehicle)
{
BasePlayer driver = vehicle.GetDriver();
if (IsHumanPlayer(driver))
{
player = driver;
return driver.userID;
}
}
if (current.OwnerID.IsSteamId())
{
player = BasePlayer.FindAwakeOrSleepingByID(current.OwnerID);
return current.OwnerID;
}
BaseEntity creator = current.creatorEntity;
if (creator != null && creator != current)
{
current = creator;
continue;
}
BaseEntity parent = current.GetParentEntity();
if (parent == null || parent == current)
{
break;
}
current = parent;
}
return 0UL;
}
private bool IsHumanPlayer(BasePlayer player)
{
return player != null && !player.IsNpc && player.userID.IsSteamId();
}
private bool HasBypassPermission(ulong userId, BasePlayer player)
{
return userId.IsSteamId() &&
((configData.Protection.AllowAdminBypass && player != null && player.IsAdmin) ||
permission.UserHasPermission(userId.ToString(CultureInfo.InvariantCulture), BypassPermission));
}
#endregion
#region Notifications
private void NotifyBlockedAttacker(
BasePlayer attacker,
BaseEntity target,
BuildingPrivlidge privilege,
float attemptedDamage)
{
if ((!configData.Notifications.NotifyBlockedAttacker && !configData.DebugLogging) ||
!IsHumanPlayer(attacker) || !attacker.IsConnected)
{
return;
}
float now = Time.realtimeSinceStartup;
if (notificationTimes.TryGetValue(attacker.userID, out float lastNotification) &&
now - lastNotification < configData.Notifications.CooldownSeconds)
{
return;
}
notificationTimes[attacker.userID] = now;
if (configData.Notifications.NotifyBlockedAttacker)
{
attacker.ChatMessage(GetMessage(
"DamageBlocked",
attacker.UserIDString,
FormatTimeString(GetRemainingBlockTime())
));
}
if (configData.DebugLogging)
{
Puts($"Blocked {attemptedDamage:0.##} damage from {attacker.displayName} ({attacker.userID}) " +
$"to {target.ShortPrefabName} under cupboard {privilege.net?.ID.Value ?? 0UL}.");
}
}
#endregion
#region Status Bar UI
private void UpdateAllStatusBars()
{
if (!blockActive || !configData.StatusBar.Enabled)
{
DestroyAllStatusBars();
return;
}
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
UpdateStatusBar(player);
}
}
private void UpdateStatusBar(BasePlayer player)
{
if (player == null || !player.IsConnected)
{
return;
}
if (!blockActive || !configData.StatusBar.Enabled || HasBypassPermission(player.userID, player))
{
CuiHelper.DestroyUi(player, UiTimerBarName);
return;
}
int remainingTime = GetRemainingBlockTime();
if (remainingTime <= 0)
{
CuiHelper.DestroyUi(player, UiTimerBarName);
return;
}
float progress = configData.BlockTimeAfterWipe > 0
? Mathf.Clamp01((float)remainingTime / configData.BlockTimeAfterWipe)
: 0f;
string progressAnchor = progress.ToString("0.####", CultureInfo.InvariantCulture);
string label = GetMessage(
"TimerLabel",
player.UserIDString,
FormatTimeString(remainingTime)
);
CuiElementContainer elements = new CuiElementContainer();
elements.Add(new CuiElement
{
Name = UiTimerBarName,
Parent = "Hud",
Components =
{
new CuiImageComponent { Color = StatusBarBackgroundColor },
new CuiRectTransformComponent
{
AnchorMin = StatusBarAnchorMin,
AnchorMax = StatusBarAnchorMax
},
new CuiOutlineComponent
{
Color = StatusBarBorderColor,
Distance = "1 1"
}
}
});
elements.Add(new CuiElement
{
Name = $"{UiTimerBarName}.fill",
Parent = UiTimerBarName,
Components =
{
new CuiImageComponent { Color = StatusBarFillColor },
new CuiRectTransformComponent
{
AnchorMin = "0 0",
AnchorMax = $"{progressAnchor} 1"
}
}
});
elements.Add(new CuiElement
{
Name = $"{UiTimerBarName}.text",
Parent = UiTimerBarName,
Components =
{
new CuiTextComponent
{
Text = label,
Align = TextAnchor.MiddleCenter,
FontSize = 12,
Color = StatusBarTextColor,
Font = "robotocondensed-regular.ttf"
},
new CuiRectTransformComponent
{
AnchorMin = "0 0",
AnchorMax = "1 1"
}
}
});
CuiHelper.DestroyUi(player, UiTimerBarName);
CuiHelper.AddUi(player, elements);
}
private void DestroyAllStatusBars()
{
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player != null)
{
CuiHelper.DestroyUi(player, UiTimerBarName);
}
}
}
#endregion
#region Time and Messages
private int GetRemainingBlockTime()
{
if (configData == null || configData.BlockTimeAfterWipe <= 0)
{
return 0;
}
double elapsedSeconds = Math.Max(0d, (DateTime.UtcNow - SaveRestore.SaveCreatedTime).TotalSeconds);
return Math.Max(0, (int)Math.Ceiling(configData.BlockTimeAfterWipe - elapsedSeconds));
}
private string FormatTimeString(int seconds)
{
seconds = Math.Max(0, seconds);
List<string> components = new List<string>(4);
int days = seconds / 86400;
seconds %= 86400;
if (days > 0)
{
components.Add($"{days}d");
}
int hours = seconds / 3600;
seconds %= 3600;
if (hours > 0)
{
components.Add($"{hours}h");
}
int minutes = seconds / 60;
seconds %= 60;
if (minutes > 0)
{
components.Add($"{minutes}m");
}
if (seconds > 0 || components.Count == 0)
{
components.Add($"{seconds}s");
}
return string.Join(" ", components);
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["DamageBlocked"] = "This structure is protected from raiding for <color=red>{0}</color>.",
["TimerLabel"] = "Raid Protection: {0}"
}, this);
}
private string GetMessage(string key, string playerId, params object[] args)
{
return string.Format(lang.GetMessage(key, this, playerId), args);
}
#endregion
#region Configuration
private class ConfigData
{
[JsonProperty("Block Time After Wipe (seconds)")]
public int BlockTimeAfterWipe { get; set; } = 86400;
[JsonProperty("Protection")]
public ProtectionSettings Protection { get; set; } = new ProtectionSettings();
[JsonProperty("Base Qualification")]
public BaseQualificationSettings BaseQualification { get; set; } = new BaseQualificationSettings();
[JsonProperty("Blocked Hit Notifications")]
public NotificationSettings Notifications { get; set; } = new NotificationSettings();
[JsonProperty("Status Bar")]
public StatusBarSettings StatusBar { get; set; } = new StatusBarSettings();
[JsonProperty("Debug Logging")]
public bool DebugLogging { get; set; }
}
private class ProtectionSettings
{
[JsonProperty("Protect Building Blocks")]
public bool ProtectBuildingBlocks { get; set; } = true;
[JsonProperty("Protect Twig Building Blocks")]
public bool ProtectTwigBuildingBlocks { get; set; } = false;
[JsonProperty("Protect Doors")]
public bool ProtectDoors { get; set; } = true;
[JsonProperty("Protect Player Deployables")]
public bool ProtectPlayerDeployables { get; set; } = true;
[JsonProperty("Require Deployable Owner Authorization")]
public bool RequireDeployableOwnerAuthorization { get; set; } = true;
[JsonProperty("Allow Cupboard Authorized Damage")]
public bool AllowCupboardAuthorizedDamage { get; set; } = true;
[JsonProperty("Allow Cupboard Owner Damage")]
public bool AllowCupboardOwnerDamage { get; set; } = true;
[JsonProperty("Allow Admin Bypass")]
public bool AllowAdminBypass { get; set; } = true;
[JsonProperty("Block Unattributed Damage")]
public bool BlockUnattributedDamage { get; set; } = true;
[JsonProperty("Allow Decay Damage")]
public bool AllowDecayDamage { get; set; } = true;
[JsonProperty("Privilege Lookup Cache Seconds")]
public float PrivilegeLookupCacheSeconds { get; set; } = 2f;
}
private class BaseQualificationSettings
{
[JsonProperty("Minimum Non-Twig Foundations")]
public int MinimumNonTwigFoundations { get; set; } = 2;
[JsonProperty("Minimum Doors")]
public int MinimumDoors { get; set; } = 1;
[JsonProperty("Cache Seconds")]
public float CacheSeconds { get; set; } = 5f;
}
private class NotificationSettings
{
[JsonProperty("Notify Blocked Attacker")]
public bool NotifyBlockedAttacker { get; set; } = true;
[JsonProperty("Cooldown Seconds")]
public float CooldownSeconds { get; set; } = 5f;
}
private class StatusBarSettings
{
[JsonProperty("Enabled")]
public bool Enabled { get; set; } = true;
[JsonProperty("Refresh Interval Seconds")]
public float RefreshIntervalSeconds { get; set; } = 10f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
JObject rawConfig = Config.ReadObject<JObject>() ?? new JObject();
bool legacyConfig = rawConfig["Blocked Items (List of items blocked during wipe timer)"] != null ||
rawConfig["Block Attack Helicopter"] != null ||
rawConfig["Block MLRS"] != null ||
rawConfig["Block Drones (Set drone.maxcontrolrange to 1 during raid block)"] != null;
configData = rawConfig.ToObject<ConfigData>() ?? new ConfigData();
if (legacyConfig)
{
PrintWarning("Migrated legacy item-blocking configuration. Attack helicopters, raid items, " +
"MLRS, and drones are no longer restricted by default.");
}
}
catch (Exception exception)
{
PrintError($"Configuration is invalid; using defaults. {exception.Message}");
configData = new ConfigData();
}
ValidateConfig();
SaveConfig();
}
private void ValidateConfig()
{
configData ??= new ConfigData();
configData.Protection ??= new ProtectionSettings();
configData.BaseQualification ??= new BaseQualificationSettings();
configData.Notifications ??= new NotificationSettings();
configData.StatusBar ??= new StatusBarSettings();
configData.BlockTimeAfterWipe = Math.Max(0, configData.BlockTimeAfterWipe);
configData.Protection.PrivilegeLookupCacheSeconds = Mathf.Clamp(
configData.Protection.PrivilegeLookupCacheSeconds,
0.1f,
10f
);
configData.BaseQualification.MinimumNonTwigFoundations = Math.Max(
1,
configData.BaseQualification.MinimumNonTwigFoundations
);
configData.BaseQualification.MinimumDoors = Math.Max(
1,
configData.BaseQualification.MinimumDoors
);
configData.BaseQualification.CacheSeconds = Mathf.Clamp(
configData.BaseQualification.CacheSeconds,
1f,
30f
);
configData.Notifications.CooldownSeconds = Mathf.Clamp(
configData.Notifications.CooldownSeconds,
0.5f,
60f
);
configData.StatusBar.RefreshIntervalSeconds = Mathf.Clamp(
configData.StatusBar.RefreshIntervalSeconds,
1f,
60f
);
}
protected override void LoadDefaultConfig()
{
configData = new ConfigData();
}
protected override void SaveConfig()
{
Config.WriteObject(configData, true);
}
#endregion
#region API
private Dictionary<string, int> API_GetRemainingBlockTime()
{
return new Dictionary<string, int>
{
["RemainingSeconds"] = GetRemainingBlockTime()
};
}
#endregion
}
}