Skip to content

Commit

Permalink
Fix beacons not reappearing after reconnecting (#2227)
Browse files Browse the repository at this point in the history
  • Loading branch information
dartasen authored Jan 6, 2025
2 parents 3b13f1a + 7d47cc3 commit 52cd05d
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 58 deletions.
1 change: 0 additions & 1 deletion Nitrox.Test/Server/Serialization/WorldPersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
69 changes: 48 additions & 21 deletions NitroxClient/GameLogic/InitialSync/StoryGoalInitialSyncProcessor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string> completedGoals = packet.StoryGoalData.CompletedGoals;
List<string> radioQueue = packet.StoryGoalData.RadioQueue;
Expand Down Expand Up @@ -61,20 +60,23 @@ 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<string> completedGoals = packet.StoryGoalData.CompletedGoals;
StoryGoalManager storyGoalManager = StoryGoalManager.main;

// Initialize CompoundGoalTracker and OnGoalUnlockTracker and clear their already completed goals
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();

storyGoalManager.compoundGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key));
completedGoals.ForEach(goal => storyGoalManager.onGoalUnlockTracker.goalUnlocks.Remove(goal));


// 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));
Expand All @@ -90,15 +92,39 @@ 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)
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 IEnumerator SetupAuroraAndSunbeam(InitialPlayerSync packet)
private static void SetupAuroraAndSunbeam(InitialPlayerSync packet)
{
TimeData timeData = packet.TimeData;

AuroraWarnings auroraWarnings = UnityEngine.Object.FindObjectOfType<AuroraWarnings>();
AuroraWarnings auroraWarnings = Player.mainObject.GetComponentInChildren<AuroraWarnings>(true);
auroraWarnings.timeSerialized = DayNightCycle.main.timePassedAsFloat;
auroraWarnings.OnProtoDeserialize(null);

Expand All @@ -115,15 +141,17 @@ 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<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
Expand All @@ -135,17 +163,17 @@ private static IEnumerator 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);
}
}

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
Expand All @@ -157,7 +185,6 @@ private static IEnumerator RefreshStoryWithLatestData()
{
PrecursorGunStoryEvents.main.Start();
}
yield break;
}

private void SetTimeData(InitialPlayerSync packet)
Expand Down
4 changes: 1 addition & 3 deletions NitroxModel/DataStructures/GameLogic/InitialStoryGoalData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public class InitialStoryGoalData
{
public List<string> CompletedGoals { get; set; }
public List<string> RadioQueue { get; set; }
public List<string> GoalUnlocks { get; set; }
public List<NitroxScheduledGoal> ScheduledGoals { get; set; }

/// <remarks>
Expand All @@ -24,11 +23,10 @@ protected InitialStoryGoalData()
// Constructor for serialization. Has to be "protected" for json serialization.
}

public InitialStoryGoalData(List<string> completedGoals, List<string> radioQueue, List<string> goalUnlocks, List<NitroxScheduledGoal> scheduledGoals, Dictionary<string, float> personalCompletedGoalsWithTimestamp)
public InitialStoryGoalData(List<string> completedGoals, List<string> radioQueue, List<NitroxScheduledGoal> scheduledGoals, Dictionary<string, float> personalCompletedGoalsWithTimestamp)
{
CompletedGoals = completedGoals;
RadioQueue = radioQueue;
GoalUnlocks = goalUnlocks;
ScheduledGoals = scheduledGoals;
PersonalCompletedGoalsWithTimestamp = personalCompletedGoalsWithTimestamp;
}
Expand Down
21 changes: 21 additions & 0 deletions NitroxPatcher/Patches/Dynamic/CrashedShipExploder_Update_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Reflection;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;

namespace NitroxPatcher.Patches.Dynamic;

/// <remarks>
/// Prevents <see cref="CrashedShipExploder.Update"/> 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.
/// </remarks>
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
60 changes: 30 additions & 30 deletions NitroxServer/GameLogic/Unlockables/StoryGoalData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,43 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;

namespace NitroxServer.GameLogic.Unlockables
{
[DataContract]
public class StoryGoalData
{
[DataMember(Order = 1)]
public ThreadSafeSet<string> CompletedGoals { get; } = new();
namespace NitroxServer.GameLogic.Unlockables;

[DataMember(Order = 2)]
public ThreadSafeList<string> RadioQueue { get; } = new();
[DataContract]
public class StoryGoalData
{
[DataMember(Order = 1)]
public ThreadSafeSet<string> CompletedGoals { get; } = [];

[DataMember(Order = 3)]
public ThreadSafeSet<string> GoalUnlocks { get; } = new();
[DataMember(Order = 2)]
public ThreadSafeQueue<string> RadioQueue { get; } = [];

[DataMember(Order = 4)]
public ThreadSafeList<NitroxScheduledGoal> ScheduledGoals { get; set; } = new();
[DataMember(Order = 3)]
public ThreadSafeList<NitroxScheduledGoal> ScheduledGoals { get; set; } = [];

public bool RemovedLatestRadioMessage()
public bool RemovedLatestRadioMessage()
{
if (RadioQueue.Count <= 0)
{
if (RadioQueue.Count <= 0)
{
return false;
}

RadioQueue.RemoveAt(0);
return true;
return false;
}

public static StoryGoalData From(StoryGoalData storyGoals, ScheduleKeeper scheduleKeeper)
{
storyGoals.ScheduledGoals = new ThreadSafeList<NitroxScheduledGoal>(scheduleKeeper.GetScheduledGoals());
return storyGoals;
}
string message = RadioQueue.Dequeue();

public InitialStoryGoalData GetInitialStoryGoalData(ScheduleKeeper scheduleKeeper, Player player)
{
return new InitialStoryGoalData(new List<string>(CompletedGoals), new List<string>(RadioQueue), new List<string>(GoalUnlocks), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp));
}
// Just like StoryGoalManager.ExecutePendingRadioMessage
CompletedGoals.Add($"OnPlay{message}");

return true;
}

public static StoryGoalData From(StoryGoalData storyGoals, ScheduleKeeper scheduleKeeper)
{
storyGoals.ScheduledGoals = new ThreadSafeList<NitroxScheduledGoal>(scheduleKeeper.GetScheduledGoals());
return storyGoals;
}

public InitialStoryGoalData GetInitialStoryGoalData(ScheduleKeeper scheduleKeeper, Player player)
{
return new InitialStoryGoalData(new List<string>(CompletedGoals), new List<string>(RadioQueue), scheduleKeeper.GetScheduledGoals(), new(player.PersonalCompletedGoalsWithTimestamp));
}
}
1 change: 0 additions & 1 deletion NitroxServer/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
""");
Expand Down

0 comments on commit 52cd05d

Please sign in to comment.