C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Oxide.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VLB;
namespace Oxide.Plugins
{
[Info("Drone Lights", "WhiteThunder", "1.0.4")]
[Description("Adds controllable search lights to RC drones.")]
internal class DroneLights : CovalencePlugin
{
#region Fields
private static DroneLights _pluginInstance;
private static Configuration _pluginConfig;
private const string PermissionAutoDeploy = "dronelights.searchlight.autodeploy";
private const string PermissionMoveLight = "dronelights.searchlight.move";
private const string SpherePrefab = "assets/prefabs/visualization/sphere.prefab";
private const string SearchLightPrefab = "assets/prefabs/deployable/search light/searchlight.deployed.prefab";
private const float SearchLightYAxisRotation = 180;
private const float SearchLightScale = 0.1f;
private static readonly Vector3 SphereEntityInitialLocalPosition = new Vector3(0, -100, 0);
private static readonly Vector3 SphereEntityLocalPosition = new Vector3(0, -0.075f, 0.25f);
private static readonly Vector3 SearchLightLocalPosition = new Vector3(0, -1.25f, -0.25f);
private ProtectionProperties ImmortalProtection;
#endregion
#region Hooks
private void Init()
{
_pluginInstance = this;
permission.RegisterPermission(PermissionAutoDeploy, this);
permission.RegisterPermission(PermissionMoveLight, this);
Unsubscribe(nameof(OnEntitySpawned));
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
SearchLightUpdater.StopControl(player);
UnityEngine.Object.Destroy(ImmortalProtection);
_pluginInstance = null;
_pluginConfig = null;
}
private void OnServerInitialized()
{
ImmortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
ImmortalProtection.name = "DroneLightsProtection";
ImmortalProtection.Add(1);
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null || !IsDroneEligible(drone))
continue;
AddOrUpdateSearchLight(drone);
}
foreach (var player in BasePlayer.activePlayerList)
{
var searchLight = GetControlledSearchLight(player);
if (searchLight == null)
continue;
SearchLightUpdater.StartControl(player, searchLight);
}
Subscribe(nameof(OnEntitySpawned));
}
private void OnEntitySpawned(Drone drone)
{
if (!IsDroneEligible(drone))
return;
MaybeAutoDeploySearchLight(drone);
}
private bool? OnServerCommand(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.cmd.FullName != "inventory.lighttoggle")
return null;
var basePlayer = arg.Player();
if (basePlayer == null)
return null;
var searchLight = GetControlledSearchLight(basePlayer);
if (searchLight == null)
return null;
searchLight.SetFlag(IOEntity.Flag_HasPower, !searchLight.IsPowered());
// Prevent other lights from toggling since they are not useful while using the computer station.return false;
}
private void OnBookmarkControlStarted(ComputerStation station, BasePlayer player, string bookmarkName, Drone drone)
{
var searchLight = GetDroneSearchLight(drone);
if (searchLight == null)
return;
if (permission.UserHasPermission(player.UserIDString, PermissionMoveLight))
SearchLightUpdater.StartControl(player, searchLight);
}
private void OnBookmarkControlEnded(ComputerStation station, BasePlayer player, Drone drone)
{
SearchLightUpdater.StopControl(player);
}
#endregion
#region Helper Methods
private static bool DeployLightWasBlocked(Drone drone)
{
object hookResult = Interface.CallHook("OnDroneSearchLightDeploy", drone);
return hookResult is bool && (bool)hookResult == false;
}
private static bool IsDroneEligible(Drone drone) =>
!(drone is DeliveryDrone);
private static Drone GetControlledDrone(ComputerStation station) =>
station.currentlyControllingEnt.Get(serverside: true) as Drone;
private static Drone GetControlledDrone(BasePlayer player)
{
var computerStation = player.GetMounted() as ComputerStation;
if (computerStation == null)
return null;
return GetControlledDrone(computerStation);
}
private static T2 GetGrandChildOfType<T1, T2>(BaseEntity entity, out T1 childOfType) where T1 : BaseEntity where T2 : BaseEntity
{
foreach (var child in entity.children)
{
childOfType = child as T1;
if (childOfType == null)
continue;
foreach (var grandChild in childOfType.children)
{
var grandChildOfType = grandChild as T2;
if (grandChildOfType != null)
return grandChildOfType;
}
}
childOfType = null;
return null;
}
private static SearchLight GetDroneSearchLight(Drone drone, out SphereEntity parentSphere) =>
GetGrandChildOfType<SphereEntity, SearchLight>(drone, out parentSphere);
private static SearchLight GetDroneSearchLight(Drone drone)
{
SphereEntity parentSphere;
return GetDroneSearchLight(drone, out parentSphere);
}
private static SearchLight GetControlledSearchLight(BasePlayer player)
{
var drone = GetControlledDrone(player);
if (drone == null)
return null;
return GetDroneSearchLight(drone);
}
private static void RemoveProblemComponents(BaseEntity entity)
{
foreach (var collider in entity.GetComponentsInChildren<Collider>())
UnityEngine.Object.DestroyImmediate(collider);
UnityEngine.Object.DestroyImmediate(entity.GetComponent<DestroyOnGroundMissing>());
UnityEngine.Object.DestroyImmediate(entity.GetComponent<GroundWatch>());
}
private static void HideInputsAndOutputs(IOEntity ioEntity)
{
// Trick to hide the inputs and outputs on the client.foreach (var input in ioEntity.inputs)
input.type = IOEntity.IOType.Generic;
foreach (var output in ioEntity.outputs)
output.type = IOEntity.IOType.Generic;
}
private static float Clamp(float x, float min, float max) => Math.Max(min, Math.Min(x, max));
private SearchLight TryDeploySearchLight(Drone drone)
{
if (DeployLightWasBlocked(drone))
return null;
var sphereLocalPosition = SphereEntityInitialLocalPosition;
var scale = drone.transform.lossyScale.y;
if (scale != 0)
sphereLocalPosition = sphereLocalPosition / scale;
// Spawn the search light below the map initially while the resize is performed.SphereEntity sphereEntity = GameManager.server.CreateEntity(SpherePrefab, sphereLocalPosition) as SphereEntity;
if (sphereEntity == null)
return null;
SetupSphereEntity(sphereEntity);
sphereEntity.currentRadius = SearchLightScale;
sphereEntity.lerpRadius = SearchLightScale;
sphereEntity.SetParent(drone);
sphereEntity.Spawn();
var localRotation = Quaternion.Euler(_pluginConfig.SearchLight.DefaultAngle - 90 % 350, SearchLightYAxisRotation, 0);
SearchLight searchLight = GameManager.server.CreateEntity(SearchLightPrefab, SearchLightLocalPosition, localRotation) as SearchLight;
if (searchLight == null)
return null;
SetupSearchLight(searchLight);
searchLight.SetParent(sphereEntity);
searchLight.Spawn();
Interface.CallHook("OnDroneSearchLightDeployed", drone, searchLight);
timer.Once(3, () =>
{
if (sphereEntity != null)
sphereEntity.transform.localPosition = SphereEntityLocalPosition;
});
return searchLight;
}
private void SetupSphereEntity(SphereEntity sphereEntity)
{
sphereEntity.EnableSaving(true);
sphereEntity.EnableGlobalBroadcast(false);
}
private void SetupSearchLight(SearchLight searchLight)
{
RemoveProblemComponents(searchLight);
HideInputsAndOutputs(searchLight);
searchLight.EnableSaving(true);
searchLight.SetFlag(BaseEntity.Flags.Busy, true);
searchLight.baseProtection = ImmortalProtection;
searchLight.pickup.enabled = false;
}
private void AddOrUpdateSearchLight(Drone drone)
{
SphereEntity sphereEntity;
var searchLight = GetDroneSearchLight(drone, out sphereEntity);
if (searchLight == null)
{
MaybeAutoDeploySearchLight(drone);
return;
}
SetupSphereEntity(sphereEntity);
SetupSearchLight(searchLight);
}
private void MaybeAutoDeploySearchLight(Drone drone)
{
if (!permission.UserHasPermission(drone.OwnerID.ToString(), PermissionAutoDeploy))
return;
TryDeploySearchLight(drone);
}
private void OnDroneScaleBegin(Drone drone, BaseEntity rootEntity, float scale, float previousScale)
{
if (scale == 0)
return;
SphereEntity parentSphere;
var searchLight = GetDroneSearchLight(drone, out parentSphere);
if (searchLight == null)
return;
var sphereTransform = parentSphere.transform;
if (sphereTransform.localPosition == SphereEntityLocalPosition)
return;
sphereTransform.localPosition = SphereEntityInitialLocalPosition / scale;
}
#endregion
#region Classes
private class SearchLightUpdater : EntityComponent<BasePlayer>
{
public static void StartControl(BasePlayer player, SearchLight searchLight) =>
player.GetOrAddComponent<SearchLightUpdater>().OnControlStarted(searchLight);
public static void StopControl(BasePlayer player) =>
player.GetComponent<SearchLightUpdater>()?.OnControlStopped();
private SearchLight _searchLight;
private Transform _searchLightTransform;
private void OnControlStarted(SearchLight searchLight)
{
CancelInvoke(DelayedDestroy);
_searchLight = searchLight;
_searchLightTransform = searchLight.transform;
}
private void DelayedDestroy() => DestroyImmediate(this);
private void OnControlStopped() => Invoke(DelayedDestroy, 0);
private void Update()
{
if (_searchLight == null)
{
OnControlStopped();
return;
}
if (!_searchLight.IsPowered())
return;
var mouseVerticalDelta = baseEntity.serverInput.current.mouseDelta.y;
if (mouseVerticalDelta == 0)
return;
// Track the performance cost with Oxide so that server owners can be informed._pluginInstance.TrackStart();
UpdateAim(mouseVerticalDelta);
_pluginInstance.TrackEnd();
}
private void UpdateAim(float mouseVerticalDelta)
{
var localX = _searchLightTransform.localRotation.eulerAngles.x;
var searchLightSettings = _pluginConfig.SearchLight;
// Temporarily translate the angle by 90 degrees so it can be clamped based on a configured 0-180 range.var newLocalX = (localX + 90) % 360;
newLocalX += searchLightSettings.AimSensitivity * mouseVerticalDelta * Time.deltaTime * 60;
newLocalX = Clamp(newLocalX, searchLightSettings.MinAngle, searchLightSettings.MaxAngle);
newLocalX = (newLocalX - 90) % 360;
_searchLightTransform.localRotation = Quaternion.Euler(newLocalX, SearchLightYAxisRotation, 0);
_searchLight.InvalidateNetworkCache();
// This is the most expensive line in terms of performance._searchLight.SendNetworkUpdate_Position();
}
}
#endregion
#region Configuration
private class Configuration : SerializableConfiguration
{
[JsonProperty("SearchLight")]
public SearchLightSettings SearchLight = new SearchLightSettings();
}
private class SearchLightSettings
{
[JsonProperty("DefaultAngle")]
public int DefaultAngle = 75;
[JsonProperty("MinAngle")]
public int MinAngle = 60;
[JsonProperty("MaxAngle")]
public int MaxAngle = 120;
[JsonProperty("AimSensitivity")]
public float AimSensitivity = 1;
}
private Configuration GetDefaultConfig() => new Configuration();
#endregion
#region Configuration Boilerplate
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)
{
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() => _pluginConfig = GetDefaultConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_pluginConfig = Config.ReadObject<Configuration>();
if (_pluginConfig == null)
{
throw new JsonException();
}
if (MaybeUpdateConfig(_pluginConfig))
{
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(_pluginConfig, true);
}
#endregion
}
}