Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync radiation leaks #2100

Merged
merged 3 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Nitrox.Test;
using Nitrox.Test.Helper;
using Nitrox.Test.Helper.Faker;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxServer_Subnautica;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
using NitroxServer.Serialization.World;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
using NitroxModel.DataStructures;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
using NitroxServer.Serialization.World;
using NitroxServer_Subnautica;

namespace NitroxServer.Serialization;

Expand Down Expand Up @@ -321,6 +320,10 @@ private static void EntityTest(Entity entity, Entity entityAfter)
case BeaconMetadata metadata when entityAfter.Metadata is BeaconMetadata metadataAfter:
Assert.AreEqual(metadata.Label, metadataAfter.Label);
break;
case RadiationMetadata metadata when entityAfter.Metadata is RadiationMetadata metadataAfter:
Assert.AreEqual(metadata.Health, metadataAfter.Health);
Assert.AreEqual(metadata.FixRealTime, metadataAfter.FixRealTime);
break;
default:
Assert.Fail($"Runtime type of {nameof(Entity)}.{nameof(Entity.Metadata)} is not equal: {entity.Metadata?.GetType().Name} - {entityAfter.Metadata?.GetType().Name}");
break;
Expand Down Expand Up @@ -413,8 +416,11 @@ private static void EntityTest(Entity entity, Entity entityAfter)
Assert.AreEqual(vehicleWorldEntity.SpawnerId, vehicleWorldEntityAfter.SpawnerId);
Assert.AreEqual(vehicleWorldEntity.ConstructionTime, vehicleWorldEntityAfter.ConstructionTime);
break;
case RadiationLeakEntity radiationLeakEntity when globalRootEntityAfter is RadiationLeakEntity radiationLeakEntityAfter:
Assert.AreEqual(radiationLeakEntity.ObjectIndex, radiationLeakEntityAfter.ObjectIndex);
break;
default:
Assert.Fail($"Runtime type of {nameof(WorldEntity)} is not equal even after the check: {worldEntity.GetType().Name} - {globalRootEntityAfter.GetType().Name}");
Assert.Fail($"Runtime type of {nameof(GlobalRootEntity)} is not equal even after the check: {worldEntity.GetType().Name} - {globalRootEntityAfter.GetType().Name}");
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public override void Process(AuroraAndTimeUpdate packet)
{
timeManager.ProcessUpdate(packet.TimeData.TimePacket);
StoryManager.UpdateAuroraData(packet.TimeData.AuroraEventData);
timeManager.AuroraRealExplosionTime = packet.TimeData.AuroraEventData.AuroraRealExplosionTime;
if (packet.Restore)
{
StoryManager.RestoreAurora();
Expand Down
3 changes: 2 additions & 1 deletion 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)
public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacketSender, EntityMetadataManager entityMetadataManager, PlayerManager playerManager, ILocalNitroxPlayer localPlayer, TimeManager timeManager)
{
this.packetSender = packetSender;
this.throttledPacketSender = throttledPacketSender;
Expand All @@ -60,6 +60,7 @@ 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(RadiationLeakEntity)] = new RadiationLeakEntitySpawner(timeManager);
entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this);
entitySpawnersByType[typeof(ModuleEntity)] = new ModuleEntitySpawner(this);
entitySpawnersByType[typeof(GhostEntity)] = new GhostEntitySpawner();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public GlobalRootInitialSyncProcessor(Entities entities)
// inventory items to them. Eventually, all of the below processors will become entities on their own
AddDependency<PlayerInitialSyncProcessor>();
AddDependency<RemotePlayerInitialSyncProcessor>();
AddDependency<StoryGoalInitialSyncProcessor>();
}

public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,7 @@ private static IEnumerator RefreshStoryWithLatestData()
private void SetTimeData(InitialPlayerSync packet)
{
timeManager.ProcessUpdate(packet.TimeData.TimePacket);
timeManager.InitRealTimeElapsed(packet.TimeData.TimePacket.RealTimeElapsed, packet.TimeData.TimePacket.UpdateTime, packet.IsFirstPlayer);
timeManager.AuroraRealExplosionTime = packet.TimeData.AuroraEventData.AuroraRealExplosionTime;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;

Expand All @@ -13,4 +14,9 @@ public Optional<EntityMetadata> From(object o)

return Optional.OfNullable(result);
}

protected T Resolve<T>() where T : class
{
return NitroxServiceLocator.Cache<T>.Value;
}
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;

namespace NitroxClient.GameLogic.Spawning.Metadata.Extractor;

