Skip to content

Commit

Permalink
Sync stasis rifles and torpedos (precise and desync-proof). Fix seamo…
Browse files Browse the repository at this point in the history
…th inventory not storing torpedos
  • Loading branch information
tornac1234 committed Jan 17, 2024
1 parent 905edef commit 3762c2c
Show file tree
Hide file tree
Showing 38 changed files with 791 additions and 212 deletions.
22 changes: 22 additions & 0 deletions Nitrox.Test/Patcher/Patches/Dynamic/Bullet_Update_PatchTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using FluentAssertions;
using HarmonyLib;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxTest.Patcher;

namespace NitroxPatcher.Patches.Dynamic;

[TestClass]
public class Bullet_Update_PatchTest
{
[TestMethod]
public void Sanity()
{
ILGenerator generator = Bullet_Update_Patch.TARGET_METHOD.GetILGenerator();
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(Bullet_Update_Patch.TARGET_METHOD);
IEnumerable<CodeInstruction> transformedIl = Bullet_Update_Patch.Transpiler(originalIl, generator);
transformedIl.Count().Should().Be(originalIl.Count() + 3);
}
}
1 change: 1 addition & 0 deletions NitroxClient/ClientAutoFacRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ private void RegisterCoreDependencies(ContainerBuilder containerBuilder)
containerBuilder.RegisterType<PlayerCinematics>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NitroxPDATabManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<TimeManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<BulletManager>().InstancePerLifetimeScope();
}

private void RegisterMetadataDependencies(ContainerBuilder containerBuilder)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel_Subnautica.Packets;
using UnityEngine;

Expand Down Expand Up @@ -42,9 +41,6 @@ public override void Process(ExosuitArmActionPacket packet)
case TechType.ExosuitGrapplingArmModule:
exosuitModuleEvent.UseGrappling(gameObject.GetComponent<ExosuitGrapplingArm>(), packet.ArmAction, packet.OpVector?.ToUnity());
break;
case TechType.ExosuitTorpedoArmModule:
exosuitModuleEvent.UseTorpedo(gameObject.GetComponent<ExosuitTorpedoArm>(), packet.ArmAction, packet.OpVector?.ToUnity(), packet.OpRotation?.ToUnity());
break;
default:
Log.Error($"Got an arm tech that is not handled: {packet.TechType} with action: {packet.ArmAction} for id {packet.ArmId}");
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
Expand Down Expand Up @@ -35,47 +35,8 @@ public override void Process(SeamothModulesAction packet)
component.charge = charge;
component.chargeScalar = slotCharge;
}

if (techType == TechType.SeamothTorpedoModule)
{
Transform muzzle = (packet.SlotID != seamoth.GetSlotIndex("SeamothModule1") && packet.SlotID != seamoth.GetSlotIndex("SeamothModule3")) ? seamoth.torpedoTubeRight : seamoth.torpedoTubeLeft;
ItemsContainer storageInSlot = seamoth.GetStorageInSlot(packet.SlotID, TechType.SeamothTorpedoModule);
TorpedoType torpedoType = null;

for (int i = 0; i < seamoth.torpedoTypes.Length; i++)
{
if (storageInSlot.Contains(seamoth.torpedoTypes[i].techType))
{
torpedoType = seamoth.torpedoTypes[i];
break;
}
}

//Original Function use Player Camera need parse owner camera values
TorpedoShot(storageInSlot, torpedoType, muzzle, packet.Forward.ToUnity(), packet.Rotation.ToUnity());
}
}
}
}

