C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Super PVx Info", "HunterZ", "1.7.0")]
[Description("Displays PvE/PvP/etc.status on player's HUD")]
public class SuperPVxInfo : RustPlugin
{
#region Plugin Data
// list of plugins whose PVP delay statuses are tracked
public enum PvpDelayType
{
AbandonedBases,
DynamicPvp,
PlayerBasePvpZones,
RaidableBases,
TruePve
}
// primary status types tracked by this plugin
public enum PVxType { PVE, PVP, PVPDelay, SafeZone }
// PVP statuses that are managed via listening to player enter/exit hooks
[Flags]
public enum PvpBubbleTypes
{
None = 0,
// Nikedemos plugins
CargoTrainEvent = 1 << 0,
// Adem plugins
Caravan = 1 << 1,
Convoy = 1 << 2,
Sputnik = 1 << 3
}
// PVP events that are managed via listening to start/stop hooks that
// provide a PVP area definition
private enum PvpLocationEventType
{
// KpucTaJl plugins
AirEvent,
ArcticBaseEvent,
FerryTerminalEvent,
GasStationEvent,
HarborEvent,
JunkyardEvent,
PowerPlantEvent,
SatelliteDishEvent,
SupermarketEvent,
WaterEvent
}
[PluginReference] private readonly Plugin
AbandonedBases, DynamicPVP, DangerousTreasures, PlayerBasePvpZones,
PopupNotifications, RaidableBases, SimpleStatus, TruePVE, ZoneManager;
private ConfigData _configData;
// active TruePVE PVP delay timers by plugin name by player ID
private readonly Dictionary<ulong, Dictionary<string, Timer>>
_excludedPlayers = new();
// NOTE: this is not to be used directly for sending messages, but rather
// for populating the default language dictionary, and for enumerating
// which messages exist
private readonly Dictionary<string, string> _notifyMessages = new()
{
["Unexpected Exit From Abandoned Or Raidable Base"] =
"{0}Left Abandoned/Raidable Base Zone",
["Unexpected Exit From Dangerous Treasures Event"] =
"{0}Left Dangerous Treasures Zone",
["Safe Zone Entry"] =
"{0}Entering Safe Zone",
["Safe Zone Exit"] =
"{0}Leaving Safe Zone",
["PVP Height Entry"] =
"{0}WARNING: Entering Sky/Portal PVP Zone",
["PVP Height Exit"] =
"{0}Leaving Sky/Portal PVP Zone",
["PVP Depth Entry"] =
"{0}WARNING: Entering Train Tunnels PVP Zone",
["PVP Depth Exit"] =
"{0}Leaving Train Tunnels PVP Zone"
};
private Timer _saveDataTimer;
private StoredData _storedData;
private const string UIName = "SuperPVxInfoUI";
#endregion Plugin Data
#region Utility Methods
private static bool IsValidPlayer(BasePlayer player, bool checkConnected) =>
player &&
!player.IsNpc &&
player.userID.IsSteamId() &&
(!checkConnected || player.IsConnected);
private static PlayerWatcher GetPlayerWatcher(BasePlayer player) =>
IsValidPlayer(player, true) ? player.GetComponent<PlayerWatcher>() : null;
private void SendCannedMessage(BasePlayer player, string key)
{
if (null == _configData ||
!_configData.NotifySettings.Enabled.TryGetValue(
key, out var enabled) ||
!enabled)
{
return;
}
var message = lang.GetMessage(key, this, player.UserIDString);
if (null == message) return;
if (_configData.NotifySettings.ChatEnabled)
{
SendReply(player, string.Format(
message, _configData.NotifySettings.ChatPrefix));
}
if (_configData.NotifySettings.PopupNotificationsEnabled &&
null != PopupNotifications)
{
PopupNotifications.Call(
"CreatePopupNotification",
string.Format(
message, _configData.NotifySettings.PopupNotificationsPrefix),
player);
}
}
private void ExcludePlayerRemove(ulong userid, string pluginName)
{
// get timers-by-plugin for player
if (!_excludedPlayers.TryGetValue(userid, out var excludeTimers) ||
null == excludeTimers)
{
return;
}
// remove timer entry if present, and destroy it if needed
if (excludeTimers.Remove(pluginName, out var removedTimer) &&
false == removedTimer?.Destroyed)
{
removedTimer.Destroy();
}
// abort if timers-by-plugin is still not empty for this player
if (excludeTimers.Count > 0) return;
// timers-by-plugin is empty - remove PVP delay status
var player = BasePlayer.FindByID(userid);
if (player)
{
SetPvpDelay(player, PvpDelayType.TruePve, false);
}
}
#endregion Utility Methods
#region Oxide Methods
protected override void LoadDefaultMessages() =>
lang.RegisterMessages(_notifyMessages, this);
private void Init()
{
LoadData();
PlayerWatcher.AllowForceUpdate =
null == _configData || _configData.ForceUpdates;
PlayerWatcher.Instance = this;
PlayerWatcher.PvpAboveHeight = _configData?.PvpAboveHeight ?? 1000.0f;
PlayerWatcher.PvpBelowHeight = _configData?.PvpBelowHeight ?? -500.0f;
PlayerWatcher.UpdateIntervalSeconds =
_configData?.UpdateIntervalSeconds ?? 1.0f;
if (null != _configData &&
!string.IsNullOrEmpty(_configData.ToggleCommand))
{
AddCovalenceCommand(_configData.ToggleCommand, nameof(ToggleUI));
}
}
private void OnServerInitialized()
{
if (Convert.ToBoolean(DynamicPVP?.Call("IsUsingExcludePlayer")))
{
Puts("OnServerInitialized(): Detected DynamicPVP support for TruePVE PVP delays");
Unsubscribe(nameof(OnPlayerAddedToPVPDelay));
Unsubscribe(nameof(OnPlayerRemovedFromPVPDelay));
}
if (Convert.ToBoolean(PlayerBasePvpZones?.Call("IsUsingExcludePlayer")))
{
Puts("OnServerInitialized(): Detected PlayerBasePvpZones support for TruePVE PVP delays");
Unsubscribe(nameof(OnPlayerBasePvpDelayStart));
Unsubscribe(nameof(OnPlayerBasePvpDelayStop));
}
if (null == ZoneManager ||
ZoneManager.Version < new VersionNumber(3, 1, 8))
{
PrintWarning("ZoneManager is outdated or not running; this plugin may not work properly");
}
if (null != _storedData)
{
// purge any mappings that Zone Manager doesn't recognize
var activeZoneIds = Pool.Get<List<string>>();
ZM_GetZoneIDsNoAlloc(activeZoneIds);
var deadZoneIds = Pool.Get<List<string>>();
foreach (var (zoneId, _) in _storedData.Mappings)
{
if (!activeZoneIds.Contains(zoneId)) deadZoneIds.Add(zoneId);
}
Pool.FreeUnmanaged(ref activeZoneIds);
foreach (var deadZoneId in deadZoneIds)
{
_storedData.Mappings.Remove(deadZoneId);
}
if (deadZoneIds.Count > 0)
{
PrintWarning($"Purging {deadZoneIds.Count} unknown/obsolete zoneId(s) from database");
SaveData();
}
Pool.FreeUnmanaged(ref deadZoneIds);
}
// setup SimpleStatus integration if appropriate
SS_CreateStatuses();
foreach (var player in BasePlayer.activePlayerList)
{
OnPlayerConnected(player);
}
if (true ==_configData?.NotifySettings.PopupNotificationsEnabled &&
null == PopupNotifications)
{
PrintWarning("Notify via PopupNotifications enabled, but required plugin is missing");
}
}
private void Unload()
{
// clear out any active TruePVE PVP delay timers
foreach (var (_, excludeTimers) in _excludedPlayers)
{
foreach (var (_, excludeTimer) in excludeTimers)
{
if (false == excludeTimer?.Destroyed)
{
excludeTimer.Destroy();
}
}
excludeTimers.Clear();
}
_excludedPlayers.Clear();
// destroy GUIs for all active players
foreach (var player in BasePlayer.activePlayerList)
{
OnPlayerDisconnected(player, UIName);
}
PlayerWatcher.Instance = null;
// if save timer active, destroy and force write
if (null != _saveDataTimer) WriteData();
}
private void OnPlayerConnected(BasePlayer player)
{
if (!IsValidPlayer(player, true)) return;
// abort if an active watcher is already attached
// isActiveAndEnabled is needed because sometimes reloading the plugin
// causes it to catch a watcher that is still in the process of being
// destroyed
var watcher = player.GetComponent<PlayerWatcher>();
if (watcher && watcher.isActiveAndEnabled) return;
watcher = player.gameObject.AddComponent<PlayerWatcher>();
watcher.Init(
IsPlayerInBase(player), IsPlayerInPVPDelay(player.userID.Get()),
GetPlayerZoneType(player), player);
watcher.StartWatching();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (IsValidPlayer(player, false))
{
player.gameObject.GetComponent<PlayerWatcher>()?.OnDestroy();
}
DestroyUI(player);
}
private void OnPlayerRespawned(BasePlayer player) =>
NextTick(() =>
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (!watcher) return;
// check everything because the player could be anywhere now
if (watcher.InBaseType != null) watcher.SetCheckBase(true);
watcher.SetCheckPVxEvent(true);
watcher.SetCheckZone(true);
watcher.SetCheckPvpDelay(true);
watcher.Force();
});
#endregion Oxide Methods
#region TruePVE Hook Handlers
// called when a plugin maps a Zone Manager zone to a TruePVE ruleset
private void AddOrUpdateMapping(string zoneId, string ruleset)
{
if (null == _storedData ||
string.IsNullOrEmpty(zoneId) || string.IsNullOrEmpty(ruleset))
{
return;
}
NextTick(() =>
{
_storedData.Mappings[zoneId] = ruleset;
SaveData();
});
}
// called when a plugin deletes a mapping
private void RemoveMapping(string zoneId)
{
if (null == _storedData || string.IsNullOrEmpty(zoneId)) return;
NextTick(() =>
{
_storedData.Mappings.Remove(zoneId);
SaveData();
});
}
// called when a plugin requests a timed rule exclusion (PVP exit delay)
private void ExcludePlayer(
ulong userid, float maxDelayLength, Plugin plugin)
{
if (null == plugin || !userid.IsSteamId()) return;
var pluginName = plugin.Name;
NextTick(() =>
{
// if delay is non-positive, just try to remove any existing record
if (maxDelayLength <= 0.0f)
{
ExcludePlayerRemove(userid, pluginName);
return;
}
// handle the case of updating an existing record
var hasTimers =
_excludedPlayers.TryGetValue(userid, out var excludeTimers);
if (hasTimers &&
excludeTimers.TryGetValue(pluginName, out var excludeTimer))
{
if (false == excludeTimer?.Destroyed)
{
excludeTimer.Reset(maxDelayLength);
return;
}
// pathological: remove defunct entry; we'll create a new one below
excludeTimers.Remove(pluginName);
}
// handle the case that no timers have ever been recorded for player
// (just create an empty timers-by-plugin sub-dictionary)
if (null == excludeTimers)
{
excludeTimers = new Dictionary<string, Timer>();
_excludedPlayers.Add(userid, excludeTimers);
}
// add a timer to the dictionary that simply removes itself on fire
// existence of a dictionary entry then represents an active PVP delay
excludeTimers.Add(pluginName, timer.Once(
maxDelayLength, () => { ExcludePlayerRemove(userid, pluginName); }));
var player = BasePlayer.FindByID(userid);
if (player)
{
SetPvpDelay(player, PvpDelayType.TruePve, true);
}
});
}
#endregion TruePVE Hook Handlers
#region ZoneManager Integration
#region ZoneManager Utilities
private void ZM_GetZoneIDsNoAlloc(List<string> list) =>
ZoneManager?.Call("GetZoneIDsNoAlloc", list);
private void ZM_GetPlayerZoneIDsNoAlloc(
BasePlayer player, List<string> list) =>
ZoneManager?.Call("GetPlayerZoneIDsNoAlloc", player, list);
// if player is in a zone, return its type if possible, else return null
public PVxType? GetPlayerZoneType(BasePlayer player)
{
if (null == _configData || !IsValidPlayer(player, true)) return null;
// get current zone (if any)
var (zoneId, zoneName) = GetSmallestZoneIdAndName(player);
// go by zone name first
if (!string.IsNullOrEmpty(zoneName))
{
if (_configData.PveZoneManagerNames.Any(
x => zoneName.Contains(x, CompareOptions.IgnoreCase))
)
{
return PVxType.PVE;
}
if (_configData.PvpZoneManagerNames.Any(
x => zoneName.Contains(x, CompareOptions.IgnoreCase))
)
{
return PVxType.PVP;
}
}
if (!string.IsNullOrEmpty(zoneId))
{
// return PVP if this is a TruePVE/NextGenPVE exclusion zone
// (needed for e.g.ZoneManagerAutoZones which doesn't put "PVP" in
// its zone names)
if (IsExcludeZone(zoneId))
{
return PVxType.PVP;
}
// check Zone Manager flags
if (ZM_GetZoneFlag(zoneId, "pvpgod"))
{
return ZM_GetZoneFlag(zoneId, "pvegod") ?
// no-PvP *and* no-PvE => treat as safe zone
PVxType.SafeZone :
// no-PvP only => treat as PvE zone
PVxType.PVE;
}
}
// give up
return null;
}
private (string, string) GetSmallestZoneIdAndName(BasePlayer player)
{
if (ZoneManager == null) return (null, null);
var smallestRadius = float.MaxValue;
string smallestId = null;
string smallestName = null;
var zoneIDs = Pool.Get<List<string>>();
ZM_GetPlayerZoneIDsNoAlloc(player, zoneIDs);
foreach (var zoneId in zoneIDs)
{
if (string.IsNullOrEmpty(zoneId)) continue;
var zoneName = ZM_GetZoneName(zoneId);
// get whichever of 2D zone size or radius is greater than zero
var zoneMagnitude2D = ZM_GetZoneSize(zoneId).Magnitude2D();
var zoneRadius = zoneMagnitude2D < float.Epsilon ?
ZM_GetZoneRadius(zoneId) : zoneMagnitude2D;
if (zoneRadius < float.Epsilon) continue;
if (zoneRadius >= smallestRadius) continue;
// zone is the smallest we've seen; record it as such
smallestRadius = zoneRadius;
smallestId = zoneId;
smallestName = zoneName;
}
Pool.FreeUnmanaged(ref zoneIDs);
return (smallestId, smallestName);
}
private bool ZM_GetZoneFlag(string zoneId, string zoneFlag) =>
Convert.ToBoolean(ZoneManager?.Call("HasFlag", zoneId, zoneFlag));
private string ZM_GetZoneName(string zoneId) =>
Convert.ToString(ZoneManager?.Call("GetZoneName", zoneId));
private float ZM_GetZoneRadius(string zoneId) =>
Convert.ToSingle(ZoneManager?.Call("GetZoneRadius", zoneId));
private Vector3 ZM_GetZoneSize(string zoneId) =>
ZoneManager?.Call("GetZoneSize", zoneId) is Vector3 zoneSize ?
zoneSize : Vector3.zero;
private bool IsExcludeZone(string zoneId) =>
null != _storedData &&
null != _configData &&
_storedData.Mappings.TryGetValue(zoneId, out var ruleset) &&
_configData.PveExclusionNames.Any(
x => ruleset.Contains(x, CompareOptions.IgnoreCase));
// common logic for setting watcher's zone check request flag
private void CheckZone(BasePlayer player) =>
NextTick(() =>
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (!watcher) return;
watcher.SetCheckZone(true);
watcher.Force();
});
#endregion ZoneManager Utilities
#region ZoneManager Hook Handlers
private void OnEnterZone(string zoneId, BasePlayer player) =>
CheckZone(player);
private void OnExitZone(string zoneId, BasePlayer player) =>
// check if player is exiting from a smaller zone into a larger one
CheckZone(player);
#endregion ZoneManager Hook Handlers
#endregion ZoneManager Integration
#region PVP Plugin Integrations
#region PVP Plugin Utilities
// create or update an event record with the given data
private void CreateOrUpdatePvpLocationEvent(
PvpLocationEventType type, Vector3 location, float radius)
{
if (null == _storedData) return;
if (_storedData.PvpEvents.TryGetValue(type, out var eventData))
{
eventData.Location = location;
eventData.Radius = radius;
}
else
{
_storedData.PvpEvents.Add(type, new PvpEventData(location, radius));
}
SaveData();
}
// delete an event record with the given data, if any
private void DeleteLocationPvpEvent(PvpLocationEventType type)
{
if (null == _storedData) return;
if (_storedData.PvpEvents.Remove(type)) SaveData();
}
// check whether player is in any Raidable Base
// TODO: add Abandoned Bases support?
// this is expensive, and should only be called if state is totally unknown
// (e.g.on connect)
private PVxType? IsPlayerInBase(BasePlayer player)
{
// get list of all active Raidable Bases
if (RaidableBases?.Call("GetAllEvents") is List<(Vector3 pos, int mode,
bool allowPVP, string a, float b, float c, float loadTime,
ulong ownerId, BasePlayer owner, List<BasePlayer> raiders,
List<BasePlayer> intruders, List<BaseEntity> entities,
string baseName, DateTime spawnDateTime, DateTime despawnDateTime,
float radius, int lootRemaining)> rbEvents
// && rbEvents.Exists(x => x.intruders.Contains(player))
)
{
// look for a base that the player is in
foreach (
var (_, _, allowPVP, _, _, _, _, _, _, _, intruders, _, _, _, _, _, _)
in rbEvents)
{
if (intruders.Contains(player))
{
// base found; return its type
return allowPVP ? PVxType.PVP : PVxType.PVE;
}
}
}
// player not in any bases
return null;
}
// check whether player is in a PVP event that only provides event
// start/stop hooks with location, requiring active polling of player
// position
private bool IsPlayerInPvpEvent(BasePlayer player)
{
if (null == _storedData) return false;
foreach (var (_, eventData) in _storedData.PvpEvents)
{
if (Vector3.Distance(eventData.Location, player.transform.position) <=
eventData.Radius)
{
return true;
}
}
return false;
}
// check whether player is in an event that can be PvE or PvP
// note that this is only useful for unexpected exits (e.g.respawning)
// because we don't get a PvP-versus-PvE indication from this
private bool IsPlayerInPVxEvent(BasePlayer player) =>
null != DangerousTreasures && Convert.ToBoolean(DangerousTreasures.Call(
"EventTerritory", player.transform.position));
// check if player has any PVP delays active
// this should only be called when hook-reported states don't exist yet, or
// can't be relied upon for some reason
private HashSet<PvpDelayType> IsPlayerInPVPDelay(ulong playerID)
{
var pvpDelays = new HashSet<PvpDelayType>();
if (AbandonedBases != null && Convert.ToBoolean(
AbandonedBases.Call("HasPVPDelay", playerID)))
{
pvpDelays.Add(PvpDelayType.AbandonedBases);
}
if (DynamicPVP != null && Convert.ToBoolean(
DynamicPVP.Call("IsPlayerInPVPDelay", playerID)))
{
pvpDelays.Add(PvpDelayType.DynamicPvp);
}
if (PlayerBasePvpZones != null && !string.IsNullOrEmpty(Convert.ToString(
PlayerBasePvpZones.Call("OnPlayerBasePvpDelayQuery", playerID))))
{
pvpDelays.Add(PvpDelayType.PlayerBasePvpZones);
}
if (RaidableBases != null && Convert.ToBoolean(
RaidableBases.Call("HasPVPDelay", playerID)))
{
pvpDelays.Add(PvpDelayType.RaidableBases);
}
if (_excludedPlayers.TryGetValue(playerID, out var excludeTimers) &&
excludeTimers.Count > 0)
{
pvpDelays.Add(PvpDelayType.TruePve);
}
return pvpDelays;
}
// common logic for Abandoned/Raidable Base entry hooks
private static void EnteredBase(
BasePlayer player, PVxType baseType,
Vector3 baseLocation, float baseRadius)
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (!watcher) return;
watcher.BaseLocation = baseLocation;
watcher.BaseRadius = baseRadius;
watcher.InBaseType = baseType;
watcher.Force();
}
// common logic for Abandoned/Raidable Base exit hooks
private static void ExitedBase(
BasePlayer player, bool checkPlayerValid = true)
{
if (checkPlayerValid && !IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (!watcher) return;
watcher.SetCheckZone(true);
watcher.InBaseType = null;
watcher.Force();
}
// common logic for "in PVP bubble" hooks to set/clear a player's state
private static void SetPvpBubble(
BasePlayer player, PvpBubbleTypes type, bool state)
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (!watcher) return;
var oldState = watcher.InPvpBubbleTypes;
if (state)
{
watcher.InPvpBubbleTypes |= type;
}
else
{
watcher.InPvpBubbleTypes &= ~type;
if (PvpBubbleTypes.None == watcher.InPvpBubbleTypes)
{
watcher.SetCheckZone(true);
}
}
if (watcher.InPvpBubbleTypes != oldState)
{
watcher.Force();
}
}
// common logic for "in PVP bubble" hooks to clear all players' states
private static void EndPvpBubble(PvpBubbleTypes type)
{
foreach (var player in BasePlayer.activePlayerList)
{
SetPvpBubble(player, type, false);
}
}
// common logic for PVP Delay hooks
private static void SetPvpDelay(
BasePlayer player, PvpDelayType type, bool state)
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (null == watcher) return;
if (state)
{
watcher.AddPvpDelay(type);
}
else if (watcher.ClearPvpDelay(type) <= 0)
{
watcher.SetCheckBase(true);
watcher.SetCheckZone(true);
}
watcher.Force();
}
// common logic for PVx event hooks
private static void SetPvxEvent(
BasePlayer player, PVxType eventType, bool state)
{
if (!IsValidPlayer(player, true)) return;
var watcher = GetPlayerWatcher(player);
if (null == watcher) return;
if (state)
{
watcher.SetInPVxEventType(eventType);
}
else
{
watcher.SetCheckZone(true);
watcher.SetInPVxEventType(null);
}
watcher.Force();
}
#endregion PVP Plugin Utilities
#region RaidableBases Hook Handlers
private void OnPlayerEnteredRaidableBase(
BasePlayer player, Vector3 location, bool allowPVP, int mode, string id,
float _, float __, float loadTime, ulong ownerId, string baseName,
DateTime spawnTime, DateTime despawnTime, float radius,
int lootRemaining) =>
NextTick(() => EnteredBase(
player, allowPVP ? PVxType.PVP : PVxType.PVE, location, radius));
private void OnPlayerExitedRaidableBase(
BasePlayer player, Vector3 location, bool allowPVP, int mode, string id,
float _, float __, float loadTime, ulong ownerId, string baseName,
DateTime spawnTime, DateTime despawnTime, float radius) =>
NextTick(() => ExitedBase(player));
private void OnRaidableBaseEnded(
Vector3 location, int mode, bool allowPvP, string id, float _,
float __, float loadTime, ulong ownerId, BasePlayer owner,
List<BasePlayer> raiders, List<BasePlayer> intruders,
List<BaseEntity> entities, string baseName, DateTime spawnDateTime,
DateTime despawnDateTime, float protectionRadius,
int lootAmountRemaining) =>
NextTick(() =>
{
// set zone check flag for any players in base radius
foreach (var player in intruders)
{
if (!IsValidPlayer(player, true)) continue;
// skip player if not within radius of raidable base
if (Vector3.Distance(location, player.transform.position) >
protectionRadius)
{
continue;
}
ExitedBase(player, false);
}
});
private void OnPlayerPvpDelayStart(
BasePlayer player, int mode, Vector3 location, bool allowPvP, string id,
float _, float __, float loadTime, ulong ownerId, string baseName,
DateTime spawnDateTime, DateTime despawnDateTime,
int lootAmountRemaining) =>
NextTick(() => SetPvpDelay(player, PvpDelayType.RaidableBases, true));
private void OnPlayerPvpDelayReset(
BasePlayer player, int mode, Vector3 location, bool allowPvP, string id,
float _, float __, float loadTime, ulong ownerId, string baseName,
DateTime spawnDateTime, DateTime despawnDateTime,
int lootAmountRemaining) =>
NextTick(() => SetPvpDelay(player, PvpDelayType.RaidableBases, true));
private void OnPlayerPvpDelayExpired(
BasePlayer player, int mode, Vector3 location, bool allowPvP, string id,
float _, float __, float loadTime, ulong ownerId, string baseName,
DateTime spawnDateTime, DateTime despawnDateTime,
int lootAmountRemaining) =>
NextTick(() => SetPvpDelay(player, PvpDelayType.RaidableBases, false));
#endregion RaidableBases Hook Handlers
#region AbandonedBases Hook Handlers
private void OnPlayerEnteredAbandonedBase(
BasePlayer player, Vector3 eventPos, float radius, bool allowPVP,
List<BasePlayer> intruders, List<ulong> intruderIds,
List<BaseEntity> entities) =>
NextTick(() => EnteredBase(
player, allowPVP ? PVxType.PVP : PVxType.PVE, eventPos, radius));
private void OnPlayerExitAbandonedBase(
BasePlayer player, Vector3 location, bool allowPVP) =>
NextTick(() => ExitedBase(player));
private void OnAbandonedBaseEnded(
Vector3 eventPos, float radius, bool allowPVP,
List<BasePlayer> participants, List<ulong> participantIds,
List<BaseEntity> entities) =>
NextTick(() =>
{
foreach (var player in participants)
{
if (!IsValidPlayer(player, true)) continue;
if (Vector3.Distance(eventPos, player.transform.position) > radius)
{
continue;
}
ExitedBase(player, false);
}
});
private void OnPlayerPvpDelayStart(
BasePlayer player, ulong userid, Vector3 eventPos,
List<BasePlayer> intruders, List<BaseEntity> entities) =>
NextTick(() => SetPvpDelay(player, PvpDelayType.AbandonedBases, true));
private void OnPlayerPvpDelayExpiredII(
BasePlayer player, ulong userid, Vector3 eventPos,
List<BasePlayer> intruders, List<BaseEntity> entities) =>
NextTick(() => SetPvpDelay(player, PvpDelayType.AbandonedBases, false));
#endregion AbandonedBases Hook Handlers
#region DangerousTreasures Hook Handlers
private static void OnPlayerEnteredDangerousEvent(
BasePlayer player, Vector3 eventPos, bool allowPVP) =>
SetPvxEvent(player, allowPVP ? PVxType.PVP : PVxType.PVE, true);
private static void OnPlayerExitedDangerousEvent(
BasePlayer player, Vector3 eventPos, bool allowPVP) =>
SetPvxEvent(player, allowPVP ? PVxType.PVP : PVxType.PVE, false);
#endregion DangerousTreasures Hook Handlers
#region PVP Bubble Hook Handlers
#region Cargo Train Event Hook Handlers
private void OnPlayerEnterPVPBubble(
TrainEngine trainEngine, BasePlayer player) =>
NextTick(() => SetPvpBubble(
player, PvpBubbleTypes.CargoTrainEvent, true));
private void OnPlayerExitPVPBubble(
TrainEngine trainEngine, BasePlayer player) =>
NextTick(() => SetPvpBubble(
player, PvpBubbleTypes.CargoTrainEvent, false));
private void OnTrainEventEnded(TrainEngine trainEngine) =>
NextTick(() => EndPvpBubble(PvpBubbleTypes.CargoTrainEvent));
#endregion Cargo Train Event Hook Handlers
#region Adem Hook Handlers
private void OnPlayerEnterCaravan(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Caravan, true));
private void OnPlayerExitCaravan(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Caravan, false));
private void OnCaravanStop() =>
NextTick(() => EndPvpBubble(PvpBubbleTypes.Caravan));
private void OnPlayerEnterConvoy(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Convoy, true));
private void OnPlayerExitConvoy(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Convoy, false));
private void OnConvoyStop() =>
NextTick(() => EndPvpBubble(PvpBubbleTypes.Convoy));
private void OnPlayerEnterSputnik(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Sputnik, true));
private void OnPlayerExitSputnik(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Sputnik, false));
#endregion Adem Hook Handlers
#endregion PVP Bubble Hook Handlers
#region PlayerBasePvpZones Hook Handlers
private void OnPlayerBasePvpDelayStart(ulong playerId, string zoneId)
{
NextTick(() =>
{
var player = BasePlayer.FindByID(playerId);
SetPvpDelay(player, PvpDelayType.PlayerBasePvpZones, true);
});
}
private void OnPlayerBasePvpDelayStop(ulong playerId, string zoneId)
{
NextTick(() =>
{
var player = BasePlayer.FindByID(playerId);
SetPvpDelay(player, PvpDelayType.PlayerBasePvpZones, false);
});
}
#endregion PlayerBasePvpZones Hook Handlers
#region DynamicPVP Hook Handlers
private void OnPlayerAddedToPVPDelay(
ulong playerId, string zoneId, float pvpDelayTime)
{
NextTick(() =>
{
var player = BasePlayer.FindByID(playerId);
SetPvpDelay(player, PvpDelayType.DynamicPvp, true);
});
}
private void OnPlayerRemovedFromPVPDelay(ulong playerId, string zoneId)
{
NextTick(() =>
{
var player = BasePlayer.FindByID(playerId);
SetPvpDelay(player, PvpDelayType.DynamicPvp, false);
});
}
#endregion DynamicPVP Hook Handlers
#region KpucTaJl Hook Handlers
private void OnAirEventStart(
HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.AirEvent, pos, radius));
private void OnAirEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.AirEvent));
private void OnArcticBaseEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.ArcticBaseEvent, pos, radius));
private void OnArcticBaseEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.ArcticBaseEvent));
private void OnFerryTerminalEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.FerryTerminalEvent, pos, radius));
private void OnFerryTerminalEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.FerryTerminalEvent));
private void OnGasStationEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.GasStationEvent, pos, radius));
private void OnGasStationEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.GasStationEvent));
private void OnHarborEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.HarborEvent, pos, radius));
private void OnHarborEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.HarborEvent));
private void OnJunkyardEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.JunkyardEvent, pos, radius));
private void OnJunkyardEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.JunkyardEvent));
private void OnPowerPlantEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.PowerPlantEvent, pos, radius));
private void OnPowerPlantEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.PowerPlantEvent));
private void OnSatDishEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.SatelliteDishEvent, pos, radius));
private void OnSatDishEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.SatelliteDishEvent));
private void OnSupermarketEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.SupermarketEvent, pos, radius));
private void OnSupermarketEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.SupermarketEvent));
private void OnWaterEventStart(
HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.WaterEvent, pos, radius));
private void OnWaterEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.WaterEvent));
#endregion KpucTaJl Hook Handlers
#endregion PVP Plugin Integrations
#region Command Handlers
private void ToggleUI(IPlayer iPlayer, string command, string[] args)
{
if (iPlayer.Object is not BasePlayer player) return;
if (!IsValidPlayer(player, true)) return;
if (null == GetPlayerWatcher(player))
{
OnPlayerConnected(player);
}
else
{
OnPlayerDisconnected(player, UIName);
}
}
#endregion Command Handlers
#region UI Handling
private void CreateUI(
BasePlayer player, PVxType type, PVxType? oldType = null)
{
if (null == _configData || oldType == type) return;
// create CUI for new type if configured and enabled
if (_configData.UISettings.TryGetValue(type, out var cuiSettings) &&
cuiSettings.Enabled)
{
var cuiJson = cuiSettings.Json;
if (!string.IsNullOrEmpty(cuiJson))
{
CuiHelper.AddUi(player, cuiJson);
}
}
// Simple Status
if (null == SimpleStatus) return;
// clear old status (if there was one)
if (null != oldType)
{
SimpleStatus.Call(
"SetStatus", player.UserIDString, oldType.ToString(), 0);
}
// enable status for new type if configured and enabled
if (_configData.SimpleStatusPVxSettings.TryGetValue(type, out var ssSettings)
&& ssSettings.Enabled)
{
SimpleStatus.Call("SetStatus", player.UserIDString, type.ToString());
}
}
// forcefully destroy any active UIs for the given player
private void DestroyUI(BasePlayer player)
{
CuiHelper.DestroyUi(player, UIName);
SS_HideAllStatuses(player);
}
#region SimpleStatus Integration
// hide SimpleStatus statues for all enabled PVxType values
private void SS_HideAllStatuses(BasePlayer player)
{
if (null == SimpleStatus || null == _configData) return;
foreach (var (type, ssData) in _configData.SimpleStatusPVxSettings)
{
if (ssData is not { Enabled: true }) continue;
SimpleStatus.Call(
"SetStatus", player.UserIDString, type.ToString(), 0);
}
}
// register SimpleStatus statuses for each enabled PVxType value
// NOTE: apparently there is no corresponding "destroy" API
private void SS_CreateStatuses()
{
if (null == SimpleStatus || null == _configData) return;
foreach (var (type, ssData) in _configData.SimpleStatusPVxSettings)
{
if (ssData is not { Enabled: true }) continue;
SimpleStatus.Call("CreateStatus",
this, type.ToString(), ssData.ToDict());
}
}
#endregion SimpleStatus Integration
#endregion UI Handling
#region Config File Handling
private sealed class NotificationSettings
{
[JsonProperty(PropertyName = "Chat notify enabled")]
public bool ChatEnabled { get; set; }
[JsonProperty(PropertyName = "Chat notify prefix (empty string to disable)")]
public string ChatPrefix { get; set; } = "[SuperPVxInfo]: ";
[JsonProperty(PropertyName = "PopupNotifications notify enabled")]
public bool PopupNotificationsEnabled { get; set; } = true;
[JsonProperty(PropertyName = "PopupNotifications notify prefix (empty string to disable)")]
public string PopupNotificationsPrefix { get; set; } = "";
[JsonProperty(PropertyName = "Individual Notification Toggles")]
public Dictionary<string, bool> Enabled { get; set; } = new();
}
private sealed class UiSettings
{
[JsonProperty(PropertyName = "Enabled")]
public bool Enabled { get; set; } = true;
[JsonProperty(PropertyName = "Min Anchor")]
public string MinAnchor { get; set; } = "0.5 0";
[JsonProperty(PropertyName = "Max Anchor")]
public string MaxAnchor { get; set; } = "0.5 0";
[JsonProperty(PropertyName = "Min Offset")]
public string MinOffset { get; set; } = "190 30";
[JsonProperty(PropertyName = "Max Offset")]
public string MaxOffset { get; set; } = "250 60";
[JsonProperty(PropertyName = "Layer")]
public string Layer { get; set; } = "Hud";
[JsonProperty(PropertyName = "Text")]
public string Text { get; set; } = "PVP";
[JsonProperty(PropertyName = "Text Size")]
public int TextSize { get; set; } = 12;
[JsonProperty(PropertyName = "Text Color")]
public string TextColor { get; set; } = "1 1 1 1";
[JsonProperty(PropertyName = "Background Color")]
public string BackgroundColor { get; set; } = "0.8 0.5 0.1 0.8";
[JsonProperty(PropertyName = "Fade In")]
public float FadeIn { get; set; } = 0.25f;
[JsonProperty(PropertyName = "Fade Out")]
public float FadeOut { get; set; } = 0.25f;
private string _json = "";
[JsonIgnore]
public string Json
{
get
{
// generate JSON for a PVxType on first use, and cache it in _json
//...unless this PVxType is disabled, in which case return the
// default empty string
if (string.IsNullOrEmpty(_json))
{
_json = new CuiElementContainer
{
{
new CuiPanel
{
Image = { Color = BackgroundColor, FadeIn = FadeIn },
RectTransform = {
AnchorMin = MinAnchor, AnchorMax = MaxAnchor,
OffsetMin = MinOffset, OffsetMax = MaxOffset
},
CursorEnabled = false,
FadeOut = FadeOut,
},
Layer, UIName, UIName
},
{
new CuiLabel
{
Text = {
Text = Text,
FontSize = TextSize,
Align = TextAnchor.MiddleCenter,
Color = TextColor,
FadeIn = FadeIn,
},
RectTransform = {
AnchorMin = "0.05 0.05", AnchorMax = "0.95 0.95"
},
FadeOut = FadeOut,
},
UIName, CuiHelper.GetGuid()
}
}.ToJson();
}
return _json;
}
}
}
// Class for managing Simple Status settings for an individual PVxType enum
// value
// Supports user-friendly JSON configuration values and uses them to
// produce a dictionary to be passed to Simple Status hooks.private sealed class SimpleStatusSettings
{
[JsonProperty(PropertyName = "Enabled")]
public bool Enabled { get; set; }
[JsonProperty(PropertyName = "Background Color")]
public string Color { get; set; } = "0.5 0.5 0.5 1.0";
[JsonProperty(PropertyName = "Title Text")]
public string TitleText { get; set; } = "PVx STATUS";
[JsonProperty(PropertyName = "Title Color")]
public string TitleColor { get; set; } = "1.0 1.0 1.0 1.0";
[JsonProperty(PropertyName = "Status Text")]
public string StatusText { get; set; } = "UNKNOWN";
[JsonProperty(PropertyName = "Status Color")]
public string StatusColor { get; set; } = "1.0 1.0 1.0 1.0";
[JsonProperty(PropertyName = "Icon Path")]
public string IconPath { get; set; } = "assets/icons/resource.png";
[JsonProperty(PropertyName = "Icon Color")]
public string IconColor { get; set; } = "1.0 1.0 1.0 1.0";
// dictionary containing SimpleStatus values
[JsonIgnore]
private Dictionary<string, object> _dict;
// accessor for SimpleStatus values dictionary
// Populates and returns the dictionary on first call, and returns the
// cached dictionary on subsequent calls
public Dictionary<string, object> ToDict()
{
return _dict ??= new Dictionary<string, object>
{
["color"] = Color,
["title"] = TitleText,
["titleColor"] = TitleColor,
["text"] = StatusText,
["textColor"] = StatusColor,
["icon"] = IconPath,
["iconColor"] = IconColor
};
}
}
private sealed class ConfigData
{
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(PropertyName = "Server Default PVx (PVP or PVE)")]
public PVxType DefaultType = PVxType.PVE;
[JsonProperty(PropertyName = "Assume PVP Below Height")]
public float PvpBelowHeight = -500.0f;
[JsonProperty(PropertyName = "Assume PVP Above Height")]
public float PvpAboveHeight = 1000.0f;
[JsonProperty(PropertyName = "Toggle UI Command (empty string to disable)")]
public string ToggleCommand = "pvxui";
[JsonProperty(PropertyName = "Seconds Between Updates")]
public float UpdateIntervalSeconds = 1.0f;
[JsonProperty(PropertyName = "Force Updates On State Change")]
public bool ForceUpdates = true;
[JsonProperty(PropertyName = "Minimum Seconds Data File Saves")]
public float SaveIntervalSeconds = 5.0f;
[JsonProperty(PropertyName = "PVE Exclusion Mapping Names (case insensitive substrings / none to disable)")]
public HashSet<string> PveExclusionNames { get; set; } = new();
[JsonProperty(PropertyName = "PVE Zone Names (case insensitive substrings / none to disable)")]
public HashSet<string> PveZoneManagerNames { get; set; } = new();
[JsonProperty(PropertyName = "PVP Zone Names (case insensitive substrings / none to disable)")]
public HashSet<string> PvpZoneManagerNames { get; set; } = new();
[JsonProperty(PropertyName = "Notification Settings")]
public NotificationSettings NotifySettings { get; set; } = new();
[JsonProperty(PropertyName = "Default UI Settings")]
public Dictionary<PVxType, UiSettings> UISettings { get; set; } = new()
{
[PVxType.PVE] = new UiSettings
{
Enabled = true,
Text = "PVE",
TextSize = 14,
TextColor = "1.0 1.0 1.0 1.0",
BackgroundColor = "0.0 1.0 0.0 0.8"
},
[PVxType.PVP] = new UiSettings
{
Enabled = true,
Text = "PVP",
TextSize = 14,
TextColor = "1.0 1.0 1.0 1.0",
BackgroundColor = "1.0 0.0 0.0 0.8"
},
[PVxType.PVPDelay] = new UiSettings
{
Enabled = true,
Text = "WAIT",
TextSize = 14,
TextColor = "1.0 1.0 1.0 1.0",
BackgroundColor = "1.0 0.5 0.0 0.8"
},
[PVxType.SafeZone] = new UiSettings
{
Enabled = true,
Text = "SAFE",
TextSize = 14,
TextColor = "1.0 1.0 1.0 1.0",
BackgroundColor = "0.0 0.0 1.0 0.8"
}
};
[JsonProperty(PropertyName = "Simple Status UI Settings")]
public Dictionary<PVxType, SimpleStatusSettings>
SimpleStatusPVxSettings { get; set; } = new()
{
[PVxType.PVE] = new SimpleStatusSettings
{
Enabled = false,
Color = "0.0 0.7 0.0 0.8",
TitleText = "PVE",
TitleColor = "1.0 1.0 1.0 1.0",
StatusText = "SuperPVxInfo",
StatusColor = "0.0 1.0 0.0 0.2",
IconPath = "assets/icons/resource.png",
IconColor = "0.5 1.0 0.5 1.0"
},
[PVxType.PVP] = new SimpleStatusSettings
{
Enabled = false,
Color = "0.7 0.0 0.0 0.8",
TitleText = "PVP",
TitleColor = "1.0 1.0 1.0 1.0",
StatusText = "SuperPVxInfo",
StatusColor = "1.0 0.0 0.0 0.2",
IconPath = "assets/icons/warning_2.png",
IconColor = "1.0 0.5 0.5 1.0"
},
[PVxType.PVPDelay] = new SimpleStatusSettings
{
Enabled = false,
Color = "0.7 0.7 0.0 0.8",
TitleText = "WAIT",
TitleColor = "1.0 1.0 1.0 1.0",
StatusText = "SuperPVxInfo",
StatusColor = "1.0 1.0 0.0 0.2",
IconPath = "assets/icons/stopwatch.png",
IconColor = "1.0 1.0 0.5 1.0"
},
[PVxType.SafeZone] = new SimpleStatusSettings
{
Enabled = false,
Color = "0.0 0.0 0.7 0.8",
TitleText = "SAFE",
TitleColor = "1.0 1.0 1.0 1.0",
StatusText = "SuperPVxInfo",
StatusColor = "0.0 0.0 1.0 0.2",
IconPath = "assets/icons/peace.png",
IconColor = "0.5 0.5 1.0 1.0"
}
};
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_configData = Config.ReadObject<ConfigData>();
if (_configData == null)
{
LoadDefaultConfig();
}
// ... [Code preview truncated due to length. Full source in attached file] ...