C#:
using Network;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VLB;
namespace Oxide.Plugins
{
[Info("Drone Settings", "WhiteThunder", "1.3.1")]
[Description("Allows changing speed, toughness and other properties of RC drones.")]
internal class DroneSettings : CovalencePlugin
{
#region Fields
[PluginReference]
private readonly Plugin DroneScaleManager;
private Configuration _config;
private const string PermissionProfilePrefix = "dronesettings";
private const string BaseDroneType = "BaseDrone";
private DroneProperties _vanillaDroneProperties;
private ProtectionProperties _vanillaDroneProtection;
private readonly List<ProtectionProperties> _customProtectionProperties = new List<ProtectionProperties>();
private readonly List<string> _reusableDroneTypeList = new List<string>();
#endregion
#region Hooks
private void Init()
{
_config.Init(this);
Unsubscribe(nameof(OnEntitySpawned));
}
private void OnServerInitialized()
{
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null)
continue;
OnEntitySpawned(drone);
}
foreach (var player in BasePlayer.activePlayerList)
{
var station = player.GetMounted() as ComputerStation;
if (station == null)
continue;
var drone = GetControlledDrone(station);
if (drone == null)
continue;
OnBookmarkControlStarted(station, player, drone.GetIdentifier(), drone);
}
Subscribe(nameof(OnEntitySpawned));
}
private void Unload()
{
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null || !IsDroneEligible(drone))
continue;
DroneConnectionFixer.RemoveFromDrone(drone);
if (!ApplySettingsWasBlocked(drone))
{
RestoreVanillaSettings(drone);
Interface.CallHook("OnDroneSettingsChanged", drone);
}
}
foreach (var protectionProperties in _customProtectionProperties)
{
UnityEngine.Object.Destroy(protectionProperties);
}
}
private void OnEntitySpawned(Drone drone)
{
if (!IsDroneEligible(drone))
return;
if (_vanillaDroneProtection == null)
{
_vanillaDroneProtection = drone.baseProtection;
}
if (_vanillaDroneProperties == null)
{
_vanillaDroneProperties = DroneProperties.FromDrone(drone);
}
var drone2 = drone;
// Delay to give other plugins a moment to cache the drone id so they can specify drone type or block this.
NextTick(() =>
{
if (drone2 == null)
return;
var profile = GetDroneProfile(drone2);
if (profile == null)
return;
TryApplyProfile(drone2, profile);
});
}
private void OnBookmarkControlStarted(ComputerStation station, BasePlayer player, string bookmarkName, Drone drone)
{
DroneConnectionFixer.OnControlStarted(this, drone, player);
}
private void OnBookmarkControlEnded(ComputerStation station, BasePlayer player, Drone drone)
{
if (drone == null)
return;
DroneConnectionFixer.OnControlEnded(drone, player);
}
private void OnDroneScaled(Drone drone, BaseEntity rootEntity, float scale, float previousScale)
{
if (scale == 1)
{
DroneConnectionFixer.OnRootEntityChanged(drone, drone);
}
else if (previousScale == 1)
{
DroneConnectionFixer.OnRootEntityChanged(drone, rootEntity);
}
}
#endregion
#region API
private void API_RefreshDroneProfile(Drone drone)
{
var profile = GetDroneProfile(drone);
if (profile == null)
return;
TryApplyProfile(drone, profile, restoreVanilla: true);
}
#endregion
#region Helper Methods
private static bool ApplySettingsWasBlocked(Drone drone)
{
var hookResult = Interface.CallHook("OnDroneSettingsChange", drone);
return hookResult is bool && (bool)hookResult == false;
}
private static bool IsDroneEligible(Drone drone)
{
return drone.skinID == 0 && !(drone is DeliveryDrone);
}
private static string GetProfilePermission(string droneType, string profileSuffix)
{
return $"{PermissionProfilePrefix}.{droneType}.{profileSuffix}";
}
private static Drone GetControlledDrone(ComputerStation station)
{
return station.currentlyControllingEnt.Get(serverside: true) as Drone;
}
private string DetermineBestDroneType(List<string> droneTypeList)
{
if (droneTypeList.Count == 0)
return null;
if (droneTypeList.Count == 1)
return droneTypeList[0];
string bestDroneType = null;
var bestDroneTypePriorityIndex = int.MaxValue;
// Sort by priority, else sort alphabetically.
foreach (var droneType in droneTypeList)
{
var priorityIndex = Array.IndexOf(_config.DroneTypePriority, droneType);
priorityIndex = priorityIndex >= 0 ? priorityIndex : int.MaxValue;
if (bestDroneType == null)
{
bestDroneType = droneType;
bestDroneTypePriorityIndex = priorityIndex;
continue;
}
if (priorityIndex < bestDroneTypePriorityIndex
|| priorityIndex == bestDroneTypePriorityIndex && string.Compare(droneType, bestDroneType, StringComparison.InvariantCultureIgnoreCase) < 0)
{
bestDroneType = droneType;
bestDroneTypePriorityIndex = priorityIndex;
}
}
return bestDroneType;
}
private string DetermineDroneType(Drone drone)
{
_reusableDroneTypeList.Clear();
var hookResult = Interface.CallHook("OnDroneTypeDetermine", drone, _reusableDroneTypeList);
return _reusableDroneTypeList.Count > 0
? DetermineBestDroneType(_reusableDroneTypeList)
: hookResult as string;
}
private DroneProfile GetDroneProfile(Drone drone)
{
var droneType = DetermineDroneType(drone) ?? BaseDroneType;
return _config.FindProfile(this, droneType, drone.OwnerID);
}
private BaseEntity GetRootEntity(Drone drone)
{
return DroneScaleManager?.Call("API_GetRootEntity", drone) as BaseEntity;
}
private BaseEntity GetDroneOrRootEntity(Drone drone)
{
var rootEntity = GetRootEntity(drone);
return rootEntity != null ? rootEntity : drone;
}
private void RestoreVanillaSettings(Drone drone)
{
if (_vanillaDroneProtection != null && _customProtectionProperties.Contains(drone.baseProtection))
{
drone.baseProtection = _vanillaDroneProtection;
}
_vanillaDroneProperties?.ApplyToDrone(drone);
}
private bool TryApplyProfile(Drone drone, DroneProfile profile, bool restoreVanilla = false)
{
if (ApplySettingsWasBlocked(drone))
return false;
if (restoreVanilla)
{
RestoreVanillaSettings(drone);
}
profile.ApplyToDrone(drone);
Interface.CallHook("OnDroneSettingsChanged", drone);
return true;
}
private ProtectionProperties CreateProtectionProperties(Dictionary<string, float> damageMap)
{
var protectionProperties = ScriptableObject.CreateInstance<ProtectionProperties>();
_customProtectionProperties.Add(protectionProperties);
foreach (var entry in damageMap)
{
DamageType damageType;
if (!Enum.TryParse(entry.Key, true, out damageType))
{
LogError($"Invalid damage type: {entry.Key}");
continue;
}
protectionProperties.Add(damageType, 1 - Mathf.Clamp(entry.Value, 0, 1));
}
return protectionProperties;
}
#endregion
#region Drone Network Fixer
// Fixes issue where fast moving drones temporarily disconnect and reconnect.
// This issue occurs because the drone's network group and the client's secondary network group cannot be changed at the same time.
private class DroneConnectionFixer : FacepunchBehaviour
{
public static void OnControlStarted(DroneSettings plugin, Drone drone, BasePlayer player)
{
var component = drone.GetOrAddComponent<DroneConnectionFixer>();
component.SetRootEntity(plugin.GetDroneOrRootEntity(drone));
component._viewers.Add(player);
}
public static void OnControlEnded(Drone drone, BasePlayer player)
{
var component = drone.GetComponent<DroneConnectionFixer>();
if (component == null)
return;
component.RemoveController(player);
}
public static void OnRootEntityChanged(Drone drone, BaseEntity rootEntity)
{
var component = drone.GetComponent<DroneConnectionFixer>();
if (component == null)
return;
component.SetRootEntity(rootEntity);
}
public static void RemoveFromDrone(Drone drone)
{
DestroyImmediate(drone.GetComponent<DroneConnectionFixer>());
}
private BaseEntity _rootEntity;
private List<BasePlayer> _viewers = new List<BasePlayer>();
private bool _isCallingCustomUpdateNetworkGroup;
private Action _updateNetworkGroup;
private Action _customUpdateNetworkGroup;
private DroneConnectionFixer()
{
_customUpdateNetworkGroup = CustomUpdateNetworkGroup;
}
private void SetRootEntity(BaseEntity rootEntity)
{
_rootEntity = rootEntity;
_updateNetworkGroup = _rootEntity.UpdateNetworkGroup;
}
private void RemoveController(BasePlayer player)
{
_viewers.Remove(player);
if (_viewers.Count == 0)
{
DestroyImmediate(this);
}
}
// Using LateUpdate since that's the soonest we can learn about a pending Invoke.
private void LateUpdate()
{
// Detect when UpdateNetworkGroup has been scheduled, in order to schedule a custom one in its place.
if (_rootEntity.isCallingUpdateNetworkGroup && !_isCallingCustomUpdateNetworkGroup)
{
_rootEntity.CancelInvoke(_updateNetworkGroup);
Invoke(_customUpdateNetworkGroup, 5);
_isCallingCustomUpdateNetworkGroup = true;
}
}
private void SendFakeUpdateNetworkGroup(BaseEntity entity, BasePlayer player, uint groupId)
{
var write = Net.sv.StartWrite();
write.PacketID(Message.Type.GroupChange);
write.EntityID(entity.net.ID);
write.GroupID(groupId);
write.Send(new SendInfo(player.net.connection));
}
private void CustomUpdateNetworkGroup()
{
foreach (var player in _viewers)
{
// Temporarily tell the client that the drone is in the global network group.
SendFakeUpdateNetworkGroup(_rootEntity, player, BaseNetworkable.GlobalNetworkGroup.ID);
// Update the client secondary network group to the one that the drone will change to.
player.net.SwitchSecondaryGroup(Net.sv.visibility.GetGroup(_rootEntity.transform.position, _rootEntity.networkRange));
}
// Update the drone's network group based on its current position.
// This will update clients to be aware that the drone is now in the new network group.
_rootEntity.UpdateNetworkGroup();
_isCallingCustomUpdateNetworkGroup = false;
}
private void OnDestroy()
{
if (_rootEntity == null || _rootEntity.IsDestroyed)
return;
if (_rootEntity.isCallingUpdateNetworkGroup && !_rootEntity.IsInvoking(_updateNetworkGroup))
{
_rootEntity.Invoke(_updateNetworkGroup, 5);
}
}
}
#endregion
#region Configuration
[JsonObject(MemberSerialization.OptIn)]
private class DroneProfile
{
[JsonProperty("PermissionSuffix", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string PermissionSuffix;
[JsonProperty("DroneProperties")]
public DroneProperties DroneProperties = new DroneProperties();
[JsonProperty("DamageScale")]
public Dictionary<string, float> DamageScale;
[JsonIgnore]
public ProtectionProperties ProtectionProperties;
[JsonIgnore]
public string Permission;
public void Init(DroneSettings plugin, string droneType, bool requiresPermission)
{
if (requiresPermission && !string.IsNullOrWhiteSpace(PermissionSuffix))
{
Permission = GetProfilePermission(droneType, PermissionSuffix);
plugin.permission.RegisterPermission(Permission, plugin);
}
if (DamageScale != null)
{
ProtectionProperties = plugin.CreateProtectionProperties(DamageScale);
}
}
public void ApplyToDrone(Drone drone)
{
if (ProtectionProperties != null)
{
drone.baseProtection = ProtectionProperties;
}
DroneProperties?.ApplyToDrone(drone);
}
}
[JsonObject(MemberSerialization.OptIn)]
private class DroneProperties
{
public static DroneProperties FromDrone(Drone drone)
{
return new DroneProperties
{
KillInWater = drone.killInWater,
MovementAcceleration = drone.movementAcceleration,
AltitudeAcceleration = drone.altitudeAcceleration,
LeanWeight = drone.leanWeight,
};
}
[JsonProperty("KillInWater")]
public bool KillInWater = true;
[JsonProperty("DisableWhenHurtChance")]
public float DisableWhenHurtChance = 25;
[JsonProperty("MovementAcceleration")]
public float MovementAcceleration = 10;
[JsonProperty("AltitudeAcceleration")]
public float AltitudeAcceleration = 10;
[JsonProperty("LeanWeight")]
public float LeanWeight = 0.025f;
public void ApplyToDrone(Drone drone)
{
drone.killInWater = KillInWater;
drone.disableWhenHurt = DisableWhenHurtChance > 0;
drone.disableWhenHurtChance = DisableWhenHurtChance / 100f;
drone.movementAcceleration = MovementAcceleration;
drone.altitudeAcceleration = AltitudeAcceleration;
drone.leanWeight = LeanWeight;
}
}
[JsonObject(MemberSerialization.OptIn)]
private class DroneTypeConfig
{
[JsonProperty("DefaultProfile")]
public DroneProfile DefaultProfile = new DroneProfile();
[JsonProperty("ProfilesRequiringPermission")]
public DroneProfile[] ProfilesRequiringPermission = Array.Empty<DroneProfile>();
public void Init(DroneSettings plugin, string droneType)
{
DefaultProfile.Init(plugin, droneType, requiresPermission: false);
foreach (var profile in ProfilesRequiringPermission)
{
profile.Init(plugin, droneType, requiresPermission: true);
}
}
public DroneProfile GetProfileForOwner(DroneSettings plugin, ulong ownerId)
{
if (ownerId == 0 || (ProfilesRequiringPermission?.Length ?? 0) == 0)
return DefaultProfile;
var ownerIdString = ownerId.ToString();
for (var i = ProfilesRequiringPermission.Length - 1; i >= 0; i--)
{
var profile = ProfilesRequiringPermission[i];
if (profile.Permission != null && plugin.permission.UserHasPermission(ownerIdString, profile.Permission))
return profile;
}
return DefaultProfile;
}
}
[JsonObject(MemberSerialization.OptIn)]
private class Configuration : BaseConfiguration
{
public override bool Migrate()
{
var changed = false;
foreach (var entry in _droneTypeAliases)
{
var oldName = entry.Key;
var newName = entry.Value;
DroneTypeConfig droneTypeConfig;
if (SettingsByDroneType.TryGetValue(oldName, out droneTypeConfig))
{
SettingsByDroneType[newName] = droneTypeConfig;
SettingsByDroneType.Remove(oldName);
changed = true;
}
}
return changed;
}
[JsonIgnore]
private readonly Dictionary<string, string> _droneTypeAliases = new Dictionary<string, string>
{
["RidableDrones"] = "DroneChair",
};
[JsonProperty("DroneTypePriority")]
public string[] DroneTypePriority =
{
"DroneTurrets",
"DroneChair",
"DroneStorage",
};
[JsonProperty("SettingsByDroneType")]
public Dictionary<string, DroneTypeConfig> SettingsByDroneType = new Dictionary<string, DroneTypeConfig>
{
[BaseDroneType] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.2f,
[DamageType.Bullet.ToString()] = 0.2f,
[DamageType.AntiVehicle.ToString()] = 0.25f,
},
},
ProfilesRequiringPermission = new[]
{
new DroneProfile
{
PermissionSuffix = "god",
DroneProperties = new DroneProperties
{
KillInWater = false,
DisableWhenHurtChance = 0,
MovementAcceleration = 30,
AltitudeAcceleration = 20,
LeanWeight = 0,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.AntiVehicle.ToString()] = 0,
[DamageType.Arrow.ToString()] = 0,
[DamageType.Bite.ToString()] = 0,
[DamageType.Bleeding.ToString()] = 0,
[DamageType.Blunt.ToString()] = 0,
[DamageType.Bullet.ToString()] = 0,
[DamageType.Cold.ToString()] = 0,
[DamageType.ColdExposure.ToString()] = 0,
[DamageType.Collision.ToString()] = 0,
[DamageType.Decay.ToString()] = 0,
[DamageType.Drowned.ToString()] = 0,
[DamageType.ElectricShock.ToString()] = 0,
[DamageType.Explosion.ToString()] = 0,
[DamageType.Fall.ToString()] = 0,
[DamageType.Fun_Water.ToString()] = 0,
[DamageType.Generic.ToString()] = 0,
[DamageType.Heat.ToString()] = 0,
[DamageType.Hunger.ToString()] = 0,
[DamageType.Poison.ToString()] = 0,
[DamageType.Radiation.ToString()] = 0,
[DamageType.RadiationExposure.ToString()] = 0,
[DamageType.Slash.ToString()] = 0,
[DamageType.Stab.ToString()] = 0,
[DamageType.Suicide.ToString()] = 0,
[DamageType.Thirst.ToString()] = 0,
},
},
},
},
["DroneBoombox"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
MovementAcceleration = 7.5f,
AltitudeAcceleration = 7.5f,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.1f,
[DamageType.Bullet.ToString()] = 0.1f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
},
},
},
["DroneChair"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
MovementAcceleration = 7.5f,
AltitudeAcceleration = 7.5f,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.1f,
[DamageType.Bullet.ToString()] = 0.1f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
},
},
},
["DroneSign"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
MovementAcceleration = 7.5f,
AltitudeAcceleration = 7.5f,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.1f,
[DamageType.Bullet.ToString()] = 0.1f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
},
},
},
["DroneStorage"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
MovementAcceleration = 7.5f,
AltitudeAcceleration = 7.5f,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.1f,
[DamageType.Bullet.ToString()] = 0.1f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
},
},
},
["DroneTurrets"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
MovementAcceleration = 5,
AltitudeAcceleration = 5,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.1f,
[DamageType.Bullet.ToString()] = 0.1f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
[DamageType.Explosion.ToString()] = 0.75f,
[DamageType.Blunt.ToString()] = 0.75f,
},
},
},
["MegaDrones"] = new DroneTypeConfig
{
DefaultProfile = new DroneProfile
{
DroneProperties = new DroneProperties
{
DisableWhenHurtChance = 0,
MovementAcceleration = 20,
AltitudeAcceleration = 20,
KillInWater = false,
LeanWeight = 0.1f,
},
DamageScale = new Dictionary<string, float>
{
[DamageType.Generic.ToString()] = 0.1f,
[DamageType.Heat.ToString()] = 0.05f,
[DamageType.Bullet.ToString()] = 0.05f,
[DamageType.AntiVehicle.ToString()] = 0.1f,
[DamageType.Explosion.ToString()] = 0.1f,
[DamageType.Blunt.ToString()] = 0.25f,
},
},
},
};
public void Init(DroneSettings plugin)
{
foreach (var entry in SettingsByDroneType)
{
entry.Value.Init(plugin, entry.Key);
}
}
public DroneProfile FindProfile(DroneSettings plugin, string droneType, ulong ownerId)
{
string alias;
if (_droneTypeAliases.TryGetValue(droneType, out alias))
{
droneType = alias;
}
DroneTypeConfig droneTypeConfig;
return SettingsByDroneType.TryGetValue(droneType, out droneTypeConfig)
? droneTypeConfig.GetProfileForOwner(plugin, ownerId)
: null;
}
}
private Configuration GetDefaultConfig() => new Configuration();
#endregion
#region Configuration Helpers
private class BaseConfiguration
{
public virtual bool Migrate() => false;
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) | config.Migrate();
}
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();
}
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
}
}