C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using Network;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Limited Drone Range", "WhiteThunder", "1.1.1")]
[Description("Limits how far RC drones can be controlled from computer stations.")]
internal class LimitedDroneRange : CovalencePlugin
{
#region Fields
private readonly object False = false;
private const float VanillaStaticDistanceFraction = 0.73f;
private const int ForcedMaxRange = 100000;
private const string DroneMaxControlRangeConVar = "drone.maxcontrolrange";
private readonly string ForcedMaxRangeString = ForcedMaxRange.ToString();
private Configuration _config;
private UIManager _uiManager = new UIManager();
#endregion
#region Hooks
private void Init()
{
_config.Init(this);
}
private void OnServerInitialized()
{
if (Drone.maxControlRange < ForcedMaxRange)
{
Drone.maxControlRange = ForcedMaxRange;
LogWarning($"Updated {DroneMaxControlRangeConVar} ConVar to {ForcedMaxRange} so that the plugin can control drone max range.");
}
foreach (var player in BasePlayer.activePlayerList)
{
var station = player.GetMounted() as ComputerStation;
if (station == null)
continue;
var drone = station.currentlyControllingEnt.Get(serverside: true) as Drone;
if (drone == null)
continue;
OnBookmarkControlStarted(station, player, string.Empty, drone);
}
}
private void Unload()
{
RangeLimiter.DestroyAll();
}
private object OnBookmarkControl(ComputerStation station, BasePlayer player, string bookmarkName, Drone drone)
{
int maxRange;
if (!ShouldLimitRange(drone, station, player, out maxRange)
|| IsWithinRange(drone, station, maxRange))
return null;
_uiManager.CreateOutOfRangeUI(this, player);
return False;
}
private void OnBookmarkControlStarted(ComputerStation station, BasePlayer player, string bookmarkName, Drone drone)
{
int maxRange;
if (!ShouldLimitRange(drone, station, player, out maxRange))
{
SendMaxRangeConVar(player, ForcedMaxRangeString);
return;
}
RangeLimiter.AddToPlayer(this, player, station, drone, maxRange);
}
private void OnBookmarkControlEnded(ComputerStation station, BasePlayer player, Drone drone)
{
RangeLimiter.RemoveFromPlayer(player);
}
#endregion
#region Helper Methods
private static bool LimitRangeWasBlocked(Drone drone, ComputerStation station, BasePlayer player)
{
var hookResult = Interface.CallHook("OnDroneRangeLimit", drone, station, player);
return hookResult is bool && (bool)hookResult == false;
}
private static bool IsWithinRange(Drone drone, ComputerStation station, float range)
{
return (station.transform.position - drone.transform.position).sqrMagnitude < range * range;
}
private static void SendReplicatedVar(Connection connection, string fullName, string value)
{
var netWrite = Net.sv.StartWrite();
netWrite.PacketID(Message.Type.ConsoleReplicatedVars);
netWrite.Int32(1);
netWrite.String(fullName);
netWrite.String(value);
netWrite.Send(new SendInfo(connection));
}
private static void SendMaxRangeConVar(BasePlayer player, string value)
{
SendReplicatedVar(player.Connection, DroneMaxControlRangeConVar, value);
}
private bool ShouldLimitRange(Drone drone, ComputerStation station, BasePlayer player, out int maxRange)
{
maxRange = _config.GetMaxRangeForPlayer(this, player.UserIDString);
if (maxRange <= 0)
return false;
return !LimitRangeWasBlocked(drone, station, player);
}
#endregion
private class RangeLimiter : FacepunchBehaviour
{
public static RangeLimiter AddToPlayer(LimitedDroneRange plugin, BasePlayer player, ComputerStation station, Drone drone, int maxDistance)
{
var component = player.gameObject.AddComponent<RangeLimiter>();
component._plugin = plugin;
component._player = player;
component._station = station;
component._stationTransform = station.transform;
component._droneTransform = drone.transform;
component._maxDistance = maxDistance;
var secondsBetweenUpdates = plugin._config.UISettings.SecondsBetweenUpdates;
component.InvokeRandomized(() =>
{
plugin.TrackStart();
component.CheckRange();
plugin.TrackEnd();
}, 0, secondsBetweenUpdates, secondsBetweenUpdates * 0.1f);
// Show 25% of vanilla static.var staticFraction = Mathf.Lerp(VanillaStaticDistanceFraction, 1, 0.25f);
SendMaxRangeConVar(player, Mathf.CeilToInt(maxDistance / staticFraction).ToString());
return component;
}
public static void RemoveFromPlayer(BasePlayer player)
{
var component = player.GetComponent<RangeLimiter>();
if (component != null)
{
DestroyImmediate(component);
}
}
public static void DestroyAll()
{
foreach (var player in BasePlayer.activePlayerList)
{
RemoveFromPlayer(player);
}
}
private LimitedDroneRange _plugin;
private BasePlayer _player;
private ComputerStation _station;
private Transform _stationTransform;
private Transform _droneTransform;
private int _maxDistance;
private int _previousDisplayedDistance;
private void CheckRange()
{
var sqrDistance = (_stationTransform.position - _droneTransform.position).sqrMagnitude;
if (sqrDistance > _maxDistance * _maxDistance)
{
_station.StopControl(_player);
_plugin._uiManager.CreateOutOfRangeUI(_plugin, _player);
return;
}
var distance = Mathf.CeilToInt(Mathf.Sqrt(sqrDistance));
if (distance == _previousDisplayedDistance)
return;
_plugin._uiManager.CreateDistanceUI(_plugin, _player, distance, _maxDistance);
_previousDisplayedDistance = distance;
}
public void OnDestroy() => UIManager.Destroy(_player);
}
#region UI
private class UIManager
{
private const string UIName = "LimitedDroneRange";
private const string PlaceholderText = "__TEXT__";
private const string PlaceholderColor = "__COLOR__";
private string _cachedJson;
public static void Destroy(BasePlayer player)
{
CuiHelper.DestroyUi(player, UIName);
}
private string GetJsonWithPlaceholders(LimitedDroneRange plugin)
{
if (_cachedJson == null)
{
var uiSettings = plugin._config.UISettings;
var cuiElements = new CuiElementContainer
{
new CuiElement
{
Parent = "Overlay",
Name = UIName,
DestroyUi = UIName,
Components =
{
new CuiRectTransformComponent
{
AnchorMin = uiSettings.AnchorMin,
AnchorMax = uiSettings.AnchorMax,
OffsetMin = uiSettings.OffsetMin,
OffsetMax = uiSettings.OffsetMax,
}
},
},
new CuiElement
{
Parent = UIName,
Components =
{
new CuiTextComponent
{
Text = PlaceholderText,
Align = TextAnchor.MiddleCenter,
Color = PlaceholderColor,
FontSize = uiSettings.TextSize,
},
new CuiOutlineComponent
{
Color = "0 0 0 1",
Distance = "0.75 0.75"
},
new CuiRectTransformComponent
{
AnchorMin = "0 0",
AnchorMax = "0 0",
OffsetMin = $"{uiSettings.TextSize * -4} 0",
OffsetMax = $"{uiSettings.TextSize * 4} {uiSettings.TextSize * 1.5f}",
},
},
},
};
_cachedJson = CuiHelper.ToJson(cuiElements);
}
return _cachedJson;
}
public void CreateDistanceUI(LimitedDroneRange plugin, BasePlayer player, int distance, int maxDistance)
{
CreateLabel(
plugin,
player,
plugin.GetMessage(player.UserIDString, LangEntry.UIDistance, distance.ToString(), maxDistance.ToString()),
plugin._config.UISettings.GetDynamicColor(distance, maxDistance)
);
}
public void CreateOutOfRangeUI(LimitedDroneRange plugin, BasePlayer player)
{
CreateLabel(
plugin,
player,
plugin.GetMessage(player.UserIDString, LangEntry.UIOutOfRange),
plugin._config.UISettings.OutOfRangeColor
);
plugin.timer.Once(1, () => Destroy(player));
}
private void CreateLabel(LimitedDroneRange plugin, BasePlayer player, string text, string color)
{
var json = GetJsonWithPlaceholders(plugin).Replace(PlaceholderText, text).Replace(PlaceholderColor, color);
CuiHelper.AddUi(player, json);
}
}
#endregion
#region Configuration
private class RangeProfile
{
[JsonProperty("PermissionSuffix")]
public string PermissionSuffix;
[JsonProperty("MaxRange")]
public int MaxRange;
[JsonIgnore]
public string Permission;
public void Init(LimitedDroneRange plugin)
{
if (string.IsNullOrWhiteSpace(PermissionSuffix))
return;
Permission = $"{nameof(LimitedDroneRange)}.{PermissionSuffix}".ToLower();
plugin.permission.RegisterPermission(Permission, plugin);
}
}
private class ColorConfig
{
[JsonProperty("DistanceRemaining")]
public int DistanceRemaining;
[JsonProperty("Color")]
public string Color;
}
private class UISettings
{
[JsonProperty("AnchorMin")]
public string AnchorMin = "0.5 0";
[JsonProperty("AnchorMax")]
public string AnchorMax = "0.5 0";
[JsonProperty("OffsetMin")]
public string OffsetMin = "0 75";
[JsonProperty("OffsetMax")]
public string OffsetMax = "0 75";
[JsonProperty("TextSize")]
public int TextSize = 24;
[JsonProperty("DefaultColor")]
public string DefaultColor = "0.75 0.75 0.75 1";
[JsonProperty("OutOfRangeColor")]
public string OutOfRangeColor = "1 0.2 0.2 1";
[JsonProperty("DynamicColors")]
public ColorConfig[] DynamicColors =
{
new ColorConfig
{
DistanceRemaining = 100,
Color = "1 0.5 0 1",
},
new ColorConfig
{
DistanceRemaining = 50,
Color = "1 0.2 0.2 1",
},
};
[JsonProperty("SecondsBetweenUpdates")]
public float SecondsBetweenUpdates = 0.5f;
public string GetDynamicColor(int distance, int maxDistance)
{
var distanceFromMax = maxDistance - distance;
foreach (var colorConfig in DynamicColors)
{
if (distanceFromMax <= colorConfig.DistanceRemaining)
return colorConfig.Color;
}
return DefaultColor;
}
public void Init()
{
var colorConfigs = DynamicColors.ToList();
colorConfigs.Sort((config1, config2) => config1.DistanceRemaining.CompareTo(config2.DistanceRemaining));
DynamicColors = colorConfigs.ToArray();
}
}
private class Configuration : BaseConfiguration
{
[JsonProperty("DefaultMaxRange")]
public int DefaultMaxRange = 500;
[JsonProperty("ProfilesRequiringPermission")]
public RangeProfile[] ProfilesRequiringPermission =
{
new RangeProfile
{
PermissionSuffix = "short",
MaxRange = 250,
},
new RangeProfile
{
PermissionSuffix = "medium",
MaxRange = 500,
},
new RangeProfile
{
PermissionSuffix = "long",
MaxRange = 1000,
},
new RangeProfile
{
PermissionSuffix = "unlimited",
MaxRange = 0,
},
};
[JsonProperty("UISettings")]
public UISettings UISettings = new UISettings();
public void Init(LimitedDroneRange pluginInstance)
{
foreach (var profile in ProfilesRequiringPermission)
profile.Init(pluginInstance);
UISettings.Init();
}
public int GetMaxRangeForPlayer(LimitedDroneRange plugin, string userId)
{
if (ProfilesRequiringPermission == null)
return DefaultMaxRange;
for (var i = ProfilesRequiringPermission.Length - 1; i >= 0; i--)
{
var profile = ProfilesRequiringPermission[i];
if (profile.Permission != null && plugin.permission.UserHasPermission(userId, profile.Permission))
return profile.MaxRange;
}
return DefaultMaxRange;
}
}
private Configuration GetDefaultConfig() => new Configuration();
#endregion
#region Configuration Helpers
private class BaseConfiguration
{
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(BaseConfiguration 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)
{
var changed = false;
foreach (var key in currentWithDefaults.Keys)
{
object currentRawValue;
if (currentRaw.TryGetValue(key, out currentRawValue))
{
var defaultDictValue = currentWithDefaults[key] as Dictionary<string, object>;
var currentDictValue = currentRawValue as Dictionary<string, object>;
if (defaultDictValue != null)
{
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();
}
var changed = MaybeUpdateConfig(_config);
if (_config.UISettings.AnchorMin == "0.5 0"
&& _config.UISettings.AnchorMax == "0.5 0"
&& _config.UISettings.OffsetMin == "0 75"
&& _config.UISettings.OffsetMax == "0 75")
{
_config.UISettings.OffsetMin = "0 47";
_config.UISettings.OffsetMax = "0 47";
changed = true;
}
if (changed)
{
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
#region Localization
private class LangEntry
{
public static readonly List<LangEntry> AllLangEntries = new List<LangEntry>();
public static readonly LangEntry UIOutOfRange = new LangEntry("UI.OutOfRange", "{0}m / {1}m");
public static readonly LangEntry UIDistance = new LangEntry("UI.Distance", "OUT OF RANGE");
public string Name;
public string English;
public LangEntry(string name, string english)
{
Name = name;
English = english;
AllLangEntries.Add(this);
}
}
private string GetMessage(string playerId, LangEntry langEntry) =>
lang.GetMessage(langEntry.Name, this, playerId);
private string GetMessage(string playerId, LangEntry langEntry, object arg1, object arg2) =>
string.Format(GetMessage(playerId, langEntry), arg1, arg2);
protected override void LoadDefaultMessages()
{
var englishLangKeys = new Dictionary<string, string>();
foreach (var langEntry in LangEntry.AllLangEntries)
{
englishLangKeys[langEntry.Name] = langEntry.English;
}
lang.RegisterMessages(englishLangKeys, this, "en");
}
#endregion
}
}