C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
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.0")]
[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);
HitNotify(drone, info);
return True;
}
private object OnEntityTakeDamage(Signage sign, HitInfo info) {
var drone = GetParentDrone(sign);
if (drone == null) return null;
drone.Hurt(info);
HitNotify(drone, 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.passengerChair.SetFlag(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.passengerChair.SetFlag(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 HitNotify(BaseEntity entity, HitInfo info) {
var player = info.Initiator as BasePlayer;
if (player == null) return;
entity.ClientRPCPlayer(null, player, "HitNotify");
}
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;
}
// Don't destroy the component immediately, in case the player is swapping chairs.private void OnDismount() => Invoke(DelayedDestroy, 0);
private void Update() {
if (_drone == null || _drone.IsDestroyed) {
DestroyImmediate(this);
return;
}
// Optimization: Skip if there was no user input this frame.if (_controller.lastTickTime < Time.time)
return;
_drone.UserInput(_controller.serverInput, _viewerId);
if (!_isPilotChair) {
// In hybrid mode, move relative to the direction the player is facing, instead of relative to the direction the drone is facing.var worldDirection = _drone.transform.InverseTransformVector(_drone.currentInput.movement);
var playerRotation = Quaternion.Euler(0, _controller.viewAngles.y, 0);
_drone.currentInput.movement = playerRotation * worldDirection;
}
}
private void OnDestroy() {
if (_drone != null && !_drone.IsDestroyed) {
if (_drone.ControllingViewerId.HasValue) {
_drone.StopControl(_viewerId);
}
ExposedHooks.OnDroneControlEnded(_drone, _controller);
}
}
}
#endregion
#region Configuration
private class Configuration : BaseConfiguration {
[JsonProperty("Chair tip chance")]
public int ChairTipChance = 25;
[JsonProperty("Sign tip chance")]
public int SignTipChance = 25;
[JsonProperty("TipChance")]
private int DeprecatedTipChance {
set {
ChairTipChance = SignTipChance = value;
}
}
}
private Configuration GetDefaultConfig() => new();
#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) {
// ... [Code preview truncated due to length. Full source in attached file] ...