From e771501cd5af00857fc301fb8988b8149f4a9fab Mon Sep 17 00:00:00 2001 From: Arys Date: Fri, 19 Jan 2024 08:47:52 +0000 Subject: [PATCH] BTR Cover Fire service and roadkill trigger implemented (!62) Todo: * Taxi service * Refinement of BTR turret aim/shooting * BTR should switch targets if it has no line of sight to current target * Blacklist player from entering BTR after antagonising it * Perhaps make blacklist persist for 3 raids (apparently on Live?) * Code refactor because it currently looks like doodoo Co-authored-by: Nympfonic Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Modules/pulls/62 Co-authored-by: Arys Co-committed-by: Arys --- project/Aki.Debugging/BTR/BTRManager.cs | 237 ++++++++++-------- .../Aki.Debugging/BTR/BTRRoadKillTrigger.cs | 38 +++ .../BTR/Patches/BTRBotInitPatch.cs | 12 +- .../BTR/Patches/BTRReceiveDamageInfoPatch.cs | 13 +- project/Aki.Debugging/BTR/Utils/BTRUtil.cs | 2 - .../TraderServices/TraderServicesManager.cs | 4 + 6 files changed, 187 insertions(+), 119 deletions(-) create mode 100644 project/Aki.Debugging/BTR/BTRRoadKillTrigger.cs diff --git a/project/Aki.Debugging/BTR/BTRManager.cs b/project/Aki.Debugging/BTR/BTRManager.cs index fc5031e..cedebe7 100644 --- a/project/Aki.Debugging/BTR/BTRManager.cs +++ b/project/Aki.Debugging/BTR/BTRManager.cs @@ -11,13 +11,15 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; +using Random = UnityEngine.Random; +using BotEventHandler = GClass595; namespace Aki.Debugging.BTR { public class BTRManager : MonoBehaviour { private GameWorld gameWorld; - private BotsController botsController; + private BotEventHandler botEventHandler; private BotBTRService btrBotService; private BTRControllerClass btrController; @@ -30,23 +32,22 @@ namespace Aki.Debugging.BTR private EPlayerBtrState previousPlayerBtrState; private BTRSide lastInteractedBtrSide; public BTRSide LastInteractedBtrSide => lastInteractedBtrSide; - + + private Coroutine _coverFireTimerCoroutine; private BTRTurretServer btrTurretServer; private Transform btrTurretDefaultTargetTransform; private Coroutine _shootingTargetCoroutine; + private IPlayer currentTarget = null; private bool isShooting = false; private BulletClass btrMachineGunAmmo; private Item btrMachineGunWeapon; private MethodInfo _updateTaxiPriceMethod; - private Dictionary> ServicePurchasedDict { get; set; } - BTRManager() { Type btrControllerType = typeof(BTRControllerClass); _updateTaxiPriceMethod = AccessTools.GetDeclaredMethods(btrControllerType).Single(IsUpdateTaxiPriceMethod); - ServicePurchasedDict = new Dictionary>(); } // Find `BTRControllerClass.method_9(PathDestination currentDestinationPoint, bool lastRoutePoint)` @@ -55,32 +56,6 @@ namespace Aki.Debugging.BTR return (method.GetParameters().Length == 2 && method.GetParameters()[0].ParameterType == typeof(PathDestination)); } - public bool IsServicePurchased(ETraderServiceType serviceType, string traderId) - { - if (ServicePurchasedDict.TryGetValue(serviceType, out var traderDict)) - { - if (traderDict.TryGetValue(traderId, out var result)) - { - return result; - } - } - - return false; - } - - public void SetServicePurchased(ETraderServiceType serviceType, string traderId) - { - if (ServicePurchasedDict.TryGetValue(serviceType, out var traderDict)) - { - traderDict[traderId] = true; - } - else - { - ServicePurchasedDict[serviceType] = new Dictionary(); - ServicePurchasedDict[serviceType][traderId] = true; - } - } - private void Start() { try @@ -90,6 +65,7 @@ namespace Aki.Debugging.BTR if (gameWorld == null) { Destroy(this); + return; } if (gameWorld.BtrController == null) @@ -121,65 +97,48 @@ namespace Aki.Debugging.BTR { btrBotShooter = btrController.BotShooterBtr; btrBotService.Reset(); // Player will be added to Neutrals list and removed from Enemies list + TraderServicesManager.Instance.OnTraderServicePurchased += TraderServicePurchased; btrBotShooterInitialized = true; } if (btrController.BotShooterBtr == null) return; - if (IsAimingAtTarget() && !isShooting) + if (HasTarget() && IsAimingAtTarget() && !isShooting) { _shootingTargetCoroutine = StaticManager.BeginCoroutine(ShootTarget()); } } - public void HandleBtrDoorState(EPlayerBtrState playerBtrState) - { - if (previousPlayerBtrState == EPlayerBtrState.Approach && playerBtrState == EPlayerBtrState.GoIn - || previousPlayerBtrState == EPlayerBtrState.Inside && playerBtrState == EPlayerBtrState.GoOut) - { - // Open Door - UpdateBTRSideDoorState(1); - } - else if (previousPlayerBtrState == EPlayerBtrState.GoIn && playerBtrState == EPlayerBtrState.Inside - || previousPlayerBtrState == EPlayerBtrState.GoOut && playerBtrState == EPlayerBtrState.Outside) - { - // Close Door - UpdateBTRSideDoorState(0); - } - - previousPlayerBtrState = playerBtrState; - } - // Please tell me there's a better way than this xd public void OnPlayerInteractDoor(PlayerInteractPacket interactPacket) { - var playerGoIn = interactPacket.InteractionType == EInteractionType.GoIn; - var playerGoOut = interactPacket.InteractionType == EInteractionType.GoOut; + btrServerSide.LeftSlot0State = 0; + btrServerSide.LeftSlot1State = 0; + btrServerSide.RightSlot0State = 0; + btrServerSide.RightSlot1State = 0; - if (interactPacket.SideId == 0) + bool playerGoIn = interactPacket.InteractionType == EInteractionType.GoIn; + + if (interactPacket.SideId == 0 && playerGoIn) { if (interactPacket.SlotId == 0) { - if (playerGoIn) btrServerSide.LeftSlot0State = 1; - else if (playerGoOut) btrServerSide.LeftSlot0State = 0; + btrServerSide.LeftSlot0State = 1; } else if (interactPacket.SlotId == 1) { - if (playerGoIn) btrServerSide.LeftSlot1State = 1; - else if (playerGoOut) btrServerSide.LeftSlot1State = 0; + btrServerSide.LeftSlot1State = 1; } } - else if (interactPacket.SideId == 1) + else if (interactPacket.SideId == 1 && playerGoIn) { if (interactPacket.SlotId == 0) { - if (playerGoIn) btrServerSide.RightSlot0State = 1; - else if (playerGoOut) btrServerSide.RightSlot0State = 0; + btrServerSide.RightSlot0State = 1; } else if (interactPacket.SlotId == 1) { - if (playerGoIn) btrServerSide.RightSlot1State = 1; - else if (playerGoOut) btrServerSide.RightSlot1State = 0; + btrServerSide.RightSlot1State = 1; } } } @@ -187,19 +146,20 @@ namespace Aki.Debugging.BTR private void InitBTR() { // Initial setup - botsController = Singleton.Instance.BotsController; + botEventHandler = Singleton.Instance; + var botsController = Singleton.Instance.BotsController; btrBotService = botsController.BotTradersServices.BTRServices; - var btrControllerType = btrController.GetType(); - AccessTools.Method(btrControllerType, "method_3").Invoke(btrController, null); // spawns server-side BTR game object + btrController.method_3(); // spawns server-side BTR game object botsController.BotSpawner.SpawnBotBTR(); // spawns the scav bot which controls the BTR's turret // Initial BTR configuration btrServerSide = btrController.BtrVehicle; + btrServerSide.transform.Find("KillBox").gameObject.AddComponent(); btrServerSide.moveSpeed = 20f; var btrMapConfig = btrController.MapPathsConfiguration; btrServerSide.CurrentPathConfig = btrMapConfig.PathsConfiguration.pathsConfigurations.RandomElement(); btrServerSide.Initialization(btrMapConfig); - AccessTools.Method(btrControllerType, "method_14").Invoke(btrController, null); // creates and assigns the BTR a fake stash + btrController.method_14(); // creates and assigns the BTR a fake stash DisableServerSideRenderers(); @@ -233,6 +193,47 @@ namespace Aki.Debugging.BTR _updateTaxiPriceMethod.Invoke(btrController, new object[] { destinationPoint, isFinal }); } + private void TraderServicePurchased(ETraderServiceType serviceType) + { + switch (serviceType) + { + case ETraderServiceType.BtrBotCover: + List passengers = gameWorld.AllAlivePlayersList.Where(x => x.BtrState == EPlayerBtrState.Inside).ToList(); + botEventHandler.ApplyTraderServiceBtrSupport(passengers); + StartCoverFireTimer(90f); + break; + } + } + + private void StartCoverFireTimer(float time) + { + _coverFireTimerCoroutine = StaticManager.BeginCoroutine(CoverFireTimer(time)); + } + + private IEnumerator CoverFireTimer(float time) + { + yield return new WaitForSecondsRealtime(time); + botEventHandler.StopTraderServiceBtrSupport(); + } + + private void HandleBtrDoorState(EPlayerBtrState playerBtrState) + { + if (previousPlayerBtrState == EPlayerBtrState.Approach && playerBtrState == EPlayerBtrState.GoIn + || previousPlayerBtrState == EPlayerBtrState.Inside && playerBtrState == EPlayerBtrState.GoOut) + { + // Open Door + UpdateBTRSideDoorState(1); + } + else if (previousPlayerBtrState == EPlayerBtrState.GoIn && playerBtrState == EPlayerBtrState.Inside + || previousPlayerBtrState == EPlayerBtrState.GoOut && playerBtrState == EPlayerBtrState.Outside) + { + // Close Door + UpdateBTRSideDoorState(0); + } + + previousPlayerBtrState = playerBtrState; + } + private void UpdateBTRSideDoorState(byte state) { var player = gameWorld.MainPlayer; @@ -243,13 +244,17 @@ namespace Aki.Debugging.BTR if (player.BtrInteractionSide != null && btrSides[i] == player.BtrInteractionSide || lastInteractedBtrSide != null && btrSides[i] == lastInteractedBtrSide) { - if (i == 0) btrServerSide.LeftSideState = state; - else if (i == 1) btrServerSide.RightSideState = state; - - if (lastInteractedBtrSide != player.BtrInteractionSide) + switch (i) { - lastInteractedBtrSide = player.BtrInteractionSide; + case 0: + btrServerSide.LeftSideState = state; + break; + case 1: + btrServerSide.RightSideState = state; + break; } + + lastInteractedBtrSide = player.BtrInteractionSide; } } } @@ -292,50 +297,64 @@ namespace Aki.Debugging.BTR } } - private bool IsAimingAtTarget() + private bool HasTarget() { - var turretInDefaultRotation = btrTurretServer.targetTransform == btrTurretDefaultTargetTransform - && btrTurretServer.targetPosition == btrTurretServer.defaultAimingPosition; - var enemies = btrBotShooter.BotsGroup.Enemies; if (enemies.Any()) { - IPlayer currentTarget = enemies.First().Key; - Transform currentTargetTransform = currentTarget.Transform.Original; - Vector3 currentTargetPosition = currentTargetTransform.position; - var currentTargetInfo = btrBotShooter.EnemiesController.EnemyInfos[currentTarget]; - + currentTarget = enemies.First().Key; if (!currentTarget.HealthController.IsAlive) { enemies.Remove(currentTarget); + currentTarget = null; + return false; } + return true; + } + + return false; + } + + private bool IsAimingAtTarget() + { + bool turretInDefaultRotation = btrTurretServer.targetTransform == btrTurretDefaultTargetTransform + && btrTurretServer.targetPosition == btrTurretServer.defaultAimingPosition; + + if (currentTarget != null) + { + Transform currentTargetTransform = currentTarget.Transform.Original; + EnemyInfo currentTargetInfo = btrBotShooter.EnemiesController.EnemyInfos[currentTarget]; + if (currentTargetInfo.IsVisible) { + Vector3 currentTargetPosition = currentTargetTransform.position; if (btrTurretServer.CheckPositionInAimingZone(currentTargetPosition)) { - btrTurretServer.EnableAimingObject(currentTargetTransform); - } - // If turret machine gun aim is close enough to target and has line of sight - if (btrBotShooter.BotBtrData.CanShoot()) - { - return true; + // If turret machine gun aim is close enough to target and has line of sight + if (btrTurretServer.targetTransform == currentTargetTransform && btrBotShooter.BotBtrData.CanShoot()) + { + return true; + } + + if (btrTurretServer.targetTransform != currentTargetTransform) + { + btrTurretServer.EnableAimingObject(currentTargetTransform); + } } } - else if (!currentTargetInfo.IsVisible) + // Turret will hold the angle where target was last seen for 3 seconds before resetting its rotation + else if (btrTurretServer.targetPosition != currentTargetInfo.EnemyLastPosition && btrTurretServer.targetTransform != null) { - // Turret will hold the angle where target was last seen for 3 seconds before resetting its rotation - if (btrTurretServer.targetPosition != currentTargetInfo.EnemyLastPosition && btrTurretServer.targetTransform != null) - { - btrTurretServer.EnableAimingPosition(currentTargetInfo.EnemyLastPosition); - } - else if (currentTargetInfo.TimeLastSeen >= 3f && !turretInDefaultRotation) - { - btrTurretServer.DisableAiming(); - } + btrTurretServer.EnableAimingPosition(currentTargetInfo.EnemyLastPosition); + } + else if (currentTargetInfo.TimeLastSeen >= 3f && !turretInDefaultRotation) + { + currentTarget = null; + btrTurretServer.DisableAiming(); } } - else if (!enemies.Any() && !turretInDefaultRotation) + else if (!turretInDefaultRotation) { btrTurretServer.DisableAiming(); } @@ -351,16 +370,22 @@ namespace Aki.Debugging.BTR isShooting = true; Transform machineGunMuzzle = btrTurretServer.machineGunLaunchPoint; - Player btrBotPlayer = btrBotShooter.GetPlayer; - - gameWorld.SharedBallisticsCalculator.Shoot(btrMachineGunAmmo, machineGunMuzzle.position, machineGunMuzzle.forward, btrBotPlayer.ProfileId, btrMachineGunWeapon, 1f, 0); - Player.FirearmController firearmController = btrBotShooter.GetComponent(); WeaponPrefab weaponPrefab = (WeaponPrefab)AccessTools.Field(firearmController.GetType(), "weaponPrefab_0").GetValue(firearmController); WeaponSoundPlayer weaponSoundPlayer = weaponPrefab.GetComponent(); - AccessTools.Method(firearmController.GetType(), "method_54").Invoke(firearmController, new object[] { weaponSoundPlayer, btrMachineGunAmmo, machineGunMuzzle.position, machineGunMuzzle.forward, false }); - yield return new WaitForSecondsRealtime(0.092308f); // 650 RPM + int burstCount = Random.Range(5, 8); + while (burstCount > 0) + { + gameWorld.SharedBallisticsCalculator.Shoot(btrMachineGunAmmo, machineGunMuzzle.position, machineGunMuzzle.forward, btrBotShooter.ProfileId, btrMachineGunWeapon, 1f, 0); + firearmController.method_54(weaponSoundPlayer, btrMachineGunAmmo, machineGunMuzzle.position, machineGunMuzzle.forward, false); + burstCount--; + yield return new WaitForSecondsRealtime(0.092308f); // 650 RPM + } + + float waitTime = Random.Range(0.8f, 1.7f); // 0.8 - 1.7 second pause between bursts + yield return new WaitForSecondsRealtime(waitTime); + isShooting = false; } @@ -385,7 +410,13 @@ namespace Aki.Debugging.BTR gameWorld.MainPlayer.OnBtrStateChanged -= HandleBtrDoorState; } + if (TraderServicesManager.Instance != null) + { + TraderServicesManager.Instance.OnTraderServicePurchased -= TraderServicePurchased; + } + StaticManager.KillCoroutine(ref _shootingTargetCoroutine); + StaticManager.KillCoroutine(ref _coverFireTimerCoroutine); Destroy(this); } } diff --git a/project/Aki.Debugging/BTR/BTRRoadKillTrigger.cs b/project/Aki.Debugging/BTR/BTRRoadKillTrigger.cs new file mode 100644 index 0000000..3de6347 --- /dev/null +++ b/project/Aki.Debugging/BTR/BTRRoadKillTrigger.cs @@ -0,0 +1,38 @@ +using EFT; +using EFT.Interactive; +using UnityEngine; + +namespace Aki.Debugging.BTR +{ + public class BTRRoadKillTrigger : DamageTrigger + { + public override bool IsStatic => false; + + public override void AddPenalty(GInterface94 player) + { + } + + public override void PlaySound() + { + } + + public override void ProceedDamage(GInterface94 player, BodyPartCollider bodyPart) + { + bodyPart.ApplyInstantKill(new DamageInfo() + { + Damage = 9999f, + Direction = Vector3.zero, + HitCollider = bodyPart.Collider, + HitNormal = Vector3.zero, + HitPoint = Vector3.zero, + DamageType = EDamageType.Btr, + HittedBallisticCollider = bodyPart, + Player = null + }); + } + + public override void RemovePenalty(GInterface94 player) + { + } + } +} diff --git a/project/Aki.Debugging/BTR/Patches/BTRBotInitPatch.cs b/project/Aki.Debugging/BTR/Patches/BTRBotInitPatch.cs index 108479f..faa1e78 100644 --- a/project/Aki.Debugging/BTR/Patches/BTRBotInitPatch.cs +++ b/project/Aki.Debugging/BTR/Patches/BTRBotInitPatch.cs @@ -25,10 +25,9 @@ namespace Aki.Debugging.BTR.Patches } [PatchPostfix] - public static void PatchPostfix(object __instance, int btrBotId, ref bool __result) + public static void PatchPostfix(BTRTurretView __instance, int btrBotId, ref bool __result) { var gameWorld = Singleton.Instance; - var btrTurretView = (BTRTurretView)__instance; foreach (var playerKeyValue in gameWorld.allAlivePlayersByID) { @@ -49,10 +48,7 @@ namespace Aki.Debugging.BTR.Patches foreach (var text in currentWeaponPrefab.RemoveChildrenOf) { var transform = currentWeaponPrefab.transform.FindTransform(text); - if (transform != null) - { - transform.gameObject.SetActive(false); - } + transform?.gameObject.SetActive(false); } } foreach (var renderer in currentWeaponPrefab.GetComponentsInChildren()) @@ -68,8 +64,8 @@ namespace Aki.Debugging.BTR.Patches } var tuple = new ValueTuple(new ObservedPlayerView(), true); - var btrTurretViewTupleField = AccessTools.Field(btrTurretView.GetType(), "valueTuple_0"); - btrTurretViewTupleField.SetValue(btrTurretView, tuple); + var btrTurretViewTupleField = AccessTools.Field(__instance.GetType(), "valueTuple_0"); + btrTurretViewTupleField.SetValue(__instance, tuple); __result = true; return; diff --git a/project/Aki.Debugging/BTR/Patches/BTRReceiveDamageInfoPatch.cs b/project/Aki.Debugging/BTR/Patches/BTRReceiveDamageInfoPatch.cs index efb9e69..7754455 100644 --- a/project/Aki.Debugging/BTR/Patches/BTRReceiveDamageInfoPatch.cs +++ b/project/Aki.Debugging/BTR/Patches/BTRReceiveDamageInfoPatch.cs @@ -4,30 +4,31 @@ using EFT; using EFT.Vehicle; using HarmonyLib; using System.Reflection; +using BotEventHandler = GClass595; namespace Aki.Debugging.BTR.Patches { /// /// Patches an empty method in to handle updating the BTR bot's Neutrals and Enemies lists in response to taking damage. /// - public class BTRReceiveDamageInfoPatch : ModulePatch + internal class BTRReceiveDamageInfoPatch : ModulePatch { protected override MethodBase GetTargetMethod() { - return AccessTools.Method(typeof(BTRView), "method_1"); + return AccessTools.Method(typeof(BTRView), nameof(BTRView.method_1)); } [PatchPrefix] - public static void PatchPrefix(DamageInfo damageInfo) + private static void PatchPrefix(DamageInfo damageInfo) { - var globalEvents = Singleton.Instance; - if (globalEvents == null) + var botEventHandler = Singleton.Instance; + if (botEventHandler == null) { return; } var shotBy = (Player)damageInfo.Player.iPlayer; - globalEvents.InterruptTraderServiceBtrSupportByBetrayer(shotBy); + botEventHandler.InterruptTraderServiceBtrSupportByBetrayer(shotBy); } } } diff --git a/project/Aki.Debugging/BTR/Utils/BTRUtil.cs b/project/Aki.Debugging/BTR/Utils/BTRUtil.cs index b672a5a..4f2d730 100644 --- a/project/Aki.Debugging/BTR/Utils/BTRUtil.cs +++ b/project/Aki.Debugging/BTR/Utils/BTRUtil.cs @@ -19,8 +19,6 @@ namespace Aki.Debugging.BTR.Utils public static readonly string BTRMachineGunWeaponTplId = "657857faeff4c850222dff1b"; // BTR PKTM machine gun public static readonly string BTRMachineGunAmmoTplId = "5e023d34e8a400319a28ed44"; // 7.62x54mmR BT - private static FieldInfo _traderAvailableServicesField = AccessTools.Field(typeof(Profile.TraderInfo), "_availableServices"); - static BTRUtil() { // Sanity checks for compile time failure in the event the GClass changes diff --git a/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs index f82e5ea..a9051db 100644 --- a/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs +++ b/project/Aki.SinglePlayer/Utils/TraderServices/TraderServicesManager.cs @@ -3,6 +3,7 @@ using Comfort.Common; using EFT; using HarmonyLib; using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Reflection; using UnityEngine; @@ -13,6 +14,8 @@ namespace Aki.SinglePlayer.Utils.TraderServices { public class TraderServicesManager { + public event Action OnTraderServicePurchased; // Subscribe to this event to trigger trader service logic + private static TraderServicesManager _instance; public static TraderServicesManager Instance @@ -146,6 +149,7 @@ namespace Aki.SinglePlayer.Utils.TraderServices _servicePurchased[serviceType] = new Dictionary(); _servicePurchased[serviceType][traderId] = true; } + OnTraderServicePurchased.Invoke(serviceType); } public bool IsServicePurchased(ETraderServiceType serviceType, string traderId)