C#:
using Facepunch;
using Facepunch.Math;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Rust;
using Rust.Ai.Gen2;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using Unity.Collections;
using UnityEngine;
namespace Oxide.Plugins {
[Info("TruePVE", "nivex", "2.4.6")]
[Description("Improvement of the default Rust PVE behavior")]
// Thanks to the original author, ignignokt84.
internal class TruePVE : RustPlugin {
#region Variables
// config/data container
private Configuration config = new();
[PluginReference]
Plugin ZoneManager, LiteZones, Clans, Friends, AbandonedBases, RaidableBases;
public string usageString;
public enum Command {
def, sched, trace, usage, enable, sleepers, map, unmap
}
;
public enum DamageResult {
None, Allow, Block
}
[Flags]
public enum RuleFlags : ulong {
None = 0, AdminsHurtSleepers = 1uL << 1, AdminsHurtPlayers = 1uL << 2, AnimalsIgnoreSleepers = 1uL << 3, AuthorizedDamage = 1uL << 4, AuthorizedDamageRequiresOwnership = 1uL << 5, CupboardOwnership = 1uL << 6, FriendlyFire = 1uL << 7, HeliDamageLocked = 1uL << 8, HumanNPCDamage = 1uL << 9, LockedBoxesImmortal = 1uL << 10, LockedDoorsImmortal = 1uL << 11, NoPlayerDamageToCar = 1uL << 12, NoPlayerDamageToMini = 1uL << 13, NoPlayerDamageToScrap = 1uL << 14, NoHeliDamage = 1uL << 15, NoHeliDamagePlayer = 1uL << 16, NoHeliDamageQuarry = 1uL << 17, NoHeliDamageRidableHorses = 1uL << 18, NoHeliDamageSleepers = 1uL << 19, NoMLRSDamage = 1uL << 20, NpcsCanHurtAnything = 1uL << 21, PlayerSamSitesIgnorePlayers = 1uL << 22, ProtectedSleepers = 1uL << 23, TrapsIgnorePlayers = 1uL << 24, TrapsIgnoreScientist = 1uL << 25, TurretsIgnorePlayers = 1uL << 26, TurretsIgnoreScientist = 1uL << 27, StaticTurretsIgnoreScientist = 1uL << 28, TwigDamage = 1uL << 29, TwigDamageRequiresOwnership = 1uL << 30, VehiclesTakeCollisionDamageWithoutDriver = 1uL << 31, SamSitesIgnoreMLRS = 1uL << 32, SelfDamage = 1uL << 33, StaticSamSitesIgnorePlayers = 1uL << 34, StaticTurretsIgnorePlayers = 1uL << 35, SafeZoneTurretsIgnorePlayers = 1uL << 36, SuicideBlocked = 1uL << 37, NoHeliDamageBuildings = 1uL << 38, WoodenDamage = 1uL << 39, WoodenDamageRequiresOwnership = 1uL << 40, AuthorizedDamageCheckPrivilege = 1uL << 41, ExcludeTugboatFromImmortalFlags = 1uL << 42, ExcludePlayerBoatFromImmortalFlags = 1uL << 43, LockedVehiclesImmortal = 1uL << 44, TurretsIgnoreBradley = 1uL << 45, AuthorizedFarmableDamage = 1uL << 46, HopperCannotTargetEnemyLoot = 1uL << 47, VehiclesTakeCollisionDamage = 1uL << 48, TrapsUnprotected = 1uL << 49,
}
[Flags]
public enum ProtectionHook : ulong {
None = 0, Damage = 1uL << 0, Familiarity = 1uL << 1, Leading = 1uL << 2, Milking = 1uL << 3, RefuseTarget = 1uL << 4, Shearing = 1uL << 5
}
private ProtectionHook protectionHooks;
private bool IsUnloading;
private Timer scheduleUpdateTimer;
// timer to check for schedule updates
private bool shareRedirectDudEnabled;
// undocumented. UAYOR.
private RuleSet dudRuleSet;
// dud ruleset when no locations are shared
private RuleSet currentRuleSet;
// current ruleset
private string currentBroadcastMessage;
// current broadcast message
private bool useZones;
// internal useZones flag
private const string Any = "any";
// constant "any" string for rules
private const string AllZones = "allzones";
// constant "allzones" string for mappings
private const string PermCanMap = "truepve.canmap";
// permission for mapping command
private bool animalsIgnoreSleepers;
// toggle flag to protect sleepers
private bool trace;
// trace flag
private const string traceFile = "ruletrace";
// tracefile name
private const float traceTimeout = 300f;
// auto-disable trace after 300s (5m)
private Timer traceTimer;
// trace timeout timer
private bool tpveEnabled = true;
// toggle flag for damage handling
private List<DamageType> _damageTypes = new() {
DamageType.Arrow, DamageType.Blunt, DamageType.Bullet, DamageType.Explosion, DamageType.Cold, DamageType.Heat, DamageType.Generic, DamageType.Slash, DamageType.Stab, DamageType.Cannon
}
;
private uint maincannonshell = 3032863244;
private uint trainbarricade = 1221760186;
private uint trainbarricadeheavy = 1363243026;
private uint loot_trash = 3279100614;
private uint giftbox_loot = 2216891097;
private uint campfire = 4160694184;
private uint oilfireballsmall = 3550347674;
private uint heli_napalm = 184893264;
private uint rocket_heli_napalm = 200672762;
private uint rocket_heli = 129320027;
private bool excludeAllZones;
private readonly Dictionary<ulong, double> _waiting = new();
private readonly HashSet<string> _deployables = new();
private readonly HashSet<string> exclusionLocationsSet = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ulong, List<PlayerExclusion>> playerDelayExclusions = new();
private readonly Dictionary<string, RuleSet> ruleSetByNameDictionary = new(StringComparer.OrdinalIgnoreCase);
private class PlayerExclusion : Pool.IPooled {
public Plugin plugin;
public double time;
public bool IsExpired => Time.realtimeSinceStartupAsDouble > time;
public void EnterPool() {
plugin = null;
time = 0f;
}
public void LeavePool() {
}
}
#endregion
#region Loading/Unloading
protected new static void Puts(string format, params object[] args) {
if (!string.IsNullOrEmpty(format)) {
Interface.Oxide.LogInfo("[{0}] {1}", "TruePVE", (args.Length != 0) ? string.Format(format, args) : format);
}
}
private void Unload() {
if (_removeMappingTimerDat is {
Destroyed : false
}
) {
_removeMappingTimerDat.Destroy();
}
if (_auMappingTimerDat is {
Destroyed : false
}
) {
_auMappingTimerDat.Destroy();
}
IsUnloading = true;
scheduleUpdateTimer?.Destroy();
SaveData();
StopUpdateLastSeenCo();
}
private void OnPluginLoaded(Plugin plugin) {
if (plugin.Name == "ZoneManager") ZoneManager = plugin;
if (plugin.Name == "LiteZones") LiteZones = plugin;
if (ZoneManager != null || LiteZones != null) SetUseZones();
}
private void OnPluginUnloaded(Plugin plugin) {
if (plugin.Name == "ZoneManager") ZoneManager = null;
if (plugin.Name == "LiteZones") LiteZones = null;
if (ZoneManager == null && LiteZones == null) SetUseZones();
}
private void OnCreatedDynamicPVP() => SetUseZones();
private void OnDeletedDynamicPVP() => SetUseZones();
protected void SetUseZones() {
if (IsUnloading) return;
bool wasUsingZones = useZones;
useZones = config != null && config.mappings != null && config.options != null && config.options.useZones && (LiteZones != null || ZoneManager != null);
if (useZones) {
useZones = false;
foreach (var mapping in config.mappings) {
if (!mapping.Key.Equals(config.defaultRuleSet)) {
useZones = true;
break;
}
}
if (!useZones) {
foreach (var mapping in data.mappings) {
if (!mapping.Key.Equals(config.defaultRuleSet)) {
useZones = true;
break;
}
}
}
}
if (wasUsingZones != useZones && currentRuleSet != null) {
ValidateProtectionHooks();
}
}
private void Init() {
if (!_canKillOfflinePlayerEnabled) {
Unsubscribe(nameof(OnPlayerDisconnected));
Unsubscribe(nameof(OnPlayerSleepEnded));
Unsubscribe(nameof(OnPlayerSleep));
}
if (!config.laptop) {
Unsubscribe(nameof(OnCrateLaptopAttack));
}
if (!config.options.Loot.NoShieldDrop) {
Unsubscribe(nameof(OnPlayerActiveShieldDrop));
}
if (!config.options.Loot.NoActiveItemDrop) {
Unsubscribe(nameof(OnPlayerDropActiveItem));
}
if (!config.options.Loot.NoRustBackpackDrop) {
Unsubscribe(nameof(OnBackpackDrop));
}
if (!config.BlockSprayCanInSafeZones) {
Unsubscribe(nameof(OnSprayCreate));
}
Unsubscribe(nameof(OnEntityDistanceCheck));
Unsubscribe(nameof(OnEntityVisibilityCheck));
Unsubscribe(nameof(OnPlayerAttack));
Unsubscribe(nameof(CanLivestockAnimalRefuseTarget));
Unsubscribe(nameof(OnLivestockAnimalFamiliarityAdd));
Unsubscribe(nameof(CanTakeCutting));
Unsubscribe(nameof(OnGrowableGather));
Unsubscribe(nameof(OnConstructionPlace));
Unsubscribe(nameof(CanLootEntity));
Unsubscribe(nameof(OnLootNetworkUpdate));
Unsubscribe(nameof(OnCodeEntered));
Unsubscribe(nameof(CanChangeCode));
Unsubscribe(nameof(OnCupboardAuthorize));
Unsubscribe(nameof(CanHelicopterStrafeTarget));
Unsubscribe(nameof(CanWaterBallSplash));
Unsubscribe(nameof(OnEntityMarkHostile));
Unsubscribe(nameof(OnEntitySpawned));
Unsubscribe(nameof(OnEntityEnter));
Unsubscribe(nameof(OnTurretTarget));
Unsubscribe(nameof(OnTimedExplosiveExplode));
Unsubscribe(nameof(OnWallpaperRemove));
Unsubscribe(nameof(OnEntityTakeDamage));
Unsubscribe(nameof(OnPlayerConnected));
Unsubscribe(nameof(OnSamSiteTarget));
Unsubscribe(nameof(OnTrapTrigger));
Unsubscribe(nameof(OnMlrsFire));
Unsubscribe(nameof(OnNpcConversationRespond));
Unsubscribe(nameof(OnVendingTransaction));
Unsubscribe(nameof(OnRentableShopBreakInComplete));
Unsubscribe(nameof(OnApartmentRoomBreakInComplete));
Unsubscribe(nameof(CanAffordApartmentMasterKey));
Unsubscribe(nameof(OnApartmentMasterKeyPurchase));
// register console commands automagically
foreach (Command command in Enum.GetValues(typeof(Command))) {
AddCovalenceCommand($"tpve.{command}", nameof(CommandDelegator));
}
// register chat commands
AddCovalenceCommand("tpve.ruleset", nameof(CmdSetRuleSet));
AddCovalenceCommand("tpve_prod", nameof(CommandDelegator));
AddCovalenceCommand("tpve_enable", nameof(CommandDelegator));
AddCovalenceCommand("tpve", nameof(CommandDelegator));
AddCovalenceCommand("finditemprefab", nameof(CommandFindItemPrefab));
permission.RegisterPermission(PermCanMap, this);
// build usage string for console (without sizing)
usageString = WrapColor("orange", GetMessage("Header_Usage")) + $" - {Version}{Environment.NewLine}" + WrapColor("cyan", $"tpve.{Command.map}") + $" - {GetMessage("Cmd_Usage_mapzone")}{Environment.NewLine}" + WrapColor("cyan", $"tpve.{Command.unmap}") + $" - {GetMessage("Cmd_Usage_unmapzone")}{Environment.NewLine}" + WrapColor("cyan", $"tpve.{Command.def}") + $" - {GetMessage("Cmd_Usage_def")}{Environment.NewLine}" + WrapColor("cyan", $"tpve.{Command.trace}") + $" - {GetMessage("Cmd_Usage_trace")}{Environment.NewLine}" + WrapColor("cyan", $"tpve.{Command.sched} [enable|disable]") + $" - {GetMessage("Cmd_Usage_sched")}{Environment.NewLine}" + WrapColor("cyan", $"/tpve_prod") + $" - {GetMessage("Cmd_Usage_prod")}{Environment.NewLine}" + WrapColor("cyan", $"/tpve map") + $" - {GetMessage("Cmd_Usage_map")}";
LoadData();
}
private void CmdSetRuleSet(IPlayer user, string command, string[] args) {
if (!user.IsAdmin) return;
if (args.Length == 0) {
user.Reply($"Active ruleset: {currentRuleSet?.name ?? "(none)"}\nUsage: tpve.ruleset <name>");
return;
}
string name = args[0];
RuleSet rs = config.ruleSets.Find(r => r.name == name && r.enabled && !r.IsEmpty());
if (rs == null) {
user.Reply($"Ruleset '{name}' not found, disabled, or empty.");
return;
}
currentRuleSet = rs;
ValidateProtectionHooks();
user.Reply($"Active ruleset is now '{name}'.");
}
private bool IsAnimalsIgnoringSleepers() => animalsIgnoreSleepers || config.ruleSets.Exists(ruleSet => ruleSet.HasFlag(RuleFlags.AnimalsIgnoreSleepers));
private bool IsAnimalsIgnoringSleepers(RuleSet ruleSet) => animalsIgnoreSleepers || ruleSet.HasFlag(RuleFlags.AnimalsIgnoreSleepers);
private bool onEntityEnter, onSamSiteTarget, onTrapTrigger, onEntityMarkHostile, canHarvest, canPurchaseMasterKey, canBribeSecurityGuard, onRentableShopBreakInComplete, onApartmentRoomBreakInComplete;
private bool HookSubscribed(string hook) {
return hook switch {
"OnEntityEnter" => onEntityEnter, "OnSamSiteTarget" => onSamSiteTarget, "OnTrapTrigger" => onTrapTrigger, "OnEntityMarkHostile" => onEntityMarkHostile, "CanHarvest" or "CanTakeCutting" or "OnGrowableGather" or "OnConstructionPlace" => canHarvest, "CanPurchaseMasterKey" or "OnVendingTransaction" or "CanAffordApartmentMasterKey" or "OnApartmentMasterKeyPurchase" => canPurchaseMasterKey, "CanBribeSecurityGuard" or "OnNpcConversationRespond" => canBribeSecurityGuard, "CanEntityBeTargeted" => onEntityEnter || onSamSiteTarget, "OnRentableShopBreakInComplete" => onRentableShopBreakInComplete, "OnApartmentRoomBreakInComplete" => onApartmentRoomBreakInComplete, "CanPlayerCompleteBreakIn" => onRentableShopBreakInComplete || onApartmentRoomBreakInComplete, _ => false
}
;
}
private void OnServerInitialized(bool isStartup) {
isServerStartingUp = false;
// check for server pve setting
if (ConVar.Server.pve) WarnPve();
// load configuration
config.Init(this);
currentRuleSet = config.GetDefaultRuleSet();
dudRuleSet = config.GetDudRuleSet();
if (currentRuleSet == null) Puts(GetMessage("Warning_NoRuleSet"), config.defaultRuleSet);
SetUseZones();
if (config.schedule.enabled) {
TimerLoop(true);
}
if (!IsAnimalsIgnoringSleepers()) {
Unsubscribe(nameof(OnNpcTarget));
}
if (config.PreventSafeZoneStrafing) {
Subscribe(nameof(CanHelicopterStrafeTarget));
}
if (config.PreventThrowingWaterInFreezingBiome || config.BlockRadioactiveWaterDamage) {
Subscribe(nameof(CanWaterBallSplash));
}
if (currentRuleSet == null) {
return;
}
if (onEntityEnter = config.ruleSets.Exists(ruleSet => (ruleSet._flags & (RuleFlags.HopperCannotTargetEnemyLoot | RuleFlags.SafeZoneTurretsIgnorePlayers | RuleFlags.StaticTurretsIgnorePlayers | RuleFlags.StaticTurretsIgnoreScientist | RuleFlags.TrapsIgnorePlayers | RuleFlags.TrapsIgnoreScientist | RuleFlags.TurretsIgnorePlayers | RuleFlags.TurretsIgnoreScientist | RuleFlags.TurretsIgnoreBradley)) != 0)) {
Subscribe(nameof(OnEntityEnter));
Subscribe(nameof(OnTurretTarget));
}
if (onSamSiteTarget = config.ruleSets.Exists(ruleSet => (ruleSet._flags & (RuleFlags.SamSitesIgnoreMLRS | RuleFlags.PlayerSamSitesIgnorePlayers | RuleFlags.StaticSamSitesIgnorePlayers)) != 0)) {
Subscribe(nameof(OnSamSiteTarget));
}
if (onTrapTrigger = config.ruleSets.Exists(ruleSet => (ruleSet._flags & (RuleFlags.TrapsIgnorePlayers | RuleFlags.TrapsIgnoreScientist)) != 0)) {
Subscribe(nameof(OnTrapTrigger));
}
if (config.schedule.enabled && config.schedule.broadcast && !string.IsNullOrEmpty(currentBroadcastMessage)) {
Subscribe(nameof(OnPlayerConnected));
}
if (config.options.disableBaseOvenSplash) {
ServerMgr.Instance.StartCoroutine(OvenOrLockCo());
}
if (onEntityMarkHostile = config.options.disableHostility) {
Subscribe(nameof(OnEntityMarkHostile));
}
ValidateProtectionHooks();
if (config.wallpaper) {
Subscribe(nameof(OnWallpaperRemove));
Subscribe(nameof(OnTimedExplosiveExplode));
}
if (canHarvest = config.options.Loot.Planters) {
Subscribe(nameof(CanTakeCutting));
Subscribe(nameof(OnGrowableGather));
Subscribe(nameof(OnConstructionPlace));
}
if (config.options.Loot.ProtectTC) {
Subscribe(nameof(OnCupboardAuthorize));
Subscribe(nameof(CanLootEntity));
Subscribe(nameof(CanChangeCode));
}
else if (config.options.Loot.Enabled) {
Subscribe(nameof(CanLootEntity));
}
if (config.options.Loot.HasLocks()) {
Subscribe(nameof(OnLootNetworkUpdate));
}
canPurchaseMasterKey = canBribeSecurityGuard = onRentableShopBreakInComplete = onApartmentRoomBreakInComplete = false;
if (config.options.Apartments.Enabled) {
if (!config.options.Apartments.Bribe) {
canBribeSecurityGuard = true;
Subscribe(nameof(OnNpcConversationRespond));
}
if (!config.options.Apartments.MasterKey) {
canPurchaseMasterKey = true;
Subscribe(nameof(OnVendingTransaction));
Subscribe(nameof(CanAffordApartmentMasterKey));
Subscribe(nameof(OnApartmentMasterKeyPurchase));
}
if (!config.options.Apartments.Shop) {
onRentableShopBreakInComplete = true;
Subscribe(nameof(OnRentableShopBreakInComplete));
}
if (!config.options.Apartments.Room) {
onApartmentRoomBreakInComplete = true;
Subscribe(nameof(OnApartmentRoomBreakInComplete));
}
}
Subscribe(nameof(OnEntitySpawned));
Subscribe(nameof(OnMlrsFire));
BuildPrefabIds();
HandleLocksOnContainers();
if (config.options.AutoRemove) RemoveTemporaryZones();
InitDeepSea();
if (_canKillOfflinePlayerEnabled) {
_updateLastSeenCo = UpdateLastSeenCo();
ServerMgr.Instance.StartCoroutine(_updateLastSeenCo);
}
}
private long GetFrameDeadline(double milliseconds = 1) => Stopwatch.GetTimestamp() + (long)Math.Ceiling(milliseconds * stopwatchTicksPerMillisecond);
private readonly double stopwatchTicksPerMillisecond = Stopwatch.Frequency / 1000d;
private IEnumerator OvenOrLockCo() {
//long start = Stopwatch.GetTimestamp();
long deadline = GetFrameDeadline();
foreach (var ent in BaseNetworkable.serverEntities) {
if (Stopwatch.GetTimestamp() >= deadline) {
yield return null;
deadline = GetFrameDeadline();
}
if (IsUnloading) {
yield break;
}
if (config.options.disableBaseOvenSplash && ent is BaseOven oven) {
oven.disabledBySplash = false;
}
if (config.options.Loot.Locks && ent is StorageContainer c && c != null && !c.isLockable) {
OnEntitySpawned(c);
}
}
//Puts("Took {0:0.##}ms to process ovens and locks.", (Stopwatch.GetTimestamp() - start) / stopwatchTicksPerMillisecond);
}
private void BuildPrefabIds() {
if (StringPool.toNumber.TryGetValue("assets/prefabs/npc/m2bradley/maincannonshell.prefab", out var prefab1)) maincannonshell = prefab1;
if (StringPool.toNumber.TryGetValue("assets/content/props/train_tunnels/trainbarricade.prefab", out var prefab2)) trainbarricade = prefab2;
if (StringPool.toNumber.TryGetValue("assets/content/props/train_tunnels/trainbarricadeheavy.prefab", out var prefab3)) trainbarricadeheavy = prefab3;
if (StringPool.toNumber.TryGetValue("assets/bundled/prefabs/radtown/loot_trash.prefab", out var prefab4)) loot_trash = prefab4;
if (StringPool.toNumber.TryGetValue("assets/prefabs/misc/xmas/giftbox/giftbox_loot.prefab", out var prefab5)) giftbox_loot = prefab5;
if (StringPool.toNumber.TryGetValue("assets/prefabs/deployable/campfire/campfire.prefab", out var prefab6)) campfire = prefab6;
if (StringPool.toNumber.TryGetValue("assets/bundled/prefabs/oilfireballsmall.prefab", out var prefab7)) oilfireballsmall = prefab7;
if (StringPool.toNumber.TryGetValue("assets/bundled/prefabs/napalm.prefab", out var prefab8)) heli_napalm = prefab8;
if (StringPool.toNumber.TryGetValue("assets/prefabs/npc/patrol helicopter/rocket_heli_napalm.prefab", out var prefab9)) rocket_heli_napalm = prefab9;
if (StringPool.toNumber.TryGetValue("assets/prefabs/npc/patrol helicopter/rocket_heli.prefab", out var prefab10)) rocket_heli = prefab10;
}
#endregion
#region Data
private void OnNewSave() {
data = new();
SaveData();
}
private class StoredData {
public Dictionary<string, string> mappings = new();
public Dictionary<ulong, int> LastSeen = new();
public DateTime LastRunTime = DateTime.MinValue;
public bool HasMapping(string key) {
return mappings.ContainsKey(key) || mappings.ContainsKey(AllZones);
}
}
private StoredData data = new();
private void LoadData() {
try {
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
}
catch (Exception ex) {
Puts(ex.ToString());
}
data ??= new();
data.LastSeen ??= new();
data.mappings ??= new();
if (data.LastRunTime != DateTime.MinValue && DateTime.Now.Subtract(data.LastRunTime).TotalHours >= 24) {
if (_canKillOfflinePlayerEnabled && data.LastSeen.Count > 0) {
Puts("Last seen data wiped due to plugin not being loaded for {0} day(s).", DateTime.Now.Subtract(data.LastRunTime).Days);
}
data = new();
data.LastRunTime = DateTime.Now;
}
if (!_canKillOfflinePlayerEnabled && data.LastSeen.Count > 0) {
data.LastSeen.Clear();
SaveData();
}
}
private IEnumerator _updateLastSeenCo;
public void StopUpdateLastSeenCo() {
if (_updateLastSeenCo != null) {
ServerMgr.Instance.StopCoroutine(_updateLastSeenCo);
_updateLastSeenCo = null;
}
}
private void SaveData() {
data.LastRunTime = DateTime.Now;
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
public IEnumerator UpdateLastSeenCo() {
var instruction1 = CoroutineEx.waitForSeconds(0.075f);
var instruction2 = CoroutineEx.waitForSeconds(60f);
while (!IsUnloading) {
long deadline = GetFrameDeadline();
bool changed = false;
foreach (var sleeper in BasePlayer.sleepingPlayerList) {
if (Stopwatch.GetTimestamp() >= deadline) {
yield return null;
deadline = GetFrameDeadline();
}
if (sleeper == null || !sleeper.userID.IsSteamId()) {
continue;
}
if (data.LastSeen.ContainsKey(sleeper.userID)) {
continue;
}
if (!IsOfflinePlayerProtected(sleeper)) {
data.LastSeen[sleeper.userID] = Epoch.Current;
changed = true;
}
yield return instruction1;
deadline = GetFrameDeadline();
}
foreach (var player in BasePlayer.activePlayerList) {
if (Stopwatch.GetTimestamp() >= deadline) {
yield return null;
deadline = GetFrameDeadline();
}
if (data.LastSeen.Remove(player.userID)) {
changed = true;
}
}
if (changed) {
SaveData();
}
yield return instruction2;
deadline = GetFrameDeadline();
}
}
private void OnPlayerSleep(BasePlayer player) {
if (player != null && !IsOfflinePlayerProtected(player)) {
data.LastSeen[player.userID] = Epoch.Current;
}
}
private void OnPlayerSleepEnded(BasePlayer player) {
if (player != null && player.IsConnected) {
data.LastSeen.Remove(player.userID);
}
}
private void OnPlayerDisconnected(BasePlayer player) {
if (player != null && !IsOfflinePlayerProtected(player)) {
data.LastSeen[player.userID] = Epoch.Current;
}
}
public bool CanKillOfflinePlayer(BasePlayer victim, out double timeLeft) {
timeLeft = 0;
if (victim.IsConnected || !victim.IsSleeping()) {
data.LastSeen.Remove(victim.userID);
return false;
}
if (!data.LastSeen.TryGetValue(victim.userID, out var lastSeen)) {
return false;
}
if (IsOfflinePlayerProtected(victim)) {
data.LastSeen.Remove(victim.userID);
return false;
}
double timeOffline = Epoch.Current - lastSeen;
double allowedOfflineTime = config.AllowKillingSleepersHoursOffline * 3600.0;
timeLeft = Math.Max(0, allowedOfflineTime - timeOffline);
return timeOffline > allowedOfflineTime;
}
private readonly List<ApartmentRoom> _rooms = new();
private bool TryGetApartmentRoom(BasePlayer player, out ApartmentRoom room) {
room = null;
if (player == null || player.IsDestroyed) return false;
if (_rooms.Count == 0) {
foreach (var triggerSafeZone in TriggerSafeZone.allSafeZones) {
if (triggerSafeZone?.triggerCollider == null) continue;
if (triggerSafeZone.Apartment?.rooms == null) continue;
_rooms.AddRange(triggerSafeZone.Apartment.rooms);
}
}
foreach (var _room in _rooms) {
if (_room == null || _room.IsDestroyed || _room.owners == null) continue;
if (!_room.IsCurrentlyRented() || !_room.IsInsideRoom(player)) continue;
room = _room;
return true;
}
return false;
}
private bool IsInsideApartmentRoom(BasePlayer player) {
if (!TryGetApartmentRoom(player, out var room)) {
return false;
}
if (room.owners.Contains(player.userID)) {
return true;
}
foreach (var owner in room.owners) {
if (IsAlly(player, owner)) {
return true;
}
}
return false;
}
private bool IsOfflinePlayerProtected(BasePlayer victim) {
if (IsInsideApartmentRoom(victim)) {
return true;
}
BaseEntity priv = victim.GetVehicleBuildingPrivilege(true) ?? victim.GetBuildingPrivilege(true);
if (priv == null) {
return false;
}
return priv switch {
BoatBuildingStation bbs => bbs.CanPlayerBuild(victim), VehiclePrivilege vp => vp.IsAuthed(victim), BuildingPrivlidge bp => bp.IsAuthed(victim), _ => false
}
;
}
#endregion Data
#region Command Handling
// delegation method for commands
private void CommandDelegator(IPlayer user, string command, string[] args) {
// return if user doesn't have access to run console command
if (!user.IsAdmin) return;
if (args.Length > 0 && (args[0] == "map" || args[0] == "unmap")) {
if (user.HasPermission(PermCanMap)) {
CommandMap(user, command, args);
}
else user.Reply("You do not have the required truepve.canmap permission.");
return;
}
if (args.Contains("pvp")) {
if (currentRuleSet == null) {
user.Reply("No active ruleset.");
return;
}
bool allow = currentRuleSet.rules.Remove("players cannot hurt players");
if (!allow && !currentRuleSet.rules.Remove("players can hurt players")) {
user.Reply($"{currentRuleSet.name} ruleset does not contain a 'players can hurt players' or 'players cannot hurt players' rule.");
return;
}
currentRuleSet.rules.Add(allow ? "players can hurt players" : "players cannot hurt players");
currentRuleSet.parsedRules.Clear();
currentRuleSet.Build(this);
ValidateProtectionHooks();
SaveConfig();
Puts("PVP toggled {0}", allow ? "on" : "off");
return;
}
if (command == "tpve_prod") {
HandleProd(user);
return;
}
if (command == "tpve_enable") {
tpveEnabled = !tpveEnabled;
ValidateProtectionHooks();
Message(user, "Enable", IsEnabled());
return;
}
if (command == "tpve" && args.Length != 0) command = args[0];
else command = command.Replace("tpve.", string.Empty);
if (!Enum.TryParse(command, out Command @enum)) {
user.Reply($"Invalid argument: {command}");
return;
}
switch (@enum) {
case Command.sleepers : HandleSleepers(user);
return;
case Command.def : HandleDef(user);
return;
case Command.sched : HandleScheduleSet(user, args);
return;
case Command.trace : HandleTrace(user);
return;
case Command.enable : tpveEnabled = !tpveEnabled;
ValidateProtectionHooks();
Message(user, "Enable", IsEnabled());
return;
case Command.map : case Command.unmap : if (user.HasPermission(PermCanMap)) {
CommandMap(user, command, args);
}
else user.Reply("You do not have the required truepve.canmap permission.");
return;
case Command.usage : default : ShowUsage(user);
return;
}
}
protected void HandleTrace(IPlayer user) {
if (user.IsServer) {
traceDistance = 0f;
}
else traceDistance = config.options.MaxTraceDistance;
trace = !trace;
if (!trace) {
tracePlayer = null;
traceEntity = null;
}
else tracePlayer = user.Object as BasePlayer;
Message(user, "Notify_TraceToggle", new object[] {
trace ? "on" : "off"
}
);
traceTimer?.Destroy();
if (trace) {
traceTimer = timer.In(traceTimeout, () => trace = false);
}
}
private void HandleSleepers(IPlayer user) {
if (animalsIgnoreSleepers) {
animalsIgnoreSleepers = false;
if (!IsAnimalsIgnoringSleepers()) {
Unsubscribe(nameof(OnNpcTarget));
}
user.Reply("Sleepers are no longer protected from animals.");
}
else {
animalsIgnoreSleepers = true;
Subscribe(nameof(OnNpcTarget));
user.Reply("Sleepers are now protected from animals.");
}
}
// handle setting defaults
private void HandleDef(IPlayer user) {
config.options = new();
Message(user, "Notify_DefConfigLoad");
LoadDefaultData();
data.mappings ??= new();
data.mappings.Clear();
allyLookupTimes.Clear();
Message(user, "Notify_DefDataLoad");
CheckData();
ValidateProtectionHooks();
SaveConfig();
}
// handle prod command (raycast to determine what player is looking at)
private void HandleProd(IPlayer user) {
var player = user.Object as BasePlayer;
if (player == null || !player.IsAdmin) {
Message(user, "Error_NoPermission");
return;
}
if (!GetRaycastTarget(player, out BaseEntity entity)) {
SendReply(player, WrapSize(12, WrapColor("red", GetMessage("Error_NoEntityFound", player.UserIDString))));
return;
}
Message(player, "Notify_ProdResult", entity.GetType(), entity.ShortPrefabName);
}
private void CommandMap(IPlayer user, string command, string[] args) {
// assume args[0] is the command (beyond /tpve)
if (args.Length > 0) command = args[0];
// shift arguments
args = args.Length > 1 ? args[1..] : Array.Empty<string>();
if (command != "map" && command != "unmap") {
Message(user, "Error_InvalidCommand");
return;
}
string from;
string to;
if (args.Length == 0) {
BasePlayer player = user.Object as BasePlayer;
if (player == null) {
Message(user, "Error_InvalidParamForCmd", command);
return;
}
using var locs = GetLocationKeys(player);
if (locs.Count == 0) {
Message(user, "Error_InvalidParamForCmd", command);
return;
}
from = locs[0];
to = command == "map" ? "exclude" : null;
}
else {
from = args[0];
to = args.Length == 2 ? args[1] : null;
}
if (to != null) {
if (to != "exclude" && !config.ruleSets.Exists(r => r.name == to)) {
Message(user, "Error_InvalidMapping", from, to);
return;
}
bool changes = false;
if (config.mappings.TryGetValue(from, out string old)) {
changes = true;
Message(user, "Notify_MappingUpdated", from, old, to);
config.mappings[from] = to;
SaveConfig();
TryBuildExclusionMappings();
}
if (data.mappings.TryGetValue(from, out old)) {
changes = true;
Message(user, "Notify_MappingUpdated", from, old, to);
data.mappings[from] = to;
SaveData();
TryBuildExclusionMappings();
}
if (!changes) // this is a command so default to config
{
Message(user, "Notify_MappingCreated", from, to);
config.mappings[from] = to;
SaveConfig();
TryBuildExclusionMappings();
}
}
else {
if (config.HasMapping(from)) {
Message(user, "Notify_MappingDeleted", from, config.mappings.TryGetValue(from, out var old) ? old : AllZones);
if (config.mappings.Remove(from)) SaveConfig();
TryBuildExclusionMappings();
}
else if (data.HasMapping(from)) {
Message(user, "Notify_MappingDeleted", from, data.mappings.TryGetValue(from, out var old) ? old : AllZones);
if (data.mappings.Remove(from)) SaveData();
TryBuildExclusionMappings();
}
else {
Message(user, args.Length == 0 ? "Error_InvalidMapping" : "Error_NoMappingToDelete", from, to);
}
}
}
// handles schedule enable/disable
private void HandleScheduleSet(IPlayer user, string[] args) {
if (args.Length < 2) {
Message(user, "Error_InvalidParamForCmd", $"tpve.{args[0]}");
Message(user, $"Syntax: tpve.{args[0]} <enable|disable>");
return;
}
if (!config.schedule.valid) {
Message(user, "Notify_InvalidSchedule");
}
else if (args[1] == "enable") {
if (config.schedule.enabled) return;
config.schedule.enabled = true;
TimerLoop();
Message(user, "Notify_SchedSetEnabled");
}
else if (args[1] == "disable") {
if (!config.schedule.enabled) return;
config.schedule.enabled = false;
if (scheduleUpdateTimer != null) scheduleUpdateTimer.Destroy();
Message(user, "Notify_SchedSetDisabled");
}
else {
Message(user, "Error_InvalidParameter", args[1]);
Message(user, $"Syntax: tpve.{args[0]} <enable|disable>");
}
}
#endregion
#region Configuration/Data
private bool _playersTriggerOption, _playersHurtOption, _canKillOfflinePlayerEnabled, _pvpReflectionEnabled, _allowKillingSleepersEnabled, _buildingBlockHandlerEnabled;
// load config
protected override void LoadConfig() {
base.LoadConfig();
canSaveConfig = false;
try {
config = Config.ReadObject<Configuration>();
if (config == null) LoadDefaultConfig();
canSaveConfig = true;
CheckData();
SaveConfig();
}
catch (Exception ex) {
Puts(ex.ToString());
LoadDefaultConfig();
}
}
protected override void LoadDefaultConfig() {
config = new() {
configVersion = Version.ToString(), options = new()
}
;
LoadDefaultData();
Puts("Loaded default config.");
}
private bool canSaveConfig = true;
// save data
protected override void SaveConfig() {
if (canSaveConfig) {
Config.WriteObject(config);
}
}
// check rulesets and groups
private void CheckData() {
if (string.IsNullOrEmpty(config.defaultRuleSet)) {
config.defaultRuleSet = "default";
Puts("Loaded default ruleset (no default ruleset was configured)");
}
if (config.mappings.IsNullOrEmpty()) {
BuildDefaultMappings();
Puts("Loaded default mappings (no mappings were configured)");
}
if (config.schedule == null) {
Puts("Loaded default schedule (schedule was null)");
BuildDefaultSchedule();
}
if (config.groups.IsNullOrEmpty()) {
BuildDefaultGroups();
Puts("Loaded default entity groups (no entity groups were configured)");
}
if (config.ruleSets.IsNullOrEmpty()) {
BuildDefaultRuleset();
Puts("Loaded default rulesets (no rulesets were configured)");
}
if (config.options.BlockHandler._Online != null) {
config.options.BlockHandler.BlockWhenOnline = config.options.BlockHandler._Online.Value;
config.options.BlockHandler._Online = null;
}
if (config.options.BlockHandler._Twig != null) {
config.options.BlockHandler.Twig = config.options.BlockHandler._Twig.Value;
config.options.BlockHandler._Twig = null;
}
config.options.Livestock ??= new();
TryUpdateConfig();
config.configVersion = Version.ToString();
BuildRuleSetDictionary();
BuildExclusionMappings();
_allowKillingSleepersEnabled = config.AllowKillingSleepersAlly || config.AllowKillingSleepers || config.EntityCountRequired || config.AllowKillingSleepersIds.Exists(x => x.IsSteamId());
_buildingBlockHandlerEnabled = config.options.BlockHandler.Any;
_pvpReflectionEnabled = config.options.Reflect.Any;
_canKillOfflinePlayerEnabled = config.AllowKillingSleepersHoursOffline > 0;
_playersTriggerOption = config.PlayersTriggerTraps || config.PlayersTriggerTurrets;
_playersHurtOption = config.PlayersHurtTraps || config.PlayersHurtTurrets || config.PlayersHurtSamSites;
}
private void TryUpdateConfig() {
if (!TryParseVersionNumber(config.configVersion, out var vn) || vn >= Version) return;
Dictionary<string, List<string>> updates = new(StringComparer.OrdinalIgnoreCase) {
["npcs"]
= new() {
nameof(SnakeHazard), nameof(ScientistNPC2)
}
, ["dispensers"] = new() {
nameof(VineSwingingTree), nameof(LivestockCorpse)
}
, ["traps"] = new() {
"spikes.trap"
}
, ["barricades2"] = new() {
"barricade.medieval", "barricade.cover.wood", "barricade.cover.wood_double"
}
, ["farm"] = new() {
nameof(Sheep), nameof(Cow)
}
,
}
;
for (int i = 0; i < config.groups.Count; i++) {
var group = config.groups[i];
if (string.IsNullOrWhiteSpace(group.members)) {
continue;
}
if (string.IsNullOrEmpty(group.name)) {
group.name = $"group{i}";
continue;
}
if (group.name == "ridablehorses" && group.members.Contains("RidableHorse2")) {
group.members = ReplaceMember(group.members, "RidableHorse2", "RidableHorse");
continue;
}
if (group.members.Contains("heli_napalm")) {
group.members = ReplaceMember(group.members, "heli_napalm", "napalm");
var fire = config.groups.Find(x => x.name == "fire");
if (fire != null && fire.members.Contains("FireBall") && !fire.exclusions.Contains("napalm")) {
fire.exclusions = fire.exclusions.Length == 0 ? "napalm" : fire.exclusions + ", napalm";
}
}
if (!updates.TryGetValue(group.name, out var members)) {
continue;
}
foreach (var member in members) {
if (!ContainedInGroups(member)) {
group.members = $"{group.members.TrimEnd(',', ' ')}, {member}";
}
}
}
}
private static string ReplaceMember(string members, string oldValue, string newValue) {
if (string.IsNullOrWhiteSpace(members)) return members;
string[] parts = members.Split(',');
for (int i = 0; i < parts.Length; i++) {
string member = parts[i].Trim();
if (member == oldValue) {
parts[i] = newValue;
}
else {
parts[i] = member;
}
}
return string.Join(",", parts);
}
private bool ContainedInGroups(string member) => config.groups.Exists(g => g.members.Contains(member, CompareOptions.OrdinalIgnoreCase) || g.exclusions.Contains(member, CompareOptions.OrdinalIgnoreCase));
private bool TryParseVersionNumber(string input, out VersionNumber version) {
version = default;
if (string.IsNullOrWhiteSpace(input)) return false;
var parts = input.Split('.');
if (parts.Length != 3) return false;
if (int.TryParse(parts[0], out int major) && int.TryParse(parts[1], out int minor) && int.TryParse(parts[2], out int patch)) {
version = new(major, minor, patch);
return true;
}
return false;
}
protected void BuildRuleSetDictionary() {
ruleSetByNameDi
// ... [Code preview truncated due to length. Full source in attached file] ...