using System; using System.Collections.Generic; using System.Linq; using NitroxClient.GameLogic.InitialSync.Abstract; using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures.GameLogic; using NitroxModel.Packets; using Story; namespace NitroxClient.GameLogic.InitialSync; public sealed class StoryGoalInitialSyncProcessor : InitialSyncProcessor { private readonly TimeManager timeManager; public StoryGoalInitialSyncProcessor(TimeManager timeManager) { this.timeManager = timeManager; AddStep(SetTimeData); AddStep(SetupStoryGoalManager); AddStep(SetupTrackers); AddStep(SetupAuroraAndSunbeam); AddStep(SetScheduledGoals); } private static void SetupStoryGoalManager(InitialPlayerSync packet) { List completedGoals = packet.StoryGoalData.CompletedGoals; List radioQueue = packet.StoryGoalData.RadioQueue; Dictionary personalGoals = packet.StoryGoalData.PersonalCompletedGoalsWithTimestamp; StoryGoalManager storyGoalManager = StoryGoalManager.main; storyGoalManager.completedGoals.AddRange(completedGoals); storyGoalManager.pendingRadioMessages.AddRange(radioQueue); storyGoalManager.PulsePendingMessages(); // Restore states of GoalManager and the (tutorial) arrow system foreach (KeyValuePair entry in personalGoals) { Goal entryGoal = GoalManager.main.goals.Find(goal => goal.customGoalName.Equals(entry.Key)); if (entryGoal != null) { entryGoal.SetTimeCompleted(entry.Value); } } GoalManager.main.completedGoalNames.AddRange(personalGoals.Keys); PlayerWorldArrows.main.completedCustomGoals.AddRange(personalGoals.Keys); // Deactivate the current arrow if it was completed if (personalGoals.Any(goal => goal.Key.Equals(WorldArrowManager.main.currentGoalText))) { WorldArrowManager.main.DeactivateArrow(); } Log.Info($""" Received initial sync packet with: - Completed story goals : {completedGoals.Count} - Personal goals : {personalGoals.Count} - Radio queue : {radioQueue.Count} """); } private static void SetupTrackers(InitialPlayerSync packet) { List completedGoals = packet.StoryGoalData.CompletedGoals; StoryGoalManager storyGoalManager = StoryGoalManager.main; OnGoalUnlockTracker onGoalUnlockTracker = storyGoalManager.onGoalUnlockTracker; CompoundGoalTracker compoundGoalTracker = storyGoalManager.compoundGoalTracker; // Initializing CompoundGoalTracker and OnGoalUnlockTracker again (with OnSceneObjectsLoaded) requires us to // we first clear what was done in the first iteration of OnSceneObjectsLoaded onGoalUnlockTracker.goalUnlocks.Clear(); compoundGoalTracker.goals.Clear(); // we force initialized to false so OnSceneObjectsLoaded actually does something storyGoalManager.initialized = false; storyGoalManager.OnSceneObjectsLoaded(); // Clean LocationGoalTracker, BiomeGoalTracker and ItemGoalTracker already completed goals storyGoalManager.locationGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key)); storyGoalManager.biomeGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key)); List techTypesToRemove = new(); foreach (KeyValuePair> entry in storyGoalManager.itemGoalTracker.goals) { // Goals are all triggered at the same time but we don't know if some entries share certain goals if (entry.Value.All(goal => completedGoals.Contains(goal.key))) { techTypesToRemove.Add(entry.Key); continue; } } techTypesToRemove.ForEach(techType => storyGoalManager.itemGoalTracker.goals.Remove(techType)); // OnGoalUnlock might trigger the creation of a signal which is later on set to invisible when getting close to it // the invisibility is managed by PingInstance_Set_Patches and is restored during PlayerPreferencesInitialSyncProcessor // So we still need to recreate the signals at every game launch // To avoid having the SignalPing play its sound we just make its notification null while triggering it // (the sound is something like "coordinates added to the gps" or something) SignalPing prefabSignalPing = onGoalUnlockTracker.signalPrefab.GetComponent(); PDANotification pdaNotification = prefabSignalPing.vo; prefabSignalPing.vo = null; foreach (OnGoalUnlock onGoalUnlock in onGoalUnlockTracker.unlockData.onGoalUnlocks) { if (completedGoals.Contains(onGoalUnlock.goal)) { // Code adapted from OnGoalUnlock.Trigger foreach (UnlockSignalData unlockSignalData in onGoalUnlock.signals) { unlockSignalData.Trigger(onGoalUnlockTracker); } } } // recover the notification sound prefabSignalPing.vo = pdaNotification; } // Must happen after CompletedGoals private static void SetupAuroraAndSunbeam(InitialPlayerSync packet) { TimeData timeData = packet.TimeData; AuroraWarnings auroraWarnings = Player.mainObject.GetComponentInChildren(true); auroraWarnings.timeSerialized = DayNightCycle.main.timePassedAsFloat; auroraWarnings.OnProtoDeserialize(null); CrashedShipExploder.main.version = 2; CrashedShipExploder.main.initialized = true; StoryManager.UpdateAuroraData(timeData.AuroraEventData); CrashedShipExploder.main.timeSerialized = DayNightCycle.main.timePassedAsFloat; CrashedShipExploder.main.OnProtoDeserialize(null); // Sunbeam countdown is deducted from the scheduled goal PrecursorGunAimCheck NitroxScheduledGoal sunbeamCountdownGoal = packet.StoryGoalData.ScheduledGoals.Find(goal => string.Equals(goal.GoalKey, "PrecursorGunAimCheck", StringComparison.OrdinalIgnoreCase)); if (sunbeamCountdownGoal != null) { StoryGoalCustomEventHandler.main.countdownActive = true; StoryGoalCustomEventHandler.main.countdownStartingTime = sunbeamCountdownGoal.TimeExecute - 2370; // See StoryGoalCustomEventHandler.endTime for calculation (endTime - 30 seconds) } } // Must happen after CompletedGoals private static void SetScheduledGoals(InitialPlayerSync packet) { List scheduledGoals = packet.StoryGoalData.ScheduledGoals; // We don't want any scheduled goal we add now to be executed before initial sync has finished, else they might not get broadcasted StoryGoalScheduler.main.paused = true; Multiplayer.OnLoadingComplete += () => StoryGoalScheduler.main.paused = false; foreach (NitroxScheduledGoal scheduledGoal in scheduledGoals) { // Clear duplicated goals that might have appeared during loading and before sync StoryGoalScheduler.main.schedule.RemoveAll(goal => goal.goalKey == scheduledGoal.GoalKey); ScheduledGoal goal = new() { goalKey = scheduledGoal.GoalKey, goalType = (Story.GoalType)scheduledGoal.GoalType, timeExecute = scheduledGoal.TimeExecute, }; if (!StoryGoalManager.main.completedGoals.Contains(goal.goalKey)) { StoryGoalScheduler.main.schedule.Add(goal); } } RefreshStoryWithLatestData(); } // Must happen after CompletedGoals private static void RefreshStoryWithLatestData() { // If those aren't set up yet, they'll initialize correctly in time // Else, we need to force them to acquire the right data if (StoryGoalCustomEventHandler.main) { StoryGoalCustomEventHandler.main.Awake(); } if (PrecursorGunStoryEvents.main) { PrecursorGunStoryEvents.main.Start(); } } 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; } }