Files
Nitrox/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs
2025-07-06 00:23:46 +02:00

198 lines
8.5 KiB
C#

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<string> completedGoals = packet.StoryGoalData.CompletedGoals;
List<string> radioQueue = packet.StoryGoalData.RadioQueue;
Dictionary<string, float> 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<string, float> 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<string> 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<TechType> techTypesToRemove = new();
foreach (KeyValuePair<TechType, List<ItemGoal>> 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<SignalPing>();
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<AuroraWarnings>(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<NitroxScheduledGoal> 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;
}
}