Skip to content

Commit

Permalink
Sync base hull (#2099)
Browse files Browse the repository at this point in the history
* Sync base hull

* Fix "entity was spawned" condition, fix ServerAutofac duplicate registering of a critical instance, fix base hull strength creating duplicate leaks, fix simulation not being acquired when creating a base

* Fix a regression: all children added to bases were duplicated, improve LiveMixin metadata broadcast conditions
  • Loading branch information
tornac1234 authored Jan 20, 2024
1 parent 06e114e commit 5c48f8c
Show file tree
Hide file tree
Showing 25 changed files with 514 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using HarmonyLib;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxTest.Patcher;

namespace NitroxPatcher.Patches.Dynamic;

[TestClass]
public class BaseHullStrength_CrushDamageUpdate_PatchTest
{
[TestMethod]
public void Sanity()
{
IEnumerable<CodeInstruction> originalIl = PatchTestHelper.GetInstructionsFromMethod(BaseHullStrength_CrushDamageUpdate_Patch.TARGET_METHOD);
IEnumerable<CodeInstruction> transformedIl = BaseHullStrength_CrushDamageUpdate_Patch.Transpiler(originalIl);
transformedIl.Count().Should().Be(originalIl.Count() + 3);
}
}
4 changes: 4 additions & 0 deletions Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ private static void EntityTest(Entity entity, Entity entityAfter)
Assert.AreEqual(installedModuleEntity.Slot, installedModuleEntityAfter.Slot);
Assert.AreEqual(installedModuleEntity.ClassId, installedModuleEntityAfter.ClassId);
break;
case BaseLeakEntity baseLeakEntity when entityAfter is BaseLeakEntity baseLeakEntityAfter:
Assert.AreEqual(baseLeakEntity.Health, baseLeakEntityAfter.Health);
Assert.AreEqual(baseLeakEntity.RelativeCell, baseLeakEntityAfter.RelativeCell);
break;
default:
Assert.Fail($"Runtime type of {nameof(Entity)} is not equal: {entity.GetType().Name} - {entityAfter.GetType().Name}");
break;
Expand Down
4 changes: 2 additions & 2 deletions NitroxClient/ClientAutoFacRegistrar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,13 @@ private void RegisterCoreDependencies(ContainerBuilder containerBuilder)
containerBuilder.RegisterType<Vehicles>().InstancePerLifetimeScope();
containerBuilder.RegisterType<AI>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerChatManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<SimulationOwnership>().InstancePerLifetimeScope();
containerBuilder.RegisterType<LiveMixinManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Entities>().InstancePerLifetimeScope();
containerBuilder.RegisterType<MedkitFabricator>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Items>().InstancePerLifetimeScope();
containerBuilder.RegisterType<EquipmentSlots>().InstancePerLifetimeScope();
containerBuilder.RegisterType<ItemContainers>().InstancePerLifetimeScope();
containerBuilder.RegisterType<SimulationOwnership>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Cyclops>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Rockets>().InstancePerLifetimeScope();
containerBuilder.RegisterType<MobileVehicleBay>().InstancePerLifetimeScope();
Expand All @@ -126,7 +127,6 @@ private void RegisterCoreDependencies(ContainerBuilder containerBuilder)
containerBuilder.RegisterType<SeamothModulesEvent>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Fires>().InstancePerLifetimeScope();
containerBuilder.RegisterType<FMODSystem>().InstancePerLifetimeScope();
containerBuilder.RegisterType<LiveMixinManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NitroxSettingsManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<ThrottledPacketSender>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerCinematics>().InstancePerLifetimeScope();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,19 @@ public IEnumerator OverwriteBase(Base @base, BuildEntity buildEntity)
ClearBaseChildren(@base);
yield return BuildEntitySpawner.SetupBase(buildEntity, @base, entities);
yield return MoonpoolManager.RestoreMoonpools(buildEntity.ChildEntities.OfType<MoonpoolEntity>(), @base);
yield return entities.SpawnBatchAsync(buildEntity.ChildEntities.OfType<PlayerWorldEntity>().ToList<Entity>(), true, false);
foreach (MapRoomEntity mapRoomEntity in buildEntity.ChildEntities.OfType<MapRoomEntity>())
yield return entities.SpawnBatchAsync(buildEntity.ChildEntities.OfType<PlayerWorldEntity>().ToList<Entity>(), false, false);

foreach (Entity childEntity in buildEntity.ChildEntities)
{
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
switch (childEntity)
{
case MapRoomEntity mapRoomEntity:
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
break;
case BaseLeakEntity baseLeakEntity:
yield return entities.SpawnEntityAsync(baseLeakEntity, true);
break;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;

namespace NitroxClient.Communication.Packets.Processors;

public class LeakRepairedProcessor : ClientPacketProcessor<LeakRepaired>
{
public override void Process(LeakRepaired packet)
{
if (NitroxEntity.TryGetComponentFrom(packet.BaseId, out BaseLeakManager baseLeakManager))
{
baseLeakManager.HealLeakToMax(packet.RelativeCell.ToUnity());
}
}
}
11 changes: 6 additions & 5 deletions NitroxClient/GameLogic/Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class Entities
public List<Entity> EntitiesToSpawn { get; private init; }
private bool spawningEntities;

public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacketSender, EntityMetadataManager entityMetadataManager, PlayerManager playerManager, ILocalNitroxPlayer localPlayer, TimeManager timeManager)
public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacketSender, EntityMetadataManager entityMetadataManager, PlayerManager playerManager, ILocalNitroxPlayer localPlayer, LiveMixinManager liveMixinManager, TimeManager timeManager)
{
this.packetSender = packetSender;
this.throttledPacketSender = throttledPacketSender;
Expand All @@ -60,8 +60,9 @@ public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacke
entitySpawnersByType[typeof(VehicleWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
entitySpawnersByType[typeof(SerializedWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
entitySpawnersByType[typeof(GlobalRootEntity)] = new GlobalRootEntitySpawner();
entitySpawnersByType[typeof(BaseLeakEntity)] = new BaseLeakEntitySpawner(liveMixinManager);
entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this, (BaseLeakEntitySpawner)entitySpawnersByType[typeof(BaseLeakEntity)]);
entitySpawnersByType[typeof(RadiationLeakEntity)] = new RadiationLeakEntitySpawner(timeManager);
entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this);
entitySpawnersByType[typeof(ModuleEntity)] = new ModuleEntitySpawner(this);
entitySpawnersByType[typeof(GhostEntity)] = new GhostEntitySpawner();
entitySpawnersByType[typeof(OxygenPipeEntity)] = new OxygenPipeEntitySpawner(this, (WorldEntitySpawner)entitySpawnersByType[typeof(WorldEntity)]);
Expand Down Expand Up @@ -104,9 +105,9 @@ public void BroadcastMetadataUpdateThrottled(NitroxId id, EntityMetadata metadat
throttledPacketSender.SendThrottled(new EntityMetadataUpdate(id, metadata), packet => packet.Id, throttleTime);
}

public void BroadcastEntitySpawnedByClient(WorldEntity entity)
public void BroadcastEntitySpawnedByClient(Entity entity, bool requireRespawn = false)
{
packetSender.Send(new EntitySpawnedByClient(entity));
packetSender.Send(new EntitySpawnedByClient(entity, requireRespawn));
}

private IEnumerator SpawnNewEntities()
Expand Down Expand Up @@ -279,7 +280,7 @@ public bool WasAlreadySpawned(Entity entity)
{
if (spawnedAsType.TryGetValue(entity.Id, out Type type))
{
return (type == entity.GetType());
return type == entity.GetType() && NitroxEntity.TryGetObjectFrom(entity.Id, out _);
}

return false;
Expand Down
30 changes: 18 additions & 12 deletions NitroxClient/GameLogic/LiveMixinManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NitroxClient.MonoBehaviours;
using System;
using NitroxModel.DataStructures;
using UnityEngine;

Expand All @@ -8,7 +8,7 @@ public class LiveMixinManager
{
private readonly SimulationOwnership simulationOwnership;

private bool processingRemoteHealthChange = false;
public bool IsRemoteHealthChanging { get; private set; }

public LiveMixinManager(SimulationOwnership simulationOwnership)
{
Expand All @@ -32,7 +32,7 @@ public bool ShouldApplyNextHealthUpdate(LiveMixin receiver, GameObject dealer =
return false;
}

if (!simulationOwnership.HasAnyLockType(id) && !processingRemoteHealthChange)
if (!simulationOwnership.HasAnyLockType(id) && !IsRemoteHealthChanging)
{
return false;
}
Expand Down Expand Up @@ -61,7 +61,7 @@ public bool ShouldApplyNextHealthUpdate(LiveMixin receiver, GameObject dealer =
return true;
}

public void SyncRemoteHealth(LiveMixin liveMixin, float remoteHealth)
public void SyncRemoteHealth(LiveMixin liveMixin, float remoteHealth, Vector3 position = default, DamageType damageType = DamageType.Normal)
{
if (liveMixin.health == remoteHealth)
{
Expand All @@ -70,22 +70,28 @@ public void SyncRemoteHealth(LiveMixin liveMixin, float remoteHealth)

float difference = remoteHealth - liveMixin.health;

processingRemoteHealthChange = true;
IsRemoteHealthChanging = true;

if (difference < 0)
// We catch the exceptions here because we don't want IsRemoteHealthChanging to be stuck to true
try
{
liveMixin.TakeDamage(difference);
}
else
if (difference < 0)
{
liveMixin.TakeDamage(difference, position, damageType);
}
else
{
liveMixin.AddHealth(difference);
}
} catch (Exception e)
{
liveMixin.AddHealth(difference);
Log.Error(e, $"Encountered an expcetion while processing health update");
}

processingRemoteHealthChange = false;
IsRemoteHealthChanging = false;

// We mainly only do the above to trigger damage effects and sounds. After those, we sync the remote value
// to ensure that any floating point discrepencies aren't an issue.
liveMixin.health = remoteHealth;
}

}
40 changes: 40 additions & 0 deletions NitroxClient/GameLogic/Spawning/Bases/BaseLeakEntitySpawner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.Util;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;

namespace NitroxClient.GameLogic.Spawning.Bases;

public class BaseLeakEntitySpawner : SyncEntitySpawner<BaseLeakEntity>
{
private readonly LiveMixinManager liveMixinManager;

public BaseLeakEntitySpawner(LiveMixinManager liveMixinManager)
{
this.liveMixinManager = liveMixinManager;
}

protected override IEnumerator SpawnAsync(BaseLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
SpawnSync(entity, result);
yield break;
}

protected override bool SpawnsOwnChildren(BaseLeakEntity entity) => false;

protected override bool SpawnSync(BaseLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
if (!NitroxEntity.TryGetComponentFrom(entity.ParentId, out BaseHullStrength baseHullStrength))
{
Log.Error($"[{nameof(BaseLeakEntitySpawner)}] Couldn't find a {nameof(BaseHullStrength)} from id {entity.ParentId}");
return true;
}

BaseLeakManager baseLeakManager = baseHullStrength.gameObject.EnsureComponent<BaseLeakManager>();
baseLeakManager.EnsureLeak(entity.RelativeCell.ToUnity(), entity.Id, entity.Health);
return true;
}
}
34 changes: 28 additions & 6 deletions NitroxClient/GameLogic/Spawning/Bases/BuildEntitySpawner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ namespace NitroxClient.GameLogic.Spawning.Bases;
public class BuildEntitySpawner : EntitySpawner<BuildEntity>
{
private readonly Entities entities;
private readonly BaseLeakEntitySpawner baseLeakEntitySpawner;

public BuildEntitySpawner(Entities entities)
public BuildEntitySpawner(Entities entities, BaseLeakEntitySpawner baseLeakEntitySpawner)
{
this.entities = entities;
this.baseLeakEntitySpawner = baseLeakEntitySpawner;
}

protected override IEnumerator SpawnAsync(BuildEntity entity, TaskResult<Optional<GameObject>> result)
Expand All @@ -50,10 +52,28 @@ protected override IEnumerator SpawnAsync(BuildEntity entity, TaskResult<Optiona
#endif
yield return entities.SpawnBatchAsync(entity.ChildEntities.OfType<PlayerWorldEntity>().ToList<Entity>());
yield return MoonpoolManager.RestoreMoonpools(entity.ChildEntities.OfType<MoonpoolEntity>(), @base);
foreach (MapRoomEntity mapRoomEntity in entity.ChildEntities.OfType<MapRoomEntity>())

TaskResult<Optional<GameObject>> childResult = new();
bool atLeastOneLeak = false;
foreach (Entity childEntity in entity.ChildEntities)
{
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
switch (childEntity)
{
case MapRoomEntity mapRoomEntity:
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
break;
case BaseLeakEntity baseLeakEntity:
atLeastOneLeak = true;
yield return baseLeakEntitySpawner.SpawnAsync(baseLeakEntity, childResult);
break;
}
}
if (atLeastOneLeak)
{
BaseHullStrength baseHullStrength = @base.GetComponent<BaseHullStrength>();
ErrorMessage.AddMessage(Language.main.GetFormat("BaseHullStrDamageDetected", baseHullStrength.totalStrength));
}

result.Set(@base.gameObject);
}

Expand Down Expand Up @@ -124,11 +144,13 @@ public static IEnumerator SetupBase(BuildEntity buildEntity, Base @base, Entitie
if (childEntity is InteriorPieceEntity || childEntity is ModuleEntity ||
childEntity.GetType() == typeof(GlobalRootEntity))
{
if (childEntity is GhostEntity ghostEntity)
switch (childEntity)
{
ghostChildrenEntities.Add(ghostEntity);
continue;
case GhostEntity ghostEntity:
ghostChildrenEntities.Add(ghostEntity);
continue;
}

yield return entities.SpawnEntityAsync(childEntity, true);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public IEnumerator SpawnAsync(WorldEntity entity, Optional<GameObject> parent, E

// The server may send us a player entity but they are not guarenteed to be actively connected at the moment - don't spawn them. In the
// future, we could make this configurable to be able to spawn disconnected players in the world.
if (remotePlayer.HasValue)
if (remotePlayer.HasValue && !remotePlayer.Value.Body)
{
GameObject remotePlayerBody = CloneLocalPlayerBodyPrototype();

Expand Down
Loading

0 comments on commit 5c48f8c

Please sign in to comment.