From 222e71876792df7927fb934bee99fdd457158681 Mon Sep 17 00:00:00 2001 From: tornac1234 <24827220+tornac1234@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:59:54 +0100 Subject: [PATCH 1/2] Fix beacons not reappearing after reconnecting, and remove useless field from server story data --- .../StoryGoalInitialSyncProcessor.cs | 45 +++++++++++++------ .../GameLogic/InitialStoryGoalData.cs | 4 +- .../GameLogic/Unlockables/StoryGoalData.cs | 8 +++- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs index a1c6d71585..1bdc741369 100644 --- a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs @@ -22,10 +22,9 @@ public StoryGoalInitialSyncProcessor(TimeManager timeManager) AddStep(SetupTrackers); AddStep(SetupAuroraAndSunbeam); AddStep(SetScheduledGoals); - AddStep(RefreshStoryWithLatestData); } - private static IEnumerator SetupStoryGoalManager(InitialPlayerSync packet) + private static void SetupStoryGoalManager(InitialPlayerSync packet) { List completedGoals = packet.StoryGoalData.CompletedGoals; List radioQueue = packet.StoryGoalData.RadioQueue; @@ -61,16 +60,16 @@ private static IEnumerator SetupStoryGoalManager(InitialPlayerSync packet) - Personal goals : {personalGoals.Count} - Radio queue : {radioQueue.Count} """); - yield break; } - private static IEnumerator SetupTrackers(InitialPlayerSync packet) + private static void SetupTrackers(InitialPlayerSync packet) { List completedGoals = packet.StoryGoalData.CompletedGoals; StoryGoalManager storyGoalManager = StoryGoalManager.main; + OnGoalUnlockTracker onGoalUnlockTracker = storyGoalManager.onGoalUnlockTracker; // Initialize CompoundGoalTracker and OnGoalUnlockTracker and clear their already completed goals - storyGoalManager.OnSceneObjectsLoaded(); + // StoryGoalManager.OnSceneObjectsLoaded() is already applied so we can directly modify the trackers' content storyGoalManager.compoundGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key)); completedGoals.ForEach(goal => storyGoalManager.onGoalUnlockTracker.goalUnlocks.Remove(goal)); @@ -90,11 +89,34 @@ private static IEnumerator SetupTrackers(InitialPlayerSync packet) } } techTypesToRemove.ForEach(techType => storyGoalManager.itemGoalTracker.goals.Remove(techType)); - yield break; + + // 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) + PDANotification pdaNotification = onGoalUnlockTracker.signalPrefab.GetComponent().vo; + onGoalUnlockTracker.signalPrefab.GetComponent().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 + onGoalUnlockTracker.signalPrefab.GetComponent().vo = pdaNotification; } // Must happen after CompletedGoals - private static IEnumerator SetupAuroraAndSunbeam(InitialPlayerSync packet) + private static void SetupAuroraAndSunbeam(InitialPlayerSync packet) { TimeData timeData = packet.TimeData; @@ -115,12 +137,10 @@ private static IEnumerator SetupAuroraAndSunbeam(InitialPlayerSync packet) StoryGoalCustomEventHandler.main.countdownStartingTime = sunbeamCountdownGoal.TimeExecute - 2370; // See StoryGoalCustomEventHandler.endTime for calculation (endTime - 30 seconds) } - - yield break; } // Must happen after CompletedGoals - private static IEnumerator SetScheduledGoals(InitialPlayerSync packet) + private static void SetScheduledGoals(InitialPlayerSync packet) { List scheduledGoals = packet.StoryGoalData.ScheduledGoals; @@ -141,11 +161,11 @@ private static IEnumerator SetScheduledGoals(InitialPlayerSync packet) } } - yield break; + RefreshStoryWithLatestData(); } // Must happen after CompletedGoals - private static IEnumerator RefreshStoryWithLatestData() + 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 @@ -157,7 +177,6 @@ private static IEnumerator RefreshStoryWithLatestData() { PrecursorGunStoryEvents.main.Start(); } - yield break; } private void SetTimeData(InitialPlayerSync packet) diff --git a/NitroxModel/DataStructures/GameLogic/InitialStoryGoalData.cs b/NitroxModel/DataStructures/GameLogic/InitialStoryGoalData.cs index 0a912d27dd..0c0fe0a850 100644 --- a/NitroxModel/DataStructures/GameLogic/InitialStoryGoalData.cs +++ b/NitroxModel/DataStructures/GameLogic/InitialStoryGoalData.cs @@ -9,7 +9,6 @@ public class InitialStoryGoalData { public List CompletedGoals { get; set; } public List RadioQueue { get; set; } - public List GoalUnlocks { get; set; } public List ScheduledGoals { get; set; } /// @@ -24,11 +23,10 @@ protected InitialStoryGoalData() // Constructor for serialization. Has to be "protected" for json serialization. } - public InitialStoryGoalData(List completedGoals, List radioQueue, List goalUnlocks, List scheduledGoals, Dictionary personalCompletedGoalsWithTimestamp) + public InitialStoryGoalData(List completedGoals, List radioQueue, List scheduledGoals, Dictionary personalCompletedGoalsWithTimestamp) { CompletedGoals = completedGoals; RadioQueue = radioQueue; - GoalUnlocks = goalUnlocks; ScheduledGoals = scheduledGoals; PersonalCompletedGoalsWithTimestamp = personalCompletedGoalsWithTimestamp; } diff --git a/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs b/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs index 4bc139578d..99b615e1b8 100644 --- a/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs +++ b/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs @@ -26,8 +26,12 @@ public bool RemovedLatestRadioMessage() { return false; } - + string message = RadioQueue[0]; RadioQueue.RemoveAt(0); + + // Just like StoryGoalManager.ExecutePendingRadioMessage + CompletedGoals.Add("OnPlay" + message); + return true; } @@ -39,7 +43,7 @@ public static StoryGoalData From(StoryGoalData storyGoals, ScheduleKeeper schedu public InitialStoryGoalData GetInitialStoryGoalData(ScheduleKeeper scheduleKeeper, Player player) { - return new InitialStoryGoalData(new List(CompletedGoals), new List(RadioQueue), new List(GoalUnlocks), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp)); + return new InitialStoryGoalData(new List(CompletedGoals), new List(RadioQueue), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp)); } } } From 7d47cc303d07c3576f3ce7d1f319b788e72afc12 Mon Sep 17 00:00:00 2001 From: tornac1234 <24827220+tornac1234@users.noreply.github.com> Date: Sun, 5 Jan 2025 23:29:02 +0100 Subject: [PATCH 2/2] Some corrections to Story initial sync, fix an aurora explosion edge case, --- .../Serialization/WorldPersistenceTest.cs | 1 - .../StoryGoalInitialSyncProcessor.cs | 34 ++++++---- .../CrashedShipExploder_Update_Patch.cs | 21 +++++++ .../StoryGoalScheduler_Schedule_Patch.cs | 3 +- .../Processors/StoryGoalExecutedProcessor.cs | 2 +- .../GameLogic/Unlockables/StoryGoalData.cs | 62 +++++++++---------- NitroxServer/Server.cs | 1 - 7 files changed, 74 insertions(+), 50 deletions(-) create mode 100644 NitroxPatcher/Patches/Dynamic/CrashedShipExploder_Update_Patch.cs diff --git a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs index 96f91dca7c..ae40802ded 100644 --- a/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs +++ b/Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs @@ -81,7 +81,6 @@ private static void StoryGoalTest(StoryGoalData storyGoal, StoryGoalData storyGo { Assert.IsTrue(storyGoal.CompletedGoals.SequenceEqual(storyGoalAfter.CompletedGoals)); Assert.IsTrue(storyGoal.RadioQueue.SequenceEqual(storyGoalAfter.RadioQueue)); - Assert.IsTrue(storyGoal.GoalUnlocks.SequenceEqual(storyGoalAfter.GoalUnlocks)); AssertHelper.IsListEqual(storyGoal.ScheduledGoals.OrderBy(x => x.GoalKey), storyGoalAfter.ScheduledGoals.OrderBy(x => x.GoalKey), (scheduledGoal, scheduledGoalAfter) => { Assert.AreEqual(scheduledGoal.TimeExecute, scheduledGoalAfter.TimeExecute); diff --git a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs index 1bdc741369..274079aa69 100644 --- a/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs +++ b/NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs @@ -1,8 +1,8 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using NitroxClient.GameLogic.InitialSync.Abstract; +using NitroxClient.MonoBehaviours; using NitroxModel.DataStructures.GameLogic; using NitroxModel.Packets; using Story; @@ -67,13 +67,16 @@ private static void SetupTrackers(InitialPlayerSync packet) List completedGoals = packet.StoryGoalData.CompletedGoals; StoryGoalManager storyGoalManager = StoryGoalManager.main; OnGoalUnlockTracker onGoalUnlockTracker = storyGoalManager.onGoalUnlockTracker; - - // Initialize CompoundGoalTracker and OnGoalUnlockTracker and clear their already completed goals - // StoryGoalManager.OnSceneObjectsLoaded() is already applied so we can directly modify the trackers' content - - storyGoalManager.compoundGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key)); - completedGoals.ForEach(goal => storyGoalManager.onGoalUnlockTracker.goalUnlocks.Remove(goal)); - + 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)); @@ -96,8 +99,9 @@ private static void SetupTrackers(InitialPlayerSync packet) // 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) - PDANotification pdaNotification = onGoalUnlockTracker.signalPrefab.GetComponent().vo; - onGoalUnlockTracker.signalPrefab.GetComponent().vo = null; + SignalPing prefabSignalPing = onGoalUnlockTracker.signalPrefab.GetComponent(); + PDANotification pdaNotification = prefabSignalPing.vo; + prefabSignalPing.vo = null; foreach (OnGoalUnlock onGoalUnlock in onGoalUnlockTracker.unlockData.onGoalUnlocks) { @@ -112,7 +116,7 @@ private static void SetupTrackers(InitialPlayerSync packet) } // recover the notification sound - onGoalUnlockTracker.signalPrefab.GetComponent().vo = pdaNotification; + prefabSignalPing.vo = pdaNotification; } // Must happen after CompletedGoals @@ -120,7 +124,7 @@ private static void SetupAuroraAndSunbeam(InitialPlayerSync packet) { TimeData timeData = packet.TimeData; - AuroraWarnings auroraWarnings = UnityEngine.Object.FindObjectOfType(); + AuroraWarnings auroraWarnings = Player.mainObject.GetComponentInChildren(true); auroraWarnings.timeSerialized = DayNightCycle.main.timePassedAsFloat; auroraWarnings.OnProtoDeserialize(null); @@ -144,6 +148,10 @@ 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 @@ -155,7 +163,7 @@ private static void SetScheduledGoals(InitialPlayerSync packet) goalType = (Story.GoalType)scheduledGoal.GoalType, timeExecute = scheduledGoal.TimeExecute, }; - if (goal.timeExecute >= DayNightCycle.main.timePassedAsDouble && !StoryGoalManager.main.completedGoals.Contains(goal.goalKey)) + if (!StoryGoalManager.main.completedGoals.Contains(goal.goalKey)) { StoryGoalScheduler.main.schedule.Add(goal); } diff --git a/NitroxPatcher/Patches/Dynamic/CrashedShipExploder_Update_Patch.cs b/NitroxPatcher/Patches/Dynamic/CrashedShipExploder_Update_Patch.cs new file mode 100644 index 0000000000..2f538baffb --- /dev/null +++ b/NitroxPatcher/Patches/Dynamic/CrashedShipExploder_Update_Patch.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using NitroxClient.MonoBehaviours; +using NitroxModel.Helper; + +namespace NitroxPatcher.Patches.Dynamic; + +/// +/// Prevents from occurring before initial sync has completed. +/// It lets us avoid a very weird edge case in which SetExplodeTime happens before server time is set on the client, +/// after what some event in this Update method might be triggered because there's a dead frame before the StoryGoalInitialSyncProcessor step +/// which sets up all the aurora story-related stuff locally. +/// +public sealed partial class CrashedShipExploder_Update_Patch : NitroxPatch, IDynamicPatch +{ + private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashedShipExploder t) => t.Update()); + + public static bool Prefix() + { + return Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted; + } +} diff --git a/NitroxPatcher/Patches/Dynamic/StoryGoalScheduler_Schedule_Patch.cs b/NitroxPatcher/Patches/Dynamic/StoryGoalScheduler_Schedule_Patch.cs index af2dda7757..8327ad3241 100644 --- a/NitroxPatcher/Patches/Dynamic/StoryGoalScheduler_Schedule_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/StoryGoalScheduler_Schedule_Patch.cs @@ -16,7 +16,8 @@ public sealed partial class StoryGoalScheduler_Schedule_Patch : NitroxPatch, IDy public static bool Prefix(StoryGoal goal, out bool __state) { __state = StoryGoalScheduler.main.schedule.Any(scheduledGoal => scheduledGoal.goalKey == goal.key) || - (goal.goalType == Story.GoalType.Radio && StoryGoalManager.main.pendingRadioMessages.Contains(goal.key)); + (goal.goalType == Story.GoalType.Radio && StoryGoalManager.main.pendingRadioMessages.Contains(goal.key)) || + StoryGoalManager.main.completedGoals.Contains(goal.key); if (__state) { diff --git a/NitroxServer/Communication/Packets/Processors/StoryGoalExecutedProcessor.cs b/NitroxServer/Communication/Packets/Processors/StoryGoalExecutedProcessor.cs index 7a621f79f0..86faffd0e8 100644 --- a/NitroxServer/Communication/Packets/Processors/StoryGoalExecutedProcessor.cs +++ b/NitroxServer/Communication/Packets/Processors/StoryGoalExecutedProcessor.cs @@ -30,7 +30,7 @@ public override void Process(StoryGoalExecuted packet, Player player) case StoryGoalExecuted.EventType.RADIO: if (added) { - storyGoalData.RadioQueue.Add(packet.Key); + storyGoalData.RadioQueue.Enqueue(packet.Key); } break; case StoryGoalExecuted.EventType.PDA: diff --git a/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs b/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs index 99b615e1b8..4ceb2af4b7 100644 --- a/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs +++ b/NitroxServer/GameLogic/Unlockables/StoryGoalData.cs @@ -3,47 +3,43 @@ using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; -namespace NitroxServer.GameLogic.Unlockables -{ - [DataContract] - public class StoryGoalData - { - [DataMember(Order = 1)] - public ThreadSafeSet CompletedGoals { get; } = new(); +namespace NitroxServer.GameLogic.Unlockables; - [DataMember(Order = 2)] - public ThreadSafeList RadioQueue { get; } = new(); +[DataContract] +public class StoryGoalData +{ + [DataMember(Order = 1)] + public ThreadSafeSet CompletedGoals { get; } = []; - [DataMember(Order = 3)] - public ThreadSafeSet GoalUnlocks { get; } = new(); + [DataMember(Order = 2)] + public ThreadSafeQueue RadioQueue { get; } = []; - [DataMember(Order = 4)] - public ThreadSafeList ScheduledGoals { get; set; } = new(); + [DataMember(Order = 3)] + public ThreadSafeList ScheduledGoals { get; set; } = []; - public bool RemovedLatestRadioMessage() + public bool RemovedLatestRadioMessage() + { + if (RadioQueue.Count <= 0) { - if (RadioQueue.Count <= 0) - { - return false; - } - string message = RadioQueue[0]; - RadioQueue.RemoveAt(0); + return false; + } - // Just like StoryGoalManager.ExecutePendingRadioMessage - CompletedGoals.Add("OnPlay" + message); + string message = RadioQueue.Dequeue(); - return true; - } + // Just like StoryGoalManager.ExecutePendingRadioMessage + CompletedGoals.Add($"OnPlay{message}"); - public static StoryGoalData From(StoryGoalData storyGoals, ScheduleKeeper scheduleKeeper) - { - storyGoals.ScheduledGoals = new ThreadSafeList(scheduleKeeper.GetScheduledGoals()); - return storyGoals; - } + return true; + } - public InitialStoryGoalData GetInitialStoryGoalData(ScheduleKeeper scheduleKeeper, Player player) - { - return new InitialStoryGoalData(new List(CompletedGoals), new List(RadioQueue), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp)); - } + public static StoryGoalData From(StoryGoalData storyGoals, ScheduleKeeper scheduleKeeper) + { + storyGoals.ScheduledGoals = new ThreadSafeList(scheduleKeeper.GetScheduledGoals()); + return storyGoals; + } + + public InitialStoryGoalData GetInitialStoryGoalData(ScheduleKeeper scheduleKeeper, Player player) + { + return new InitialStoryGoalData(new List(CompletedGoals), new List(RadioQueue), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp)); } } diff --git a/NitroxServer/Server.cs b/NitroxServer/Server.cs index 4f9164486f..e4f636e0bd 100644 --- a/NitroxServer/Server.cs +++ b/NitroxServer/Server.cs @@ -72,7 +72,6 @@ public string GetSaveSummary(Perms viewerPerms = Perms.CONSOLE) - Story goals completed: {world.GameData.StoryGoals.CompletedGoals.Count} - Radio messages stored: {world.GameData.StoryGoals.RadioQueue.Count} - World gamemode: {serverConfig.GameMode} - - Story goals unlocked: {world.GameData.StoryGoals.GoalUnlocks.Count} - Encyclopedia entries: {world.GameData.PDAState.EncyclopediaEntries.Count} - Known tech: {world.GameData.PDAState.KnownTechTypes.Count} """);