Files
Nitrox/NitroxServer/GameLogic/StoryManager.cs
2025-07-06 00:23:46 +02:00

211 lines
8.4 KiB
C#

using System;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Helper;
using NitroxServer.GameLogic.Unlockables;
using NitroxModel.Helper;
using NitroxModel;
namespace NitroxServer.GameLogic;
/// <summary>
/// Keeps track of time and Aurora-related events.
/// </summary>
public class StoryManager : IDisposable
{
private readonly PlayerManager playerManager;
private readonly PDAStateData pdaStateData;
private readonly StoryGoalData storyGoalData;
private readonly TimeKeeper timeKeeper;
private readonly string seed;
/// <summary>
/// Time at which the Aurora explosion countdown will start (last warning is sent).
/// </summary>
/// <remarks>
/// It is required to calculate the time at which the Aurora warnings will be sent (along with <see cref="AuroraWarningTimeMs"/>, look into AuroraWarnings.cs and CrashedShipExploder.cs for more information).
/// </remarks>
public double AuroraCountdownTimeMs;
/// <summary>
/// Time at which the Aurora Events start (you start receiving warnings).
/// </summary>
public double AuroraWarningTimeMs;
/// <summary>
/// In seconds
/// </summary>
public double AuroraRealExplosionTime;
private double ElapsedMilliseconds => timeKeeper.ElapsedMilliseconds;
private double ElapsedSeconds => timeKeeper.ElapsedSeconds;
public StoryManager(PlayerManager playerManager, PDAStateData pdaStateData, StoryGoalData storyGoalData, TimeKeeper timeKeeper, string seed, double? auroraExplosionTime, double? auroraWarningTime, double? auroraRealExplosionTime)
{
this.playerManager = playerManager;
this.pdaStateData = pdaStateData;
this.storyGoalData = storyGoalData;
this.timeKeeper = timeKeeper;
this.seed = seed;
AuroraCountdownTimeMs = auroraExplosionTime ?? GenerateDeterministicAuroraTime(seed);
AuroraWarningTimeMs = auroraWarningTime ?? ElapsedMilliseconds;
// +27 is from CrashedShipExploder.IsExploded, -480 is from the default time (see TimeKeeper)
AuroraRealExplosionTime = auroraRealExplosionTime ?? AuroraCountdownTimeMs * 0.001 + 27 - TimeKeeper.DEFAULT_TIME;
timeKeeper.TimeSkipped += ReadjustAuroraRealExplosionTime;
}
public void ReadjustAuroraRealExplosionTime(double skipAmount)
{
// Readjust the aurora real explosion time when time skipping because it's based on in-game time
if (AuroraRealExplosionTime > timeKeeper.RealTimeElapsed)
{
double newTime = timeKeeper.RealTimeElapsed + skipAmount;
if (newTime > AuroraRealExplosionTime)
{
AuroraRealExplosionTime = timeKeeper.RealTimeElapsed;
}
else
{
AuroraRealExplosionTime -= skipAmount;
}
}
}
/// <param name="instantaneous">Whether we should make Aurora explode instantly or after a short countdown</param>
public void BroadcastExplodeAurora(bool instantaneous)
{
// Calculations from CrashedShipExploder.OnConsoleCommand_countdownship()
// We add 3 seconds to the cooldown (Subnautica adds only 1) so that players have enough time to receive the packet and process it
AuroraCountdownTimeMs = ElapsedMilliseconds + 3000;
AuroraWarningTimeMs = AuroraCountdownTimeMs;
AuroraRealExplosionTime = timeKeeper.RealTimeElapsed + 30; // 27 + 3
if (instantaneous)
{
// Calculations from CrashedShipExploder.OnConsoleCommand_explodeship()
// Removes 25 seconds to the countdown time, jumping to the exact moment of the explosion
AuroraCountdownTimeMs -= 25000;
// Is 1 second less than countdown time to have the game understand that we only want the explosion.
AuroraWarningTimeMs = AuroraCountdownTimeMs - 1000;
AuroraRealExplosionTime -= 25;
Log.Info("Aurora's explosion initiated");
}
else
{
Log.Info("Aurora's explosion countdown will start in 3 seconds");
}
playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), false));
}
public void BroadcastRestoreAurora()
{
AuroraWarningTimeMs = ElapsedMilliseconds;
AuroraCountdownTimeMs = GenerateDeterministicAuroraTime(seed);
// Current time + deltaTime before countdown + 27 seconds before explosion
AuroraRealExplosionTime = timeKeeper.RealTimeElapsed + (AuroraCountdownTimeMs - timeKeeper.ElapsedMilliseconds) * 0.001 + 27;
// We need to clear these entries from PdaLog and CompletedGoals to make sure that the client, when reconnecting, doesn't have false information
foreach (string eventKey in AuroraEventData.GoalNames)
{
pdaStateData.PdaLog.RemoveAll(entry => entry.Key == eventKey);
storyGoalData.CompletedGoals.Remove(eventKey);
}
playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), true));
Log.Info($"Restored Aurora, will explode again in {GetMinutesBeforeAuroraExplosion()} minutes");
}
/// <summary>
/// Calculate the time at Aurora's explosion countdown will begin.
/// </summary>
/// <remarks>
/// Takes the current time into account.
/// </remarks>
private double GenerateDeterministicAuroraTime(string seed)
{
// Copied from CrashedShipExploder.SetExplodeTime() and changed from seconds to ms
DeterministicGenerator generator = new(seed, nameof(StoryManager));
return ElapsedMilliseconds + generator.NextDouble(2.3d, 4d) * 1200d * 1000d;
}
/// <summary>
/// Clears the already completed sunbeam events to come and broadcasts it to all players along with the rescheduling of the specified sunbeam event.
/// </summary>
public void StartSunbeamEvent(string sunbeamEventKey)
{
int beginIndex = PlaySunbeamEvent.SunbeamGoals.GetIndex(sunbeamEventKey);
if (beginIndex == -1)
{
Log.Error($"Couldn't find the corresponding sunbeam event in {nameof(PlaySunbeamEvent.SunbeamGoals)} for key {sunbeamEventKey}");
return;
}
for (int i = beginIndex; i < PlaySunbeamEvent.SunbeamGoals.Length; i++)
{
storyGoalData.CompletedGoals.Remove(PlaySunbeamEvent.SunbeamGoals[i]);
}
playerManager.SendPacketToAllPlayers(new PlaySunbeamEvent(sunbeamEventKey));
}
/// <returns>Either the time in before Aurora explodes or -1 if it has already exploded.</returns>
private double GetMinutesBeforeAuroraExplosion()
{
return AuroraCountdownTimeMs > ElapsedMilliseconds ? Math.Round((AuroraCountdownTimeMs - ElapsedMilliseconds) / 60000) : -1;
}
/// <summary>
/// Makes a nice status of the Aurora events progress for the summary command.
/// </summary>
public string GetAuroraStateSummary()
{
double minutesBeforeExplosion = GetMinutesBeforeAuroraExplosion();
if (minutesBeforeExplosion < 0)
{
return "already exploded";
}
// Based on AuroraWarnings.Update calculations
// auroraWarningNumber is the amount of received Aurora warnings (there are 4 in total)
int auroraWarningNumber = 0;
if (ElapsedMilliseconds >= AuroraCountdownTimeMs)
{
auroraWarningNumber = 4;
}
else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.8f))
{
auroraWarningNumber = 3;
}
else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.5f))
{
auroraWarningNumber = 2;
}
else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.2f))
{
auroraWarningNumber = 1;
}
return $"explodes in {minutesBeforeExplosion} minutes [{auroraWarningNumber}/4]";
}
public AuroraEventData MakeAuroraData()
{
return new((float)AuroraCountdownTimeMs * 0.001f, (float)AuroraWarningTimeMs * 0.001f, (float)AuroraRealExplosionTime);
}
public TimeData GetTimeData()
{
return new(timeKeeper.MakeTimePacket(), MakeAuroraData());
}
public void Dispose()
{
timeKeeper.TimeSkipped -= ReadjustAuroraRealExplosionTime;
GC.SuppressFinalize(this);
}
public enum TimeModification
{
DAY, NIGHT, SKIP
}
}