//Copied this from the Vehicle class
public static bool TorpedoShot(ItemsContainer container, TorpedoType torpedoType, Transform muzzle, Vector3 forward, Quaternion rotation)
{
if (torpedoType != null && container.DestroyItem(torpedoType.techType))
{
GameObject gameObject = UnityEngine.Object.Instantiate<GameObject>(torpedoType.prefab);
Transform component = gameObject.GetComponent<Transform>();
SeamothTorpedo component2 = gameObject.GetComponent<SeamothTorpedo>();
Vector3 zero = Vector3.zero;
Rigidbody componentInParent = muzzle.GetComponentInParent<Rigidbody>();
Vector3 rhs = (!(componentInParent != null)) ? Vector3.zero : componentInParent.velocity;
float speed = Vector3.Dot(forward, rhs);
component2.Shoot(muzzle.position, rotation, speed, -1f);

return true;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class StasisSphereHitProcessor : ClientPacketProcessor<StasisSphereHit>
{
private readonly BulletManager bulletManager;

public StasisSphereHitProcessor(BulletManager bulletManager)
{
this.bulletManager = bulletManager;
}

public override void Process(StasisSphereHit packet)
{
bulletManager.StasisSphereHit(packet.PlayerId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.ChargeNormalized, packet.Consumption);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class StasisSphereShotProcessor : ClientPacketProcessor<StasisSphereShot>
{
private readonly BulletManager bulletManager;

public StasisSphereShotProcessor(BulletManager bulletManager)
{
this.bulletManager = bulletManager;
}

public override void Process(StasisSphereShot packet)
{
bulletManager.ShootStasisSphere(packet.PlayerId, packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.Speed, packet.LifeTime, packet.ChargeNormalized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class TorpedoHitProcessor : ClientPacketProcessor<TorpedoHit>
{
private readonly BulletManager bulletManager;

public TorpedoHitProcessor(BulletManager bulletManager)
{
this.bulletManager = bulletManager;
}

public override void Process(TorpedoHit packet)
{
bulletManager.TorpedoHit(packet.BulletId, packet.Position.ToUnity(), packet.Rotation.ToUnity());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class TorpedoShotProcessor : ClientPacketProcessor<TorpedoShot>
{
private readonly BulletManager bulletManager;

public TorpedoShotProcessor(BulletManager bulletManager)
{
this.bulletManager = bulletManager;
}

public override void Process(TorpedoShot packet)
{
bulletManager.ShootSeamothTorpedo(packet.BulletId, packet.TechType.ToUnity(), packet.Position.ToUnity(), packet.Rotation.ToUnity(), packet.Speed, packet.LifeTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class TorpedoTargetAcquiredProcessor : ClientPacketProcessor<TorpedoTargetAcquired>
{
private readonly BulletManager bulletManager;

public TorpedoTargetAcquiredProcessor(BulletManager bulletManager)
{
this.bulletManager = bulletManager;
}

public override void Process(TorpedoTargetAcquired packet)
{
bulletManager.TorpedoTargetAcquired(packet.BulletId, packet.TargetId, packet.Position.ToUnity(), packet.Rotation.ToUnity());
}
}
166 changes: 166 additions & 0 deletions NitroxClient/GameLogic/BulletManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Collections;
using System.Collections.Generic;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using UnityEngine;

namespace NitroxClient.GameLogic;

public class BulletManager
{
private readonly PlayerManager playerManager;

// This only allows for one stasis sphere per player
// (which is the normal capacity, but could be adapted for a mod letting multiple stasis spheres)
private readonly Dictionary<ushort, StasisSphere> stasisSpherePerPlayerId = [];

/// <summary>
/// TechTypes of objects which should have a Vehicle MB
/// </summary>
public static readonly HashSet<TechType> PreloadedVehicleTypes = [
TechType.Seamoth, TechType.Exosuit
];

private readonly Dictionary<TechType, GameObject> torpedoPrefabByTechType = [];

private GameObject stasisSpherePrefab { get; set; }

public BulletManager(PlayerManager playerManager)
{
this.playerManager = playerManager;
}

public void ShootSeamothTorpedo(NitroxId bulletId, TechType techType, Vector3 position, Quaternion rotation, float speed, float lifeTime)
{
if (!torpedoPrefabByTechType.TryGetValue(techType, out GameObject prefab))
{
Log.ErrorOnce($"[{nameof(BulletManager)}] Received ShootSeamothTorpedo request with TechType: {techType} but no prefab was loaded for it");
return;
}

GameObject torpedoClone = GameObjectHelper.SpawnFromPrefab(prefab, bulletId);
// We mark it to be able to ignore events from remote bullets
torpedoClone.AddComponent<RemotePlayerBullet>();

// We cast it to Bullet to ensure we're calling the same method as in Vehicle.TorpedoShot
Bullet seamothTorpedo = torpedoClone.GetComponent<SeamothTorpedo>();
seamothTorpedo.Shoot(position, rotation, speed, lifeTime);
}

public void TorpedoHit(NitroxId bulletId, Vector3 position, Quaternion rotation)
{
if (NitroxEntity.TryGetComponentFrom(bulletId, out SeamothTorpedo torpedo))
{
torpedo.tr.position = position;
torpedo.tr.rotation = rotation;
torpedo.OnHit(default);
torpedo.Deactivate();
}
}

public void TorpedoTargetAcquired(NitroxId bulletId, NitroxId targetId, Vector3 position, Quaternion rotation)
{
// The target object might not be findable in which case we'll just ignore it
// because the explosion will still be moved to the right spot
if (NitroxEntity.TryGetComponentFrom(bulletId, out SeamothTorpedo torpedo) &&
NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject))
{
torpedo.tr.position = position;
torpedo.tr.rotation = rotation;
// Stuff from SeamothTorpedo.RepeatingTargeting
torpedo.homingTarget = targetObject;
torpedo.CancelInvoke();
}
}

public void ShootStasisSphere(ushort playerId, Vector3 position, Quaternion rotation, float speed, float lifeTime, float chargeNormalized)
{
if (!stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere cloneSphere) || !cloneSphere)
{
cloneSphere = EnsurePlayerHasSphere(playerId);
}

cloneSphere.Shoot(position, rotation, speed, lifeTime, chargeNormalized);
}

public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotation, float chargeNormalized, float consumption)
{
StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId);

// Setup the sphere in case it the shot was sent earlier
cloneSphere.Shoot(position, rotation, 0, 0, chargeNormalized);
// We override this field (set by .Shoot) with the right data
cloneSphere._consumption = consumption;

// Code from Bullet.Update when finding an object to hit
cloneSphere._visible = true;
cloneSphere.OnMadeVisible();
cloneSphere.OnHit(default);
cloneSphere.Deactivate();
}

private StasisSphere EnsurePlayerHasSphere(ushort playerId)
{
if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere remoteSphere) && remoteSphere)
{
return remoteSphere;
}

// It should be set to inactive automatically in Bullet.Awake
GameObject playerSphereClone = GameObject.Instantiate(stasisSpherePrefab);
// We mark it to be able to ignore events from remote bullets
playerSphereClone.AddComponent<RemotePlayerBullet>();
StasisSphere stasisSphere = playerSphereClone.GetComponent<StasisSphere>();

stasisSpherePerPlayerId[playerId] = stasisSphere;
return stasisSphere;
}

private void DestroyPlayerSphere(ushort playerId)
{
if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere stasisSphere) && stasisSphere)
{
GameObject.Destroy(stasisSphere.gameObject);
}
stasisSpherePerPlayerId.Remove(playerId);
}

public IEnumerator Initialize()
{
TaskResult<GameObject> result = new();

// Load torpedo types prefab and store them by tech type
foreach (TechType techType in PreloadedVehicleTypes)
{
yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result);
if (result.value && result.value.TryGetComponent(out Vehicle vehicle) && vehicle.torpedoTypes != null)
{
foreach (TorpedoType torpedoType in vehicle.torpedoTypes)
{
torpedoPrefabByTechType[torpedoType.techType] = torpedoType.prefab;
}
}
}

// Load the stasis sphere prefab
yield return DefaultWorldEntitySpawner.RequestPrefab(TechType.StasisRifle, result);
StasisRifle rifle = result.value.GetComponent<StasisRifle>();
if (rifle)
{
stasisSpherePrefab = rifle.effectSpherePrefab;
}

// Setup remote players' stasis spheres
foreach (RemotePlayer remotePlayer in playerManager.GetAll())
{
EnsurePlayerHasSphere(remotePlayer.PlayerId);
}

playerManager.onCreate += (playerId, _) => { EnsurePlayerHasSphere(playerId); };
playerManager.onRemove += (playerId, _) => { DestroyPlayerSphere(playerId); };
}

public class RemotePlayerBullet : MonoBehaviour { }
}
Loading

0 comments on commit 3762c2c

Please sign in to comment.