0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:10:44 -05:00
modules/project/SPT.Custom/BTR/BTRManager.cs
Nympfonic 8f8c56d528 Fixed BTR doing a wheelie in Woods (!139)
I've done testing to ensure the player can't phase through the BTR as well.
Have not tested anything else besides that.

Reviewed-on: SPT/Modules#139
Co-authored-by: Nympfonic <arys.steam@gmail.com>
Co-committed-by: Nympfonic <arys.steam@gmail.com>
2024-06-16 08:35:24 +00:00

518 lines
20 KiB
C#

using SPT.Custom.BTR.Utils;
using SPT.SinglePlayer.Utils.TraderServices;
using Comfort.Common;
using EFT;
using EFT.InventoryLogic;
using EFT.UI;
using EFT.Vehicle;
using HarmonyLib;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;
namespace SPT.Custom.BTR
{
public class BTRManager : MonoBehaviour
{
private GameWorld gameWorld;
private BotEventHandler botEventHandler;
private BotBTRService btrBotService;
private BTRControllerClass btrController;
private BTRVehicle btrServerSide;
private BTRView btrClientSide;
private BotOwner btrBotShooter;
private BTRDataPacket btrDataPacket = default;
private bool btrInitialized = false;
private bool btrBotShooterInitialized = false;
private float coverFireTime = 90f;
private Coroutine _coverFireTimerCoroutine;
private BTRSide lastInteractedBtrSide;
public BTRSide LastInteractedBtrSide => lastInteractedBtrSide;
private Coroutine _shootingTargetCoroutine;
private BTRTurretServer btrTurretServer;
private bool isTurretInDefaultRotation;
private EnemyInfo currentTarget = null;
private bool isShooting = false;
private float machineGunAimDelay = 0.4f;
private Vector2 machineGunBurstCount;
private Vector2 machineGunRecoveryTime;
private BulletClass btrMachineGunAmmo;
private Item btrMachineGunWeapon;
private Player.FirearmController firearmController;
private WeaponSoundPlayer weaponSoundPlayer;
private float originalDamageCoeff;
private async void Awake()
{
try
{
gameWorld = Singleton<GameWorld>.Instance;
if (gameWorld == null)
{
Destroy(this);
return;
}
if (gameWorld.BtrController == null)
{
gameWorld.BtrController = new BTRControllerClass();
}
btrController = gameWorld.BtrController;
await InitBtr();
}
catch
{
ConsoleScreen.LogError("[SPT-BTR] Unable to spawn BTR. Check logs.");
Destroy(this);
throw;
}
}
public void OnPlayerInteractDoor(PlayerInteractPacket interactPacket)
{
btrServerSide.LeftSlot0State = 0;
btrServerSide.LeftSlot1State = 0;
btrServerSide.RightSlot0State = 0;
btrServerSide.RightSlot1State = 0;
bool playerGoIn = interactPacket.InteractionType == EInteractionType.GoIn;
if (interactPacket.SideId == 0 && playerGoIn)
{
if (interactPacket.SlotId == 0)
{
btrServerSide.LeftSlot0State = 1;
}
else if (interactPacket.SlotId == 1)
{
btrServerSide.LeftSlot1State = 1;
}
}
else if (interactPacket.SideId == 1 && playerGoIn)
{
if (interactPacket.SlotId == 0)
{
btrServerSide.RightSlot0State = 1;
}
else if (interactPacket.SlotId == 1)
{
btrServerSide.RightSlot1State = 1;
}
}
// If the player is going into the BTR, store their damage coefficient
// and set it to 0, so they don't die while inside the BTR
if (interactPacket.InteractionType == EInteractionType.GoIn)
{
originalDamageCoeff = gameWorld.MainPlayer.ActiveHealthController.DamageCoeff;
gameWorld.MainPlayer.ActiveHealthController.SetDamageCoeff(0f);
}
// Otherwise restore the damage coefficient
else if (interactPacket.InteractionType == EInteractionType.GoOut)
{
gameWorld.MainPlayer.ActiveHealthController.SetDamageCoeff(originalDamageCoeff);
}
}
private void Update()
{
if (!btrInitialized) return;
btrController.SyncBTRVehicleFromServer(UpdateDataPacket());
if (btrController.BotShooterBtr == null) return;
// BotShooterBtr doesn't get assigned to BtrController immediately so we check this in Update
if (!btrBotShooterInitialized)
{
InitBtrBotService();
btrBotShooterInitialized = true;
}
UpdateTarget();
if (HasTarget())
{
SetAim();
if (!isShooting && CanShoot())
{
StartShooting();
}
}
else if (!isTurretInDefaultRotation)
{
btrTurretServer.DisableAiming();
}
}
private async Task InitBtr()
{
// Initial setup
await btrController.InitBtrController();
botEventHandler = Singleton<BotEventHandler>.Instance;
var botsController = Singleton<IBotGame>.Instance.BotsController;
btrBotService = botsController.BotTradersServices.BTRServices;
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;
btrClientSide = btrController.BtrView;
btrServerSide.transform.Find("KillBox").gameObject.AddComponent<BTRRoadKillTrigger>();
// Get config from server and initialise respective settings
ConfigureSettingsFromServer();
var btrMapConfig = btrController.MapPathsConfiguration;
if (btrMapConfig == null)
{
ConsoleScreen.LogError($"{nameof(btrController.MapPathsConfiguration)}");
return;
}
btrServerSide.CurrentPathConfig = btrMapConfig.PathsConfiguration.pathsConfigurations.RandomElement();
btrServerSide.Initialization(btrMapConfig);
btrController.method_14(); // creates and assigns the BTR a fake stash
DisableServerSideObjects();
gameWorld.MainPlayer.OnBtrStateChanged += HandleBtrDoorState;
btrServerSide.MoveEnable();
btrServerSide.IncomingToDestinationEvent += ToDestinationEvent;
// Sync initial position and rotation
UpdateDataPacket();
btrClientSide.transform.position = btrDataPacket.position;
btrClientSide.transform.rotation = btrDataPacket.rotation;
// Initialise turret variables
btrTurretServer = btrServerSide.BTRTurret;
var btrTurretDefaultTargetTransform = (Transform)AccessTools.Field(btrTurretServer.GetType(), "defaultTargetTransform").GetValue(btrTurretServer);
isTurretInDefaultRotation = btrTurretServer.targetTransform == btrTurretDefaultTargetTransform
&& btrTurretServer.targetPosition == btrTurretServer.defaultAimingPosition;
btrMachineGunAmmo = (BulletClass)BTRUtil.CreateItem(BTRUtil.BTRMachineGunAmmoTplId);
btrMachineGunWeapon = BTRUtil.CreateItem(BTRUtil.BTRMachineGunWeaponTplId);
// Pull services data for the BTR from the server
TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId);
btrInitialized = true;
}
private void ConfigureSettingsFromServer()
{
var serverConfig = BTRUtil.GetConfigFromServer();
btrServerSide.moveSpeed = serverConfig.MoveSpeed;
btrServerSide.pauseDurationRange.x = serverConfig.PointWaitTime.Min;
btrServerSide.pauseDurationRange.y = serverConfig.PointWaitTime.Max;
btrServerSide.readyToDeparture = serverConfig.TaxiWaitTime;
coverFireTime = serverConfig.CoverFireTime;
machineGunAimDelay = serverConfig.MachineGunAimDelay;
machineGunBurstCount = new Vector2(serverConfig.MachineGunBurstCount.Min, serverConfig.MachineGunBurstCount.Max);
machineGunRecoveryTime = new Vector2(serverConfig.MachineGunRecoveryTime.Min, serverConfig.MachineGunRecoveryTime.Max);
}
private void InitBtrBotService()
{
btrBotShooter = btrController.BotShooterBtr;
firearmController = btrBotShooter.GetComponent<Player.FirearmController>();
var weaponPrefab = (WeaponPrefab)AccessTools.Field(firearmController.GetType(), "weaponPrefab_0").GetValue(firearmController);
weaponSoundPlayer = weaponPrefab.GetComponent<WeaponSoundPlayer>();
btrBotService.Reset(); // Player will be added to Neutrals list and removed from Enemies list
TraderServicesManager.Instance.OnTraderServicePurchased += BtrTraderServicePurchased;
}
/**
* BTR has arrived at a destination, re-calculate taxi prices and remove purchased taxi service
*/
private void ToDestinationEvent(PathDestination destinationPoint, bool isFirst, bool isFinal, bool isLastRoutePoint)
{
// Remove purchased taxi service
TraderServicesManager.Instance.RemovePurchasedService(ETraderServiceType.PlayerTaxi, BTRUtil.BTRTraderId);
// Update the prices for the taxi service
btrController.UpdateTaxiPrice(destinationPoint, isFinal);
// Update the UI
TraderServicesManager.Instance.GetTraderServicesDataFromServer(BTRUtil.BTRTraderId);
}
private bool IsBtrService(ETraderServiceType serviceType)
{
return serviceType == ETraderServiceType.BtrItemsDelivery
|| serviceType == ETraderServiceType.PlayerTaxi
|| serviceType == ETraderServiceType.BtrBotCover;
}
private void BtrTraderServicePurchased(ETraderServiceType serviceType, string subserviceId)
{
if (!IsBtrService(serviceType))
{
return;
}
List<Player> passengers = gameWorld.AllAlivePlayersList.Where(x => x.BtrState == EPlayerBtrState.Inside).ToList();
List<int> playersToNotify = passengers.Select(x => x.Id).ToList();
btrController.method_6(playersToNotify, serviceType); // notify BTR passengers that a service has been purchased
switch (serviceType)
{
case ETraderServiceType.BtrBotCover:
botEventHandler.ApplyTraderServiceBtrSupport(passengers);
StartCoverFireTimer(coverFireTime);
break;
case ETraderServiceType.PlayerTaxi:
btrController.BtrVehicle.IsPaid = true;
btrController.BtrVehicle.MoveToDestination(subserviceId.Split('/')[1]); // TODO: Look into fixing the main cause of this issue.
break;
}
}
private void StartCoverFireTimer(float time)
{
_coverFireTimerCoroutine = StaticManager.BeginCoroutine(CoverFireTimer(time));
}
private IEnumerator CoverFireTimer(float time)
{
yield return new WaitForSeconds(time);
botEventHandler.StopTraderServiceBtrSupport();
}
private void HandleBtrDoorState(EPlayerBtrState playerBtrState)
{
if (playerBtrState == EPlayerBtrState.GoIn || playerBtrState == EPlayerBtrState.GoOut)
{
// Open Door
UpdateBTRSideDoorState(1);
}
else if (playerBtrState == EPlayerBtrState.Inside || playerBtrState == EPlayerBtrState.Outside)
{
// Close Door
UpdateBTRSideDoorState(0);
}
}
private void UpdateBTRSideDoorState(byte state)
{
try
{
var player = gameWorld.MainPlayer;
BTRSide btrSide = player.BtrInteractionSide != null
? player.BtrInteractionSide
: lastInteractedBtrSide;
byte sideId = btrClientSide.GetSideId(btrSide);
switch (sideId)
{
case 0:
btrServerSide.LeftSideState = state;
break;
case 1:
btrServerSide.RightSideState = state;
break;
}
lastInteractedBtrSide = player.BtrInteractionSide;
}
catch
{
ConsoleScreen.LogError($"[SPT-BTR] {nameof(lastInteractedBtrSide)} is null when it shouldn't be. Check logs.");
throw;
}
}
private BTRDataPacket UpdateDataPacket()
{
btrDataPacket.position = btrServerSide.transform.position;
btrDataPacket.rotation = btrServerSide.transform.rotation;
if (btrTurretServer != null && btrTurretServer.gunsBlockRoot != null)
{
btrDataPacket.turretRotation = btrTurretServer.transform.localEulerAngles.y;
btrDataPacket.gunsBlockRotation = btrTurretServer.gunsBlockRoot.localEulerAngles.x;
}
btrDataPacket.State = (byte)btrServerSide.BtrState;
btrDataPacket.RouteState = (byte)btrServerSide.VehicleRouteState;
btrDataPacket.LeftSideState = btrServerSide.LeftSideState;
btrDataPacket.LeftSlot0State = btrServerSide.LeftSlot0State;
btrDataPacket.LeftSlot1State = btrServerSide.LeftSlot1State;
btrDataPacket.RightSideState = btrServerSide.RightSideState;
btrDataPacket.RightSlot0State = btrServerSide.RightSlot0State;
btrDataPacket.RightSlot1State = btrServerSide.RightSlot1State;
btrDataPacket.currentSpeed = btrServerSide.currentSpeed;
btrDataPacket.timeToEndPause = btrServerSide.timeToEndPause;
btrDataPacket.moveDirection = (byte)btrServerSide.VehicleMoveDirection;
btrDataPacket.MoveSpeed = btrServerSide.moveSpeed;
if (btrController != null && btrController.BotShooterBtr != null)
{
btrDataPacket.BtrBotId = btrController.BotShooterBtr.Id;
}
return btrDataPacket;
}
private void DisableServerSideObjects()
{
var meshRenderers = btrServerSide.transform.GetComponentsInChildren<MeshRenderer>();
foreach (var renderer in meshRenderers)
{
renderer.enabled = false;
}
btrServerSide.turnCheckerObject.GetComponent<Renderer>().enabled = false; // Disables the red debug sphere
// For some reason the client BTR collider is disabled but the server collider is enabled.
// Initially we assumed there was a reason for this so it was left as is.
// Turns out disabling the server collider in favour of the client collider fixes the "BTR doing a wheelie" bug,
// while preventing the player from walking through the BTR.
const string exteriorColliderName = "BTR_82_exterior_COLLIDER";
var serverExteriorCollider = btrServerSide.GetComponentsInChildren<Collider>(true)
.First(x => x.gameObject.name == exteriorColliderName);
var clientExteriorCollider = btrClientSide.GetComponentsInChildren<Collider>(true)
.First(x => x.gameObject.name == exteriorColliderName);
serverExteriorCollider.gameObject.SetActive(false);
clientExteriorCollider.gameObject.SetActive(true);
}
private void UpdateTarget()
{
currentTarget = btrBotShooter.Memory.GoalEnemy;
}
private bool HasTarget()
{
return currentTarget != null;
}
private void SetAim()
{
if (currentTarget.IsVisible)
{
Vector3 targetPos = currentTarget.CurrPosition;
Transform targetTransform = currentTarget.Person.Transform.Original;
if (btrTurretServer.CheckPositionInAimingZone(targetPos) && btrTurretServer.targetTransform != targetTransform)
{
btrTurretServer.EnableAimingObject(targetTransform);
}
}
else
{
Vector3 targetLastPos = currentTarget.EnemyLastPositionReal;
if (btrTurretServer.CheckPositionInAimingZone(targetLastPos)
&& Time.time - currentTarget.PersonalLastSeenTime < 3f
&& btrTurretServer.targetPosition != targetLastPos)
{
btrTurretServer.EnableAimingPosition(targetLastPos);
}
else if (Time.time - currentTarget.PersonalLastSeenTime >= 3f && !isTurretInDefaultRotation)
{
btrTurretServer.DisableAiming();
}
}
}
private bool CanShoot()
{
return currentTarget.IsVisible && btrBotShooter.BotBtrData.CanShoot();
}
private void StartShooting()
{
_shootingTargetCoroutine = StaticManager.BeginCoroutine(ShootMachineGun());
}
/// <summary>
/// Custom method to make the BTR coaxial machine gun shoot.
/// </summary>
private IEnumerator ShootMachineGun()
{
isShooting = true;
yield return new WaitForSeconds(machineGunAimDelay);
if (currentTarget?.Person == null || currentTarget?.IsVisible == false || !btrBotShooter.BotBtrData.CanShoot())
{
isShooting = false;
yield break;
}
Transform machineGunMuzzle = btrTurretServer.machineGunLaunchPoint;
var ballisticCalculator = gameWorld.SharedBallisticsCalculator;
int burstMin = Mathf.FloorToInt(machineGunBurstCount.x);
int burstMax = Mathf.FloorToInt(machineGunBurstCount.y);
int burstCount = Random.Range(burstMin, burstMax + 1);
Vector3 targetHeadPos = currentTarget.Person.PlayerBones.Head.position;
while (burstCount > 0)
{
// Only update shooting position if the target isn't null
if (currentTarget?.Person != null)
{
targetHeadPos = currentTarget.Person.PlayerBones.Head.position;
}
Vector3 aimDirection = Vector3.Normalize(targetHeadPos - machineGunMuzzle.position);
ballisticCalculator.Shoot(btrMachineGunAmmo, machineGunMuzzle.position, aimDirection, btrBotShooter.ProfileId, btrMachineGunWeapon, 1f, 0);
firearmController.PlayWeaponSound(weaponSoundPlayer, btrMachineGunAmmo, machineGunMuzzle.position, aimDirection, false);
burstCount--;
yield return new WaitForSeconds(0.092308f); // 650 RPM
}
float waitTime = Random.Range(machineGunRecoveryTime.x, machineGunRecoveryTime.y);
yield return new WaitForSeconds(waitTime);
isShooting = false;
}
private void OnDestroy()
{
if (gameWorld == null)
{
return;
}
StaticManager.KillCoroutine(ref _shootingTargetCoroutine);
StaticManager.KillCoroutine(ref _coverFireTimerCoroutine);
if (TraderServicesManager.Instance != null)
{
TraderServicesManager.Instance.OnTraderServicePurchased -= BtrTraderServicePurchased;
}
if (gameWorld.MainPlayer != null)
{
gameWorld.MainPlayer.OnBtrStateChanged -= HandleBtrDoorState;
}
if (btrClientSide != null)
{
Debug.LogWarning($"[SPT-BTR] {nameof(BTRManager)} - Destroying btrClientSide");
Destroy(btrClientSide.gameObject);
}
if (btrServerSide != null)
{
Debug.LogWarning($"[SPT-BTR] {nameof(BTRManager)} - Destroying btrServerSide");
Destroy(btrServerSide.gameObject);
}
}
}
}