C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VLB;
namespace Oxide.Plugins
{
[Info("Ridable Drones", "WhiteThunder", "2.1.2")]
[Description("Allows players to deploy signs and chairs onto RC drones to allow riding them.")]
internal class RidableDrones : CovalencePlugin
{
#region Fields
[PluginReference]
private readonly Plugin DroneSettings;
private Configuration _config;
private const string PermissionSignDeploy = "ridabledrones.sign.deploy";
private const string PermissionSignDeployFree = "ridabledrones.sign.deploy.free";
private const string PermissionChairDeploy = "ridabledrones.chair.deploy";
private const string PermissionChairDeployFree = "ridabledrones.chair.deploy.free";
private const string PermissionChairAutoDeploy = "ridabledrones.chair.autodeploy";
private const string PermissionChairPilot = "ridabledrones.chair.pilot";
private const string SmallWoodenSignPrefab = "assets/prefabs/deployable/signs/sign.small.wood.prefab";
private const string DeploySignEffectPrefab = "assets/prefabs/deployable/signs/effects/wood-sign-deploy.prefab";
private const string PilotChairPrefab = "assets/prefabs/vehicle/seats/miniheliseat.prefab";
private const string PassengerChairPrefab = "assets/prefabs/deployable/secretlab chair/secretlabchair.deployed.prefab";
private const string DeprecatedPassengerChairPrefab = "assets/prefabs/deployable/chair/chair.deployed.prefab";
private const string DeprecatedVisibleChairPrefab = "assets/prefabs/vehicle/seats/passengerchair.prefab";
private const string ChairDeployEffectPrefab = "assets/prefabs/deployable/chair/effects/chair-deploy.prefab";
private const int SignItemId = -1138208076;
private const int ChairItemId = 1534542921;
private static SlotConfig ChairSlots = new(BaseEntity.Slot.UpperModifier);
private static SlotConfig SignSlots = new(BaseEntity.Slot.MiddleModifier, BaseEntity.Slot.UpperModifier);
private readonly object True = true;
private readonly object False = false;
private static readonly Vector3 SignLocalPosition = new(0, 0.114f, 0.265f);
private static readonly Vector3 SignLocalRotationAngles = new(270, 0, 0);
private static readonly Vector3 PassengerChairLocalPosition = new(0, 0.081f, 0);
private static readonly Vector3 PilotChairLocalPosition = new(-0.0045f, 0.0845f, 0.41f);
private readonly Dictionary<string, object> _signRemoveInfo = new();
private readonly Dictionary<string, object> _refundInfo = new()
{
["sign.wooden.small"] = new Dictionary<string, object>
{
["Amount"] = 1,
},
};
private readonly Dictionary<string, object> _chairRemoveInfo = new();
private readonly Dictionary<string, object> _chairRefundInfo = new()
{
["chair"] = new Dictionary<string, object>
{
["Amount"] = 1,
},
};
private readonly ObservableHashSet<BaseEntity> _chairDrones = new();
private readonly ObservableHashSet<BaseEntity> _mountedChairDrones = new();
private readonly ObservableHashSet<BaseEntity> _signDrones = new();
private readonly GatedHookCollection[] _hookCollections;
public RidableDrones()
{
var anySignDrones = new ObservableGate(_chairDrones, () => _chairDrones.Count > 0);
var anyChairDrones = new ObservableGate(_chairDrones, () => _chairDrones.Count > 0);
var anyChairDronesMounted = new ObservableGate(_mountedChairDrones, () => _mountedChairDrones.Count > 0);
_hookCollections = new[]
{
new GatedHookCollection(
this,
anyChairDrones,
nameof(OnEntityMounted),
nameof(OnEntityDismounted),
nameof(OnPlayerDismountFailed)
),
new GatedHookCollection(
this,
anyChairDronesMounted,
nameof(OnServerCommand)
),
new GatedHookCollection(
this,
new MultiObservableGate(anySignDrones, anyChairDrones),
nameof(OnEntityTakeDamage),
nameof(CanPickupEntity),
nameof(OnRemovableEntityInfo),
nameof(canRemove)
),
};
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermissionSignDeploy, this);
permission.RegisterPermission(PermissionSignDeployFree, this);
permission.RegisterPermission(PermissionChairDeploy, this);
permission.RegisterPermission(PermissionChairDeployFree, this);
permission.RegisterPermission(PermissionChairAutoDeploy, this);
permission.RegisterPermission(PermissionChairPilot, this);
Unsubscribe(nameof(OnEntitySpawned));
foreach (var hookCollection in _hookCollections)
{
hookCollection.Unsubscribe();
}
}
private void OnServerInitialized()
{
foreach (var drone in BaseNetworkable.serverEntities.OfType<Drone>().ToList())
{
if (drone == null || !IsDroneEligible(drone))
continue;
MaybeRefreshDroneSign(drone);
MaybeAddOrRefreshChairs(drone);
}
foreach (var player in BasePlayer.activePlayerList)
{
var drone = GetMountedDrone(player, out var currentChair);
if (drone == null)
continue;
if (!TryGetChairs(drone, out var pilotChair, out _))
continue;
if (!permission.UserHasPermission(player.UserIDString, PermissionChairPilot))
continue;
var isPilotChair = currentChair == pilotChair;
DroneController.Mount(this, player, drone, isPilotChair);
}
Subscribe(nameof(OnEntitySpawned));
}
private void Unload()
{
foreach (var entity in BaseNetworkable.serverEntities)
{
var drone = entity as Drone;
if (drone == null)
continue;
var sign = GetDroneSign(drone);
if (sign != null)
{
SignComponent.RemoveFromSign(sign);
}
if (TryGetPassengerChair(drone, out var passengerChair))
{
ChairComponent.RemoveFromChair(passengerChair);
}
}
foreach (var player in BasePlayer.activePlayerList)
{
DroneController.RemoveFromPlayer(player);
}
}
private void OnEntitySpawned(Drone drone)
{
if (!IsDroneEligible(drone))
return;
var drone2 = drone;
// Delay to give other plugins a moment to cache the drone id so they can block this.
NextTick(() =>
{
if (drone2 == null || drone2.IsDestroyed)
return;
MaybeAutoDeployChair(drone2);
});
}
private void OnEntityBuilt(Planner planner, GameObject go)
{
if (planner == null || go == null)
return;
var drone = go.ToBaseEntity() as Drone;
if (drone == null)
return;
var player = planner.GetOwnerPlayer();
if (player == null)
return;
var drone2 = drone;
NextTick(() =>
{
// Delay this check to allow time for other plugins to deploy an entity to this slot.
if (drone2 == null || player == null)
return;
if (SignSlots.IsCompatibleWithHost(drone2)
&& permission.UserHasPermission(player.UserIDString, PermissionSignDeploy)
&& UnityEngine.Random.Range(0, 100) < _config.SignTipChance)
{
ChatMessage(player, Lang.TipDeploySignCommand);
}
if (ChairSlots.IsCompatibleWithHost(drone2)
&& permission.UserHasPermission(player.UserIDString, PermissionChairDeploy)
&& !permission.UserHasPermission(player.UserIDString, PermissionChairAutoDeploy)
&& UnityEngine.Random.Range(0, 100) < _config.ChairTipChance)
{
ChatMessage(player, Lang.TipDeployChairCommand);
}
});
}
private object OnEntityTakeDamage(BaseChair mountable, HitInfo info)
{
if (mountable.PrefabName != PassengerChairPrefab)
return null;
var drone = GetParentDrone(mountable);
if (drone == null)
return null;
drone.Hurt(info);
return True;
}
private object OnEntityTakeDamage(Signage sign, HitInfo info)
{
var drone = GetParentDrone(sign);
if (drone == null)
return null;
drone.Hurt(info);
return True;
}
// Allow swapping between the seating modes
private void OnServerCommand(ConsoleSystem.Arg arg)
{
if (arg.Connection == null || arg.cmd.FullName != "vehicle.swapseats")
return;
var player = arg.Player();
if (player == null)
return;
var drone = GetMountedDrone(player, out var currentChair);
if (drone == null)
return;
// Only players with the pilot permission may switch chairs.
if (!permission.UserHasPermission(player.UserIDString, PermissionChairPilot))
return;
if (!TryGetChairs(drone, out var pilotChair, out var passengerChair))
return;
var desiredChair = currentChair == passengerChair
? pilotChair
: passengerChair;
SwitchToChair(player, currentChair, desiredChair);
}
private void OnEntityMounted(BaseMountable currentChair, BasePlayer player)
{
var drone = GetParentDrone(currentChair);
if (drone == null)
return;
if (!TryGetChairs(drone, out var pilotChair, out var passengerChair))
return;
// The rest of the logic is only for pilots.
if (!permission.UserHasPermission(player.UserIDString, PermissionChairPilot))
return;
var isPilotChair = currentChair == pilotChair;
if (isPilotChair)
{
// Since the passenger chair is the mount ingress, prevent it from being mounted while the pilot chair is mounted.
using var flagsScope = passengerChair.StartSetFlags(BaseEntity.FlagsUpdateMode.SendNetworkUpdate_Flags);
flagsScope.Set(BaseEntity.Flags.Busy, true);
}
else if (!DroneController.Exists(player))
{
// The player is mounting the drone fresh (not switching chairs), so automatically switch to the pilot chair.
SwitchToChair(player, currentChair, pilotChair);
return;
}
DroneController.Mount(this, player, drone, isPilotChair);
}
private void OnEntityDismounted(BaseMountable previousChair, BasePlayer player)
{
var drone = GetParentDrone(previousChair);
if (drone == null)
return;
if (!TryGetChairs(drone, out var pilotChair, out var passengerChair))
return;
if (previousChair == pilotChair)
{
// Since the passenger chair is the mount ingress, re-enable it when the pilot chair is dismounted.
using var flagsScope = passengerChair.StartSetFlags(BaseEntity.FlagsUpdateMode.SendNetworkUpdate_Flags);
flagsScope.Set(BaseEntity.Flags.Busy, false);
}
DroneController.Dismount(this, player, drone);
}
private void OnPlayerDismountFailed(BasePlayer player, BaseMountable mountable)
{
var drone = GetMountedDrone(player);
if (drone == null)
return;
var droneTransform = drone.transform;
droneTransform.rotation = Quaternion.Euler(0, droneTransform.rotation.eulerAngles.y, 0);
}
private object CanPickupEntity(BasePlayer player, Drone drone)
{
if (CanPickupInternal(drone, out var errorLangKey))
return null;
ChatMessage(player, errorLangKey);
return False;
}
private object CanPickupEntity(BasePlayer player, Signage sign)
{
if (CanPickupInternal(sign, out var errorLangKey))
return null;
ChatMessage(player, errorLangKey);
return False;
}
private object CanPickupEntity(BasePlayer player, BaseChair chair)
{
if (CanPickupInternal(chair, out var errorLangKey))
return null;
ChatMessage(player, errorLangKey);
return False;
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private Dictionary<string, object> OnRemovableEntityInfo(Signage sign, BasePlayer player)
{
if (!IsDroneSign(sign))
return null;
_signRemoveInfo["DisplayName"] = GetMessage(player.UserIDString, Lang.InfoSignName);
if (sign.pickup.enabled)
{
_signRemoveInfo["Refund"] = _refundInfo;
}
else
{
_signRemoveInfo.Remove("Refund");
}
return _signRemoveInfo;
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private Dictionary<string, object> OnRemovableEntityInfo(BaseChair chair, BasePlayer player)
{
if (!IsDroneChair(chair))
return null;
_chairRemoveInfo["DisplayName"] = GetMessage(player.UserIDString, Lang.InfoChairName);
if (chair.pickup.enabled)
{
_chairRemoveInfo["Refund"] = _chairRefundInfo;
}
else
{
_chairRemoveInfo.Remove("Refund");
}
return _chairRemoveInfo;
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private string canRemove(BasePlayer player, Drone drone)
{
if (CanPickupInternal(drone, out var errorLangKey))
return null;
return GetMessage(player.UserIDString, errorLangKey);
}
// This hook is exposed by plugin: Drone Settings (DroneSettings).
private string OnDroneTypeDetermine(Drone drone, List<string> droneTypeList)
{
var droneType = DetermineDroneType(drone);
if (droneType == null)
return null;
if (droneTypeList == null)
return droneType;
droneTypeList.Add(droneType);
return null;
}
#endregion
#region Exposed Hooks
private static class ExposedHooks
{
public static object OnDroneSignDeploy(Drone drone, BasePlayer deployer)
{
return Interface.CallHook("OnDroneSignDeploy", drone, deployer);
}
public static void OnDroneSignDeployed(Drone drone, BasePlayer deployer)
{
Interface.CallHook("OnDroneSignDeployed", drone, deployer);
}
public static object OnDroneChairDeploy(Drone drone, BasePlayer deployer)
{
return Interface.CallHook("OnDroneChairDeploy", drone, deployer);
}
public static void OnDroneChairDeployed(Drone drone, BasePlayer deployer)
{
Interface.CallHook("OnDroneChairDeployed", drone, deployer);
}
public static void OnDroneControlStarted(Drone drone, BasePlayer player)
{
Interface.CallHook("OnDroneControlStarted", drone, player);
}
public static void OnDroneControlEnded(Drone drone, BasePlayer player)
{
Interface.CallHook("OnDroneControlEnded", drone, player);
}
}
#endregion
#region Commands
[Command("dronesign")]
private void DroneSignCommand(IPlayer player)
{
if (!VerifyPlayer(player, out var basePlayer)
|| !VerifyPermission(player, PermissionSignDeploy)
|| !VerifyDroneFound(player, out var drone))
return;
if (GetDroneSign(drone) != null)
{
ReplyToPlayer(player, Lang.ErrorAlreadyHasSign);
return;
}
if (!VerifyDroneHasSlotVacant(player, drone, SignSlots))
return;
var isFree = player.HasPermission(PermissionSignDeployFree);
if (!isFree && basePlayer.inventory.FindItemByItemID(SignItemId) == null)
{
ReplyToPlayer(player, Lang.ErrorNoSignItem);
return;
}
if (TryDeploySign(drone, basePlayer, allowRefund: !isFree) == null)
{
ReplyToPlayer(player, Lang.ErrorDeploySignFailed);
}
else if (!isFree)
{
basePlayer.inventory.Take(null, SignItemId, 1);
basePlayer.Command("note.inv", SignItemId, -1);
}
}
[Command("dronechair", "droneseat")]
private void DroneChairCommand(IPlayer player)
{
if (!VerifyPlayer(player, out var basePlayer)
|| !VerifyPermission(player, PermissionChairDeploy)
|| !VerifyDroneFound(player, out var drone))
return;
if (HasChair(drone))
{
ReplyToPlayer(player, Lang.ErrorAlreadyHasChair);
return;
}
if (!VerifyDroneHasSlotVacant(player, drone, ChairSlots))
return;
var isFree = player.HasPermission(PermissionChairDeployFree);
if (!isFree && basePlayer.inventory.FindItemByItemID(ChairItemId) == null)
{
ReplyToPlayer(player, Lang.ErrorNoChairItem);
return;
}
if (TryDeployChairs(drone, basePlayer, allowRefund: !isFree) == null)
{
ReplyToPlayer(player, Lang.ErrorDeployChairFailed);
}
else if (!isFree)
{
basePlayer.inventory.Take(null, ChairItemId, 1);
basePlayer.Command("note.inv", ChairItemId, -1);
}
}
#endregion
#region Helper Methods - Command Checks
private bool VerifyPlayer(IPlayer player, out BasePlayer basePlayer)
{
basePlayer = player.Object as BasePlayer;
return !player.IsServer && basePlayer != null;
}
private bool VerifyPermission(IPlayer player, string perm)
{
if (player.HasPermission(perm))
return true;
ReplyToPlayer(player, Lang.ErrorNoPermission);
return false;
}
private bool VerifyDroneFound(IPlayer player, out Drone drone)
{
var basePlayer = player.Object as BasePlayer;
drone = GetLookEntity(basePlayer, 3) as Drone;
if (drone != null && IsDroneEligible(drone))
return true;
ReplyToPlayer(player, Lang.ErrorNoDroneFound);
return false;
}
private bool VerifyDroneHasSlotVacant(IPlayer player, Drone drone, SlotConfig slotConfig)
{
if (slotConfig.IsCompatibleWithHost(drone))
return true;
ReplyToPlayer(player, Lang.ErrorIncompatibleAttachment);
return false;
}
#endregion
#region Helpers
private static class RCUtils
{
public static bool IsRCDrone(Drone drone)
{
return drone is not DeliveryDrone;
}
}
private static bool IsDroneEligible(Drone drone)
{
return drone.skinID == 0 && RCUtils.IsRCDrone(drone);
}
private static Drone GetParentDrone(BaseEntity entity)
{
return entity.GetParentEntity() as Drone;
}
private static Drone GetMountedDrone(BasePlayer player, out BaseMountable currentChair)
{
currentChair = player.GetMounted();
if (currentChair == null)
return null;
return currentChair.PrefabName is PilotChairPrefab or PassengerChairPrefab
? GetParentDrone(currentChair)
: null;
}
private static Drone GetMountedDrone(BasePlayer player)
{
return GetMountedDrone(player, out _);
}
private static Signage GetDroneSign(Drone drone)
{
return SignSlots.GetOccupant(drone) as Signage;
}
private static bool IsDroneSign(Signage sign)
{
return GetParentDrone(sign) != null;
}
private static bool TryGetChairs(Drone drone, out BaseMountable pilotChair, out BaseMountable passengerChair)
{
pilotChair = null;
passengerChair = null;
foreach (var child in drone.children)
{
var mountable = child as BaseMountable;
if (mountable == null)
continue;
switch (mountable.PrefabName)
{
case PilotChairPrefab:
pilotChair = mountable;
break;
case PassengerChairPrefab:
passengerChair = mountable;
break;
}
}
return pilotChair != null && passengerChair != null;
}
private static bool TryGetPassengerChair(Drone drone, out BaseMountable passengerChair)
{
return TryGetChairs(drone, out _, out passengerChair);
}
private static bool HasChair(Drone drone)
{
return TryGetPassengerChair(drone, out _);
}
private static bool IsDroneChair(BaseChair chair)
{
return GetParentDrone(chair) != null;
}
private static void RemoveProblemComponents(BaseEntity entity)
{
foreach (var collider in entity.GetComponentsInChildren<MeshCollider>())
{
UnityEngine.Object.DestroyImmediate(collider);
}
}
private static void SetupChair(BaseMountable mountable)
{
if (!BaseMountable.AllMountables.Contains(mountable))
{
BaseMountable.AllMountables.Add(mountable);
}
mountable.isMobile = true;
mountable.EnableSaving(true);
RemoveProblemComponents(mountable);
}
private static BaseEntity GetLookEntity(BasePlayer basePlayer, float maxDistance = 3)
{
return Physics.Raycast(basePlayer.eyes.HeadRay(), out var hit, maxDistance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore)
? hit.GetEntity()
: null;
}
private static void SwitchToChair(BasePlayer player, BaseMountable currentChair, BaseMountable desiredChair)
{
currentChair.DismountPlayer(player, lite: true);
desiredChair.MountPlayer(player);
}
private static string DetermineDroneType(Drone drone)
{
if (GetDroneSign(drone) != null)
return "DroneSign";
if (HasChair(drone))
return "DroneChair";
return null;
}
private static bool CanPickupInternal(Drone drone, out string errorLangKey)
{
errorLangKey = null;
if (!RCUtils.IsRCDrone(drone))
return true;
// Prevent drone pickup while there is a sign attached that can be picked up.
var sign = GetDroneSign(drone);
if (sign != null)
{
errorLangKey = Lang.ErrorCannotPickupWithSign;
return !sign.pickup.enabled;
}
// Prevent drone pickup while there is a chair attached that can be picked up.
if (TryGetPassengerChair(drone, out var passengerChair))
{
errorLangKey = Lang.ErrorCannotPickupWithChair;
return !passengerChair.pickup.enabled;
}
return true;
}
private static bool CanPickupInternal(Signage sign, out string errorLangKey)
{
errorLangKey = null;
if (!IsDroneSign(sign))
return true;
errorLangKey = Lang.ErrorCannotPickupAttachment;
return sign.pickup.enabled;
}
private static bool CanPickupInternal(BaseChair chair, out string errorLangKey)
{
errorLangKey = null;
if (!IsDroneChair(chair))
return true;
errorLangKey = Lang.ErrorCannotPickupAttachment;
return chair.pickup.enabled;
}
private void RefreshDroneSettingsProfile(Drone drone)
{
DroneSettings?.Call("API_RefreshDroneProfile", drone);
}
private void SetupSign(Drone drone, Signage sign)
{
drone.playerCheckRadius = 0;
// Damage will be processed by the drone.
sign.baseProtection = null;
UnityEngine.Object.DestroyImmediate(sign.GetComponent<GroundWatch>());
UnityEngine.Object.DestroyImmediate(sign.GetComponent<DestroyOnGroundMissing>());
SignComponent.AddToDrone(this, drone, sign);
}
private Signage TryDeploySign(Drone drone, BasePlayer deployer = null, bool allowRefund = false)
{
if (ExposedHooks.OnDroneSignDeploy(drone, deployer) is false)
return null;
var sign = GameManager.server.CreateEntity(SmallWoodenSignPrefab, SignLocalPosition, Quaternion.Euler(SignLocalRotationAngles)) as Signage;
if (sign == null)
return null;
SetupSign(drone, sign);
if (deployer != null)
{
sign.OwnerID = deployer.userID;
}
sign.SetParent(drone);
sign.Spawn();
// Claim slots to prevent deploying incompatible attachments.
SignSlots.OccupyHost(drone, sign);
// This flag is used to remember whether the sign should be refundable.
// This information is lost on restart but that's a minor concern.
sign.pickup.enabled = allowRefund;
Effect.server.Run(DeploySignEffectPrefab, sign.transform.position);
ExposedHooks.OnDroneSignDeployed(drone, deployer);
RefreshDroneSettingsProfile(drone);
return sign;
}
private void SetupAllChairs(Drone drone, BaseMountable pilotChair, BaseMountable passengerChair)
{
SetupChair(pilotChair);
SetupChair(passengerChair);
pilotChair.dismountPositions = passengerChair.dismountPositions;
// Damage will be processed by the drone.
passengerChair.baseProtection = null;
UnityEngine.Object.DestroyImmediate(passengerChair.GetComponent<GroundWatch>());
UnityEngine.Object.DestroyImmediate(passengerChair.GetComponent<DestroyOnGroundMissing>());
// Box colliders on the deployable chair block dismount from the pilot chair.
foreach (var collider in passengerChair.GetComponentsInChildren<BoxCollider>())
{
UnityEngine.Object.DestroyImmediate(collider);
}
ChairComponent.AddToDrone(this, drone, pilotChair, passengerChair);
}
private BaseMountable SpawnPassengerChair(Drone drone, BasePlayer deployer = null, bool allowRefund = false)
{
var passengerChair = GameManager.server.CreateEntity(PassengerChairPrefab, PassengerChairLocalPosition) as BaseMountable;
if (passengerChair == null)
return null;
passengerChair.pickup.enabled = allowRefund;
if (deployer != null)
{
passengerChair.OwnerID = deployer.userID;
}
passengerChair.SetParent(drone);
passengerChair.Spawn();
// Claim slots to prevent deploying incompatible attachments.
ChairSlots.OccupyHost(drone, passengerChair);
return passengerChair;
}
private BaseMountable TryDeployChairs(Drone drone, BasePlayer deployer = null, bool allowRefund = false)
{
var hookResult = ExposedHooks.OnDroneChairDeploy(drone, deployer);
if (hookResult is false)
return null;
// The driver chair is ideal for mouse movement since it locks the player view angles.
var pilotChair = GameManager.server.CreateEntity(PilotChairPrefab, PilotChairLocalPosition) as BaseMountable;
if (pilotChair == null)
return null;
pilotChair.SetParent(drone);
pilotChair.Spawn();
// The passenger chair shows the "mount" prompt and allows for unlocking view angles.
var passengerChair = SpawnPassengerChair(drone, deployer, allowRefund);
if (passengerChair == null)
{
pilotChair.Kill();
return null;
}
SetupAllChairs(drone, pilotChair, passengerChair);
Effect.server.Run(ChairDeployEffectPrefab, passengerChair.transform.position);
ExposedHooks.OnDroneChairDeployed(drone, deployer);
RefreshDroneSettingsProfile(drone);
_chairDrones.Add(drone);
return passengerChair;
}
private void MaybeRefreshDroneSign(Drone drone)
{
var sign = GetDroneSign(drone);
if (sign == null)
return;
SetupSign(drone, sign);
}
private void MaybeAutoDeployChair(Drone drone)
{
if (drone.OwnerID == 0
|| !ChairSlots.IsCompatibleWithHost(drone)
|| !permission.UserHasPermission(drone.OwnerID.ToString(), PermissionChairAutoDeploy))
return;
TryDeployChairs(drone);
}
private void ReplaceOldChairs(Drone drone)
{
BaseMountable pilotChair = null;
BaseMountable passengerChair = null;
BaseMountable oldPassengerChair = null;
BaseMountable oldVisibleChair = null;
foreach (var child in drone.children)
{
var mountable = child as BaseMountable;
if (mountable == null)
continue;
switch (mountable.PrefabName)
{
case PilotChairPrefab:
pilotChair = mountable;
break;
case PassengerChairPrefab:
passengerChair = mountable;
break;
case DeprecatedPassengerChairPrefab:
oldPassengerChair = mountable;
break;
case DeprecatedVisibleChairPrefab:
oldVisibleChair = mountable;
break;
}
}
// Skip if the pilot chair is not present, as that indicates this is not a ridable drone.
if (pilotChair is null)
return;
// Skip if the new passenger chair is already present.
if (passengerChair is not null)
return;
// Skip if either the old passenger or visible chairs are not present.
if (oldPassengerChair == null || oldVisibleChair == null)
return;
var mountedPlayer = oldPassengerChair.GetMounted();
if (mountedPlayer != null)
{
mountedPlayer.EnsureDismounted();
}
passengerChair = SpawnPassengerChair(drone);
passengerChair.OwnerID = oldPassengerChair.OwnerID;
passengerChair.pickup.enabled = oldPassengerChair.pickup.enabled;
oldPassengerChair.Kill();
oldVisibleChair.Kill();
if (mountedPlayer is not null)
{
passengerChair.AttemptMount(mountedPlayer, doMountChecks: false);
}
}
private void MaybeAddOrRefreshChairs(Drone drone)
{
ReplaceOldChairs(drone);
if (!TryGetChairs(drone, out var pilotChair, out var passengerChair))
{
MaybeAutoDeployChair(drone);
return;
}
SetupAllChairs(drone, pilotChair, passengerChair);
RefreshDroneSettingsProfile(drone);
_chairDrones.Add(drone);
}
private class ChairComponent : FacepunchBehaviour
{
public static void AddToDrone(RidableDrones plugin, Drone drone, BaseMountable pilotChair, BaseMountable passengerChair)
{
var component = passengerChair.gameObject.AddComponent<ChairComponent>();
component._plugin = plugin;
component._drone = drone;
component._chairs = new[] { pilotChair, passengerChair };
component.CreateCollider(drone, passengerChair);
}
public static void RemoveFromChair(BaseMountable chair)
{
var component = chair.gameObject.GetComponent<ChairComponent>();
if (component == null)
return;
component._isUnloading = true;
DestroyImmediate(component);
}
private const float ColliderHeight = 3;
private RidableDrones _plugin;
private BaseMountable[] _chairs;
private BaseEntity _drone;
private GameObject _child;
private bool _isUnloading;
private void CreateCollider(Drone drone, BaseMountable passengerChair)
{
var centerOfMass = drone.body.centerOfMass;
_child = passengerChair.gameObject.CreateChild();
// Layers that seem to work as desired (no player collision): 9, 12, 15, 20, 22, 26.
_child.gameObject.layer = (int)Rust.Layer.Vehicle_World;
_child.transform.localPosition = new Vector3(0, ColliderHeight / 4, 0);
var collider = _child.AddComponent<BoxCollider>();
var droneExtents = drone.bounds.extents;
collider.size = droneExtents.WithY(ColliderHeight / 2);
drone.body.centerOfMass = centerOfMass;
}
private void OnDestroy()
{
if (_child != null)
{
Destroy(_child);
}
if (!_isUnloading)
{
foreach (var chair in _chairs)
{
if (chair == null || chair.IsDestroyed)
continue;
chair.Kill();
}
}
_plugin._chairDrones.Remove(_drone);
_plugin._mountedChairDrones.Remove(_drone);
}
}
#endregion
#region Dynamic Hooks
private interface IGate
{
bool Enabled { get; }
}
private interface IObservable
{
event Action OnChange;
}
private interface IObservableGate : IGate, IObservable {}
private class ObservableHashSet<T> : HashSet<T>, IObservable
{
public event Action OnChange;
public new bool Add(T item)
{
var added = base.Add(item);
if (added)
{
OnChange?.Invoke();
}
return added;
}
public new bool Remove(T item)
{
var removed = base.Remove(item);
if (removed)
{
OnChange?.Invoke();
}
return removed;
}
public new void Clear()
{
if (Count > 0)
return;
base.Clear();
OnChange?.Invoke();
}
}
private class ObservableGate : IObservableGate
{
public event Action OnChange;
private readonly Func<bool> _enableWhen;
public bool Enabled => _enableWhen();
public ObservableGate(IObservable observable, Func<bool> enableWhen)
{
_enableWhen = enableWhen;
observable.OnChange += HandleChange;
}
private void HandleChange()
{
OnChange?.Invoke();
}
}
private class MultiObservableGate : IObservableGate
{
public event Action OnChange;
private readonly IObservableGate[] _gates;
public bool Enabled
{
get
{
foreach (var gate in _gates)
{
if (gate.Enabled)
return true;
}
return false;
}
}
public MultiObservableGate(params IObservableGate[] gates)
{
_gates = gates;
var handleChange = new Action(HandleChange);
foreach (var gate in gates)
{
gate.OnChange += handleChange;
}
}
private void HandleChange()
{
OnChange?.Invoke();
}
}
private class GatedHookCollection
{
public bool IsSubscribed { get; private set; } = true;
private readonly RidableDrones _plugin;
private readonly IObservableGate _gate;
private readonly string[] _hookNames;
public GatedHookCollection(RidableDrones plugin, IObservableGate gate, params string[] hookNames)
{
_plugin = plugin;
_gate = gate;
_hookNames = hookNames;
gate.OnChange += Refresh;
}
public void Subscribe()
{
foreach (var hookName in _hookNames)
{
_plugin.Subscribe(hookName);
}
IsSubscribed = true;
}
public void Unsubscribe()
{
foreach (var hookName in _hookNames)
{
_plugin.Unsubscribe(hookName);
}
IsSubscribed = false;
}
public void Refresh(bool shouldSubscribe)
{
if (shouldSubscribe)
{
if (!IsSubscribed)
{
Subscribe();
}
}
else if (IsSubscribed)
{
Unsubscribe();
}
}
public void Refresh()
{
Refresh(_gate.Enabled);
}
}
#endregion
#region Slot Config
private class SlotConfig
{
public readonly BaseEntity.Slot[] Slots;
public SlotConfig(params BaseEntity.Slot[] slots)
{
if (slots.Length == 0)
throw new ArgumentOutOfRangeException(nameof(slots), "Must not be empty");
Slots = slots;
}
public BaseEntity GetOccupant(BaseEntity host)
{
return host.GetSlot(Slots[0]);
}
public bool IsCompatibleWithHost(BaseEntity host)
{
foreach (var slot in Slots)
{
if (host.GetSlot(slot) != null)
return false;
}
return true;
}
public void OccupyHost(BaseEntity host, BaseEntity occupant)
{
foreach (var slot in Slots)
{
host.SetSlot(slot, occupant);
}
}
}
#endregion
#region Parent Trigger
private class SignTriggerParentEnclosed : TriggerParentEnclosed
{
public static SignTriggerParentEnclosed AddToDrone(Drone drone, GameObject host)
{
var component = host.gameObject.AddComponent<SignTriggerParentEnclosed>();
component._drone = drone;
return component;
}
private Drone _drone;
public override bool ShouldParent(BaseEntity entity, bool bypassOtherTriggerCheck = false)
{
// This avoids the drone trying to parent itself when using the Targetable Drones plugin.
// Targetable Drones uses a child object with the player layer, which the parent trigger is interested in.
// This also avoids other drones being parented which can create some problems such as recursive parenting.
if (entity is Drone)
return false;
var player = entity as BasePlayer;
if ((object)player != null && Vector3.Dot(Vector3.up, _drone.transform.up) < 0.8f)
return false;
return base.ShouldParent(entity, bypassOtherTriggerCheck);
}
}
private class SignComponent : EntityComponent<BaseEntity>
{
public static void AddToDrone(RidableDrones plugin, Drone drone, Signage sign)
{
var component = sign.gameObject.AddComponent<SignComponent>();
component._plugin = plugin;
component._drone = drone;
component.CreateParentTrigger(drone, sign);
plugin._signDrones.Add(drone);
}
public static void RemoveFromSign(Signage sign)
{
DestroyImmediate(sign.gameObject.GetComponent<SignComponent>());
}
private const float ColliderHeight = 1.8f;
private RidableDrones _plugin;
private Drone _drone;
private GameObject _triggerHost;
private void CreateParentTrigger(Drone drone, Signage sign)
{
var signExtents = sign.bounds.extents;
var colliderExtents = new Vector3(signExtents.x, ColliderHeight / 2f, signExtents.y);
_triggerHost = drone.gameObject.CreateChild();
_triggerHost.transform.localPosition += new Vector3(0, colliderExtents.y, 0);
// Without this hack, the drone's sweep test can collide with other entities using the
// parent trigger collider, causing the drone to occasionally reduce altitude.
_triggerHost.GetOrAddComponent<Rigidbody>().isKinematic = true;
var triggerCollider = _triggerHost.gameObject.AddComponent<BoxCollider>();
triggerCollider.isTrigger = true;
triggerCollider.gameObject.layer = (int)Rust.Layer.Trigger;
triggerCollider.size = 2 * colliderExtents;
var triggerParent = SignTriggerParentEnclosed.AddToDrone(drone, _triggerHost);
triggerParent.intersectionMode = TriggerParentEnclosed.TriggerMode.PivotPoint;
triggerParent.interestLayers = Rust.Layers.Mask.Player_Server;
}
private void OnDestroy()
{
if (_triggerHost != null)
{
Destroy(_triggerHost);
}
_plugin._signDrones.Remove(_drone);
}
}
#endregion
#region DroneController
private class DroneController : FacepunchBehaviour
{
public static bool Exists(BasePlayer player)
{
return player.GetComponent<DroneController>() != null;
}
public static void Mount(RidableDrones plugin, BasePlayer player, Drone drone, bool isPilotChair)
{
var component = player.GetComponent<DroneController>();
var alreadyExists = component != null;
if (!alreadyExists)
{
component = player.gameObject.AddComponent<DroneController>();
}
component.OnMount(player, drone, isPilotChair);
if (!alreadyExists)
{
ExposedHooks.OnDroneControlStarted(drone, player);
}
plugin._mountedChairDrones.Add(drone);
}
public static void Dismount(RidableDrones plugin, BasePlayer player, Drone drone)
{
player.GetComponent<DroneController>()?.OnDismount();
plugin._mountedChairDrones.Remove(drone);
}
public static void RemoveFromPlayer(BasePlayer player)
{
DestroyImmediate(player.GetComponent<DroneController>());
}
private Drone _drone;
private BasePlayer _controller;
private CameraViewerId _viewerId;
private bool _isPilotChair;
private void DelayedDestroy() => DestroyImmediate(this);
private void OnMount(BasePlayer controller, Drone drone, bool isPilotChair)
{
// If they were swapping chairs, cancel destroying this component.
CancelInvoke(DelayedDestroy);
_drone = drone;
_controller = controller;
_viewerId = new CameraViewerId(controller.userID, 0);
_isPilotChair = isPilotChair;
if (drone.ControllingViewerId.HasValue)
{
drone.StopControl(drone.ControllingViewerId.Value);
}
drone.InitializeControl(_viewerId);
drone.playerCheckRadius = 0;
}
// Do
// ... [Code preview truncated due to length. Full source in attached file] ...