C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using System.Globalization;
namespace Oxide.Plugins {
[Info("Better Chinook Patrol", "WhiteThunder", "0.2.1")]
[Description("Allows customizing which monuments chinooks will visit.")]
internal class BetterChinookPatrol : CovalencePlugin {
#region Fields
private const float VanillaDropZoneDistanceTolerance = 200;
private Configuration _config;
private List<Vector3> _eligiblePatrolPoints = new();
#endregion
#region Hooks
private void Init() {
_config.Init(this);
}
private void OnServerInitialized() {
PrintDropZones();
}
private void OnEntitySpawned(CH47HelicopterAIController ch47) {
// Ignore reinforcement chinooks.if (ch47.ShouldLand())
return;
var brain = ch47.GetComponent<CH47AIBrain>();
if (brain == null) return;
NextTick(() => {
// If the brain doesn't have a path finder, perhaps another plugin is controlling it.if (brain.PathFinder is not CH47PathFinder pathFinder)
return;
if (ChinookWasBlocked(ch47)) return;
brain.PathFinder = new BetterCH47PathFinder(_eligiblePatrolPoints);
// If the chinook is already in a patrol state, its interest point must be updated.if (brain.CurrentState is { StateType: AIState.Patrol })
{
brain.mainInterestPoint = brain.PathFinder.GetRandomPatrolPoint();
}
if (_config.MinCrateDropsPerChinook > 1 && _config.MaxCrateDropsPerChinook > 1) {
ch47.numCrates = UnityEngine.Random.Range(_config.MinCrateDropsPerChinook, _config.MaxCrateDropsPerChinook + 1);
}
}
);
}
private void OnMonumentPrefabCreated(GameObject gameObject, Component monument, Guid guid) {
if (gameObject.HasComponent<CH47DropZone>()) {
Log($"Detected new Monument Addons drop zone at {gameObject.transform.position}");
}
}
#endregion
#region Exposed Hooks
private bool ChinookWasBlocked(CH47HelicopterAIController ch47) {
return Interface.CallHook("OnBetterChinookPatrol", ch47) is false;
}
#endregion
#region Helpers
private static class StringUtils {
public static bool Equals(string a, string b) {
return string.Compare(a, b, StringComparison.OrdinalIgnoreCase) == 0;
}
public static bool Contains(string haystack, string needle) {
return haystack.Contains(needle, CompareOptions.IgnoreCase);
}
}
private void PrintDropZones() {
var sb = new StringBuilder();
var dropZoneCount = 0;
foreach (var monumentInfo in TerrainMeta.Path.Monuments) {
if (_config.DisallowSafeZoneMonuments && monumentInfo.IsSafeZone) continue;
if (!_config.AllowsMonument(monumentInfo, out var monumentName)) continue;
var monumentPosition = monumentInfo.transform.position;
_eligiblePatrolPoints.Add(monumentPosition);
var hasDropZone = false;
var closestDropZone = CH47DropZone.GetClosest(monumentPosition);
if (closestDropZone != null) {
hasDropZone = Vector3Ex.Distance2D(closestDropZone.transform.position, monumentPosition) < VanillaDropZoneDistanceTolerance;
}
if (hasDropZone) {
dropZoneCount++;
}
var dropZoneInfo = hasDropZone ? " -- HAS DROP ZONE" : string.Empty;
sb.AppendLine($"- {monumentName}{dropZoneInfo}");
}
Log($"{_eligiblePatrolPoints.Count} monuments on this map may be visited by Chinooks.{dropZoneCount} have drop zones.This list may not show drop zones managed by other plugins (such as Monument Addons), particularly if the drop zones have not spawned yet.\n{sb}");
}
#endregion
#region Pathfinder
private class BetterCH47PathFinder : CH47PathFinder {
private const float RevisitMaxProximity = 100;
public List<Vector3> _patrolPath;
private int _patrolPathIndex;
public BetterCH47PathFinder(List<Vector3> eligiblePatrolPoints) {
// Randomly shuffle the patrol points._patrolPath = eligiblePatrolPoints.OrderBy(x => UnityEngine.Random.Range(0, 1000)).ToList();
for (var i = _patrolPath.Count - 1; i >= 0; i--) {
for (var j = 0; j < i; j++) {
// Remove any patrol points that are close to previous patrol points.if (Vector3Ex.Distance2D(_patrolPath[i], _patrolPath[j]) < RevisitMaxProximity)
{
_patrolPath.RemoveAt(i);
break;
}
}
}
}
public override Vector3 GetRandomPatrolPoint() {
if (_patrolPath.Count == 0) return Vector3.zero;
if (_patrolPathIndex >= _patrolPath.Count) {
_patrolPathIndex = 0;
}
return _patrolPath[_patrolPathIndex++];
}
}
#endregion
#region Configuration
[JsonObject(MemberSerialization.OptIn)]
private class Configuration : SerializableConfiguration {
[JsonIgnore]
public List<MonumentType> DisallowedMonumentTypes = new();
[JsonIgnore]
public MonumentTier DisallowedMonumentTiersMask;
[JsonProperty("Min crate drops per chinook")]
public int MinCrateDropsPerChinook = 1;
[JsonProperty("Max crate drops per chinook")]
public int MaxCrateDropsPerChinook = 1;
[JsonProperty("Disallow safe zone monuments")]
public bool DisallowSafeZoneMonuments = true;
[JsonProperty("Disallowed monument types")]
private string[] DisallowedMonumentTypesNames = {
"Cave", "WaterWell",
}
;
[JsonProperty("Disallowed monument tiers")]
private string[] DisallowedMonumentTierNames = {
"Tier0"
}
;
[JsonProperty("Disallowed monument prefabs (partial match)")]
private string[] DisallowedMonumentPartialPrefabs = Array.Empty<string>();
[JsonProperty("Disallowed monument prefabs (exact match)")]
private string[] DisallowedMonumentExactPrefabs = Array.Empty<string>();
[JsonProperty("Force allow monument prefabs (partial match)")]
private string[] ForceAllowedMonumentPartialPrefabs = Array.Empty<string>();
[JsonProperty("Force allow monument prefabs (exact match)")]
private string[] ForceAllowedMonumentExactPrefabs = Array.Empty<string>();
public void Init(BetterChinookPatrol pluginInstance) {
if (DisallowedMonumentTypesNames != null) {
foreach (var monumentTypeName in DisallowedMonumentTypesNames) {
if (Enum.TryParse(monumentTypeName, ignoreCase : true, result : out MonumentType monumentType)) {
DisallowedMonumentTypes.Add(monumentType);
}
else {
pluginInstance.LogError($"Invalid monument type: {monumentTypeName}");
}
}
}
if (DisallowedMonumentTierNames != null) {
foreach (var monumentTierName in DisallowedMonumentTierNames) {
if (Enum.TryParse(monumentTierName, ignoreCase : true, result : out MonumentTier monumentTier)) {
DisallowedMonumentTiersMask |= monumentTier;
}
else {
pluginInstance.LogError($"Invalid monument tier: {monumentTierName}");
}
}
}
}
public bool AllowsMonument(MonumentInfo monumentInfo, out string monumentName) {
monumentName = monumentInfo.name;
if (monumentName.Contains("monument_marker.prefab")) {
monumentName = monumentInfo.transform.root.name;
}
if (ForceAllowedMonumentPartialPrefabs != null) {
foreach (var partialPrefab in ForceAllowedMonumentPartialPrefabs) {
if (!string.IsNullOrWhiteSpace(partialPrefab) && StringUtils.Contains(monumentName, partialPrefab)) return true;
}
}
if (ForceAllowedMonumentExactPrefabs != null) {
foreach (var exactPrefab in ForceAllowedMonumentExactPrefabs) {
if (!string.IsNullOrWhiteSpace(exactPrefab) && StringUtils.Equals(monumentName, exactPrefab)) return true;
}
}
if (DisallowedMonumentPartialPrefabs != null) {
foreach (var partialPrefab in DisallowedMonumentPartialPrefabs) {
if (!string.IsNullOrWhiteSpace(partialPrefab) && StringUtils.Contains(monumentName, partialPrefab)) return false;
}
}
if (DisallowedMonumentExactPrefabs != null) {
foreach (var partialPrefab in DisallowedMonumentExactPrefabs) {
if (!string.IsNullOrWhiteSpace(partialPrefab) && StringUtils.Equals(monumentName, partialPrefab)) return false;
}
}
if ((DisallowedMonumentTiersMask & monumentInfo.Tier) != 0) return false;
if (DisallowedMonumentTypes.Contains(monumentInfo.Type)) return false;
return true;
}
}
private Configuration GetDefaultConfig() => new();
#region Configuration Helpers
private class SerializableConfiguration {
public string ToJson() => JsonConvert.SerializeObject(this);
public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
}
private static class JsonHelper {
public static object Deserialize(string json) => ToObject(JToken.Parse(json));
private static object ToObject(JToken token) {
switch (token.Type) {
case JTokenType.Object : return token.Children<JProperty>().ToDictionary(prop => prop.Name, prop => ToObject(prop.Value));
case JTokenType.Array : return token.Select(ToObject).ToList();
default : return ((JValue)token).Value;
}
}
}
private bool MaybeUpdateConfig(SerializableConfiguration config) {
var currentWithDefaults = config.ToDictionary();
var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
}
private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw) {
bool changed = false;
foreach (var key in currentWithDefaults.Keys) {
if (currentRaw.TryGetValue(key, out var currentRawValue)) {
var currentDictValue = currentRawValue as Dictionary<string, object>;
if (currentWithDefaults[key] is Dictionary<string, object> defaultDictValue) {
if (currentDictValue == null) {
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue)) {
changed = true;
}
}
}
else {
currentRaw[key] = currentWithDefaults[key];
changed = true;
}
}
return changed;
}
protected override void LoadDefaultConfig() => _config = GetDefaultConfig();
protected override void LoadConfig() {
base.LoadConfig();
try {
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new JsonException();
if (MaybeUpdateConfig(_config)) {
LogWarning("Configuration appears to be outdated; updating and saving");
SaveConfig();
}
}
catch (Exception e) {
LogError(e.Message);
LogWarning($"Configuration file {Name}.json is invalid; using defaults");
LoadDefaultConfig();
}
}
protected override void SaveConfig() {
Log($"Configuration changes saved to {Name}.json");
Config.WriteObject(_config, true);
}
#endregion
#endregion
}
}