C#:
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 UnityEngine;
namespace Oxide.Plugins;
[Info("Super PVx Info", "HunterZ", "1.14.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
private 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]
private enum PvpBubbleTypes
{
None = 0,
ArmoredTrain = 1 << 0, // Adem
Caravan = 1 << 1, // Adem
CargoTrainEvent = 1 << 2, // Nikedemos - PVP only
Convoy = 1 << 3, // Adem
DefendableHomes = 1 << 4, // KpucTaJl
Shipwreck = 1 << 5, // Adem
Space = 1 << 6, // Adem
Sputnik = 1 << 7, // Adem
Triangulation = 1 << 8 // KpucTaJl
}
private readonly Dictionary<PvpBubbleTypes, List<string>>
_pvpBubbleEventHooks = new()
{
{ PvpBubbleTypes.ArmoredTrain, new List<string>
{ nameof(OnPlayerEnterArmoredTrain), nameof(OnPlayerExitArmoredTrain),
nameof(OnArmoredTrainStartMoving), nameof(OnArmoredTrainEventStop)
}
},
{ PvpBubbleTypes.Caravan, new List<string>
{ nameof(OnPlayerEnterCaravan), nameof(OnPlayerExitCaravan),
nameof(OnCaravanStop)
}
},
{ PvpBubbleTypes.Convoy, new List<string>
{ nameof(OnPlayerEnterConvoy), nameof(OnPlayerExitConvoy),
nameof(OnConvoyStop)
}
},
{ PvpBubbleTypes.DefendableHomes, new List<string>
{ nameof(OnPlayerEnterDefendableHomes),
nameof(OnPlayerExitDefendableHomes)
}
},
{ PvpBubbleTypes.Shipwreck, new List<string>
{ nameof(OnPlayerEnterShipwreck), nameof(OnPlayerExitShipwreck),
nameof(OnShipwreckStop)
}
},
{ PvpBubbleTypes.Space, new List<string>
{ nameof(OnPlayerEnterSpace), nameof(OnPlayerExitSpace),
nameof(OnSpaceEventStop)
}
},
{ PvpBubbleTypes.Sputnik, new List<string>
{ nameof(OnPlayerEnterSputnik), nameof(OnPlayerExitSputnik)
}
},
{ PvpBubbleTypes.Triangulation, new List<string>
{ nameof(OnPlayerEnterTriangulation), nameof(OnPlayerExitTriangulation),
nameof(OnTriangulationEnd)
}
}
};
// PVP events that are managed via listening to start/stop hooks that provide
// a PVP area definition
private enum PvpLocationEventType
{
// KpucTaJl plugins
Air,
ArcticBase,
FerryTerminal,
GasStation,
Harbor,
Junkyard,
PowerPlant,
SatelliteDish,
Supermarket,
Water
}
private readonly Dictionary<PvpLocationEventType, List<string>>
_pvpLocationEventHooks = new()
{ { PvpLocationEventType.Air, new List<string>
{ nameof(OnAirEventStart), nameof(OnAirEventEnd)
}
},
{ PvpLocationEventType.ArcticBase, new List<string>
{ nameof(OnArcticBaseEventStart), nameof(OnArcticBaseEventEnd)
}
},
{ PvpLocationEventType.FerryTerminal, new List<string>
{ nameof(OnFerryTerminalEventStart), nameof(OnFerryTerminalEventEnd)
}
},
{ PvpLocationEventType.GasStation, new List<string>
{ nameof(OnGasStationEventStart), nameof(OnGasStationEventEnd)
}
},
{ PvpLocationEventType.Harbor, new List<string>
{ nameof(OnHarborEventStart), nameof(OnHarborEventEnd)
}
},
{ PvpLocationEventType.Junkyard, new List<string>
{ nameof(OnJunkyardEventStart), nameof(OnJunkyardEventEnd)
}
},
{ PvpLocationEventType.PowerPlant, new List<string>
{ nameof(OnPowerPlantEventStart), nameof(OnPowerPlantEventEnd)
}
},
{ PvpLocationEventType.SatelliteDish, new List<string>
{ nameof(OnSatDishEventStart), nameof(OnSatDishEventEnd)
}
},
{ PvpLocationEventType.Supermarket, new List<string>
{ nameof(OnSupermarketEventStart), nameof(OnSupermarketEventEnd)
}
},
{ PvpLocationEventType.Water, new List<string>
{ nameof(OnWaterEventStart), nameof(OnWaterEventEnd)
}
}
};
[PluginReference] 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",
["PVP Apartment Entry"] =
"{0}WARNING: Entering Apartment PVP Zone",
["PVP Apartment Exit"] =
"{0}Leaving Apartment PVP Zone",
["PVP Deep Sea Entry"] =
"{0}WARNING: Entering Deep Sea PVP Zone",
["PVP Deep Sea Exit"] =
"{0}Leaving Deep Sea PVP Zone"
};
private Timer _saveDataTimer;
private StoredData _storedData;
private const string UIName = "SuperPVxInfoUI";
#endregion Plugin Data
#region Utility Methods
private bool IsEventEnabled(PvpBubbleTypes type) => type switch
{
PvpBubbleTypes.None
=> true, // return true to avoid a _pvpBubbleEventHooks dictionary lookup
PvpBubbleTypes.ArmoredTrain
=> _configData?.PvpPlugins?.ArmoredTrain is true,
PvpBubbleTypes.Caravan
=> _configData?.PvpPlugins?.Caravan is true,
PvpBubbleTypes.CargoTrainEvent
=> true, // always enabled because it provides PVP specific hooks
PvpBubbleTypes.Convoy
=> _configData?.PvpPlugins?.Convoy is true,
PvpBubbleTypes.DefendableHomes
=> _configData?.PvpPlugins?.DefendableHomes is true,
PvpBubbleTypes.Shipwreck
=> _configData?.PvpPlugins?.Shipwreck is true,
PvpBubbleTypes.Space
=> _configData?.PvpPlugins?.Space is true,
PvpBubbleTypes.Sputnik
=> _configData?.PvpPlugins?.Sputnik is true,
PvpBubbleTypes.Triangulation
=> _configData?.PvpPlugins?.Triangulation is true
};
private bool IsEventEnabled(PvpLocationEventType type) => type switch
{
PvpLocationEventType.Air
=> _configData?.PvpPlugins?.AirEvent is true,
PvpLocationEventType.ArcticBase
=> _configData?.PvpPlugins?.ArcticBaseEvent is true,
PvpLocationEventType.FerryTerminal
=> _configData?.PvpPlugins?.FerryTerminalEvent is true,
PvpLocationEventType.GasStation
=> _configData?.PvpPlugins?.GasStationEvent is true,
PvpLocationEventType.Harbor
=> _configData?.PvpPlugins?.HarborEvent is true,
PvpLocationEventType.Junkyard
=> _configData?.PvpPlugins?.JunkyardEvent is true,
PvpLocationEventType.PowerPlant
=> _configData?.PvpPlugins?.PowerPlantEvent is true,
PvpLocationEventType.SatelliteDish
=> _configData?.PvpPlugins?.SatelliteDishEvent is true,
PvpLocationEventType.Supermarket
=> _configData?.PvpPlugins?.SupermarketEvent is true,
PvpLocationEventType.Water
=> _configData?.PvpPlugins?.WaterEvent is true
};
private bool IsEventEnabled<T>(T type) => type switch
{
PvpBubbleTypes pvpBubbleTypes
=> IsEventEnabled(pvpBubbleTypes),
PvpLocationEventType pvpLocationEventType
=> IsEventEnabled(pvpLocationEventType),
_
=> true
};
private void InitEventHooks<T>(Dictionary<T, List<string>> hookDictionary)
where T : Enum
{
// var nameOfT = typeof(T).Name;
foreach (
T eventType in Enum.GetValues(typeof(T)))
{
if (IsEventEnabled(eventType))
{
// Puts($"{nameOfT} event {eventType} support enabled");
continue;
}
// Puts($"Disabling {nameOfT} event {eventType} support:");
if (!hookDictionary.TryGetValue(eventType, out var eventHooks))
{
// PrintWarning($" No hooks defined for {nameOfT} event {eventType}");
continue;
}
foreach (var eventHook in eventHooks)
{
Unsubscribe(eventHook);
// Puts($" Unsubscribed hook {eventHook} for {nameOfT} event {eventType}");
}
}
}
private int ScrubEventData()
{
var removeCount = 0;
foreach (PvpLocationEventType eventType
in Enum.GetValues(typeof(PvpLocationEventType)))
{
if (IsEventEnabled(eventType)) continue;
if (!_storedData.PvpEvents.Remove(eventType)) continue;
++removeCount;
}
return removeCount;
}
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))
{
DestroyTimer(removedTimer);
}
// 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);
}
}
private static void DestroyTimer(Timer t)
{
if (TimerValid(t)) t.Destroy();
}
private static bool TimerValid(Timer t) => t is { Destroyed: false };
#endregion Utility Methods
#region Oxide Methods
protected override void LoadDefaultMessages() =>
lang.RegisterMessages(_notifyMessages, this);
private void Init()
{
LoadData();
PlayerWatcher.AllowForceUpdate = _configData?.ForceUpdates is not false;
PlayerWatcher.Instance = this;
var deepSeaBounds = DeepSeaManager.DeepSeaBounds;
PlayerWatcher.DeepSeaMin = deepSeaBounds.center - deepSeaBounds.extents;
PlayerWatcher.DeepSeaMax = deepSeaBounds.center + deepSeaBounds.extents;
PlayerWatcher.UpdateIntervalSeconds =
_configData?.UpdateIntervalSeconds ?? 1.0f;
if (!string.IsNullOrEmpty(_configData?.ToggleCommand))
{
AddCovalenceCommand(_configData?.ToggleCommand, nameof(ToggleUI));
}
// only listen to hooks from event plugins that are configured as PVP
InitEventHooks(_pvpBubbleEventHooks);
InitEventHooks(_pvpLocationEventHooks);
}
private void OnServerInitialized(bool isStartup)
{
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, 10))
{
PrintWarning("ZoneManager is outdated or not running; this plugin may not work properly");
}
var toDict = Pool.Get<Dictionary<string, object>>();
if (true != _configData?.SyncAssumptions)
{
Puts("TruePVE config query is disabled - skipping");
}
else if (true != TruePVE?.IsLoaded)
{
Puts("TruePVE not loaded - skipping config query");
}
else if (TruePVE?.Call("GetOptionsNoAlloc", toDict) is not true)
{
Puts("TruePVE config query failed");
}
else
{
Puts("Got TruePVE config:");
_configData.PvpAboveHeight = (float)toDict["world_above"];
Puts($"- PVP above height: {_configData.PvpAboveHeight}");
_configData.PvpBelowHeight = (float)toDict["world_under"];
Puts($"- PVP below height: {_configData.PvpBelowHeight}");
_configData.PvpApartments = (bool)toDict["apartment_pvp"];
Puts($"- PVP apartments: {_configData.PvpApartments}");
_configData.PvpDeepSea = (bool)toDict["deepsea_pvp"];
Puts($"- PVP deep sea: {_configData.PvpDeepSea}");
}
Pool.FreeUnmanaged(ref toDict);
PlayerWatcher.PvpAboveHeight = _configData?.PvpAboveHeight ?? 1000.0f;
PlayerWatcher.PvpBelowHeight = _configData?.PvpBelowHeight ?? -500.0f;
PlayerWatcher.PvpApartments = _configData?.PvpApartments ?? false;
PlayerWatcher.PvpDeepSea = _configData?.PvpDeepSea ?? false;
var saveData = false;
if (null != _storedData)
{
// sync with TruePVE mappings in case of reload
TP_GetMappingsToStoredData();
Puts($"OnServerInitialized(): mappings count after TruePVE sync: {_storedData.Mappings.Count}");
// purge any mappings that Zone Manager doesn't recognize
var activeZoneIds = Pool.Get<List<string>>();
ZM_GetZoneIDsNoAlloc(activeZoneIds);
Puts($"OnServerInitialized(): ZoneManager active zone count: {activeZoneIds.Count}");
var deadZoneIds = Pool.Get<List<string>>();
foreach (var zoneId in _storedData.Mappings.Keys)
{
if ("default" == zoneId || activeZoneIds.Contains(zoneId)) continue;
deadZoneIds.Add(zoneId);
}
Pool.FreeUnmanaged(ref activeZoneIds);
foreach (var deadZoneId in deadZoneIds)
{
_storedData.Mappings.Remove(deadZoneId);
}
if (deadZoneIds.Count > 0)
{
PrintWarning($"OnServerInitialized(): Purged {deadZoneIds.Count} unknown/obsolete zoneId(s) from database");
saveData = true;
}
Pool.FreeUnmanaged(ref deadZoneIds);
// migrate or clear old PVP events
var oldEventsCount = _storedData.PendingPvpEvents.Count;
if (oldEventsCount > 0)
{
if (isStartup)
{
// don't use events from a previous server run
Puts($"OnServerInitialized(): Purging {oldEventsCount} obsolete saved PVP event record(s)");
saveData = true;
}
else
{
// migrate events that haven't already been re-added by another
// plugin's OnServerInitialized()
var migratedCount = 0;
foreach (var (eventType, eventData) in _storedData.PendingPvpEvents)
{
if (!IsEventEnabled(eventType)) continue;
if (!_storedData.PvpEvents.TryAdd(eventType, eventData)) continue;
++migratedCount;
}
Puts($"OnServerInitialized(): Migrated {migratedCount}/{oldEventsCount} saved PVP event record(s)");
// schedule a save if we didn't just retain everything
saveData |= migratedCount != _storedData.PendingPvpEvents.Count ||
migratedCount != _storedData.PvpEvents.Count;
}
// purge pending list regardless, as we're done with it
_storedData.PendingPvpEvents.Clear();
}
// be paranoid and scrub any disabled event data, in case it somehow got
// added before we disabled hooks
var removeCount = ScrubEventData();
if (removeCount > 0)
{
Puts($"OnServerInitialized(): Purged {removeCount} saved PVP event record(s) for disabled events");
saveData = true;
}
}
if (saveData)
{
SaveData();
}
// 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.Values)
{
foreach (var excludeTimer in excludeTimers.Values)
{
DestroyTimer(excludeTimer);
}
excludeTimers.Clear();
}
_excludedPlayers.Clear();
// destroy GUIs for all active players
foreach (var player in BasePlayer.activePlayerList)
{
OnPlayerDisconnected(player);
}
PlayerWatcher.Instance = null;
// if save timer active, force immediate write
if (TimerValid(_saveDataTimer))
{
WriteData();
}
_saveDataTimer = null;
}
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>();
HashSet<PvpDelayType> pvpDelays = new();
GetPlayerPvpDelays(pvpDelays, player.userID.Get());
watcher.Init(
IsPlayerInBase(player), pvpDelays, GetPlayerZoneType(player), player);
watcher.StartWatching();
}
private void OnPlayerDisconnected(BasePlayer player)
{
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 Integration
#region TruePVE Utilities
// use Interface.CallHook() instead of explicitly calling TruePVE, in case
// other PVE plugins implement support for this
private void TP_GetMappingsToStoredData() =>
Interface.CallHook("GetMappingsDictionaryNoAlloc", _storedData.Mappings);
#endregion TruePVE Utilities
#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;
}
_storedData.Mappings[zoneId] = ruleset;
SaveData();
}
// called when a plugin deletes a mapping
private void RemoveMapping(string zoneId)
{
if (null == _storedData ||
string.IsNullOrEmpty(zoneId) ||
!_storedData.Mappings.Remove(zoneId))
{
return;
}
SaveData();
}
// called when a plugin does a mappings bulk remove
private void RemoveMappings(List<string> keys, List<string> _ = null)
{
if (null == _storedData) return;
var dataChanged = false;
foreach (var key in keys)
{
if (!string.IsNullOrEmpty(key) && _storedData.Mappings.Remove(key))
{
dataChanged = true;
}
}
if (dataChanged)
{
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) is true)
{
if (TimerValid(excludeTimer))
{
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
#endregion TruePVE Integration
#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))
{
foreach (var pveZoneName in _configData.PveZoneManagerNames)
{
if (zoneName.Contains(pveZoneName, CompareOptions.IgnoreCase))
{
return PVxType.PVE;
}
}
foreach (var pvpZoneName in _configData.PvpZoneManagerNames)
{
if (zoneName.Contains(pvpZoneName, 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)
{
if (null == _storedData || null == _configData ||
!_storedData.Mappings.TryGetValue(zoneId, out var ruleset))
{
return false;
}
foreach (var name in _configData.PveExclusionNames)
{
if (ruleset.Contains(name, CompareOptions.IgnoreCase)) return true;
}
return false;
}
// 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)
{
// Puts($"PVP location event create/update: type={type}, location={location}, radius={radius}");
if (null == _storedData) return;
_storedData.PvpEvents[type] = new PvpEventData(location, radius);
SaveData();
}
// delete an event record with the given data, if any
private void DeleteLocationPvpEvent(PvpLocationEventType type)
{
// Puts($"PVP location event delete: type={type}");
if (null == _storedData || !_storedData.PvpEvents.Remove(type)) return;
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 not List<(
Vector3, string, int, bool allowPvp, string, float, float, float,
ulong, BasePlayer, List<BasePlayer>, List<BasePlayer> intruders,
HashSet<BaseEntity>, string, DateTime, DateTime, float, int)>
rbEvents)
{
PrintError("IsPlayerInBase failure - Raidable Bases GetAllEvents API mismatch");
return null;
}
// 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.Values)
{
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 void GetPlayerPvpDelays(
HashSet<PvpDelayType> pvpDelays, ulong playerID)
{
pvpDelays.Clear();
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);
}
}
// 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)
{
// PlayerWatcher.Instance?.Puts($"PVP bubble event update: player={player}, type={type}, state={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 (!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 (!watcher) return;
if (state)
{
watcher.SetInPVxEventType(eventType);
}
else
{
watcher.SetCheckZone(true);
watcher.SetInPVxEventType(null);
}
watcher.Force();
}
#endregion PVP Plugin Utilities
#region Nivex Hook Handlers
#region RaidableBases Hook Handlers
// NOTE: Only need to include up to the last parameter that we actually use;
// Oxide/Carbon will call the closest-matching hook signature
// temporary shim for people too lazy to upgrade :p
private void OnPlayerEnteredRaidableBase(
BasePlayer player, Vector3 location, bool allowPVP, int mode, string id,
float _, float __, double loadTime, ulong ownerId, string baseName,
DateTime spawnTime, DateTime despawnTime, float radius) =>
OnPlayerEnteredRaidableBase(
player, location, allowPVP, mode, id, _, __, (float)loadTime, ownerId,
baseName, spawnTime, despawnTime, radius);
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) =>
NextTick(() => EnteredBase(
player, allowPVP ? PVxType.PVP : PVxType.PVE, location, radius));
private void OnPlayerExitedRaidableBase(BasePlayer player) =>
NextTick(() => ExitedBase(player));
// temporary shim for people too lazy to upgrade :p
private void OnRaidableBaseEnded(
Vector3 location, int mode, bool allowPvP, string id, float _,
float __, double loadTime, ulong ownerId, BasePlayer owner,
List<BasePlayer> raiders, List<BasePlayer> intruders,
List<BaseEntity> entities, string baseName, DateTime spawnDateTime,
DateTime despawnDateTime, float protectionRadius) =>
OnRaidableBaseEnded(
location, mode, allowPvP, id, _, __, (float)loadTime, ownerId, owner,
raiders, intruders, entities, baseName, spawnDateTime, despawnDateTime,
protectionRadius);
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) =>
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);
}
});
#endregion RaidableBases Hook Handlers
#region AbandonedBases Hook Handlers
private void OnPlayerEnteredAbandonedBase(
BasePlayer player, Vector3 eventPos, float radius, bool allowPVP) =>
NextTick(() => EnteredBase(
player, allowPVP ? PVxType.PVP : PVxType.PVE, eventPos, radius));
private void OnPlayerExitAbandonedBase(BasePlayer player) =>
NextTick(() => ExitedBase(player));
private void OnAbandonedBaseEnded(
Vector3 eventPos, float radius, bool allowPVP,
List<BasePlayer> participants) =>
NextTick(() =>
{
foreach (var player in participants)
{
if (!IsValidPlayer(player, true)) continue;
if (Vector3.Distance(eventPos, player.transform.position) > radius)
{
continue;
}
ExitedBase(player, false);
}
});
#endregion AbandonedBases Hook Handlers
// NOTE: The same hook handlers can now be used for both Abandoned Bases and
// Raidable Bases
private void OnPlayerPvpDelayStart(
BasePlayer player, ulong userId, string pluginName) => NextTick(() =>
{
if (!Enum.TryParse(pluginName, out PvpDelayType type)) return;
SetPvpDelay(player, type, true);
});
private void OnPlayerPvpDelayReset(
BasePlayer player, ulong userId, string pluginName) =>
OnPlayerPvpDelayStart(player, userId, pluginName);
private void OnPlayerPvpDelayExpired(
BasePlayer player, ulong userId, string pluginName) => NextTick(() =>
{
if (!Enum.TryParse(pluginName, out PvpDelayType type)) return;
SetPvpDelay(player, type, false);
});
#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
#endregion Nivex Hook Handlers
#region PVP Bubble Hook Handlers
#region Adem Hook Handlers
private void OnPlayerEnterArmoredTrain(BasePlayer player) =>
NextTick(() => SetPvpBubble(
player, PvpBubbleTypes.ArmoredTrain, true));
private void OnPlayerExitArmoredTrain(BasePlayer player) =>
NextTick(() => SetPvpBubble(
player, PvpBubbleTypes.ArmoredTrain, false));
private void OnArmoredTrainStartMoving(Vector3 position) =>
NextTick(static () => EndPvpBubble(PvpBubbleTypes.ArmoredTrain));
private void OnArmoredTrainEventStop() =>
NextTick(static () => EndPvpBubble(PvpBubbleTypes.ArmoredTrain));
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(static () => 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(static () => EndPvpBubble(PvpBubbleTypes.Convoy));
private void OnPlayerEnterShipwreck(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Shipwreck, true));
private void OnPlayerExitShipwreck(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Shipwreck, false));
private void OnShipwreckStop() =>
NextTick(static () => EndPvpBubble(PvpBubbleTypes.Shipwreck));
private void OnPlayerEnterSpace(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Space, true));
private void OnPlayerExitSpace(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Space, false));
private void OnSpaceEventStop() =>
NextTick(static () => EndPvpBubble(PvpBubbleTypes.Space));
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
#region KpucTaJl Hook Handlers
private void OnPlayerEnterDefendableHomes(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.DefendableHomes, true));
private void OnPlayerExitDefendableHomes(BasePlayer player) =>
NextTick(() => SetPvpBubble(
player, PvpBubbleTypes.DefendableHomes, false));
private void OnPlayerEnterTriangulation(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Triangulation, true));
private void OnPlayerExitTriangulation(BasePlayer player) =>
NextTick(() => SetPvpBubble(player, PvpBubbleTypes.Triangulation, false));
private void OnTriangulationEnd() =>
NextTick(static () => EndPvpBubble(PvpBubbleTypes.Triangulation));
#endregion KpucTaJl Hook Handlers
#region Nikedemos 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(static () => EndPvpBubble(PvpBubbleTypes.CargoTrainEvent));
#endregion Nikedemos 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 PVP Location Hook Handlers
#region KpucTaJl Hook Handlers
private void OnAirEventStart(
HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.Air, pos, radius));
private void OnAirEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.Air));
private void OnArcticBaseEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.ArcticBase, pos, radius));
private void OnArcticBaseEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.ArcticBase));
private void OnFerryTerminalEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.FerryTerminal, pos, radius));
private void OnFerryTerminalEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.FerryTerminal));
private void OnGasStationEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.GasStation, pos, radius));
private void OnGasStationEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.GasStation));
private void OnHarborEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.Harbor, pos, radius));
private void OnHarborEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.Harbor));
private void OnJunkyardEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.Junkyard, pos, radius));
private void OnJunkyardEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.Junkyard));
private void OnPowerPlantEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.PowerPlant, pos, radius));
private void OnPowerPlantEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.PowerPlant));
private void OnSatDishEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.SatelliteDish, pos, radius));
private void OnSatDishEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.SatelliteDish));
private void OnSupermarketEventStart(Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.Supermarket, pos, radius));
private void OnSupermarketEventEnd() =>
NextTick(() => DeleteLocationPvpEvent(
PvpLocationEventType.Supermarket));
private void OnWaterEventStart(
HashSet<BaseEntity> entities, Vector3 pos, float radius) =>
NextTick(() => CreateOrUpdatePvpLocationEvent(
PvpLocationEventType.Water, pos, radius));
private void OnWaterEventEnd() =>
NextTick(() => DeleteLocationP
// ... [Code preview truncated due to length. Full source in attached file] ...