public class RadiationMetadataExtractor : EntityMetadataExtractor<RadiationLeak, RadiationMetadata>
{
public override RadiationMetadata Extract(RadiationLeak leak)
{
// Note: this extractor should only be used when this radiation leak is being repaired
float realTimeFix = leak.liveMixin.IsFullHealth() ? (float)Resolve<TimeManager>().RealTimeElapsed : -1;
return new(leak.liveMixin.health, realTimeFix);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using UnityEngine;

Expand All @@ -11,4 +12,9 @@ public void ProcessMetadata(GameObject gameObject, EntityMetadata metadata)
{
ProcessMetadata(gameObject, (T)metadata);
}

protected T Resolve<T>() where T : class
{
return NitroxServiceLocator.Cache<T>.Value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using UnityEngine;

namespace NitroxClient.GameLogic.Spawning.Metadata.Processor;

public class RadiationMetadataProcessor : EntityMetadataProcessor<RadiationMetadata>
{
public override void ProcessMetadata(GameObject gameObject, RadiationMetadata metadata)
{
if (!gameObject.TryGetComponent(out LiveMixin liveMixin))
{
Log.Error($"[{nameof(RadiationMetadataProcessor)}] Couldn't find LiveMixin on {gameObject}");
return;
}
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
Resolve<LiveMixinManager>().SyncRemoteHealth(liveMixin, metadata.Health);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.GameLogic.Spawning.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using UnityEngine;

namespace NitroxClient.GameLogic.Spawning.WorldEntities;

public class RadiationLeakEntitySpawner : SyncEntitySpawner<RadiationLeakEntity>
{
// This constant is defined by Subnautica and should never be modified (same as for SubnauticaWorldModifier)
private const int TOTAL_LEAKS = 11;
tornac1234 marked this conversation as resolved.
Show resolved Hide resolved
private readonly TimeManager timeManager;
private readonly List<float> registeredLeaksFixTime = new();

public RadiationLeakEntitySpawner(TimeManager timeManager)
{
this.timeManager = timeManager;
}

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

protected override bool SpawnSync(RadiationLeakEntity entity, TaskResult<Optional<GameObject>> result)
{
// This script is located under (Aurora Scene) //Aurora-Main/Aurora so it's a good starting point to search through the GameObjects
CrashedShipExploder crashedShipExploder = CrashedShipExploder.main;
LeakingRadiation leakingRadiation = LeakingRadiation.main;
if (!crashedShipExploder || !leakingRadiation || entity.Metadata is not RadiationMetadata metadata)
{
return true;
}
Transform radiationLeaksHolder = crashedShipExploder.transform.Find("radiationleaks").GetChild(0);
RadiationLeak radiationLeak = radiationLeaksHolder.GetChild(entity.ObjectIndex).GetComponent<RadiationLeak>();
NitroxEntity.SetNewId(radiationLeak.gameObject, entity.Id);
radiationLeak.liveMixin.health = metadata.Health;
registeredLeaksFixTime.Add(metadata.FixRealTime);

// We can only calculate the radiation increment and dissipation once we got all radiation leaks info
if (crashedShipExploder.IsExploded() && registeredLeaksFixTime.Count == TOTAL_LEAKS)
{
RecalculateRadiationRadius(leakingRadiation);
}

return true;
}

public void RecalculateRadiationRadius(LeakingRadiation leakingRadiation)
{
float realElapsedTime = (float)timeManager.RealTimeElapsed;
// We substract the explosion time from the real time because before that, the radius doesn't increment
float realExplosionTime = timeManager.AuroraRealExplosionTime;
float maxRegisteredLeakFixTime = registeredLeaksFixTime.Max();

// Note: Only increment radius if leaks were fixed AFTER explosion (before, game code doesn't increase radius)

// If leaks aren't all fixed yet we calculate from current real elapsed time
float deltaTimeAfterExplosion = realElapsedTime - realExplosionTime;
if (maxRegisteredLeakFixTime == -1)
{
if (deltaTimeAfterExplosion > 0)
{
float radiusIncrement = deltaTimeAfterExplosion * leakingRadiation.kGrowRate;
// Calculation lines from LeakingRadiation.Update
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.kStartRadius + radiusIncrement, 0f, leakingRadiation.kMaxRadius);
leakingRadiation.damagePlayerInRadius.damageRadius = leakingRadiation.currentRadius;
leakingRadiation.radiatePlayerInRange.radiateRadius = leakingRadiation.currentRadius;
}
// If leaks aren't fixed, we won't need to calculate a radius decrement
return;
}
leakingRadiation.radiationFixed = true;

// If all leaks are fixed we calculate from the time they were fixed
float deltaAliveTime = maxRegisteredLeakFixTime - realExplosionTime;
if (deltaAliveTime > 0)
{
float radiusIncrement = deltaAliveTime * leakingRadiation.kGrowRate;
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.kStartRadius + radiusIncrement, 0f, leakingRadiation.kMaxRadius);
}

// Now calculate the natural dissipation decrement from the time leaks are fixed
// If they were fixed before real explosion time, we calculate from real explosion time
float deltaFixedTimeAfterExplosion = realElapsedTime - Mathf.Max(maxRegisteredLeakFixTime, realExplosionTime);
if (deltaFixedTimeAfterExplosion > 0)
{
float radiusDecrement = deltaFixedTimeAfterExplosion * leakingRadiation.kNaturalDissipation;
leakingRadiation.currentRadius = Mathf.Clamp(leakingRadiation.currentRadius + radiusDecrement, 0f, leakingRadiation.kMaxRadius);
}
leakingRadiation.damagePlayerInRadius.damageRadius = leakingRadiation.currentRadius;
leakingRadiation.radiatePlayerInRange.radiateRadius = leakingRadiation.currentRadius;
}

protected override bool SpawnsOwnChildren(RadiationLeakEntity entity) => false;
}
68 changes: 64 additions & 4 deletions NitroxClient/GameLogic/TimeManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;

Expand All @@ -7,15 +8,56 @@ namespace NitroxClient.GameLogic;
public class TimeManager
{
/// <summary>
/// Latest moment at which we updated the time
/// When first player connects to the server, time will resume when time will be resumed on server-side.
/// According to this, we need to freeze time on first player connecting before it has fully loaded.
/// </summary>
private bool freezeTime = true;

/// <summary>
/// Latest moment at which we updated the time
/// </summary>
private DateTimeOffset latestRegistrationTime;
/// <summary>
/// Latest registered value of the time
/// Latest registered value of the time
/// </summary>
private double latestRegisteredTime;

private const double DEFAULT_TIME = 480;
/// <summary>
/// Moment at which real time elapsed was determined
/// </summary>
private DateTimeOffset realTimeElapsedRegistrationTime;
/// <summary>
/// Only registered value of real time elapsed given when connecting. Associated to <see cref="realTimeElapsedRegistrationTime"/>
/// </summary>
private double realTimeElapsed;

public float AuroraRealExplosionTime { get; set; }

private const double DEFAULT_REAL_TIME = 0;

/// <summary>
/// Calculates the exact real time elapsed from an offset (<see cref="realTimeElapsedRegistrationTime"/>) and the delta time between
/// <see cref="DateTimeOffset.UtcNow"/> and the offset's exact <see cref="DateTimeOffset"/> (<see cref="latestRegistrationTime"/>).
/// </summary>
public double RealTimeElapsed
{
get
{
// Unitialized state
if (realTimeElapsedRegistrationTime == default)
{
return DEFAULT_REAL_TIME;
}
if (freezeTime)
{
return realTimeElapsed;
}

return (DateTimeOffset.UtcNow - realTimeElapsedRegistrationTime).TotalMilliseconds * 0.001 + realTimeElapsed;
}
}

private const double DEFAULT_SUBNAUTICA_TIME = 480;

/// <summary>
/// Calculates the current exact time from an offset (<see cref="latestRegisteredTime"/>) and the delta time between
Expand All @@ -32,7 +74,11 @@ public double CurrentTime
// Unitialized state
if (latestRegisteredTime == 0)
{
return DEFAULT_TIME;
return DEFAULT_SUBNAUTICA_TIME;
}
if (freezeTime)
{
return latestRegisteredTime;
}
return (DateTimeOffset.UtcNow - latestRegistrationTime).TotalMilliseconds * 0.001 + latestRegisteredTime;
}
Expand All @@ -56,6 +102,13 @@ public double CurrentTime

public void ProcessUpdate(TimeChange packet)
{
if (freezeTime && Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted)
{
freezeTime = false;
}
realTimeElapsedRegistrationTime = DateTimeOffset.FromUnixTimeMilliseconds(packet.UpdateTime);
realTimeElapsed = packet.RealTimeElapsed;

latestRegistrationTime = DateTimeOffset.FromUnixTimeMilliseconds(packet.UpdateTime);
latestRegisteredTime = packet.CurrentTime;

Expand All @@ -76,4 +129,11 @@ public double CalculateCurrentTime()
DeltaTime = (float)(currentTime - DayNightCycle.main.timePassedAsDouble);
return currentTime;
}

public void InitRealTimeElapsed(double realTimeElapsed, long registrationTime, bool isFirstPlayer)
{
this.realTimeElapsed = realTimeElapsed;
realTimeElapsedRegistrationTime = DateTimeOffset.FromUnixTimeMilliseconds(registrationTime);
freezeTime = isFirstPlayer;
}
}
9 changes: 8 additions & 1 deletion NitroxModel/DataStructures/GameLogic/AuroraEventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,23 @@ public class AuroraEventData
[DataMember(Order = 2)]
public float TimeToStartWarning;

/// <summary>
/// Real time in seconds at which Aurora's considered exploded
/// </summary>
[DataMember(Order = 3)]
public float AuroraRealExplosionTime;

[IgnoreConstructor]
protected AuroraEventData()
{
// Constructor for serialization. Has to be "protected" for json serialization.
}

public AuroraEventData(float timeToStartCountdown, float timeToStartWarning)
public AuroraEventData(float timeToStartCountdown, float timeToStartWarning, float auroraRealExplosionTime)
{
TimeToStartCountdown = timeToStartCountdown;
TimeToStartWarning = timeToStartWarning;
AuroraRealExplosionTime = auroraRealExplosionTime;
}

[NonSerialized]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace NitroxModel.DataStructures.GameLogic.Entities;
[ProtoInclude(56, typeof(PlanterEntity))]
[ProtoInclude(57, typeof(PlayerWorldEntity))]
[ProtoInclude(58, typeof(VehicleWorldEntity))]
[ProtoInclude(59, typeof(RadiationLeakEntity))]
public class GlobalRootEntity : WorldEntity
{
[IgnoreConstructor]
Expand Down
Loading