C#:
// -----------------------------------------------------------------------
// Copyright (c) Yac Vaguer. All rights reserved.
// Author: Yac Vaguer
// This is proprietary software. Unauthorized copying, distribution, or modification is strictly prohibited.
// Licensed for use only on servers owned by the purchaser.
// -----------------------------------------------------------------------
using Oxide.Core.Plugins;
using Oxide.Core;
using System.Collections.Generic;
using System.Globalization;
using System;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("SimplePVESphere", "Yac Vaguer", "1.1.0")]
[Description("Spawns a visible sphere and a trigger collider to allow or disallow players for certain areas to be use by events.")]
public class SimplePVESphere : RustPlugin
{
private Dictionary<string, PveSphere> spheres = new Dictionary<string, PveSphere>();
public bool ignoreAdmins = false;
public bool debugLogging = false;
#region Configuration
protected override void LoadDefaultConfig()
{
Config["IgnoreAdmins"] = ignoreAdmins = false;
Config["DebugLogging"] = debugLogging = false;
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
ignoreAdmins = Convert.ToBoolean(Config["IgnoreAdmins"]);
if (Config["DebugLogging"] == null)
{
Config["DebugLogging"] = false;
SaveConfig();
}
debugLogging = Convert.ToBoolean(Config["DebugLogging"]);
}
#endregion
#region Diagnostics
public void Log(string line)
{
if (debugLogging) Interface.Oxide.LogInfo(line);
}
#endregion
#region Hooks
private void Init()
{
UpdateHookSubscriptions();
}
private void OnEntityDeath(BaseCombatEntity victim, HitInfo info)
{
if (victim == null || info == null)
{
return;
}
BasePlayer killer = info.InitiatorPlayer;
if (killer == null || !killer.userID.Get().IsSteamId())
{
return;
}
if (!(victim is HumanNPC || victim is ScientistNPC))
{
return;
}
foreach (var sphereEntry in spheres)
{
PveSphere pveSphere = sphereEntry.Value;
if (pveSphere.Owner != null)
{
continue;
}
float distance = Vector3.Distance(victim.transform.position, pveSphere.Center);
if (distance <= pveSphere.Radius)
{
pveSphere.Owner = killer;
Interface.Oxide.CallHook("OnSimplePveOwnerAssigned", pveSphere.EventName, killer);
break;
}
}
}
private object CanEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (info == null) return null;
BasePlayer victim = entity as BasePlayer;
if (victim == null || !victim.userID.Get().IsSteamId()) return null;
BasePlayer attacker = info.InitiatorPlayer;
if (attacker == null || !attacker.userID.Get().IsSteamId()) return null;
if (attacker == victim) return null;
foreach (var kvp in spheres)
{
PveSphere sphere = kvp.Value;
if (!sphere.IsPlayerInside(victim)) continue;
bool? verdict = SphereAccess.CanDamage(true, sphere.IsAuthorized(attacker), sphere.AllowPvP);
sphere.LogDamage(attacker, victim, verdict);
return verdict;
}
return null;
}
private void Unload()
{
foreach (var pair in spheres)
{
pair.Value.Destroy();
Log(SphereAccess.DescribeDestroyed(pair.Key));
}
spheres.Clear();
UpdateHookSubscriptions();
}
private void UpdateHookSubscriptions()
{
if (spheres.Count > 0)
{
Subscribe(nameof(OnEntityDeath));
Subscribe(nameof(CanEntityTakeDamage));
}
else
{
Unsubscribe(nameof(OnEntityDeath));
Unsubscribe(nameof(CanEntityTakeDamage));
}
}
#endregion
#region API
[HookMethod("CreateSphere")]
public void CreateSphere(string eventName, Vector3 position, float radius, int opacity, BasePlayer owner = null, bool eventDebugMode = false)
{
if (spheres.ContainsKey(eventName))
{
return;
}
var newSphere = new PveSphere(eventName, position, radius, opacity, owner, this, eventDebugMode);
newSphere.SpawnVisualSphere();
newSphere.SpawnTriggerCollider();
spheres.Add(eventName, newSphere);
Log(SphereAccess.DescribeCreated(eventName, radius, position.x, position.y, position.z,
owner?.displayName, owner == null ? 0UL : (ulong)owner.userID));
UpdateHookSubscriptions();
}
[HookMethod("RemoveSphere")]
public void RemoveSphere(string eventName)
{
if (!spheres.ContainsKey(eventName))
{
return;
}
spheres[eventName].Destroy();
spheres.Remove(eventName);
Log(SphereAccess.DescribeDestroyed(eventName));
UpdateHookSubscriptions();
}
[HookMethod("SetAllowPvP")]
public void SetAllowPvP(string eventName, bool allowPvP)
{
if (!spheres.ContainsKey(eventName)) return;
spheres[eventName].AllowPvP = allowPvP;
}
#endregion
#region Test commands
private const string TestSphereName = "sphere-test";
private const int TestSphereOpacity = 5;
[ConsoleCommand("simplepvesphere.test")]
private void TestSphereCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin) return;
if (spheres.ContainsKey(TestSphereName))
{
arg.ReplyWith("A test sphere already exists. Remove it with simplepvesphere.remove first.");
return;
}
string ownerArg = arg.GetString(1, "me");
BasePlayer owner = null;
if (ownerArg != "none")
{
owner = ownerArg == "me" ? arg.Player() : BasePlayer.FindAwakeOrSleeping(ownerArg);
if (owner == null)
{
arg.ReplyWith($"No player found for '{ownerArg}'. Use 'me', 'none' or a steam id.");
return;
}
}
BasePlayer at = arg.Player() ?? owner;
if (at == null)
{
arg.ReplyWith("Run this in game, or name a connected owner so the sphere has a position.");
return;
}
float radius = arg.GetFloat(0, 25f);
bool sphereDebugMode = arg.GetBool(2, false);
CreateSphere(TestSphereName, at.transform.position, radius, TestSphereOpacity, owner, sphereDebugMode);
arg.ReplyWith($"Test sphere created: radius {radius}, owner {(owner == null ? "none" : owner.displayName)}, debug mode {sphereDebugMode}.");
}
[ConsoleCommand("simplepvesphere.remove")]
private void RemoveTestSphereCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin) return;
bool existed = spheres.ContainsKey(TestSphereName);
RemoveSphere(TestSphereName);
arg.ReplyWith(existed ? "Test sphere removed." : "There is no test sphere to remove.");
}
[ConsoleCommand("simplepvesphere.pvp")]
private void TestSpherePvPCommand(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin) return;
if (!spheres.ContainsKey(TestSphereName))
{
arg.ReplyWith("There is no test sphere. Create one with simplepvesphere.test.");
return;
}
bool allowPvP = arg.GetBool(0, true);
SetAllowPvP(TestSphereName, allowPvP);
arg.ReplyWith($"Test sphere PvP {(allowPvP ? "enabled" : "disabled")}.");
}
#endregion
}
public class PveSphere
{
private readonly SimplePVESphere plugin;
private readonly bool isEventDebugModeActive;
private readonly int opacity;
public string EventName { get; private set; }
public Vector3 Center { get; private set; }
public float Radius { get; private set; }
public BasePlayer Owner { get; set; }
public bool AllowPvP { get; set; }
private List<SphereEntity> visualSpheres = new List<SphereEntity>();
private GameObject triggerObject;
private SphereCollider triggerCollider;
private SphereTrigger trigger;
public PveSphere(string eventName, Vector3 position, float radius, int opacity, BasePlayer owner, SimplePVESphere pluginInstance, bool eventDebugMode)
{
this.EventName = eventName;
this.Center = position;
this.Radius = radius;
this.opacity = opacity;
this.Owner = owner;
this.plugin = pluginInstance;
this.isEventDebugModeActive = eventDebugMode;
}
#region Public
public void SpawnVisualSphere()
{
for (int i = 0; i < opacity; i++)
{
var sphere = GameManager.server.CreateEntity("assets/prefabs/visualization/sphere.prefab", Center) as SphereEntity;
if (sphere == null)
{
continue;
}
sphere.enableSaving = false;
sphere.currentRadius = Radius * 2;
sphere.lerpRadius = Radius * 2;
sphere.Spawn();
visualSpheres.Add(sphere);
}
}
public void SpawnTriggerCollider()
{
triggerObject = new GameObject($"SphereTrigger_{Center}");
UnityEngine.Object.DontDestroyOnLoad(triggerObject);
triggerObject.transform.position = Center;
triggerObject.layer = 3;
triggerCollider = triggerObject.AddComponent<SphereCollider>();
triggerCollider.isTrigger = true;
triggerCollider.radius = Radius;
trigger = triggerObject.AddComponent<SphereTrigger>();
trigger.Initialize(this);
}
public void Destroy()
{
foreach (var sphere in visualSpheres)
{
if (sphere != null && !sphere.IsDestroyed)
sphere.Kill();
}
visualSpheres.Clear();
trigger = null;
if (triggerObject != null)
{
UnityEngine.Object.Destroy(triggerObject);
}
}
public bool IsPlayerInside(BasePlayer player) => trigger != null && trigger.IsInside(player);
public bool IsAuthorized(BasePlayer player)
{
if (player == null)
{
return false;
}
SphereAccess.AccessReason reason = SphereAccess.Authorize(
player.userID,
Owner == null ? 0UL : (ulong)Owner.userID,
Owner?.Team?.members,
player.IsAdmin,
plugin.ignoreAdmins,
isEventDebugModeActive);
if (plugin.debugLogging)
{
LogOnChange(AccessLog, player.userID,
SphereAccess.DescribeAccess(EventName, player.displayName, player.userID, reason));
}
return SphereAccess.IsAuthorized(reason);
}
#endregion
#region Diagnostics
private const string AccessLog = "access";
private const string DamageLog = "damage";
private const string EjectionLog = "ejection";
private readonly Dictionary<string, string> lastLine = new Dictionary<string, string>();
public void LogBoundary(BasePlayer player, bool entered)
{
if (!entered) ForgetPlayer(player.userID);
plugin.Log(SphereAccess.DescribeBoundary(EventName, player.displayName, player.userID, entered));
}
public void ForgetPlayer(ulong playerId)
{
lastLine.Remove(AccessLog + playerId);
lastLine.Remove(DamageLog + playerId);
lastLine.Remove(EjectionLog + playerId);
}
public void LogDamage(BasePlayer attacker, BasePlayer victim, bool? verdict)
{
if (!plugin.debugLogging) return;
LogOnChange(DamageLog, attacker.userID, SphereAccess.DescribeDamage(
EventName, attacker.displayName, attacker.userID, victim.displayName, victim.userID, verdict));
}
public void LogEjection(BasePlayer player)
{
if (!plugin.debugLogging) return;
LogOnChange(EjectionLog, player.userID,
SphereAccess.DescribeEjection(EventName, player.displayName, player.userID));
}
private void LogOnChange(string category, ulong playerId, string line)
{
string key = category + playerId;
string previous;
if (lastLine.TryGetValue(key, out previous) && previous == line) return;
lastLine[key] = line;
plugin.Log(line);
}
#endregion
}
public class SphereTrigger : FacepunchBehaviour
{
private PveSphere pveSphere;
private const float EJECTION_COOLDOWN = 1.0f;
private readonly HashSet<BasePlayer> insidePlayers = new HashSet<BasePlayer>();
private readonly List<BasePlayer> evalBuffer = new List<BasePlayer>();
private readonly Dictionary<ulong, float> lastEjectionTime = new Dictionary<ulong, float>();
public void Initialize(PveSphere pveSphere)
{
this.pveSphere = pveSphere;
}
public bool IsInside(BasePlayer player) => player != null && insidePlayers.Contains(player);
private void OnTriggerEnter(Collider other)
{
var player = other.GetComponentInParent<BasePlayer>();
if (player == null)
{
return;
}
insidePlayers.Add(player);
pveSphere.LogBoundary(player, true);
if (!player.IsAlive())
{
return;
}
if (pveSphere.IsAuthorized(player))
{
return;
}
TryEjectPlayer(player);
}
private void OnTriggerExit(Collider other)
{
var player = other.GetComponentInParent<BasePlayer>();
if (player == null)
return;
insidePlayers.Remove(player);
pveSphere.LogBoundary(player, false);
}
private void FixedUpdate()
{
if (insidePlayers.Count == 0)
return;
evalBuffer.Clear();
evalBuffer.AddRange(insidePlayers);
for (int i = 0; i < evalBuffer.Count; i++)
{
var player = evalBuffer[i];
if (player == null || player.IsDestroyed || !player.IsAlive())
{
insidePlayers.Remove(player);
if (player != null) pveSphere.ForgetPlayer(player.userID);
continue;
}
if (pveSphere.IsAuthorized(player))
continue;
TryEjectPlayer(player);
}
}
private void TryEjectPlayer(BasePlayer player)
{
if (player == null) return;
float currentTime = Time.realtimeSinceStartup;
if (lastEjectionTime.TryGetValue(player.userID, out float lastEject))
{
if (currentTime - lastEject < EJECTION_COOLDOWN)
return;
}
lastEjectionTime[player.userID] = currentTime;
pveSphere.LogEjection(player);
EjectPlayerOrVehicle(player);
}
private void EjectPlayerOrVehicle(BasePlayer player)
{
if (player == null) return;
try
{
var mountable = player.GetMounted();
if (mountable != null)
{
var vehicle = mountable.VehicleParent();
if (vehicle != null)
{
EjectVehicle(vehicle);
return;
}
else
{
player.EnsureDismounted();
}
}
EjectPlayerCharacter(player);
}
catch (Exception ex)
{
Interface.Oxide.LogError($"[SimplePVESphere] Error ejecting player/vehicle for {player.displayName}: {ex.Message}\n{ex.StackTrace}");
}
}
private void EjectVehicle(BaseVehicle vehicle)
{
if (vehicle == null) return;
Vector3 sphereCenter = transform.position;
float sphereRadius = pveSphere.Radius;
Vector3 directionFromCenter = (vehicle.transform.position.XZ3D() - sphereCenter.XZ3D()).normalized;
if (directionFromCenter == Vector3.zero) directionFromCenter = Vector3.forward;
Vector3 targetHorizontalPos = sphereCenter.XZ3D() + directionFromCenter * (sphereRadius + 10f);
float groundY = GetGroundPosition(targetHorizontalPos).y;
Vector3 newVehiclePosition = new Vector3(targetHorizontalPos.x, groundY + 0.5f, targetHorizontalPos.z);
vehicle.transform.position = newVehiclePosition;
vehicle.transform.rotation = Quaternion.LookRotation(-directionFromCenter, Vector3.up);
if (vehicle.rigidBody != null) vehicle.rigidBody.linearVelocity *= -0.5f;
vehicle.SendNetworkUpdateImmediate();
}
private void EjectPlayerCharacter(BasePlayer player)
{
if (player == null) return;
Vector3 sphereCenter = transform.position;
float sphereRadius = pveSphere.Radius;
Vector3 directionFromCenter = (player.transform.position.XZ3D() - sphereCenter.XZ3D()).normalized;
if (directionFromCenter == Vector3.zero) directionFromCenter = player.transform.forward;
if (directionFromCenter == Vector3.zero) directionFromCenter = Vector3.forward;
Vector3 targetHorizontalPos = sphereCenter.XZ3D() + directionFromCenter * (sphereRadius + 5f);
Vector3 newPosition = GetGroundPosition(targetHorizontalPos);
player.MovePosition(newPosition);
player.ClientRPC(RpcTarget.Player("ForcePositionTo", player), newPosition);
player.SendNetworkUpdateImmediate();
}
private Vector3 GetGroundPosition(Vector3 horizontalPosition)
{
Vector3 raycastStart = new Vector3(horizontalPosition.x, 500f, horizontalPosition.z);
RaycastHit hitInfo;
int groundCheckLayers = LayerMask.GetMask("Terrain", "World", "Construction", "Deployed", "Default");
if (Physics.Raycast(raycastStart, Vector3.down, out hitInfo, 700f, groundCheckLayers, QueryTriggerInteraction.Ignore))
{
return hitInfo.point;
}
else
{
float terrainHeight = TerrainMeta.HeightMap.GetHeight(horizontalPosition);
return new Vector3(horizontalPosition.x, terrainHeight, horizontalPosition.z);
}
}
}
internal static class SphereAccess
{
public enum AccessReason
{
AdminBypass,
NoOwner,
IsOwner,
IsTeammate,
Denied
}
public static AccessReason Authorize(
ulong playerId,
ulong ownerId,
IEnumerable<ulong> ownerTeamMemberIds,
bool isAdmin,
bool ignoreAdmins,
bool debugMode)
{
if (isAdmin && ignoreAdmins && !debugMode) return AccessReason.AdminBypass;
if (ownerId == 0UL) return AccessReason.NoOwner;
if (playerId == ownerId) return AccessReason.IsOwner;
if (ownerTeamMemberIds != null)
{
foreach (ulong memberId in ownerTeamMemberIds)
{
if (memberId == playerId) return AccessReason.IsTeammate;
}
}
return AccessReason.Denied;
}
public static bool IsAuthorized(AccessReason reason) => reason != AccessReason.Denied;
public static bool? CanDamage(bool victimIsProtectedPlayer, bool attackerIsAuthorized, bool allowPvP)
{
if (!victimIsProtectedPlayer) return null;
return allowPvP || attackerIsAuthorized;
}
#region Log rendering
public const string LogPrefix = "[SimplePVESphere]";
public static string DescribeCreated(string sphereName, float radius, float x, float y, float z, string ownerName, ulong ownerId)
=> Line(sphereName, "created: radius " + N(radius) + " at " + N(x) + " " + N(y) + " " + N(z) + ", owner " + Who(ownerName, ownerId));
public static string DescribeDestroyed(string sphereName)
=> Line(sphereName, "destroyed");
public static string DescribeBoundary(string sphereName, string playerName, ulong playerId, bool entered)
=> Line(sphereName, (entered ? "entered" : "left") + " by " + Who(playerName, playerId));
public static string DescribeAccess(string sphereName, string playerName, ulong playerId, AccessReason reason)
=> Line(sphereName, "access " + (IsAuthorized(reason) ? "granted" : "denied") + " to " + Who(playerName, playerId) + ": " + Explain(reason));
public static string DescribeDamage(string sphereName, string attackerName, ulong attackerId, string victimName, ulong victimId, bool? verdict)
=> Line(sphereName, "damage " + Verdict(verdict) + ": " + Who(attackerName, attackerId) + " -> " + Who(victimName, victimId));
public static string DescribeEjection(string sphereName, string playerName, ulong playerId)
=> Line(sphereName, "ejected " + Who(playerName, playerId));
private static string Line(string sphereName, string message) => LogPrefix + " [" + sphereName + "] " + message;
private static string Who(string playerName, ulong playerId)
=> playerId == 0UL ? "none" : playerName + " (" + playerId + ")";
private static string Explain(AccessReason reason)
{
switch (reason)
{
case AccessReason.AdminBypass: return "admin bypass";
case AccessReason.NoOwner: return "sphere has no owner";
case AccessReason.IsOwner: return "is the owner";
case AccessReason.IsTeammate: return "is on the owner's team";
default: return "not the owner, a teammate, or a bypassing admin";
}
}
private static string Verdict(bool? verdict)
=> verdict == null ? "no opinion" : verdict.Value ? "allowed" : "blocked";
private static string N(float value) => value.ToString("0.#", CultureInfo.InvariantCulture);
#endregion
}
}