using System; using NitroxModel.DataStructures.GameLogic; using NitroxModel.Packets; using NitroxServer.Helper; using NitroxServer.GameLogic.Unlockables; using NitroxModel.Helper; using NitroxModel; namespace NitroxServer.GameLogic; /// /// Keeps track of time and Aurora-related events. /// public class StoryManager : IDisposable { private readonly PlayerManager playerManager; private readonly PDAStateData pdaStateData; private readonly StoryGoalData storyGoalData; private readonly TimeKeeper timeKeeper; private readonly string seed; /// /// Time at which the Aurora explosion countdown will start (last warning is sent). /// /// /// It is required to calculate the time at which the Aurora warnings will be sent (along with , look into AuroraWarnings.cs and CrashedShipExploder.cs for more information). /// public double AuroraCountdownTimeMs; /// /// Time at which the Aurora Events start (you start receiving warnings). /// public double AuroraWarningTimeMs; /// /// In seconds /// 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; } } } /// Whether we should make Aurora explode instantly or after a short countdown 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"); } /// /// Calculate the time at Aurora's explosion countdown will begin. /// /// /// Takes the current time into account. /// 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; } /// /// Clears the already completed sunbeam events to come and broadcasts it to all players along with the rescheduling of the specified sunbeam event. /// 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)); } /// Either the time in before Aurora explodes or -1 if it has already exploded. private double GetMinutesBeforeAuroraExplosion() { return AuroraCountdownTimeMs > ElapsedMilliseconds ? Math.Round((AuroraCountdownTimeMs - ElapsedMilliseconds) / 60000) : -1; } /// /// Makes a nice status of the Aurora events progress for the summary command. /// 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 } }