using System; using System.Diagnostics; using System.Timers; using NitroxModel.Networking; using NitroxModel.Packets; using static NitroxServer.GameLogic.StoryManager; namespace NitroxServer.GameLogic; public class TimeKeeper { private readonly PlayerManager playerManager; private readonly NtpSyncer ntpSyncer; private readonly Stopwatch stopWatch = new(); /// /// Default time in Base SN is 480s /// public const int DEFAULT_TIME = 480; /// /// Latest registered time without taking the current stopwatch time in account. /// private double elapsedTimeOutsideStopWatchMs; private readonly double realTimeElapsed; /// /// Total elapsed time in milliseconds (adding the current stopwatch time with the latest registered time ). /// public double ElapsedMilliseconds { get => stopWatch.ElapsedMilliseconds + elapsedTimeOutsideStopWatchMs; internal set { elapsedTimeOutsideStopWatchMs = value - stopWatch.ElapsedMilliseconds; } } /// /// Total elapsed time in seconds (converted from ). /// public double ElapsedSeconds { get => ElapsedMilliseconds * 0.001; set => ElapsedMilliseconds = value * 1000; } public double RealTimeElapsed => stopWatch.ElapsedMilliseconds * 0.001 + realTimeElapsed; /// /// Subnautica's equivalent of days. /// /// /// Uses ceiling because days count start at 1 and not 0. /// public int Day => (int)Math.Ceiling(ElapsedMilliseconds / TimeSpan.FromMinutes(20).TotalMilliseconds); /// /// Timer responsible for periodically sending time resync packets. /// /// /// Is created by . /// public Timer ResyncTimer; /// /// Time in seconds between each resync packet sending. /// /// /// AKA Interval of . /// private const int RESYNC_INTERVAL = 60; public TimeSkippedEventHandler TimeSkipped; /// /// Time in seconds between each ntp connection attempt. /// private const int NTP_RETRY_INTERVAL = 60; public TimeKeeper(PlayerManager playerManager, NtpSyncer ntpSyncer, double elapsedSeconds, double realTimeElapsed) { this.playerManager = playerManager; this.ntpSyncer = ntpSyncer; // We only need the correction offset to be calculated once ntpSyncer.Setup(true, (onlineMode, _) => // TODO: set to false after tests { if (!onlineMode) { // until we get online even once, we'll retry the ntp sync sequence every NTP_RETRY_INTERVAL StartNtpTimer(); } }); ntpSyncer.RequestNtpService(); elapsedTimeOutsideStopWatchMs = elapsedSeconds == 0 ? TimeSpan.FromSeconds(DEFAULT_TIME).TotalMilliseconds : elapsedSeconds * 1000; this.realTimeElapsed = realTimeElapsed; ResyncTimer = MakeResyncTimer(); } /// /// Creates a timer that periodically sends resync packets to players. /// public Timer MakeResyncTimer() { Timer resyncTimer = new() { Interval = TimeSpan.FromSeconds(RESYNC_INTERVAL).TotalMilliseconds, AutoReset = true }; resyncTimer.Elapsed += delegate { playerManager.SendPacketToAllPlayers(MakeTimePacket()); }; return resyncTimer; } private void StartNtpTimer() { Timer retryTimer = new(TimeSpan.FromSeconds(NTP_RETRY_INTERVAL).TotalMilliseconds) { AutoReset = true, }; retryTimer.Elapsed += delegate { // Reset the syncer before starting another round of it ntpSyncer.Dispose(); ntpSyncer.Setup(true, (onlineMode, _) => // TODO: set to false after tests { if (onlineMode) { retryTimer.Close(); } }); ntpSyncer.RequestNtpService(); }; retryTimer.Start(); } public void StartCounting() { stopWatch.Start(); ResyncTimer.Start(); playerManager.SendPacketToAllPlayers(MakeTimePacket()); } public void ResetCount() { stopWatch.Reset(); } public void StopCounting() { stopWatch.Stop(); ResyncTimer.Stop(); } /// /// Set current time depending on the current time in the day (replication of SN's system, see DayNightCycle.cs commands for more information). /// /// Time to which you want to get to. public void ChangeTime(TimeModification type) { double skipAmount = 0; switch (type) { case TimeModification.DAY: skipAmount = 1200 - (ElapsedSeconds % 1200) + 600; break; case TimeModification.NIGHT: skipAmount = 1200 - (ElapsedSeconds % 1200); break; case TimeModification.SKIP: skipAmount = 600 - (ElapsedSeconds % 600); break; } if (skipAmount > 0) { ElapsedSeconds += skipAmount; TimeSkipped?.Invoke(skipAmount); playerManager.SendPacketToAllPlayers(MakeTimePacket()); } } public TimeChange MakeTimePacket() { return new(ElapsedSeconds, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), RealTimeElapsed, ntpSyncer.OnlineMode, ntpSyncer.CorrectionOffset.Ticks); } public delegate void TimeSkippedEventHandler(double skipAmount); }