C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("TruePVE", "ignignokt84", "0.9.0", ResourceId = 1789)]
[Description("Improvement of the default Rust PVE behavior")]
class TruePVE : RustPlugin
{
#region Variables
static TruePVE Instance;
// config/data container
TruePVEData data = new TruePVEData();
// ZoneManager plugin
[PluginReference]
Plugin ZoneManager;
// LiteZone plugin (private)
[PluginReference]
Plugin LiteZones;
// usage information string with formatting
public string usageString;
// valid commands
enum Command { def, sched, trace, usage };
// valid configuration options
public enum Option {
handleDamage, // (true) enable TruePVE damage handling hooks
useZones // (true) use ZoneManager/LiteZones for zone-specific damage behavior (requires modification of ZoneManager.cs)
};
// default values array
bool[] defaults = {
true, // handleDamage
true // useZones
};
// flags for RuleSets
[Flags]
enum RuleFlags
{
None = 0,
SuicideBlocked = 1,
AuthorizedDamage = 1 << 1,
NoHeliDamage = 1 << 2,
HeliDamageLocked = 1 << 3,
NoHeliDamagePlayer = 1 << 4,
HumanNPCDamage = 1 << 5,
LockedBoxesImmortal = 1 << 6,
LockedDoorsImmortal = 1 << 7,
AdminsHurtSleepers = 1 << 8,
ProtectedSleepers = 1 << 9,
TrapsIgnorePlayers = 1 << 10,
TurretsIgnorePlayers = 1 << 11,
CupboardOwnership = 1 << 12
}
// timer to check for schedule updates
Timer scheduleUpdateTimer;
// current ruleset
RuleSet currentRuleSet;
// current broadcast message
string currentBroadcastMessage;
// internal useZones flag
bool useZones = false;
// constant "any" string for rules
const string Any = "any";
// constant "allzones" string for mappings
const string AllZones = "allzones";
// flag to prevent certain things from happening before server initialized
bool serverInitialized = false;
// permission for mapping command
string PermCanMap = "truepve.canmap";
// trace flag
bool trace = false;
// tracefile name
string traceFile = "ruletrace";
// auto-disable trace after 300s (5m)
float traceTimeout = 300f;
// trace timeout timer
Timer traceTimer;
#endregion
#region Lang
// load default messages to Lang
void LoadDefaultMessages()
{
var messages = new Dictionary<string, string>
{
{"Prefix", "<color=#FFA500>[ TruePVE ]</color>" },
{"Header_Usage", "---- TruePVE usage ----"},
{"Cmd_Usage_def", "Loads default configuration and data"},
{"Cmd_Usage_sched", "Enable or disable the schedule" },
{"Cmd_Usage_prod", "Show the prefab name and type of the entity being looked at"},
{"Cmd_Usage_map", "Create/remove a mapping entry" },
{"Cmd_Usage_trace", "Toggle tracing on/off" },
{"Warning_PveMode", "Server is set to PVE mode! TruePVE is designed for PVP mode, and may cause unexpected behavior in PVE mode."},
{"Warning_OldConfig", "Old config detected - moving to {0}" },
{"Warning_NoRuleSet", "No RuleSet found for \"{0}\"" },
{"Warning_DuplicateRuleSet", "Multiple RuleSets found for \"{0}\"" },
{"Error_InvalidCommand", "Invalid command" },
{"Error_InvalidParameter", "Invalid parameter: {0}"},
{"Error_InvalidParamForCmd", "Invalid parameters for command \"{0}\""},
{"Error_InvalidMapping", "Invalid mapping: {0} => {1}; Target must be a valid RuleSet or \"exclude\"" },
{"Error_NoMappingToDelete", "Cannot delete mapping: \"{0}\" does not exist" },
{"Error_NoPermission", "Cannot execute command: No permission"},
{"Error_NoSuicide", "You are not allowed to commit suicide"},
{"Error_NoEntityFound", "No entity found"},
{"Notify_AvailOptions", "Available Options: {0}"},
{"Notify_DefConfigLoad", "Loaded default configuration"},
{"Notify_DefDataLoad", "Loaded default mapping data"},
{"Notify_ProdResult", "Prod results: type={0}, prefab={1}"},
{"Notify_SchedSetEnabled", "Schedule enabled" },
{"Notify_SchedSetDisabled", "Schedule disabled" },
{"Notify_InvalidSchedule", "Schedule is not valid" },
{"Notify_MappingCreated", "Mapping created for \"{0}\" => \"{1}\"" },
{"Notify_MappingUpdated", "Mapping for \"{0}\" changed from \"{1}\" to \"{2}\"" },
{"Notify_MappingDeleted", "Mapping for \"{0}\" => \"{1}\" deleted" },
{"Notify_TraceToggle", "Trace mode toggled {0}" },
{"Format_NotifyColor", "#00FFFF"}, // cyan
{"Format_NotifySize", "12"},
{"Format_HeaderColor", "#FFA500"}, // orange
{"Format_HeaderSize", "14"},
{"Format_ErrorColor", "#FF0000"}, // red
{"Format_ErrorSize", "12"},
};
lang.RegisterMessages(messages, this);
}
// get message from Lang
string GetMessage(string key, string userId = null) => lang.GetMessage(key, this, userId);
#endregion
#region Loading/Unloading
// load things
void Loaded()
{
Instance = this;
LoadDefaultMessages();
string baseCommand = "tpve";
// register console commands automagically
foreach(Command command in Enum.GetValues(typeof(Command)))
cmd.AddConsoleCommand((baseCommand + "." + command.ToString()), this, "CommandDelegator");
// register chat commands
cmd.AddChatCommand(baseCommand + "_prod", this, "HandleProd");
cmd.AddChatCommand(baseCommand, this, "ChatCommandDelegator");
// build usage string for console (without sizing)
usageString = WrapColor("orange", GetMessage("Header_Usage")) + "\n" +
WrapColor("cyan", $"{baseCommand}.{Command.def.ToString()}") + $" - {GetMessage("Cmd_Usage_def")}{Environment.NewLine}" +
WrapColor("cyan", $"{baseCommand}.{Command.trace.ToString()}") + $" - {GetMessage("Cmd_Usage_trace")}{Environment.NewLine}" +
WrapColor("cyan", $"{baseCommand}.{Command.sched.ToString()} [enable|disable]") + $" - {GetMessage("Cmd_Usage_sched")}{Environment.NewLine}" +
WrapColor("cyan", $"/{baseCommand}_prod") + $" - {GetMessage("Cmd_Usage_prod")}{Environment.NewLine}" +
WrapColor("cyan", $"/{baseCommand} map") + $" - {GetMessage("Cmd_Usage_map")}";
permission.RegisterPermission(PermCanMap, this);
}
// on unloaded
void Unload()
{
if(scheduleUpdateTimer != null)
scheduleUpdateTimer.Destroy();
Instance = null;
}
// plugin loaded
void OnPluginLoaded(Plugin plugin)
{
if(plugin.Name == "ZoneManager")
ZoneManager = plugin;
if (plugin.Name == "LiteZones")
LiteZones = plugin;
if (!serverInitialized) return;
if (ZoneManager != null || LiteZones != null)
useZones = data.config[Option.useZones];
}
// plugin unloaded
void OnPluginUnloaded(Plugin plugin)
{
if (plugin.Name == "ZoneManager")
ZoneManager = null;
if (plugin.Name == "LiteZones")
LiteZones = null;
if (!serverInitialized) return;
if (ZoneManager == null && LiteZones == null)
useZones = false;
traceTimer?.Destroy();
}
// server initialized
void OnServerInitialized()
{
// check for server pve setting
if (ConVar.Server.pve)
WarnPve();
// load configuration
LoadConfiguration();
data.Init();
currentRuleSet = data.GetDefaultRuleSet();
if (currentRuleSet == null)
PrintWarning(GetMessage("Warning_NoRuleSet"), data.defaultRuleSet);
useZones = data.config[Option.useZones] && (LiteZones != null || ZoneManager != null);
if (useZones && data.mappings.Count == 1 && data.mappings.First().Key.Equals(data.defaultRuleSet))
useZones = false;
if (data.schedule.enabled)
TimerLoop(true);
serverInitialized = true;
}
#endregion
#region Command Handling
// delegation method for console commands
void CommandDelegator(ConsoleSystem.Arg arg)
{
// return if user doesn't have access to run console command
if(!HasAccess(arg)) return;
string cmd = arg.cmd.Name;
if(!Enum.IsDefined(typeof(Command), cmd))
{
// shouldn't hit this
SendMessage(arg, "Error_InvalidParameter");
}
else
{
switch((Command) Enum.Parse(typeof(Command), cmd))
{
case Command.def:
HandleDef(arg);
return;
case Command.sched:
HandleScheduleSet(arg);
return;
case Command.trace:
trace = !trace;
SendMessage(arg, "Notify_TraceToggle", new object[] { trace ? "on" : "off" });
if (trace)
traceTimer = timer.In(traceTimeout, () => trace = false);
else
traceTimer?.Destroy();
return;
case Command.usage:
ShowUsage(arg);
return;
}
SendMessage(arg, "Error_InvalidParamForCmd", new object[] {cmd});
}
ShowUsage(arg);
}
// handle setting defaults
void HandleDef(ConsoleSystem.Arg arg)
{
LoadDefaultConfiguration();
SendMessage(arg, "Notify_DefConfigLoad");
LoadDefaultData();
SendMessage(arg, "Notify_DefDataLoad");
SaveData();
}
// handle prod command (raycast to determine what player is looking at)
void HandleProd(BasePlayer player, string command, string[] args)
{
if(!IsAdmin(player))
SendMessage(player, "Error_NoPermission");
object entity;
if(!GetRaycastTarget(player, out entity) || entity == null)
{
SendReply(player, WrapSize(12, WrapColor("red", GetMessage("Error_NoEntityFound", player.UserIDString))));
return;
}
SendMessage(player, "Notify_ProdResult", new object[] { entity.GetType(), (entity as BaseEntity).ShortPrefabName });
}
// delegation method for chat commands
void ChatCommandDelegator(BasePlayer player, string command, string[] args)
{
if (!hasPermission(player, PermCanMap))
{
SendMessage(player, "Error_NoPermission");
return;
}
// assume args[0] is the command (beyond /tpve)
if (args != null && args.Length > 0)
command = args[0];
// shift arguments
if (args != null)
{
if (args.Length > 1)
args = args.Skip(1).ToArray();
else
args = new string[] { };
}
string message = "";
object[] opts = new object[] { };
if (command == null || command != "map")
{
message = "Error_InvalidCommand";
}
else if (args == null || args.Length == 0)
{
message = "Error_InvalidParamForCmd";
opts = new object[] { command };
}
else
{
// args[0] should be mapping name
// args[1] if exists should be target ruleset or "exclude"
// if args[1] is empty, delete mapping
string from = args[0];
string to = null;
if(args.Length == 2)
to = args[1];
if (to != null && !data.ruleSets.Select(r => r.name).Contains(to) && to != "exclude")
{
// target ruleset must exist, or be "exclude"
message = "Error_InvalidMapping";
opts = new object[] { from, to };
}
else
{
bool dirty = false;
if (to != null)
{
dirty = true;
if (data.HasMapping(from))
{
// update existing mapping
string old = data.mappings[from];
data.mappings[from] = to;
message = "Notify_MappingUpdated";
opts = new object[] { from, old, to };
}
else
{
// add new mapping
data.mappings.Add(from, to);
message = "Notify_MappingCreated";
opts = new object[] { from, to };
}
}
else
{
if (data.HasMapping(from))
{
dirty = true;
// remove mapping
string old = data.mappings[from];
data.mappings.Remove(from);
message = "Notify_MappingDeleted";
opts = new object[] { from, old };
}
else
{
message = "Error_NoMappingToDelete";
opts = new object[] { from };
}
}
if(dirty)
// save changes to config file
SaveData();
}
}
SendMessage(player, message, opts);
}
// handles schedule enable/disable
void HandleScheduleSet(ConsoleSystem.Arg arg)
{
if (arg == null || arg.Args == null || arg.Args.Length == 0)
{
SendMessage(arg, "Error_InvalidParamForCmd");
return;
}
string message = "";
if(!data.schedule.valid)
{
message = "Notify_InvalidSchedule";
}
else if(arg.Args[0] == "enable")
{
if(data.schedule.enabled) return;
data.schedule.enabled = true;
TimerLoop();
message = "Notify_SchedSetEnabled";
}
else if(arg.Args[0] == "disable")
{
if (!data.schedule.enabled) return;
data.schedule.enabled = false;
if (scheduleUpdateTimer != null)
scheduleUpdateTimer.Destroy();
message = "Notify_SchedSetDisabled";
}
object[] opts = new object[] { };
if(message == "")
{
message = "Error_InvalidParameter";
opts = new object[] { arg.Args[0] };
}
SendMessage(arg, message, opts);
}
#endregion
#region Configuration/Data
// load config
void LoadConfiguration()
{
CheckVersion();
Config.Settings.NullValueHandling = NullValueHandling.Include;
bool dirty = false;
try {
data = Config.ReadObject<TruePVEData>() ?? null;
} catch (Exception) {
data = new TruePVEData();
}
if (data == null)
LoadDefaultConfig();
dirty |= CheckConfig();
dirty |= CheckData();
// check config version, update version to current version
if (data.configVersion == null || !data.configVersion.Equals(Version.ToString()))
{
data.configVersion = Version.ToString();
dirty |= true;
}
if (dirty)
SaveData();
}
// save data
void SaveData() => Config.WriteObject(data);
// verify/update configuration
bool CheckConfig()
{
bool dirty = false;
foreach(Option option in Enum.GetValues(typeof(Option)))
if(!data.config.ContainsKey(option))
{
data.config[option] = defaults[(int)option];
dirty = true;
}
return dirty;
}
// check rulesets and groups
bool CheckData()
{
bool dirty = false;
if ((data.ruleSets == null || data.ruleSets.Count == 0) ||
(data.groups == null || data.groups.Count == 0))
dirty = LoadDefaultData();
if (data.schedule == null)
{
data.schedule = new Schedule();
dirty = true;
}
dirty |= CheckMappings();
return dirty;
}
// rebuild mappings
bool CheckMappings()
{
bool dirty = false;
foreach (RuleSet rs in data.ruleSets)
if (!data.mappings.ContainsValue(rs.name))
{
data.mappings[rs.name] = rs.name;
dirty = true;
}
return dirty;
}
// default config creation
protected override void LoadDefaultConfig()
{
data = new TruePVEData();
data.configVersion = Version.ToString();
LoadDefaultConfiguration();
LoadDefaultData();
SaveData();
}
void CheckVersion()
{
if (Config["configVersion"] == null) return;
Version config = new Version(Config["configVersion"].ToString());
if (config < new Version("0.7.0"))
{
string fname = Config.Filename.Replace(".json", ".old.json");
Config.Save(fname);
PrintWarning(string.Format(GetMessage("Warning_OldConfig"), fname));
Config.Clear();
}
}
// populates default configuration entries
bool LoadDefaultConfiguration()
{
foreach (Option option in Enum.GetValues(typeof(Option)))
data.config[option] = defaults[(int)option];
return true;
}
// load default data to mappings, rulesets, and groups
bool LoadDefaultData()
{
data.mappings.Clear();
data.ruleSets.Clear();
data.groups.Clear();
data.schedule = new Schedule();
data.defaultRuleSet = "default";
// build groups first
EntityGroup dispenser = new EntityGroup("dispensers");
dispenser.Add(typeof(BaseCorpse).Name);
dispenser.Add(typeof(HelicopterDebris).Name);
data.groups.Add(dispenser);
EntityGroup players = new EntityGroup("players");
players.Add(typeof(BasePlayer).Name);
data.groups.Add(players);
EntityGroup traps = new EntityGroup("traps");
traps.Add(typeof(AutoTurret).Name);
traps.Add(typeof(BearTrap).Name);
traps.Add(typeof(FlameTurret).Name);
traps.Add(typeof(Landmine).Name);
traps.Add(typeof(GunTrap).Name);
traps.Add(typeof(ReactiveTarget).Name); // include targets with traps, since behavior is the same
traps.Add("spikes.floor");
data.groups.Add(traps);
EntityGroup barricades = new EntityGroup("barricades");
barricades.Add(typeof(Barricade).Name);
data.groups.Add(barricades);
EntityGroup highwalls = new EntityGroup("highwalls");
highwalls.Add("wall.external.high.stone");
highwalls.Add("wall.external.high.wood");
highwalls.Add("gates.external.high.wood");
highwalls.Add("gates.external.high.wood");
data.groups.Add(highwalls);
EntityGroup heli = new EntityGroup("heli");
heli.Add(typeof(BaseHelicopter).Name);
data.groups.Add(heli);
EntityGroup npcs = new EntityGroup("npcs");
npcs.Add(typeof(NPCPlayerApex).Name);
npcs.Add(typeof(BradleyAPC).Name);
data.groups.Add(npcs);
EntityGroup fire = new EntityGroup("fire"); ;
fire.Add(typeof(FireBall).Name);
data.groups.Add(fire);
EntityGroup resources = new EntityGroup("resources");
resources.Add(typeof(ResourceEntity).Name);
resources.Add(typeof(TreeEntity).Name);
resources.Add(typeof(OreResourceEntity).Name);
data.groups.Add(resources);
// create default ruleset
RuleSet defaultRuleSet = new RuleSet(data.defaultRuleSet);
defaultRuleSet.flags = RuleFlags.HumanNPCDamage | RuleFlags.LockedBoxesImmortal | RuleFlags.LockedDoorsImmortal;
// create rules and add to ruleset
defaultRuleSet.AddRule("anything can hurt " + dispenser.name); // anything hurts dispensers
defaultRuleSet.AddRule("anything can hurt " + players.name); // anything hurts players
defaultRuleSet.AddRule(players.name + " cannot hurt " + players.name); // players cannot hurt other players
defaultRuleSet.AddRule("anything can hurt " + traps.name); // anything hurts traps
defaultRuleSet.AddRule(traps.name + " cannot hurt " + players.name); // traps cannot hurt players
defaultRuleSet.AddRule(players.name + " can hurt " + barricades.name); // players can hurt barricades
defaultRuleSet.AddRule(barricades.name + " cannot hurt " + players.name); // barricades cannot hurt players
defaultRuleSet.AddRule(highwalls.name + " cannot hurt " + players.name); // highwalls cannot hurt players
defaultRuleSet.AddRule("anything can hurt " + heli.name); // anything can hurt heli
defaultRuleSet.AddRule("anything can hurt " + npcs.name); // anything can hurt npcs
defaultRuleSet.AddRule(fire.name + " cannot hurt " + players.name); // fire cannot hurt players
defaultRuleSet.AddRule("anything can hurt " + resources.name); // anything can hurt resources (gather)
data.ruleSets.Add(defaultRuleSet); // add ruleset to rulesets list
data.mappings[data.defaultRuleSet] = data.defaultRuleSet; // create mapping for ruleset
return true;
}
#endregion
#region Hooks/Handler Procedures
void OnPlayerInit(BasePlayer player)
{
if (data.schedule.enabled && data.schedule.broadcast && currentBroadcastMessage != null)
SendReply(player, GetMessage("Prefix") + currentBroadcastMessage);
}
// handle damage - if another mod must override TruePVE damages or take priority,
// set handleDamage to false and reference HandleDamage from the other mod(s)
object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitinfo)
{
if(!data.config[Option.handleDamage])
return null;
return HandleDamage(entity, hitinfo);
}
// handle damage
object HandleDamage(BaseCombatEntity entity, HitInfo hitinfo)
{
if (!AllowDamage(entity, hitinfo))
return true;
return null;
}
// determines if an entity is "allowed" to take damage
bool AllowDamage(BaseEntity entity, HitInfo hitinfo)
{
object extCanTakeDamage = Interface.CallHook("CanEntityTakeDamage", new object[] { entity, hitinfo });
if (extCanTakeDamage != null)
return (bool) extCanTakeDamage;
// if default global is not enabled, return true (allow all damage)
if (currentRuleSet == null || currentRuleSet.IsEmpty() || !currentRuleSet.enabled)
return true;
if (entity == null || hitinfo == null) return true;
// allow decay
if (hitinfo.damageTypes.Get(DamageType.Decay) > 0)
return true;
// allow NPCs to take damage
if (entity is BaseNpc)
return true;
// allow damage to door barricades and covers
if(entity is Barricade && (entity.ShortPrefabName.Contains("door_barricade") || entity.ShortPrefabName.Contains("cover")))
return true;
// if entity is a barrel, trash can, or giftbox, allow damage (exclude water barrels)
if(entity.ShortPrefabName.Contains("barrel") ||
entity.ShortPrefabName.Equals("loot_trash") ||
entity.ShortPrefabName.Equals("giftbox_loot"))
if(!entity.ShortPrefabName.Equals("waterbarrel"))
return true;
if (trace)
{
Trace("======================" + Environment.NewLine +
"== STARTING TRACE ==" + Environment.NewLine +
"== " + DateTime.Now.ToString("HH:mm:ss.fffff") + " ==" + Environment.NewLine +
"======================");
Trace($"From: {hitinfo.Initiator.GetType().Name}, {hitinfo.Initiator.ShortPrefabName}", 1);
Trace($"To: {entity.GetType().Name}, {entity.ShortPrefabName}", 1);
}
// get entity and initiator locations (zones)
List<string> entityLocations = GetLocationKeys(entity);
List<string> initiatorLocations = GetLocationKeys(hitinfo.Initiator);
// check for exclusion zones (zones with no rules mapped)
if (CheckExclusion(entityLocations, initiatorLocations)) return true;
if (trace) Trace("No exclusion found - looking up RuleSet...", 1);
// process location rules
RuleSet ruleSet = GetRuleSet(entityLocations, initiatorLocations);
if (trace) Trace($"Using RuleSet \"{ruleSet.name}\"", 1);
// handle suicide
if (hitinfo.damageTypes.Get(DamageType.Suicide) > 0)
{
if (trace) Trace($"DamageType is suicide; blocked? { (ruleSet.HasFlag(RuleFlags.SuicideBlocked) ? "true; block and return" : "false; continue processing") }", 1);
if (ruleSet.HasFlag(RuleFlags.SuicideBlocked))
{
SendMessage(entity as BasePlayer, "Error_NoSuicide");
return false;
}
return true;
}
// Check storage containers and doors for locks
if ((entity is StorageContainer && ruleSet.HasFlag(RuleFlags.LockedBoxesImmortal)) ||
(entity is Door && ruleSet.HasFlag(RuleFlags.LockedDoorsImmortal)))
{
// check for lock
object hurt = CheckLock(ruleSet, entity, hitinfo);
if (trace) Trace($"Door/StorageContainer detected with immortal flag; lock check results: { (hurt == null ? "null (no lock or unlocked); continue checks" : (bool)hurt ? "allow and return" : "block and return") }", 1);
if (hurt != null)
return (bool)hurt;
}
// check heli
object heli = CheckHeliInitiator(ruleSet, hitinfo);
if(heli != null)
{
if (entity is BasePlayer)
{
if (trace) Trace($"Initiator is heli, and target is player; flag check results: { (ruleSet.HasFlag(RuleFlags.NoHeliDamagePlayer) ? "flag set; block and return" : "flag not set; allow and return") }", 1);
return !ruleSet.HasFlag(RuleFlags.NoHeliDamagePlayer);
}
if (trace) Trace($"Initiator is heli, target is non-player; results: { ((bool)heli ? "allow and return" : "block and return") }", 1);
return (bool)heli;
}
// after heli check, return true if initiator is null
if (hitinfo.Initiator == null)
{
if (trace) Trace("Initiator empty; allow and return", 1);
return true;
}
// check for sleeper protection - return false if sleeper protection is on (true)
if (ruleSet.HasFlag(RuleFlags.ProtectedSleepers) && hitinfo.Initiator is BaseNpc && entity is BasePlayer && (entity as BasePlayer).IsSleeping())
{
if (trace) Trace("Target is sleeping player, with ProtectedSleepers flag set; block and return", 1);
return false;
}
// allow NPC damage to other entities if sleeper protection is off
if (hitinfo.Initiator is BaseNpc)
{
if (trace) Trace("Initiator is NPC animal; allow and return", 1);
return true;
}
// ignore checks if authorized damage enabled (except for players)
if (ruleSet.HasFlag(RuleFlags.AuthorizedDamage) && !(entity is BasePlayer) && hitinfo.Initiator is BasePlayer && CheckAuthorized(entity, hitinfo.Initiator as BasePlayer, ruleSet))
{
if (trace) Trace("Initiator is player with authorization over non-player target; allow and return", 1);
return true;
}
// allow sleeper damage by admins if configured
if (ruleSet.HasFlag(RuleFlags.AdminsHurtSleepers) && entity is BasePlayer && hitinfo.Initiator is BasePlayer)
if ((entity as BasePlayer).IsSleeping() && IsAdmin(hitinfo.Initiator as BasePlayer))
{
if (trace) Trace("Initiator is admin player and target is sleeping player, with AdminsHurtSleepers flag set; allow and return", 1);
return true;
}
// allow Human NPC damage if configured
if (ruleSet.HasFlag(RuleFlags.HumanNPCDamage) && entity is BasePlayer && hitinfo.Initiator is BasePlayer)
if (IsHumanNPC(entity as BasePlayer) || IsHumanNPC(hitinfo.Initiator as BasePlayer))
{
if (trace) Trace("Initiator or target is HumanNPC, with HumanNPCDamage flag set; allow and return", 1);
return true;
}
if (trace) Trace("No match in pre-checks; evaluating RuleSet rules...", 1);
return EvaluateRules(entity, hitinfo, ruleSet);
}
// process rules to determine whether to allow damage
bool EvaluateRules(BaseEntity entity, HitInfo hitinfo, RuleSet ruleSet)
{
List<string> e0Groups = data.ResolveEntityGroups(hitinfo.Initiator);
List<string> e1Groups = data.ResolveEntityGroups(entity);
if (trace)
{
Trace($"Initator EntityGroup matches: { (e0Groups.Count == 0 ? "none" : string.Join(", ", e0Groups.ToArray())) }", 2);
Trace($"Target EntityGroup matches: { (e1Groups.Count == 0 ? "none" : string.Join(", ", e1Groups.ToArray())) }", 2);
}
return ruleSet.Evaluate(e0Groups, e1Groups);
}
// checks for a lock
object CheckLock(RuleSet ruleSet, BaseEntity entity, HitInfo hitinfo)
{
// exclude deployed items in storage container lock check (since they can't have locks)
if(entity.ShortPrefabName.Equals("lantern.deployed") ||
entity.ShortPrefabName.Equals("ceilinglight.deployed") ||
entity.ShortPrefabName.Equals("furnace.large") ||
entity.ShortPrefabName.Equals("campfire") ||
entity.ShortPrefabName.Equals("furnace") ||
entity.ShortPrefabName.Equals("refinery_small_deployed") ||
entity.ShortPrefabName.Equals("waterbarrel") ||
entity.ShortPrefabName.Equals("jackolantern.angry") ||
entity.ShortPrefabName.Equals("jackolantern.happy") ||
entity.ShortPrefabName.Equals("repairbench_deployed") ||
entity.ShortPrefabName.Equals("researchtable_deployed") ||
entity.ShortPrefabName.Contains("shutter"))
return null;
// if unlocked damage allowed - check for lock
BaseLock alock = entity.GetSlot(BaseEntity.Slot.Lock) as BaseLock; // get lock
if (alock == null) return null; // no lock, return null
if (alock.IsLocked()) // is locked, cancel damage except heli
{
// if heliDamageLocked option is false or heliDamage is false, all damage is cancelled
if(!ruleSet.HasFlag(RuleFlags.HeliDamageLocked) || ruleSet.HasFlag(RuleFlags.NoHeliDamage)) return false;
object heli = CheckHeliInitiator(ruleSet, hitinfo);
if(heli != null)
return (bool) heli;
return false;
}
return null;
}
// check for heli
object CheckHeliInitiator(RuleSet ruleSet, HitInfo hitinfo)
{
// Check for heli initiator
if(hitinfo.Initiator is BaseHelicopter ||
(hitinfo.Initiator != null && (
hitinfo.Initiator.ShortPrefabName.Equals("oilfireballsmall") ||
hitinfo.Initiator.ShortPrefabName.Equals("napalm"))))
return !ruleSet.HasFlag(RuleFlags.NoHeliDamage);
else if(hitinfo.WeaponPrefab != null) // prevent null spam
{
if(hitinfo.WeaponPrefab.ShortPrefabName.Equals("rocket_heli") ||
hitinfo.WeaponPrefab.ShortPrefabName.Equals("rocket_heli_napalm"))
return !ruleSet.HasFlag(RuleFlags.NoHeliDamage);
}
return null;
}
// checks if the player is authorized to damage the entity
bool CheckAuthorized(BaseEntity entity, BasePlayer player, RuleSet ruleSet)
{
// check if the player is the owner of the entity
if ((!ruleSet.HasFlag(RuleFlags.CupboardOwnership) && player.userID == entity.OwnerID) || entity.OwnerID == 0L)
return true; // player is the owner or the owner is undefined, allow damage/looting
// block if building blocked
if (player.IsBuildingBlocked(entity.transform.position, entity.transform.rotation, entity.bounds))
return false;
// if not CupboardOwnership, check for build authorization
if (!ruleSet.HasFlag(RuleFlags.CupboardOwnership))
return player.IsBuildingAuthed(entity.transform.position, entity.transform.rotation, entity.bounds);
// else, allow damage
return true;
}
// handle player attacking an entity - specifically, checks resource dispensers
// to determine whether to prevent gathering, based on rules
object OnPlayerAttack(BasePlayer attacker, HitInfo hitinfo)
{
if(hitinfo?.HitEntity is ResourceEntity)
{
if (!AllowDamage(hitinfo.HitEntity, hitinfo))
return false;
}
return null;
}
// check if entity can be targeted
object CanBeTargeted(BaseCombatEntity target, object turret)
{
if (!serverInitialized || target == null || turret == null) return null;
if (turret as HelicopterTurret)
return null;
if (target.GetType().IsSubclassOf(typeof(BasePlayer))) return null;
BasePlayer player = target as BasePlayer;
if (player == null) return null;
RuleSet ruleSet = GetRuleSet(player, turret as BaseCombatEntity);
if (ruleSet.HasFlag(RuleFlags.TurretsIgnorePlayers))
return false;
return null;
}
// ignore players stepping on traps if configured
object OnTrapTrigger(BaseTrap trap, GameObject go)
{
BasePlayer player = go.GetComponent<BasePlayer>();
if (player == null || trap == null) return null;
RuleSet ruleSet = GetRuleSet(trap, player);
if(ruleSet.HasFlag(RuleFlags.TrapsIgnorePlayers))
return false;
return null;
}
// Check exclusion for entities
bool CheckExclusion(BaseEntity entity0, BaseEntity entity1)
{
// check for exclusion zones (zones with no rules mapped)
List<string> e0Locations = GetLocationKeys(entity0);
List<string> e1Locations = GetLocationKeys(entity1);
return CheckExclusion(e0Locations, e1Locations);
}
RuleSet GetRuleSet(List<string> e0Locations, List<string> e1Locations)
{
RuleSet ruleSet = currentRuleSet;
if (e0Locations != null && e1Locations != null && e0Locations.Count() > 0 && e1Locations.Count() > 0)
{
if(trace) Trace($"Beginning RuleSet lookup for [{ (e0Locations.Count == 0 ? "empty" : string.Join(", ", e0Locations.ToArray())) }] and [{ (e1Locations.Count == 0 ? "empty" : string.Join(", ", e1Locations.ToArray())) }]", 2);
List<string> locations = GetSharedLocations(e0Locations, e1Locations);
if (trace) Trace($"Shared locations: { (locations.Count == 0 ? "none" : string.Join(", ", locations.ToArray())) }", 3);
if (locations != null && locations.Count > 0)
{
List<string> names = locations.Select(s => data.mappings[s]).ToList();
List<RuleSet> sets = data.ruleSets.Where(r => names.Contains(r.name)).ToList();
if (trace) Trace($"Found {names.Count} location names, with {sets.Count} mapped RuleSets", 3);
if (sets.Count == 0 && data.mappings.ContainsKey(AllZones) && data.ruleSets.Any(r => r.name == data.mappings[AllZones]))
{
sets.Add(data.ruleSets.FirstOrDefault(r => r.name == data.mappings[AllZones]));
if (trace) Trace($"Found allzones mapped RuleSet", 3);
}
if (sets.Count > 1)
{
if (trace) Trace($"WARNING: Found multiple RuleSets: {string.Join(", ", sets.Select(s => s.name).ToArray())}", 3);
PrintWarning(GetMessage("Warning_MultipleRuleSets"), string.Join(", ", sets.Select(s => s.name).ToArray()));
}
ruleSet = sets.FirstOrDefault();
if (trace && ruleSet != null) Trace($"Found RuleSet: {ruleSet.name}", 3);
}
}
if (ruleSet == null)
{
ruleSet = currentRuleSet;
if (trace) Trace($"No RuleSet found; assigned current global RuleSet: {ruleSet.name}", 3);
}
return ruleSet;
}
RuleSet GetRuleSet(BaseEntity e0, BaseEntity e1)
{
List<string> e0Locations = GetLocationKeys(e0);
List<string> e1Locations = GetLocationKeys(e1);
return GetRuleSet(e0Locations, e1Locations);
}
// get locations shared between the two passed location lists
List<string> GetSharedLocations(List<string> e0Locations, List<string> e1Locations)
{
return e0Locations.Intersect(e1Locations).Where(s => data.HasMapping(s)).ToList();
}
// Check exclusion for given entity locations
bool CheckExclusion(List<string> e0Locations, List<string> e1Locations)
{
if (e0Locations == null || e1Locations == null)
{
if (trace) Trace("No shared locations (empty location) - no exclusions", 3);
return false;
}
if (trace) Trace($"Checking exclusions between [{ (e0Locations.Count == 0 ? "empty" : string.Join(", ", e0Locations.ToArray())) }] and [{ (e1Locations.Count == 0 ? "empty" : string.Join(", ", e1Locations.ToArray())) }]", 2);
List<string> locations = GetSharedLocations(e0Locations, e1Locations);
if (trace) Trace($"Shared locations: {(locations.Count == 0 ? "none" : string.Join(", ", locations.ToArray()))}", 3);
if (locations != null && locations.Count > 0)
foreach (string loc in locations)
if (data.HasEmptyMapping(loc))
{
if (trace) Trace($"Found exclusion mapping for location: {loc}", 3);
return true;
}
if (trace) Trace("No shared locations, or no matching exclusion mapping - no exclusions)", 3);
return false;
}
// add or update a mapping
bool AddOrUpdateMapping(string key, string ruleset)
{
if (string.IsNullOrEmpty(key))
return false;
if (ruleset == null || (!data.ruleSets.Select(r => r.name).Contains(ruleset) && ruleset != "exclude"))
return false;
if (data.HasMapping(key))
// update existing mapping
data.mappings[key] = ruleset;
else
// add new mapping
data.mappings.Add(key, ruleset);
SaveData();
return true;
}
// remove a mapping
bool RemoveMapping(String key)
{
if (string.IsNullOrEmpty(key))
return false;
if (data.HasMapping(key))
{
data.mappings.Remove(key);
SaveData();
return true;
}
return false;
}
#endregion
#region Messaging
// send message to player (chat)
void SendMessage(BasePlayer player, string key, object[] options = null) => SendReply(player, BuildMessage(player, key, options));
// send message to player (console)
void SendMessage(ConsoleSystem.Arg arg, string key, object[] options = null) => SendReply(arg, BuildMessage(null, key, options));
// build message string
string BuildMessage(BasePlayer player, string key, object[] options = null)
{
string message = player == null ? GetMessage(key) : GetMessage(key, player.UserIDString);
if (options != null && options.Length > 0)
message = string.Format(message, options);
string type = key.Split('_')[0];
if (player != null)
{
string size = GetMessage("Format_" + type + "Size");
string color = GetMessage("Format_" + type + "Color");
return WrapSize(size, WrapColor(color, message));
}
else
{
string color = GetMessage("Format_" + type + "Color");
return WrapColor(color, message);
}
}
// prints the value of an Option
private void PrintValue(ConsoleSystem.Arg arg, Option opt)
{
SendReply(arg, WrapSize(GetMessage("Format_NotifySize"), WrapColor(GetMessage("Format_NotifyColor"), opt + ": ") + data.config[opt]));
}
// wrap string in <size> tag, handles parsing size string to integer
string WrapSize(string size, string input)
{
int i = 0;
if(int.TryParse(size, out i))
return WrapSize(i, input);
return input;
}
// wrap a string in a <size> tag with the passed size
string WrapSize(int size, string input)
{
if(input == null || input.Equals(""))
return input;
return "<size=" + size + ">" + input + "</size>";
}
// wrap a string in a <color> tag with the passed color
string WrapColor(string color, string input)
{
if(input == null || input.Equals("") || color == null || color.Equals(""))
return input;
return "<color=" + color + ">" + input + "</color>";
}
// show usage information
void ShowUsage(ConsoleSystem.Arg arg) => SendReply(arg, usageString);
// warn that the server is set to PVE mdoe
void WarnPve() => PrintWarning(GetMessage("Warning_PveMode"));
#endregion
#region Helper Procedures
// is admin
private bool IsAdmin(BasePlayer player)
{
if (player == null) return false;
if (player?.net?.connection == null) return true;
return player.net.connection.authLevel > 0;
}
// check if player has permission or is an admin
private bool hasPermission(BasePlayer player, string permname)
{
return IsAdmin(player) || permission.UserHasPermission(player.UserIDString, permname);
}
// is player a HumanNPC
private bool IsHumanNPC(BasePlayer player)
{
return player.userID < 76560000000000000L && player.userID > 0L && !player.IsDestroyed;
}
// get location keys from ZoneManager (zone IDs) or LiteZones (zone names)
private List<string> GetLocationKeys(BaseEntity entity)
{
if(!useZones || entity == null) return null;
List<string> locations = new List<string>();
if (ZoneManager != null)
{
List<string> zmloc = (List<string>)ZoneManager.Call("GetEntityZones", new object[] { entity });
if (zmloc != null && zmloc.Count > 0)
locations.AddRange(zmloc);
}
if (LiteZones != null)
{
List<string> lzloc = (List<string>) LiteZones?.Call("GetEntityZones", new object[] { entity });
if (lzloc != null && lzloc.Count > 0)
locations.AddRange(lzloc);
}
if (locations == null || locations.Count == 0) return null;
return locations;
}
// check user access
bool
// ... [Code preview truncated due to length. Full source in attached file] ...