You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rb ryan.gerleve #ROBOMERGE-AUTHOR: brian.bekich #ROBOMERGE-SOURCE: CL 20370672 via CL 20370684 via CL 20370691 #ROBOMERGE-BOT: UE5 (Release-Engine-Staging -> Main) (v949-20362246) [CL 20372134 by brian bekich in ue5-main branch]
5489 lines
182 KiB
C++
5489 lines
182 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
UDemoNetDriver.cpp: Simulated network driver for recording and playing back game sessions.
|
|
=============================================================================*/
|
|
|
|
|
|
// @todo: LowLevelSend now includes the packet size in bits, but this is ignored locally.
|
|
// Tracking of this must be added, if demos are to support PacketHandler's in the future (not presently needed).
|
|
|
|
|
|
#include "Engine/DemoNetDriver.h"
|
|
#include "EngineGlobals.h"
|
|
#include "Engine/World.h"
|
|
#include "UObject/Package.h"
|
|
#include "GameFramework/GameModeBase.h"
|
|
#include "GameFramework/PlayerStart.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "EngineUtils.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Engine/DemoPendingNetGame.h"
|
|
#include "Net/DataReplication.h"
|
|
#include "Engine/ActorChannel.h"
|
|
#include "Engine/NetworkObjectList.h"
|
|
#include "Net/RepLayout.h"
|
|
#include "GameFramework/SpectatorPawn.h"
|
|
#include "Engine/LevelStreamingDynamic.h"
|
|
#include "GameFramework/SpectatorPawnMovement.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "UnrealEngine.h"
|
|
#include "Net/NetworkProfiler.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
#include "HAL/LowLevelMemTracker.h"
|
|
#include "Stats/StatsMisc.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "ProfilingDebugging/CsvProfiler.h"
|
|
#include "Misc/EngineVersion.h"
|
|
#include "Stats/Stats2.h"
|
|
#include "Engine/ChildConnection.h"
|
|
#include "Net/ReplayPlaylistTracker.h"
|
|
#include "Net/NetworkGranularMemoryLogging.h"
|
|
|
|
DEFINE_LOG_CATEGORY( LogDemo );
|
|
|
|
#define DEMO_CSV_PROFILING_HELPERS_ENABLED (CSV_PROFILER && (!UE_BUILD_SHIPPING))
|
|
|
|
#if UE_BUILD_SHIPPING
|
|
CSV_DEFINE_CATEGORY(Demo, false);
|
|
#else
|
|
CSV_DEFINE_CATEGORY(Demo, true);
|
|
#endif
|
|
|
|
TAutoConsoleVariable<float> CVarDemoRecordHz( TEXT( "demo.RecordHz" ), 8, TEXT( "Maximum number of demo frames recorded per second" ) );
|
|
TAutoConsoleVariable<float> CVarDemoMinRecordHz(TEXT("demo.MinRecordHz"), 0, TEXT("Minimum number of demo frames recorded per second (use with care)"));
|
|
static TAutoConsoleVariable<float> CVarDemoTimeDilation( TEXT( "demo.TimeDilation" ), -1.0f, TEXT( "Override time dilation during demo playback (-1 = don't override)" ) );
|
|
static TAutoConsoleVariable<float> CVarDemoSkipTime( TEXT( "demo.SkipTime" ), 0, TEXT( "Skip fixed amount of network replay time (in seconds)" ) );
|
|
TAutoConsoleVariable<int32> CVarEnableCheckpoints( TEXT( "demo.EnableCheckpoints" ), 1, TEXT( "Whether or not checkpoints save on the server" ) );
|
|
static TAutoConsoleVariable<float> CVarGotoTimeInSeconds( TEXT( "demo.GotoTimeInSeconds" ), -1, TEXT( "For testing only, jump to a particular time" ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoFastForwardDestroyTearOffActors( TEXT( "demo.FastForwardDestroyTearOffActors" ), 1, TEXT( "If true, the driver will destroy any torn-off actors immediately while fast-forwarding a replay." ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoFastForwardSkipRepNotifies( TEXT( "demo.FastForwardSkipRepNotifies" ), 1, TEXT( "If true, the driver will optimize fast-forwarding by deferring calls to RepNotify functions until the fast-forward is complete. " ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoQueueCheckpointChannels( TEXT( "demo.QueueCheckpointChannels" ), 1, TEXT( "If true, the driver will put all channels created during checkpoint loading into queuing mode, to amortize the cost of spawning new actors across multiple frames." ) );
|
|
static TAutoConsoleVariable<int32> CVarUseAdaptiveReplayUpdateFrequency( TEXT( "demo.UseAdaptiveReplayUpdateFrequency" ), 1, TEXT( "If 1, NetUpdateFrequency will be calculated based on how often actors actually write something when recording to a replay" ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoAsyncLoadWorld( TEXT( "demo.AsyncLoadWorld" ), 0, TEXT( "If 1, we will use seamless server travel to load the replay world asynchronously" ) );
|
|
TAutoConsoleVariable<float> CVarCheckpointUploadDelayInSeconds( TEXT( "demo.CheckpointUploadDelayInSeconds" ), 30.0f, TEXT( "" ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoLoadCheckpointGarbageCollect( TEXT( "demo.LoadCheckpointGarbageCollect" ), 1, TEXT("If nonzero, CollectGarbage will be called during LoadCheckpoint after the old actors and connection are cleaned up." ) );
|
|
TAutoConsoleVariable<float> CVarCheckpointSaveMaxMSPerFrameOverride( TEXT( "demo.CheckpointSaveMaxMSPerFrameOverride" ), -1.0f, TEXT( "If >= 0, this value will override the CheckpointSaveMaxMSPerFrame member variable, which is the maximum time allowed each frame to spend on saving a checkpoint. If 0, it will save the checkpoint in a single frame, regardless of how long it takes." ) );
|
|
TAutoConsoleVariable<int32> CVarDemoClientRecordAsyncEndOfFrame( TEXT( "demo.ClientRecordAsyncEndOfFrame" ), 0, TEXT( "If true, TickFlush will be called on a thread in parallel with Slate." ) );
|
|
static TAutoConsoleVariable<int32> CVarForceDisableAsyncPackageMapLoading( TEXT( "demo.ForceDisableAsyncPackageMapLoading" ), 0, TEXT( "If true, async package map loading of network assets will be disabled." ) );
|
|
TAutoConsoleVariable<int32> CVarDemoUseNetRelevancy( TEXT( "demo.UseNetRelevancy" ), 0, TEXT( "If 1, will enable relevancy checks and distance culling, using all connected clients as reference." ) );
|
|
static TAutoConsoleVariable<float> CVarDemoCullDistanceOverride( TEXT( "demo.CullDistanceOverride" ), 0.0f, TEXT( "If > 0, will represent distance from any viewer where actors will stop being recorded." ) );
|
|
static TAutoConsoleVariable<float> CVarDemoRecordHzWhenNotRelevant( TEXT( "demo.RecordHzWhenNotRelevant" ), 2.0f, TEXT( "Record at this frequency when actor is not relevant." ) );
|
|
static TAutoConsoleVariable<int32> CVarLoopDemo(TEXT("demo.Loop"), 0, TEXT("<1> : play replay from beginning once it reaches the end / <0> : stop replay at the end"));
|
|
static TAutoConsoleVariable<int32> CVarDemoFastForwardIgnoreRPCs( TEXT( "demo.FastForwardIgnoreRPCs" ), 1, TEXT( "If true, RPCs will be discarded during playback fast forward." ) );
|
|
static TAutoConsoleVariable<int32> CVarDemoLateActorDormancyCheck(TEXT("demo.LateActorDormancyCheck"), 1, TEXT("If true, check if an actor should become dormant as late as possible- when serializing it to the demo archive."));
|
|
|
|
static TAutoConsoleVariable<int32> CVarDemoJumpToEndOfLiveReplay(TEXT("demo.JumpToEndOfLiveReplay"), 1, TEXT("If true, fast forward to a few seconds before the end when starting playback, if the replay is still being recorded."));
|
|
static TAutoConsoleVariable<int32> CVarDemoInternalPauseChannels(TEXT("demo.InternalPauseChannels"), 1, TEXT("If true, run standard logic for PauseChannels rather than letting the game handle it via FOnPauseChannelsDelegate."));
|
|
|
|
static int32 GDemoLoopCount = 0;
|
|
static FAutoConsoleVariableRef CVarDemoLoopCount( TEXT( "demo.LoopCount" ), GDemoLoopCount, TEXT( "If > 1, will play the replay that many times before stopping." ) );
|
|
|
|
static int32 GDemoSaveRollbackActorState = 1;
|
|
static FAutoConsoleVariableRef CVarDemoSaveRollbackActorState( TEXT( "demo.SaveRollbackActorState" ), GDemoSaveRollbackActorState, TEXT( "If true, rollback actors will save some replicated state to apply when respawned." ) );
|
|
|
|
TAutoConsoleVariable<int32> CVarWithLevelStreamingFixes(TEXT("demo.WithLevelStreamingFixes"), 0, TEXT("If 1, provides fixes for level streaming (but breaks backwards compatibility)."));
|
|
TAutoConsoleVariable<int32> CVarWithDemoTimeBurnIn(TEXT("demo.WithTimeBurnIn"), 0, TEXT("If true, adds an on screen message with the current DemoTime and Changelist."));
|
|
TAutoConsoleVariable<int32> CVarWithDeltaCheckpoints(TEXT("demo.WithDeltaCheckpoints"), 0, TEXT("If true, record checkpoints as a delta from the previous checkpoint."));
|
|
TAutoConsoleVariable<int32> CVarWithGameSpecificFrameData(TEXT("demo.WithGameSpecificFrameData"), 0, TEXT("If true, allow game specific data to be recorded with each demo frame."));
|
|
|
|
static TAutoConsoleVariable<float> CVarDemoIncreaseRepPrioritizeThreshold(TEXT("demo.IncreaseRepPrioritizeThreshold"), 0.9, TEXT("The % of Replicated to Prioritized actors at which prioritize time will be decreased."));
|
|
static TAutoConsoleVariable<float> CVarDemoDecreaseRepPrioritizeThreshold(TEXT("demo.DecreaseRepPrioritizeThreshold"), 0.7, TEXT("The % of Replicated to Prioritized actors at which prioritize time will be increased."));
|
|
static TAutoConsoleVariable<float> CVarDemoMinimumRepPrioritizeTime(TEXT("demo.MinimumRepPrioritizePercent"), 0.3, TEXT("Minimum percent of time that must be spent prioritizing actors, regardless of throttling."));
|
|
static TAutoConsoleVariable<float> CVarDemoMaximumRepPrioritizeTime(TEXT("demo.MaximumRepPrioritizePercent"), 0.7, TEXT("Maximum percent of time that may be spent prioritizing actors, regardless of throttling."));
|
|
|
|
static TAutoConsoleVariable<int32> CVarFastForwardLevelsPausePlayback(TEXT("demo.FastForwardLevelsPausePlayback"), 0, TEXT("If true, pause channels and playback while fast forward levels task is running."));
|
|
|
|
namespace ReplayTaskNames
|
|
{
|
|
static FName SkipTimeInSecondsTask(TEXT("SkipTimeInSecondsTask"));
|
|
static FName JumpToLiveReplayTask(TEXT("JumpToLiveReplayTask"));
|
|
static FName GotoTimeInSecondsTask(TEXT("GotoTimeInSecondsTask"));
|
|
static FName FastForwardLevelsTask(TEXT("FastForwardLevelsTask"));
|
|
};
|
|
|
|
// This is only intended for testing purposes
|
|
// A "better" way might be to throw together a GameplayDebuggerComponent or Category, so we could populate
|
|
// more than just the DemoTime.
|
|
static void ConditionallyDisplayBurnInTime(uint32 RecordedCL, float CurrentDemoTime)
|
|
{
|
|
if (CVarWithDemoTimeBurnIn.GetValueOnAnyThread() != 0)
|
|
{
|
|
GEngine->AddOnScreenDebugMessage(INDEX_NONE, 0.f, FColor::Red, FString::Printf(TEXT("Current CL: %lu | Recorded CL: %lu | Time: %f"), FEngineVersion::Current().GetChangelist(), RecordedCL, CurrentDemoTime), true, FVector2D(3.f, 3.f));
|
|
}
|
|
}
|
|
|
|
static bool ShouldActorGoDormantForDemo(const AActor* Actor, const UActorChannel* Channel)
|
|
{
|
|
if ( Actor->NetDormancy <= DORM_Awake || !Channel || Channel->bPendingDormancy || Channel->Dormant )
|
|
{
|
|
// Either shouldn't go dormant, or is already dormant
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
namespace DemoNetDriverRecordingPrivate
|
|
{
|
|
static float WarningTimeInterval = 60.f;
|
|
static FAutoConsoleVariableRef CVarExceededBudgetWarningInterval(
|
|
TEXT("Demo.ExceededBudgetWarningInterval"),
|
|
WarningTimeInterval,
|
|
TEXT("When > 0, we will wait this many seconds between logging warnings for demo recording exceeding time budgets.")
|
|
);
|
|
|
|
static bool RecordUnicastRPCs = false;
|
|
static FAutoConsoleVariableRef CVarRecordUnicastRPCs(
|
|
TEXT("demo.RecordUnicastRPCs"),
|
|
RecordUnicastRPCs,
|
|
TEXT("When true, also record unicast client rpcs on actors that share a net driver name with the demo driver.")
|
|
);
|
|
|
|
static TAutoConsoleVariable<int32> CVarDemoForcePersistentLevelPriority(TEXT("demo.ForcePersistentLevelPriority"), 0, TEXT("If true, force persistent level to record first when prioritizing and using streaming level fixes."));
|
|
static TAutoConsoleVariable<int32> CVarDemoDestructionInfoPriority(TEXT("demo.DestructionInfoPriority"), MAX_int32, TEXT("Replay net priority assigned to destruction infos during recording."));
|
|
static TAutoConsoleVariable<int32> CVarDemoLateDestructionInfoPrioritize(TEXT("demo.LateDestructionInfoPrioritize"), 0, TEXT("If true, process destruction infos at the end of the prioritization phase."));
|
|
static TAutoConsoleVariable<float> CVarDemoViewTargetPriorityScale(TEXT("demo.ViewTargetPriorityScale"), 3.0, TEXT("Scale view target priority by this value when prioritization is enabled."));
|
|
static TAutoConsoleVariable<float> CVarDemoMaximumRecDestructionInfoTime(TEXT("demo.MaximumRecDestructionInfoTime"), 0.2, TEXT("Maximum percentage of frame to use replicating destruction infos, if per frame limit is enabled."));
|
|
|
|
static FAutoConsoleCommandWithWorldAndArgs DemoMaxDesiredRecordTimeMS(
|
|
TEXT("Demo.MaxDesiredRecordTimeMS"),
|
|
TEXT("Set max desired record time in MS on demo driver of the current world."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(
|
|
[](const TArray<FString>& Params, UWorld* World)
|
|
{
|
|
if (World)
|
|
{
|
|
if (UDemoNetDriver* Driver = World->GetDemoNetDriver())
|
|
{
|
|
if (Params.Num() > 0)
|
|
{
|
|
const float TimeInMS = FCString::Atof(*Params[0]);
|
|
|
|
Driver->SetMaxDesiredRecordTimeMS(TimeInMS);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
static FAutoConsoleCommandWithWorldAndArgs DemoCheckpointSaveMaxMSPerFrame(
|
|
TEXT("Demo.CheckpointSaveMaxMSPerFrame"),
|
|
TEXT("Set max checkpoint record time in MS on demo driver of the current world."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(
|
|
[](const TArray<FString>& Params, UWorld* World)
|
|
{
|
|
if (World)
|
|
{
|
|
if (UDemoNetDriver* Driver = World->GetDemoNetDriver())
|
|
{
|
|
if (Params.Num() > 0)
|
|
{
|
|
const float TimeInMS = FCString::Atof(*Params[0]);
|
|
|
|
Driver->SetCheckpointSaveMaxMSPerFrame(TimeInMS);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
static FAutoConsoleCommandWithWorldAndArgs DemoActorPrioritizationEnabled(
|
|
TEXT("Demo.ActorPrioritizationEnabled"),
|
|
TEXT("Set whether or not actor prioritization is enabled on demo driver of the current world."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(
|
|
[](const TArray<FString>& Params, UWorld* World)
|
|
{
|
|
if (World)
|
|
{
|
|
if (UDemoNetDriver* Driver = World->GetDemoNetDriver())
|
|
{
|
|
if (Params.Num() > 0)
|
|
{
|
|
const bool bPrioritize = FCString::ToBool(*Params[0]);
|
|
|
|
Driver->SetActorPrioritizationEnabled(bPrioritize);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
static FAutoConsoleCommandWithWorldAndArgs DemoSetLocalViewerOverride(
|
|
TEXT("Demo.SetLocalViewerOverride"),
|
|
TEXT("Set first local player controller as the viewer override on demo driver of the current world."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(
|
|
[](const TArray<FString>& Params, UWorld* World)
|
|
{
|
|
if (World)
|
|
{
|
|
if (UDemoNetDriver* Driver = World->GetDemoNetDriver())
|
|
{
|
|
if (APlayerController* ViewerPC = GEngine->GetFirstLocalPlayerController(World))
|
|
{
|
|
Driver->SetViewerOverride(ViewerPC);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
struct FDemoBudgetLogHelper
|
|
{
|
|
enum EBudgetCategory
|
|
{
|
|
Prioritization,
|
|
Replication,
|
|
CATEGORY_COUNT
|
|
};
|
|
|
|
FDemoBudgetLogHelper(FString&& Identifier)
|
|
: Identifier(MoveTemp(Identifier))
|
|
{
|
|
ResetCounters();
|
|
}
|
|
|
|
void NewFrame()
|
|
{
|
|
if (FirstWarningTime != 0.f)
|
|
{
|
|
++NumFrames;
|
|
bOverBudgetThisFrame = false;
|
|
|
|
const double Time = FPlatformTime::Seconds();
|
|
if (Time - FirstWarningTime > DemoNetDriverRecordingPrivate::WarningTimeInterval)
|
|
{
|
|
if (UE_LOG_ACTIVE(LogDemo, Log))
|
|
{
|
|
TArray<FString> LogLines;
|
|
LogLines.Reserve((CATEGORY_COUNT * 2) + 1);
|
|
|
|
LogLines.Emplace(FString::Printf(TEXT("%s: Recorded Frames: %d, Frames Over Budget: %d"), *Identifier, NumFrames, NumFramesOverBudget));
|
|
|
|
for (int32 i = 0; i < CATEGORY_COUNT; ++i)
|
|
{
|
|
LogLines.Emplace(FString::Printf(TEXT("Total number of over budget frames in category %d: %d"), i, NumFramesOverBudgetByCategory[i]));
|
|
|
|
if (NumFramesOverBudgetByCategory[i] > 0)
|
|
{
|
|
LogLines.Emplace(MoveTemp(LogSamplesByBudget[i]));
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogDemo, Log, TEXT("%s"), *FString::Join(LogLines, TEXT("\n")));
|
|
}
|
|
|
|
ResetCounters();
|
|
}
|
|
}
|
|
}
|
|
|
|
template<size_t N, typename... T>
|
|
void MarkFrameOverBudget(EBudgetCategory Category, TCHAR const (&Format)[N], T... Args)
|
|
{
|
|
if (!UE_LOG_ACTIVE(LogDemo, Log))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (DemoNetDriverRecordingPrivate::WarningTimeInterval == 0.f)
|
|
{
|
|
UE_LOG(LogDemo, Log, Format, Args...);
|
|
return;
|
|
}
|
|
|
|
if (!bOverBudgetThisFrame)
|
|
{
|
|
bOverBudgetThisFrame = true;
|
|
++NumFramesOverBudget;
|
|
|
|
if (FirstWarningTime == 0.f)
|
|
{
|
|
FirstWarningTime = FPlatformTime::Seconds();
|
|
}
|
|
}
|
|
|
|
++NumFramesOverBudgetByCategory[Category];
|
|
if (LogSamplesByBudget[Category].IsEmpty())
|
|
{
|
|
LogSamplesByBudget[Category] = FString::Printf(Format, Args...);
|
|
}
|
|
}
|
|
|
|
void ResetCounters()
|
|
{
|
|
NumFrames = 0;
|
|
NumFramesOverBudget = 0;
|
|
FirstWarningTime = 0.f;
|
|
for (int32 i = 0; i < CATEGORY_COUNT; ++i)
|
|
{
|
|
NumFramesOverBudgetByCategory[i] = 0;
|
|
LogSamplesByBudget[i] = FString();
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
bool bOverBudgetThisFrame = false;
|
|
|
|
int32 NumFrames;
|
|
int32 NumFramesOverBudget;
|
|
|
|
double FirstWarningTime = 0.f;
|
|
|
|
int32 NumFramesOverBudgetByCategory[CATEGORY_COUNT];
|
|
FString LogSamplesByBudget[CATEGORY_COUNT];
|
|
|
|
FString Identifier;
|
|
};
|
|
|
|
class FPendingTaskHelper
|
|
{
|
|
// TODO: Consider making these private, and adding explicit friend access for the tasks that need them.
|
|
public:
|
|
|
|
static bool LoadCheckpoint(UDemoNetDriver* DemoNetDriver, const FGotoResult& GotoResult)
|
|
{
|
|
return DemoNetDriver->LoadCheckpoint(GotoResult);
|
|
}
|
|
|
|
static bool FastForwardLevels(UDemoNetDriver* DemoNetDriver, const FGotoResult& GotoResult)
|
|
{
|
|
return DemoNetDriver->FastForwardLevels(GotoResult);
|
|
}
|
|
|
|
static float GetLastProcessedPacketTime(UDemoNetDriver* DemoNetDriver)
|
|
{
|
|
return DemoNetDriver->LastProcessedPacketTime;
|
|
}
|
|
};
|
|
|
|
class FScopedAllowExistingChannelIndex
|
|
{
|
|
public:
|
|
FScopedAllowExistingChannelIndex(FScopedAllowExistingChannelIndex&&) = delete;
|
|
FScopedAllowExistingChannelIndex(const FScopedAllowExistingChannelIndex&) = delete;
|
|
FScopedAllowExistingChannelIndex& operator=(const FScopedAllowExistingChannelIndex&) = delete;
|
|
FScopedAllowExistingChannelIndex& operator=(FScopedAllowExistingChannelIndex&&) = delete;
|
|
|
|
FScopedAllowExistingChannelIndex(UNetConnection* InConnection):
|
|
Connection(InConnection)
|
|
{
|
|
if (Connection.IsValid())
|
|
{
|
|
Connection->SetAllowExistingChannelIndex(true);
|
|
}
|
|
}
|
|
|
|
~FScopedAllowExistingChannelIndex()
|
|
{
|
|
if (Connection.IsValid())
|
|
{
|
|
Connection->SetAllowExistingChannelIndex(false);
|
|
}
|
|
}
|
|
|
|
private:
|
|
TWeakObjectPtr<UNetConnection> Connection;
|
|
};
|
|
|
|
class FJumpToLiveReplayTask : public FQueuedReplayTask
|
|
{
|
|
public:
|
|
FJumpToLiveReplayTask(UDemoNetDriver* InDriver) : FQueuedReplayTask(InDriver)
|
|
{
|
|
if (Driver.IsValid())
|
|
{
|
|
InitialTotalDemoTime = Driver->GetDemoTotalTime();
|
|
TaskStartTime = FPlatformTime::Seconds();
|
|
}
|
|
}
|
|
|
|
virtual void StartTask()
|
|
{
|
|
}
|
|
|
|
virtual bool Tick() override
|
|
{
|
|
if (!Driver.IsValid())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!Driver->GetReplayStreamer()->IsLive())
|
|
{
|
|
// The replay is no longer live, so don't try to jump to end
|
|
return true;
|
|
}
|
|
|
|
// Wait for the most recent live time
|
|
const bool bHasNewReplayTime = (Driver->GetDemoTotalTime() != InitialTotalDemoTime);
|
|
|
|
// If we haven't gotten a new time from the demo by now, assume it might not be live, and just jump to the end now so we don't hang forever
|
|
const bool bTimeExpired = (FPlatformTime::Seconds() - TaskStartTime >= 15.0);
|
|
|
|
if (bHasNewReplayTime || bTimeExpired)
|
|
{
|
|
if (bTimeExpired)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FJumpToLiveReplayTask::Tick: Too much time since last live update."));
|
|
}
|
|
|
|
// We're ready to jump to the end now
|
|
Driver->JumpToEndOfLiveReplay();
|
|
return true;
|
|
}
|
|
|
|
// Waiting to get the latest update
|
|
return false;
|
|
}
|
|
|
|
virtual FName GetName() const override
|
|
{
|
|
return ReplayTaskNames::JumpToLiveReplayTask;
|
|
}
|
|
|
|
private:
|
|
float InitialTotalDemoTime; // Initial total demo time. This is used to wait until we get a more updated time so we jump to the most recent end time
|
|
double TaskStartTime; // This is the time the task started. If too much real-time passes, we'll just jump to the current end
|
|
};
|
|
|
|
class FGotoTimeInSecondsTask : public FQueuedReplayTask
|
|
{
|
|
public:
|
|
FGotoTimeInSecondsTask(UDemoNetDriver* InDriver, const float InTimeInSeconds) : FQueuedReplayTask(InDriver), TimeInSeconds(InTimeInSeconds)
|
|
{
|
|
}
|
|
|
|
virtual void StartTask() override
|
|
{
|
|
if (!Driver.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!GotoResult.IsSet());
|
|
check(!Driver->IsFastForwarding());
|
|
|
|
OldTimeInSeconds = Driver->GetDemoCurrentTime(); // Rember current time, so we can restore on failure
|
|
Driver->SetDemoCurrentTime(TimeInSeconds); // Also, update current time so HUD reflects desired scrub time now
|
|
|
|
// Clamp time
|
|
Driver->SetDemoCurrentTime(FMath::Clamp(Driver->GetDemoCurrentTime(), 0.0f, Driver->GetDemoTotalTime() - 0.01f));
|
|
|
|
EReplayCheckpointType CheckpointType = Driver->HasDeltaCheckpoints() ? EReplayCheckpointType::Delta : EReplayCheckpointType::Full;
|
|
|
|
// Tell the streamer to start going to this time
|
|
Driver->GetReplayStreamer()->GotoTimeInMS(Driver->GetDemoCurrentTimeInMS(), FGotoCallback::CreateSP(this, &FGotoTimeInSecondsTask::CheckpointReady), CheckpointType);
|
|
|
|
// Pause channels while we wait (so the world is paused while we wait for the new stream location to load)
|
|
Driver->PauseChannels(true);
|
|
}
|
|
|
|
virtual bool Tick() override
|
|
{
|
|
if (!Driver.IsValid())
|
|
{
|
|
// Detect failure case
|
|
return true;
|
|
}
|
|
else if (GotoResult.IsSet())
|
|
{
|
|
if (!GotoResult->WasSuccessful())
|
|
{
|
|
return true;
|
|
}
|
|
else if (GotoResult->ExtraTimeMS > 0 && !Driver->GetReplayStreamer()->IsDataAvailable())
|
|
{
|
|
// Wait for rest of stream before loading checkpoint
|
|
// We do this so we can load the checkpoint and fastforward the stream all at once
|
|
// We do this so that the OnReps don't stay queued up outside of this frame
|
|
return false;
|
|
}
|
|
|
|
// We're done
|
|
return FPendingTaskHelper::LoadCheckpoint(Driver.Get(), GotoResult.GetValue());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
virtual FName GetName() const override
|
|
{
|
|
return ReplayTaskNames::GotoTimeInSecondsTask;
|
|
}
|
|
|
|
void CheckpointReady(const FGotoResult& Result)
|
|
{
|
|
check(!GotoResult.IsSet());
|
|
GotoResult = Result;
|
|
|
|
if (!Driver.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Result.WasSuccessful())
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FGotoTimeInSecondsTask::CheckpointReady: Failed to go to checkpoint."));
|
|
|
|
// Restore old demo time
|
|
Driver->SetDemoCurrentTime(OldTimeInSeconds);
|
|
|
|
// Call delegate if any
|
|
Driver->NotifyGotoTimeFinished(false);
|
|
}
|
|
}
|
|
|
|
// So we can restore on failure
|
|
float OldTimeInSeconds;
|
|
float TimeInSeconds;
|
|
TOptional<FGotoResult> GotoResult;
|
|
};
|
|
|
|
class FSkipTimeInSecondsTask : public FQueuedReplayTask
|
|
{
|
|
public:
|
|
FSkipTimeInSecondsTask(UDemoNetDriver* InDriver, const float InSecondsToSkip) : FQueuedReplayTask(InDriver), SecondsToSkip(InSecondsToSkip)
|
|
{
|
|
}
|
|
|
|
virtual void StartTask() override
|
|
{
|
|
if (!Driver.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!Driver->IsFastForwarding());
|
|
|
|
const uint32 TimeInMSToCheck = FMath::Clamp(Driver->GetDemoCurrentTimeInMS() + (uint32)(SecondsToSkip * 1000), (uint32)0, Driver->GetReplayStreamer()->GetTotalDemoTime());
|
|
|
|
Driver->GetReplayStreamer()->SetHighPriorityTimeRange(Driver->GetDemoCurrentTimeInMS(), TimeInMSToCheck);
|
|
|
|
Driver->SkipTimeInternal(SecondsToSkip, true, false);
|
|
}
|
|
|
|
virtual bool Tick() override
|
|
{
|
|
// The real work was done in StartTask, so we're done
|
|
return true;
|
|
}
|
|
|
|
virtual FName GetName() const override
|
|
{
|
|
return ReplayTaskNames::SkipTimeInSecondsTask;
|
|
}
|
|
|
|
float SecondsToSkip;
|
|
};
|
|
|
|
class FFastForwardLevelsTask : public FQueuedReplayTask
|
|
{
|
|
public:
|
|
|
|
FFastForwardLevelsTask( UDemoNetDriver* InDriver ) : FQueuedReplayTask( InDriver ), GotoTime(0), bSkipWork(false)
|
|
{
|
|
}
|
|
|
|
virtual void StartTask() override
|
|
{
|
|
if (!Driver.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!Driver->IsFastForwarding());
|
|
|
|
// If there's a GotoTimeInSeconds task pending, we don't need to do any work.
|
|
// That task should trigger a full checkpoint load.
|
|
// Only check the next task, to avoid issues with SkipTime / JumpToLive not having updated levels.
|
|
if (Driver->GetNextQueuedTaskName() == ReplayTaskNames::GotoTimeInSecondsTask)
|
|
{
|
|
bSkipWork = true;
|
|
}
|
|
else
|
|
{
|
|
// Make sure we request all the data we need so we don't end up doing a "partial" fast forward which
|
|
// could cause the level to miss network updates.
|
|
const float LastProcessedPacketTime = FPendingTaskHelper::GetLastProcessedPacketTime(Driver.Get());
|
|
GotoTime = LastProcessedPacketTime * 1000;
|
|
|
|
EReplayCheckpointType CheckpointType = Driver->HasDeltaCheckpoints() ? EReplayCheckpointType::Delta : EReplayCheckpointType::Full;
|
|
|
|
Driver->GetReplayStreamer()->GotoTimeInMS(GotoTime, FGotoCallback::CreateSP(this, &FFastForwardLevelsTask::CheckpointReady), CheckpointType);
|
|
|
|
if (CVarFastForwardLevelsPausePlayback.GetValueOnAnyThread() != 0)
|
|
{
|
|
// Pause channels while we wait (so the world is paused while we wait for the new stream location to load)
|
|
Driver->PauseChannels(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual bool Tick() override
|
|
{
|
|
if (bSkipWork)
|
|
{
|
|
return true;
|
|
}
|
|
else if (!Driver.IsValid())
|
|
{
|
|
return true;
|
|
}
|
|
else if (GotoResult.IsSet())
|
|
{
|
|
// if this task is not pausing the rest of the replay stream, make sure there is data available for the current time or we could miss packets
|
|
const float LastProcessedPacketTime = FPendingTaskHelper::GetLastProcessedPacketTime(Driver.Get());
|
|
const uint32 AvailableDataEndTime = (CVarFastForwardLevelsPausePlayback.GetValueOnAnyThread() != 0) ? GotoTime : LastProcessedPacketTime * 1000;
|
|
|
|
if (!GotoResult->WasSuccessful())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If not all data is available, we could end only partially fast forwarding the levels.
|
|
// Note, IsDataAvailable may return false even if IsDataAvailableForTimeRange is true.
|
|
// So, check both to ensure that we don't end up skipping data in FastForwardLevels.
|
|
else if (GotoResult->ExtraTimeMS > 0 && !(Driver->GetReplayStreamer()->IsDataAvailable() && Driver->GetReplayStreamer()->IsDataAvailableForTimeRange(GotoTime - GotoResult->ExtraTimeMS, AvailableDataEndTime)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return FPendingTaskHelper::FastForwardLevels(Driver.Get(), GotoResult.GetValue());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
virtual FName GetName() const override
|
|
{
|
|
return ReplayTaskNames::FastForwardLevelsTask;
|
|
}
|
|
|
|
virtual bool ShouldPausePlayback() const override
|
|
{
|
|
return (CVarFastForwardLevelsPausePlayback.GetValueOnAnyThread() != 0);
|
|
}
|
|
|
|
void CheckpointReady(const FGotoResult& Result)
|
|
{
|
|
check(!GotoResult.IsSet());
|
|
|
|
GotoResult = Result;
|
|
|
|
if (!Result.WasSuccessful())
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FFastForwardLevelsTask::CheckpointReady: Failed to get checkpoint."));
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
uint32 GotoTime;
|
|
bool bSkipWork;
|
|
TOptional<FGotoResult> GotoResult;
|
|
};
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
UDemoNetDriver.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
void UDemoNetDriver::InitDefaults()
|
|
{
|
|
DemoSessionID = FGuid::NewGuid().ToString().ToLower();
|
|
SetCurrentLevelIndex(0);
|
|
bIsWaitingForHeaderDownload = false;
|
|
bIsWaitingForStream = false;
|
|
MaxArchiveReadPos = 0;
|
|
bNeverApplyNetworkEmulationSettings = true;
|
|
bSkipServerReplicateActors = true;
|
|
bSkipClearVoicePackets = true;
|
|
bSkipStartupActorRollback = false;
|
|
|
|
if (!HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
LevelIntervals.Reserve(512);
|
|
}
|
|
|
|
RecordBuildConsiderAndPrioritizeTimeSlice = CVarDemoMaximumRepPrioritizeTime.GetValueOnGameThread();
|
|
RecordDestructionInfoReplicationTimeSlice = DemoNetDriverRecordingPrivate::CVarDemoMaximumRecDestructionInfoTime.GetValueOnAnyThread();
|
|
}
|
|
|
|
UDemoNetDriver::UDemoNetDriver(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
InitDefaults();
|
|
}
|
|
|
|
UDemoNetDriver::UDemoNetDriver(FVTableHelper& Helper)
|
|
: Super(Helper)
|
|
{
|
|
InitDefaults();
|
|
}
|
|
|
|
UDemoNetDriver::~UDemoNetDriver()
|
|
{
|
|
}
|
|
|
|
void UDemoNetDriver::AddReplayTask(FQueuedReplayTask* NewTask)
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("UDemoNetDriver::AddReplayTask. Name: %s"), *NewTask->GetName().ToString());
|
|
|
|
QueuedReplayTasks.Emplace(NewTask);
|
|
|
|
// Give this task a chance to immediately start if nothing else is happening
|
|
if (!IsAnyTaskPending())
|
|
{
|
|
ProcessReplayTasks();
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::IsAnyTaskPending() const
|
|
{
|
|
return (QueuedReplayTasks.Num() > 0) || ActiveReplayTask.IsValid();
|
|
}
|
|
|
|
void UDemoNetDriver::ClearReplayTasks()
|
|
{
|
|
QueuedReplayTasks.Empty();
|
|
|
|
ActiveReplayTask = nullptr;
|
|
}
|
|
|
|
bool UDemoNetDriver::ProcessReplayTasks()
|
|
{
|
|
// Store a shared pointer to the current task in a local variable so that if
|
|
// the task itself causes tasks to be cleared (for example, if it calls StopDemo()
|
|
// in StartTask() or Tick()), the current task won't be destroyed immediately.
|
|
TSharedPtr<FQueuedReplayTask> LocalActiveTask;
|
|
|
|
if (!ActiveReplayTask.IsValid() && QueuedReplayTasks.Num() > 0)
|
|
{
|
|
// If we don't have an active task, pull one off now
|
|
ActiveReplayTask = QueuedReplayTasks[0];
|
|
LocalActiveTask = ActiveReplayTask;
|
|
QueuedReplayTasks.RemoveAt(0);
|
|
|
|
UE_LOG(LogDemo, Verbose, TEXT("UDemoNetDriver::ProcessReplayTasks. Name: %s"), *ActiveReplayTask->GetName().ToString());
|
|
|
|
// Start the task
|
|
LocalActiveTask->StartTask();
|
|
}
|
|
|
|
// Tick the currently active task
|
|
if (ActiveReplayTask.IsValid())
|
|
{
|
|
LocalActiveTask = ActiveReplayTask;
|
|
|
|
if (!LocalActiveTask->Tick())
|
|
{
|
|
// Task isn't done, we can return
|
|
return !LocalActiveTask->ShouldPausePlayback();
|
|
}
|
|
|
|
// This task is now done
|
|
ActiveReplayTask = nullptr;
|
|
}
|
|
|
|
return true; // No tasks to process
|
|
}
|
|
|
|
bool UDemoNetDriver::IsNamedTaskInQueue(const FName& Name) const
|
|
{
|
|
if (ActiveReplayTask.IsValid() && ActiveReplayTask->GetName() == Name)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (const TSharedRef<FQueuedReplayTask>& Task : QueuedReplayTasks)
|
|
{
|
|
if (Task->GetName() == Name)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
FName UDemoNetDriver::GetNextQueuedTaskName() const
|
|
{
|
|
return QueuedReplayTasks.Num() > 0 ? QueuedReplayTasks[0]->GetName() : NAME_None;
|
|
}
|
|
|
|
bool UDemoNetDriver::InitBase(bool bInitAsClient, FNetworkNotify* InNotify, const FURL& URL, bool bReuseAddressAndPort, FString& Error)
|
|
{
|
|
if (Super::InitBase(bInitAsClient, InNotify, URL, bReuseAddressAndPort, Error))
|
|
{
|
|
bChannelsArePaused = false;
|
|
ResetElapsedTime();
|
|
bIsFastForwarding = false;
|
|
bIsFastForwardingForCheckpoint = false;
|
|
bIsRestoringStartupActors = false;
|
|
bWasStartStreamingSuccessful = true;
|
|
SavedReplicatedWorldTimeSeconds = 0.0f;
|
|
SavedSecondsToSkip = 0.0f;
|
|
MaxDesiredRecordTimeMS = -1.0f;
|
|
ViewerOverride = nullptr;
|
|
bPrioritizeActors = false;
|
|
PlaybackPacketIndex = 0;
|
|
CheckpointSaveMaxMSPerFrame = -1.0f;
|
|
|
|
if (FParse::Param(FCommandLine::Get(), TEXT("skipreplayrollback")))
|
|
{
|
|
bSkipStartupActorRollback = true;
|
|
}
|
|
|
|
RecordBuildConsiderAndPrioritizeTimeSlice = CVarDemoMaximumRepPrioritizeTime.GetValueOnAnyThread();
|
|
RecordDestructionInfoReplicationTimeSlice = DemoNetDriverRecordingPrivate::CVarDemoMaximumRecDestructionInfoTime.GetValueOnAnyThread();
|
|
|
|
if (RelevantTimeout == 0.0f)
|
|
{
|
|
RelevantTimeout = 5.0f;
|
|
}
|
|
|
|
ResetDemoState();
|
|
|
|
ReplayStreamer = ReplayHelper.Init(URL);
|
|
|
|
ReplayHelper.SetAnalyticsProvider(AnalyticsProvider);
|
|
ReplayHelper.CheckpointSaveMaxMSPerFrame = CheckpointSaveMaxMSPerFrame;
|
|
|
|
// if the helper encounters an error, stop the presses
|
|
ReplayHelper.OnReplayRecordError.AddUObject(this, &UDemoNetDriver::StopDemo);
|
|
ReplayHelper.OnReplayPlaybackError.AddUObject(this, &UDemoNetDriver::NotifyDemoPlaybackFailure);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UDemoNetDriver::FinishDestroy()
|
|
{
|
|
if (!HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
// Make sure we stop any recording/playing that might be going on
|
|
if (IsRecording() || IsPlaying())
|
|
{
|
|
StopDemo();
|
|
}
|
|
}
|
|
|
|
CleanUpSplitscreenConnections(true);
|
|
FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this);
|
|
|
|
ReplayHelper.OnReplayRecordError.RemoveAll(this);
|
|
ReplayHelper.OnReplayPlaybackError.RemoveAll(this);
|
|
|
|
Super::FinishDestroy();
|
|
}
|
|
|
|
FString UDemoNetDriver::LowLevelGetNetworkNumber()
|
|
{
|
|
return FString(TEXT(""));
|
|
}
|
|
|
|
void UDemoNetDriver::ResetDemoState()
|
|
{
|
|
SetDemoCurrentTime(0.0f);
|
|
SetDemoTotalTime(0.0f);
|
|
LastProcessedPacketTime = 0.0f;
|
|
PlaybackPacketIndex = 0;
|
|
|
|
bIsFastForwarding = false;
|
|
bIsFastForwardingForCheckpoint = false;
|
|
bIsRestoringStartupActors = false;
|
|
bWasStartStreamingSuccessful = false;
|
|
bIsWaitingForHeaderDownload = false;
|
|
bIsWaitingForStream = false;
|
|
bIsFinalizingFastForward = false;
|
|
|
|
PlaybackPackets.Empty();
|
|
|
|
ReplayHelper.ResetState();
|
|
}
|
|
|
|
bool UDemoNetDriver::InitConnect(FNetworkNotify* InNotify, const FURL& ConnectURL, FString& Error)
|
|
{
|
|
if (World == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("World == nullptr"));
|
|
return false;
|
|
}
|
|
|
|
if (World->GetGameInstance() == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("World->GetGameInstance() == nullptr"));
|
|
return false;
|
|
}
|
|
|
|
// handle default initialization
|
|
if (!InitBase(true, InNotify, ConnectURL, false, Error))
|
|
{
|
|
World->GetGameInstance()->HandleDemoPlaybackFailure(EDemoPlayFailure::InitBase, FString(TEXT("InitBase FAILED")));
|
|
return false;
|
|
}
|
|
|
|
GuidCache->SetNetworkChecksumMode(FNetGUIDCache::ENetworkChecksumMode::SaveButIgnore);
|
|
|
|
if (CVarForceDisableAsyncPackageMapLoading.GetValueOnGameThread() > 0)
|
|
{
|
|
GuidCache->SetAsyncLoadMode(FNetGUIDCache::EAsyncLoadMode::ForceDisable);
|
|
}
|
|
else
|
|
{
|
|
GuidCache->SetAsyncLoadMode(FNetGUIDCache::EAsyncLoadMode::UseCVar);
|
|
}
|
|
|
|
// Playback, local machine is a client, and the demo stream acts "as if" it's the server.
|
|
ServerConnection = NewObject<UNetConnection>(GetTransientPackage(), UDemoNetConnection::StaticClass());
|
|
ServerConnection->InitConnection(this, USOCK_Pending, ConnectURL, 1000000);
|
|
|
|
const TCHAR* const LevelPrefixOverrideOption = ConnectURL.GetOption(TEXT("LevelPrefixOverride="), nullptr);
|
|
if (LevelPrefixOverrideOption)
|
|
{
|
|
SetDuplicateLevelID(FCString::Atoi(LevelPrefixOverrideOption));
|
|
}
|
|
|
|
if (GetDuplicateLevelID() == -1)
|
|
{
|
|
// Set this driver as the demo net driver for the source level collection.
|
|
FLevelCollection* const SourceCollection = World->FindCollectionByType(ELevelCollectionType::DynamicSourceLevels);
|
|
if (SourceCollection)
|
|
{
|
|
SourceCollection->SetDemoNetDriver(this);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Set this driver as the demo net driver for the duplicate level collection.
|
|
FLevelCollection* const DuplicateCollection = World->FindCollectionByType(ELevelCollectionType::DynamicDuplicatedLevels);
|
|
if (DuplicateCollection)
|
|
{
|
|
DuplicateCollection->SetDemoNetDriver(this);
|
|
}
|
|
}
|
|
|
|
bIsWaitingForStream = true;
|
|
bWasStartStreamingSuccessful = true;
|
|
|
|
ReplayHelper.ActiveReplayName = ConnectURL.Map;
|
|
|
|
TArray<int32> UserIndices;
|
|
for (FLocalPlayerIterator LocalPlayerIt(GEngine, World); LocalPlayerIt; ++LocalPlayerIt)
|
|
{
|
|
if (*LocalPlayerIt)
|
|
{
|
|
UserIndices.Add(LocalPlayerIt->GetControllerId());
|
|
}
|
|
}
|
|
|
|
FStartStreamingParameters Params;
|
|
Params.CustomName = ConnectURL.Map;
|
|
Params.DemoURL = GetDemoURL();
|
|
Params.UserIndices = MoveTemp(UserIndices);
|
|
Params.bRecord = false;
|
|
Params.ReplayVersion = FNetworkVersion::GetReplayVersion();
|
|
|
|
GetReplayStreamer()->StartStreaming(Params, FStartStreamingCallback::CreateUObject(this, &UDemoNetDriver::ReplayStreamingReady));
|
|
|
|
return bWasStartStreamingSuccessful;
|
|
}
|
|
|
|
bool UDemoNetDriver::InitConnectInternal(FString& Error)
|
|
{
|
|
ResetDemoState();
|
|
|
|
if (!ReplayHelper.ReadPlaybackDemoHeader(Error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Set network version on connection
|
|
ReplayHelper.SetPlaybackNetworkVersions(ServerConnection);
|
|
|
|
// Create fake control channel
|
|
CreateInitialClientChannels();
|
|
|
|
// Default async world loading to the cvar value...
|
|
bool bAsyncLoadWorld = CVarDemoAsyncLoadWorld.GetValueOnGameThread() > 0;
|
|
|
|
// ...but allow it to be overridden via a command-line option.
|
|
const TCHAR* const AsyncLoadWorldOverrideOption = ReplayHelper.DemoURL.GetOption(TEXT("AsyncLoadWorldOverride="), nullptr);
|
|
if (AsyncLoadWorldOverrideOption)
|
|
{
|
|
bAsyncLoadWorld = FCString::ToBool(AsyncLoadWorldOverrideOption);
|
|
}
|
|
|
|
// Hook up to get notifications so we know when a travel is complete (LoadMap or Seamless).
|
|
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &ThisClass::OnPostLoadMapWithWorld);
|
|
|
|
if (GetDuplicateLevelID() == -1)
|
|
{
|
|
if (bAsyncLoadWorld && World->WorldType != EWorldType::PIE) // Editor doesn't support async map travel
|
|
{
|
|
ReplayHelper.LevelNamesAndTimes = ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes;
|
|
|
|
// FIXME: Test for failure!!!
|
|
ProcessSeamlessTravel(0);
|
|
}
|
|
else
|
|
{
|
|
// Bypass UDemoPendingNetLevel
|
|
FString LoadMapError;
|
|
|
|
FURL LocalDemoURL;
|
|
LocalDemoURL.Map = ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes[0].LevelName;
|
|
|
|
if (!GEngine->MakeSureMapNameIsValid(LocalDemoURL.Map))
|
|
{
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::LoadMap);
|
|
return false;
|
|
}
|
|
|
|
FWorldContext * WorldContext = GEngine->GetWorldContextFromWorld(World);
|
|
|
|
if (WorldContext == nullptr)
|
|
{
|
|
UGameInstance* GameInstance = World->GetGameInstance();
|
|
|
|
Error = FString::Printf(TEXT("No world context"));
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::InitConnect: %s"), *Error);
|
|
GameInstance->HandleDemoPlaybackFailure(EDemoPlayFailure::Generic, FString(TEXT("No world context")));
|
|
return false;
|
|
}
|
|
|
|
World->ClearDemoNetDriver();
|
|
SetWorld(nullptr);
|
|
|
|
auto NewPendingNetGame = NewObject<UDemoPendingNetGame>();
|
|
|
|
// Set up the pending net game so that the engine can call LoadMap on the next tick.
|
|
NewPendingNetGame->SetDemoNetDriver(this);
|
|
NewPendingNetGame->URL = LocalDemoURL;
|
|
NewPendingNetGame->bSuccessfullyConnected = true;
|
|
|
|
WorldContext->PendingNetGame = NewPendingNetGame;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReplayHelper.ResetLevelStatuses();
|
|
ReplayHelper.ResetLevelMap();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::InitListen(FNetworkNotify* InNotify, FURL& ListenURL, bool bReuseAddressAndPort, FString& Error)
|
|
{
|
|
if (!InitBase(false, InNotify, ListenURL, bReuseAddressAndPort, Error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
check(World != nullptr);
|
|
|
|
AWorldSettings* WorldSettings = World->GetWorldSettings();
|
|
|
|
if (!WorldSettings)
|
|
{
|
|
Error = TEXT("No WorldSettings!!");
|
|
return false;
|
|
}
|
|
|
|
// Recording, local machine is server, demo stream acts "as if" it's a client.
|
|
UDemoNetConnection* Connection = NewObject<UDemoNetConnection>();
|
|
Connection->InitConnection(this, USOCK_Open, ListenURL, 1000000);
|
|
|
|
AddClientConnection(Connection);
|
|
|
|
// Technically, NetDriver's can be renamed so this could become stale.
|
|
// However, it's only used for logging and DemoNetDriver's are typically given a special name.
|
|
BudgetLogHelper = MakeUnique<FDemoBudgetLogHelper>(NetDriverName.ToString());
|
|
|
|
ReplayHelper.StartRecording(Connection);
|
|
|
|
// Spawn the demo recording spectator.
|
|
SpawnDemoRecSpectator(Connection, ListenURL);
|
|
|
|
return true;
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyStreamingLevelUnload( ULevel* InLevel )
|
|
{
|
|
if (InLevel && !InLevel->bClientOnlyVisible && HasLevelStreamingFixes() && IsPlaying())
|
|
{
|
|
const FName FilterLevelName = InLevel->GetOutermost()->GetFName();
|
|
|
|
// We can't just iterate over the levels actors, because the ones in the queue will already have been destroyed.
|
|
for (TMap<FString, FRollbackNetStartupActorInfo>::TIterator RollbackIt = RollbackNetStartupActors.CreateIterator(); RollbackIt; ++RollbackIt)
|
|
{
|
|
if (RollbackIt.Value().LevelName == FilterLevelName)
|
|
{
|
|
RollbackIt.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
|
|
Super::NotifyStreamingLevelUnload(InLevel);
|
|
}
|
|
|
|
void UDemoNetDriver::OnPostLoadMapWithWorld(UWorld* InWorld)
|
|
{
|
|
if (InWorld != nullptr && InWorld == World)
|
|
{
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
if (IsPlaying())
|
|
{
|
|
ReplayHelper.ResetLevelStatuses();
|
|
}
|
|
else
|
|
{
|
|
ReplayHelper.ClearLevelStreamingState();
|
|
}
|
|
}
|
|
|
|
if (IsPlaying())
|
|
{
|
|
ReplayHelper.ResetLevelMap();
|
|
}
|
|
else
|
|
{
|
|
ReplayHelper.ClearLevelMap();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::DiffActorProperties(UActorChannel* const ActorChannel)
|
|
{
|
|
if (!ActorChannel || !ActorChannel->GetActor())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto DiffObjectProperties = [](const FObjectReplicator& ObjectReplicator)
|
|
{
|
|
if (const UObject* const ReplicatedObject = ObjectReplicator.GetObject())
|
|
{
|
|
FReceivingRepState* const ReceivingRepState = ObjectReplicator.RepState->GetReceivingRepState();
|
|
const FRepShadowDataBuffer ShadowData(ReceivingRepState->StaticBuffer.GetData());
|
|
const FConstRepObjectDataBuffer RepObjectData(ReplicatedObject);
|
|
|
|
ObjectReplicator.RepLayout->DiffProperties(&(ReceivingRepState->RepNotifies), ShadowData, RepObjectData, EDiffPropertiesFlags::Sync);
|
|
}
|
|
};
|
|
|
|
// Make sure we diff Actor first
|
|
const FObjectReplicator& ActorReplicator = ActorChannel->GetActorReplicationData();
|
|
DiffObjectProperties(ActorReplicator);
|
|
|
|
// Diff any Components and SubObjects
|
|
for (const auto& ReplicatorPair : ActorChannel->ReplicationMap)
|
|
{
|
|
const FObjectReplicator& ObjectReplicator = ReplicatorPair.Value.Get();
|
|
// We don't need to diff Actor again
|
|
if (ActorReplicator.GetObject() == ObjectReplicator.GetObject())
|
|
{
|
|
continue;
|
|
}
|
|
DiffObjectProperties(ObjectReplicator);
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::ContinueListen(FURL& ListenURL)
|
|
{
|
|
if (IsRecording() && ensure(IsRecordingPaused()))
|
|
{
|
|
SetCurrentLevelIndex(GetCurrentLevelIndex() + 1);
|
|
|
|
PauseRecording(false);
|
|
|
|
// Delete the old player controller, we're going to create a new one (and we can't leave this one hanging around)
|
|
if (SpectatorController != nullptr)
|
|
{
|
|
SpectatorController->Player = nullptr; // Force APlayerController::DestroyNetworkActorHandled to return false
|
|
World->DestroyActor(SpectatorController, true);
|
|
SpectatorControllers.Empty();
|
|
SpectatorController = nullptr;
|
|
}
|
|
|
|
SpawnDemoRecSpectator(ClientConnections[0], ListenURL);
|
|
|
|
// Force a checkpoint to be created in the next tick - We need a checkpoint right after traveling so that scrubbing
|
|
// from a different level will have essentially an "empty" checkpoint to work from.
|
|
SetLastCheckpointTime(-1 * CVarCheckpointUploadDelayInSeconds.GetValueOnGameThread());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UDemoNetDriver::IsRecording() const
|
|
{
|
|
return ClientConnections.Num() > 0 && ClientConnections[0] != nullptr && ClientConnections[0]->GetConnectionState() != USOCK_Closed;
|
|
}
|
|
|
|
bool UDemoNetDriver::IsPlaying() const
|
|
{
|
|
// ServerConnection may be deleted / recreated during checkpoint loading.
|
|
return IsLoadingCheckpoint() || (ServerConnection != nullptr && ServerConnection->GetConnectionState() != USOCK_Closed);
|
|
}
|
|
|
|
bool UDemoNetDriver::IsServer() const
|
|
{
|
|
return (ServerConnection == nullptr) || IsRecording();
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldTickFlushAsyncEndOfFrame() const
|
|
{
|
|
return GEngine && GEngine->ShouldDoAsyncEndOfFrameTasks() && CVarDemoClientRecordAsyncEndOfFrame.GetValueOnAnyThread() != 0 && World && World->IsRecordingClientReplay();
|
|
}
|
|
|
|
void UDemoNetDriver::TickFlush(float DeltaSeconds)
|
|
{
|
|
if (!ShouldTickFlushAsyncEndOfFrame())
|
|
{
|
|
TickFlushInternal(DeltaSeconds);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::TickFlushAsyncEndOfFrame(float DeltaSeconds)
|
|
{
|
|
if (ShouldTickFlushAsyncEndOfFrame())
|
|
{
|
|
TickFlushInternal(DeltaSeconds);
|
|
SetIsInTick(false); //PostTickFlush isn't called after the async TickFlush, so set bInTick to false here
|
|
}
|
|
}
|
|
|
|
/** Accounts for the network time we spent in the demo driver. */
|
|
double GTickFlushDemoDriverTimeSeconds = 0.0;
|
|
|
|
void UDemoNetDriver::TickFlushInternal(float DeltaSeconds)
|
|
{
|
|
LLM_SCOPE(ELLMTag::Replays);
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(DemoRecording);
|
|
|
|
GTickFlushDemoDriverTimeSeconds = 0.0;
|
|
FSimpleScopeSecondsCounter ScopedTimer(GTickFlushDemoDriverTimeSeconds);
|
|
|
|
// Set the context on the world for this driver's level collection.
|
|
const int32 FoundCollectionIndex = World ? World->GetLevelCollections().IndexOfByPredicate([this](const FLevelCollection& Collection)
|
|
{
|
|
return Collection.GetDemoNetDriver() == this;
|
|
}) : INDEX_NONE;
|
|
|
|
FScopedLevelCollectionContextSwitch LCSwitch(FoundCollectionIndex, World);
|
|
|
|
Super::TickFlush(DeltaSeconds);
|
|
|
|
if (!IsRecording() || bIsWaitingForStream)
|
|
{
|
|
// Nothing to do
|
|
return;
|
|
}
|
|
|
|
TSharedPtr<INetworkReplayStreamer> Streamer = GetReplayStreamer();
|
|
|
|
if (Streamer->GetLastError() != ENetworkReplayError::None)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::TickFlush: ReplayStreamer ERROR: %s"), ENetworkReplayError::ToString(Streamer->GetLastError()));
|
|
StopDemo();
|
|
return;
|
|
}
|
|
|
|
if (IsRecordingPaused())
|
|
{
|
|
return;
|
|
}
|
|
|
|
FArchive* FileAr = Streamer->GetStreamingArchive();
|
|
|
|
if (FileAr == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::TickFlush: FileAr == nullptr"));
|
|
StopDemo();
|
|
return;
|
|
}
|
|
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Net replay record time"), STAT_ReplayRecordTime, STATGROUP_Net);
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
TickDemoRecord(DeltaSeconds);
|
|
|
|
const double EndTime = FPlatformTime::Seconds();
|
|
|
|
const double RecordTotalTime = (EndTime - StartTime);
|
|
|
|
// While recording, the CurrentCL is the same as the recording CL.
|
|
ConditionallyDisplayBurnInTime(FEngineVersion::Current().GetChangelist(), GetDemoCurrentTime());
|
|
|
|
MaxRecordTime = FMath::Max(MaxRecordTime, RecordTotalTime);
|
|
|
|
AccumulatedRecordTime += RecordTotalTime;
|
|
|
|
RecordCountSinceFlush++;
|
|
|
|
const double DemoElapsedTime = EndTime - LastRecordAvgFlush;
|
|
|
|
const double AVG_FLUSH_TIME_IN_SECONDS = 2;
|
|
|
|
if (DemoElapsedTime > AVG_FLUSH_TIME_IN_SECONDS && RecordCountSinceFlush > 0)
|
|
{
|
|
const float AvgTimeMS = (AccumulatedRecordTime / RecordCountSinceFlush) * 1000;
|
|
const float MaxRecordTimeMS = MaxRecordTime * 1000;
|
|
|
|
if (AvgTimeMS > 8.0f)//|| MaxRecordTimeMS > 6.0f )
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("UDemoNetDriver::TickFlush: SLOW FRAME. Avg: %2.2f, Max: %2.2f, Actors: %i"), AvgTimeMS, MaxRecordTimeMS, GetNetworkObjectList().GetActiveObjects().Num());
|
|
}
|
|
|
|
LastRecordAvgFlush = EndTime;
|
|
AccumulatedRecordTime = 0;
|
|
MaxRecordTime = 0;
|
|
RecordCountSinceFlush = 0;
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::TickDispatch(float DeltaSeconds)
|
|
{
|
|
LLM_SCOPE(ELLMTag::Replays);
|
|
|
|
// Set the context on the world for this driver's level collection.
|
|
const int32 FoundCollectionIndex = World ? World->GetLevelCollections().IndexOfByPredicate([this](const FLevelCollection& Collection)
|
|
{
|
|
return Collection.GetDemoNetDriver() == this;
|
|
}) : INDEX_NONE;
|
|
|
|
FScopedLevelCollectionContextSwitch LCSwitch(FoundCollectionIndex, World);
|
|
|
|
Super::TickDispatch(DeltaSeconds);
|
|
|
|
if (!IsPlaying() || bIsWaitingForStream)
|
|
{
|
|
// Nothing to do
|
|
return;
|
|
}
|
|
|
|
if (GetReplayStreamer()->GetLastError() != ENetworkReplayError::None)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::TickDispatch: ReplayStreamer ERROR: %s"), ENetworkReplayError::ToString(GetReplayStreamer()->GetLastError()));
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::ReplayStreamerInternal);
|
|
return;
|
|
}
|
|
|
|
FArchive* FileAr = GetReplayStreamer()->GetStreamingArchive();
|
|
|
|
if (FileAr == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::TickDispatch: FileAr == nullptr"));
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::ReplayStreamerInternal);
|
|
return;
|
|
}
|
|
|
|
if (!HasLevelStreamingFixes())
|
|
{
|
|
// Wait until all levels are streamed in
|
|
for (ULevelStreaming* StreamingLevel : World->GetStreamingLevels())
|
|
{
|
|
if (StreamingLevel && StreamingLevel->ShouldBeLoaded() && (!StreamingLevel->IsLevelLoaded() || !StreamingLevel->GetLoadedLevel()->GetOutermost()->IsFullyLoaded() || !StreamingLevel->IsLevelVisible()))
|
|
{
|
|
// Abort, we have more streaming levels to load
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (CVarDemoTimeDilation.GetValueOnGameThread() >= 0.0f)
|
|
{
|
|
World->GetWorldSettings()->DemoPlayTimeDilation = CVarDemoTimeDilation.GetValueOnGameThread();
|
|
}
|
|
|
|
// DeltaSeconds that is padded in, is unclampped and not time dilated
|
|
DeltaSeconds = FReplayHelper::GetClampedDeltaSeconds( World, DeltaSeconds );
|
|
|
|
// Update time dilation on spectator pawn to compensate for any demo dilation
|
|
// (we want to continue to fly around in real-time)
|
|
for (APlayerController* CurSpectatorController : SpectatorControllers)
|
|
{
|
|
if (CurSpectatorController == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( World->GetWorldSettings()->DemoPlayTimeDilation > UE_KINDA_SMALL_NUMBER )
|
|
{
|
|
CurSpectatorController->CustomTimeDilation = 1.0f / World->GetWorldSettings()->DemoPlayTimeDilation;
|
|
}
|
|
else
|
|
{
|
|
CurSpectatorController->CustomTimeDilation = 1.0f;
|
|
}
|
|
|
|
if (CurSpectatorController->GetSpectatorPawn() != nullptr)
|
|
{
|
|
CurSpectatorController->GetSpectatorPawn()->CustomTimeDilation = CurSpectatorController->CustomTimeDilation;
|
|
|
|
CurSpectatorController->GetSpectatorPawn()->PrimaryActorTick.bTickEvenWhenPaused = true;
|
|
|
|
USpectatorPawnMovement* SpectatorMovement = Cast<USpectatorPawnMovement>(CurSpectatorController->GetSpectatorPawn()->GetMovementComponent());
|
|
|
|
if ( SpectatorMovement )
|
|
{
|
|
//SpectatorMovement->bIgnoreTimeDilation = true;
|
|
SpectatorMovement->PrimaryComponentTick.bTickEvenWhenPaused = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
TickDemoPlayback(DeltaSeconds);
|
|
|
|
// Used LastProcessedPacketTime because it will correlate better with recorded frame time.
|
|
ConditionallyDisplayBurnInTime(ReplayHelper.PlaybackDemoHeader.EngineVersion.GetChangelist(), LastProcessedPacketTime);
|
|
}
|
|
|
|
void UDemoNetDriver::ProcessRemoteFunction(AActor* Actor, UFunction* Function, void* Parameters, FOutParmRec* OutParms, FFrame* Stack, UObject* SubObject)
|
|
{
|
|
#if !UE_BUILD_SHIPPING
|
|
bool bBlockSendRPC = false;
|
|
|
|
SendRPCDel.ExecuteIfBound(Actor, Function, Parameters, OutParms, Stack, SubObject, bBlockSendRPC);
|
|
|
|
if (!bBlockSendRPC)
|
|
#endif
|
|
{
|
|
if (IsRecording())
|
|
{
|
|
const bool bRecordRPC = DemoNetDriverRecordingPrivate::RecordUnicastRPCs ? ShouldReplicateFunction(Actor, Function) : EnumHasAnyFlags(Function->FunctionFlags, FUNC_NetMulticast);
|
|
|
|
if (bRecordRPC)
|
|
{
|
|
const bool bIsRelevant = !Actor->bOnlyRelevantToOwner || (Actor->GetNetDriverName() == NetDriverName);
|
|
|
|
if (bIsRelevant)
|
|
{
|
|
// Handle role swapping if this is a client-recorded replay.
|
|
FScopedActorRoleSwap RoleSwap(Actor);
|
|
|
|
InternalProcessRemoteFunction(Actor, SubObject, ClientConnections[0], Function, Parameters, OutParms, Stack, IsServer());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldClientDestroyTearOffActors() const
|
|
{
|
|
if (CVarDemoFastForwardDestroyTearOffActors.GetValueOnGameThread() != 0)
|
|
{
|
|
return bIsFastForwarding;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldSkipRepNotifies() const
|
|
{
|
|
if (CVarDemoFastForwardSkipRepNotifies.GetValueOnAnyThread() != 0)
|
|
{
|
|
return bIsFastForwarding;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UDemoNetDriver::StopDemo()
|
|
{
|
|
if (!IsRecording() && !IsPlaying())
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("StopDemo: No demo is playing"));
|
|
ClearReplayTasks();
|
|
ReplayHelper.ActiveReplayName.Empty();
|
|
ResetDemoState();
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogDemo, Log, TEXT("StopDemo: Demo %s stopped at frame %d"), *ReplayHelper.DemoURL.Map, GetDemoFrameNum());
|
|
|
|
if (!ServerConnection)
|
|
{
|
|
// let GC cleanup the object
|
|
if (ClientConnections.Num() > 0 && ClientConnections[0] != nullptr)
|
|
{
|
|
ClientConnections[0]->Close();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// flush out any pending network traffic
|
|
ServerConnection->FlushNet();
|
|
|
|
ServerConnection->SetConnectionState(USOCK_Closed);
|
|
ServerConnection->Close();
|
|
}
|
|
|
|
ReplayHelper.StopReplay();
|
|
|
|
ClearReplayTasks();
|
|
ResetDemoState();
|
|
|
|
check(!IsRecording() && !IsPlaying());
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Demo Recording tick.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
bool UDemoNetDriver::DemoReplicateActor(AActor* Actor, UNetConnection* Connection, bool bMustReplicate)
|
|
{
|
|
return ReplayHelper.ReplicateActor(Actor, Connection, bMustReplicate);
|
|
}
|
|
|
|
void UDemoNetDriver::AddEvent(const FString& Group, const FString& Meta, const TArray<uint8>& Data)
|
|
{
|
|
AddOrUpdateEvent(FString(), Group, Meta, Data);
|
|
}
|
|
|
|
void UDemoNetDriver::AddOrUpdateEvent(const FString& Name, const FString& Group, const FString& Meta, const TArray<uint8>& Data)
|
|
{
|
|
ReplayHelper.AddOrUpdateEvent(Name, Group, Meta, Data);
|
|
}
|
|
|
|
void UDemoNetDriver::EnumerateEvents(const FString& Group, const FEnumerateEventsCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->EnumerateEvents(Group, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::RequestEventData(const FString& EventID, const FRequestEventDataCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->RequestEventData(EventID, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::EnumerateEventsForActiveReplay(const FString& Group, const FEnumerateEventsCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->EnumerateEvents(GetActiveReplayName(), Group, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::EnumerateEventsForActiveReplay(const FString& Group, const int32 UserIndex, const FEnumerateEventsCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->EnumerateEvents(GetActiveReplayName(), Group, UserIndex, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::RequestEventDataForActiveReplay(const FString& EventID, const FRequestEventDataCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->RequestEventData(GetActiveReplayName(), EventID, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::RequestEventDataForActiveReplay(const FString& EventID, const int32 UserIndex, const FRequestEventDataCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->RequestEventData(GetActiveReplayName(), EventID, UserIndex, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::RequestEventGroupDataForActiveReplay(const FString& Group, const FRequestEventGroupDataCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->RequestEventGroupData(GetActiveReplayName(), Group, Delegate);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::RequestEventGroupDataForActiveReplay(const FString& Group, const int32 UserIndex, const FRequestEventGroupDataCallback& Delegate)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->RequestEventGroupData(GetActiveReplayName(), Group, UserIndex, Delegate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FReplayViewer
|
|
* Used when demo.UseNetRelevancy enabled
|
|
* Tracks all of the possible viewers of a replay that we use to determine relevancy
|
|
*/
|
|
class FReplayViewer
|
|
{
|
|
public:
|
|
FReplayViewer(const UNetConnection* Connection) :
|
|
Viewer(Connection->PlayerController ? Connection->PlayerController : Connection->OwningActor),
|
|
ViewTarget(Connection->PlayerController ? Connection->PlayerController->GetViewTarget() : ToRawPtr(Connection->OwningActor))
|
|
{
|
|
Location = ViewTarget ? ViewTarget->GetActorLocation() : FVector::ZeroVector;
|
|
}
|
|
|
|
AActor* Viewer;
|
|
AActor* ViewTarget;
|
|
FVector Location;
|
|
};
|
|
|
|
class FRepActorsParams
|
|
{
|
|
public:
|
|
FRepActorsParams(FRepActorsParams&&) = delete;
|
|
FRepActorsParams(const FRepActorsParams&) = delete;
|
|
FRepActorsParams& operator=(const FRepActorsParams&) = delete;
|
|
FRepActorsParams& operator=(FRepActorsParams&&) = delete;
|
|
|
|
FRepActorsParams(UDemoNetConnection* InConnection, const bool bInUseAdaptiveNetFrequency, const bool bInDoFindActorChannel, const bool bInDoCheckDormancy,
|
|
const float InMinRecordHz, const float InMaxRecordHz, const float InServerTickTime,
|
|
const double InReplicationStartTimeSeconds, const double InTimeLimitSeconds, const double InDestructionInfoTimeLimitSeconds):
|
|
Connection(InConnection),
|
|
bUseAdapativeNetFrequency(bInUseAdaptiveNetFrequency),
|
|
bDoFindActorChannel(bInDoFindActorChannel),
|
|
bDoCheckDormancy(bInDoCheckDormancy),
|
|
NumActorsReplicated(0),
|
|
NumDestructionInfosReplicated(0),
|
|
MinRecordHz(InMinRecordHz),
|
|
MaxRecordHz(InMaxRecordHz),
|
|
ServerTickTime(InServerTickTime),
|
|
ReplicationStartTimeSeconds(InReplicationStartTimeSeconds),
|
|
TimeLimitSeconds(InTimeLimitSeconds),
|
|
DestructionInfoTimeLimitSeconds(InDestructionInfoTimeLimitSeconds),
|
|
TotalDestructionInfoRecordTime(0.0)
|
|
{
|
|
}
|
|
|
|
UDemoNetConnection* Connection;
|
|
const bool bUseAdapativeNetFrequency;
|
|
const bool bDoFindActorChannel;
|
|
const bool bDoCheckDormancy;
|
|
int32 NumActorsReplicated;
|
|
int32 NumDestructionInfosReplicated;
|
|
const float MinRecordHz;
|
|
const float MaxRecordHz;
|
|
const float ServerTickTime;
|
|
const double ReplicationStartTimeSeconds;
|
|
const double TimeLimitSeconds;
|
|
const double DestructionInfoTimeLimitSeconds;
|
|
double TotalDestructionInfoRecordTime;
|
|
};
|
|
|
|
void UDemoNetDriver::TickDemoRecord(float DeltaSeconds)
|
|
{
|
|
if (!IsRecording() || IsRecordingPaused())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CSV_SCOPED_TIMING_STAT(Demo, DemoRecordTime);
|
|
|
|
// DeltaSeconds that is padded in, is unclamped and not time dilated
|
|
SetDemoCurrentTime(GetDemoCurrentTime() + FReplayHelper::GetClampedDeltaSeconds(World, DeltaSeconds));
|
|
|
|
ReplayHelper.ReplayStreamer->UpdateTotalDemoTime(GetDemoCurrentTimeInMS());
|
|
|
|
if (ReplayHelper.GetCheckpointSaveState() != FReplayHelper::ECheckpointSaveState::Idle)
|
|
{
|
|
// If we're in the middle of saving a checkpoint, then update that now and return
|
|
ReplayHelper.TickCheckpoint(ClientConnections[0]);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
TickDemoRecordFrame(DeltaSeconds);
|
|
|
|
// Save a checkpoint if it's time
|
|
if (CVarEnableCheckpoints.GetValueOnAnyThread() == 1)
|
|
{
|
|
check(ReplayHelper.GetCheckpointSaveState() == FReplayHelper::ECheckpointSaveState::Idle); // We early out above, so this shouldn't be possible
|
|
|
|
if (ReplayHelper.ShouldSaveCheckpoint())
|
|
{
|
|
ReplayHelper.SaveCheckpoint(ClientConnections[0]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::BuildSortedLevelPriorityOnLevels(const TArray<FDemoActorPriority>& PrioritizedActorList, TArray<FLevelnterval>& OutLevelIntervals)
|
|
{
|
|
OutLevelIntervals.Reset();
|
|
|
|
// Find level intervals
|
|
const int32 Count = PrioritizedActorList.Num();
|
|
const FDemoActorPriority* Priorities = PrioritizedActorList.GetData();
|
|
|
|
const bool bHighPriorityPersistentLevel = DemoNetDriverRecordingPrivate::CVarDemoForcePersistentLevelPriority.GetValueOnAnyThread() != 0;
|
|
|
|
for (int32 Index = 0; Index < Count;)
|
|
{
|
|
const UObject* CurrentLevel = Priorities[Index].Level;
|
|
|
|
FLevelnterval Interval;
|
|
Interval.StartIndex = Index;
|
|
|
|
if (bHighPriorityPersistentLevel && World && (CurrentLevel == World->PersistentLevel))
|
|
{
|
|
Interval.Priority = MAX_int32;
|
|
}
|
|
else
|
|
{
|
|
Interval.Priority = Priorities[Index].ActorPriority.Priority;
|
|
}
|
|
|
|
Interval.LevelIndex = (CurrentLevel != nullptr ? ReplayHelper.FindOrAddLevelStatus(*Cast<ULevel>(CurrentLevel)).LevelIndex + 1 : 0);
|
|
|
|
while (Index < Count && Priorities[Index].Level == CurrentLevel)
|
|
{
|
|
++Index;
|
|
}
|
|
|
|
Interval.Count = Index - Interval.StartIndex;
|
|
|
|
OutLevelIntervals.Add(Interval);
|
|
}
|
|
|
|
// Sort intervals on priority
|
|
OutLevelIntervals.Sort([](const FLevelnterval& A, const FLevelnterval& B) { return (B.Priority < A.Priority) || ((A.Priority == B.Priority) && (A.LevelIndex < B.LevelIndex)); });
|
|
}
|
|
|
|
void UDemoNetDriver::TickDemoRecordFrame(float DeltaSeconds)
|
|
{
|
|
FArchive* FileAr = GetReplayStreamer()->GetStreamingArchive();
|
|
|
|
if (FileAr == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const double RecordFrameStartTime = FPlatformTime::Seconds();
|
|
const double RecordTimeLimit = (MaxDesiredRecordTimeMS / 1000.f);
|
|
|
|
// Mark any new streaming levels, so that they are saved out this frame
|
|
if (!HasLevelStreamingFixes())
|
|
{
|
|
for (ULevelStreaming* StreamingLevel : World->GetStreamingLevels())
|
|
{
|
|
if (StreamingLevel == nullptr || !StreamingLevel->ShouldBeLoaded() || StreamingLevel->ShouldBeAlwaysLoaded())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TWeakObjectPtr<UObject> WeakStreamingLevel;
|
|
WeakStreamingLevel = StreamingLevel;
|
|
if (!ReplayHelper.UniqueStreamingLevels.Contains(WeakStreamingLevel))
|
|
{
|
|
ReplayHelper.UniqueStreamingLevels.Add(WeakStreamingLevel);
|
|
ReplayHelper.NewStreamingLevelsThisFrame.Add(WeakStreamingLevel);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save out a frame
|
|
ReplayHelper.DemoFrameNum++;
|
|
|
|
ReplicationFrame++;
|
|
BudgetLogHelper->NewFrame();
|
|
|
|
UDemoNetConnection* ClientConnection = CastChecked<UDemoNetConnection>(ClientConnections[0]);
|
|
|
|
// flush out any pending network traffic
|
|
FReplayHelper::FlushNetChecked(*ClientConnection);
|
|
|
|
float ServerTickTime = GEngine->GetMaxTickRate( DeltaSeconds );
|
|
if (ServerTickTime == 0.0)
|
|
{
|
|
ServerTickTime = DeltaSeconds;
|
|
}
|
|
else
|
|
{
|
|
ServerTickTime = 1.0 / ServerTickTime;
|
|
}
|
|
|
|
// Build priority list
|
|
FNetworkObjectList& NetObjectList = GetNetworkObjectList();
|
|
const FNetworkObjectList::FNetworkObjectSet& ActiveObjectSet = NetObjectList.GetActiveObjects();
|
|
const int32 NumActiveObjects = ActiveObjectSet.Num();
|
|
|
|
PrioritizedActors.Reset(NumActiveObjects);
|
|
|
|
// Set the location of the connection's viewtarget for prioritization.
|
|
FVector ViewLocation = FVector::ZeroVector;
|
|
FVector ViewDirection = FVector::ZeroVector;
|
|
APlayerController* CachedViewerOverride = ViewerOverride.Get();
|
|
APlayerController* Viewer = CachedViewerOverride ? CachedViewerOverride : ClientConnection->GetPlayerController(World);
|
|
AActor* ViewTarget = Viewer ? Viewer->GetViewTarget() : nullptr;
|
|
|
|
if (ViewTarget)
|
|
{
|
|
ViewLocation = ViewTarget->GetActorLocation();
|
|
ViewDirection = ViewTarget->GetActorRotation().Vector();
|
|
}
|
|
|
|
const bool bDoCheckDormancyEarly = CVarDemoLateActorDormancyCheck.GetValueOnAnyThread() == 0;
|
|
const bool bLateDestructionInfos = DemoNetDriverRecordingPrivate::CVarDemoLateDestructionInfoPrioritize.GetValueOnAnyThread() != 0;
|
|
const bool bDoPrioritizeActors = bPrioritizeActors;
|
|
const bool bDoFindActorChannelEarly = bDoPrioritizeActors || bDoCheckDormancyEarly;
|
|
|
|
int32 ActorsPrioritized = 0;
|
|
int32 DestructionInfosPrioritized = 0;
|
|
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Replay prioritize time"), STAT_ReplayPrioritizeTime, STATGROUP_Net);
|
|
|
|
const double ConsiderTimeLimit = RecordTimeLimit * RecordBuildConsiderAndPrioritizeTimeSlice;
|
|
auto HasConsiderTimeBeenExhausted = [ConsiderTimeLimit, RecordFrameStartTime, RecordTimeLimit]() -> bool
|
|
{
|
|
return RecordTimeLimit > 0.f && ((FPlatformTime::Seconds() - RecordFrameStartTime) > ConsiderTimeLimit);
|
|
};
|
|
|
|
auto PrioritizeDestructionInfos = [ClientConnection, this, &HasConsiderTimeBeenExhausted]()
|
|
{
|
|
SCOPED_NAMED_EVENT(UDemoNetDriver_PrioritizeDestroyedOrDormantActors, FColor::Green);
|
|
|
|
// Add destroyed actors that the client may not have a channel for
|
|
FDemoActorPriority DestroyedActorPriority;
|
|
DestroyedActorPriority.ActorPriority.Priority = DemoNetDriverRecordingPrivate::CVarDemoDestructionInfoPriority.GetValueOnAnyThread();
|
|
|
|
for (auto DestroyedOrDormantGUID = ClientConnection->GetDestroyedStartupOrDormantActorGUIDs().CreateIterator(); DestroyedOrDormantGUID; ++DestroyedOrDormantGUID)
|
|
{
|
|
TUniquePtr<FActorDestructionInfo>& DInfo = DestroyedStartupOrDormantActors.FindChecked(*DestroyedOrDormantGUID);
|
|
DestroyedActorPriority.ActorPriority.DestructionInfo = DInfo.Get();
|
|
DestroyedActorPriority.Level = HasLevelStreamingFixes() ? DestroyedActorPriority.ActorPriority.DestructionInfo->Level.Get() : nullptr;
|
|
|
|
PrioritizedActors.Add(DestroyedActorPriority);
|
|
|
|
if (HasConsiderTimeBeenExhausted())
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("Consider time exhaused prioritizing destruction infos."));
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!bLateDestructionInfos)
|
|
{
|
|
PrioritizeDestructionInfos();
|
|
|
|
DestructionInfosPrioritized = PrioritizedActors.Num();
|
|
}
|
|
|
|
if (!HasConsiderTimeBeenExhausted())
|
|
{
|
|
TArray< FReplayViewer, TInlineAllocator<16> > ReplayViewers;
|
|
|
|
const bool bUseNetRelevancy = CVarDemoUseNetRelevancy.GetValueOnAnyThread() > 0 && World->NetDriver != nullptr && World->NetDriver->IsServer();
|
|
|
|
// If we're using relevancy, consider all connections as possible viewing sources
|
|
if (bUseNetRelevancy)
|
|
{
|
|
for (UNetConnection* Connection : World->NetDriver->ClientConnections)
|
|
{
|
|
FReplayViewer ReplayViewer(Connection);
|
|
if (ReplayViewer.ViewTarget != nullptr)
|
|
{
|
|
ReplayViewers.Add(MoveTemp(ReplayViewer));
|
|
}
|
|
}
|
|
}
|
|
|
|
const float CullDistanceOverride = CVarDemoCullDistanceOverride.GetValueOnAnyThread();
|
|
const float CullDistanceOverrideSq = CullDistanceOverride > 0.0f ? FMath::Square(CullDistanceOverride) : 0.0f;
|
|
|
|
const float RecordHzWhenNotRelevant = CVarDemoRecordHzWhenNotRelevant.GetValueOnAnyThread();
|
|
const float UpdateDelayWhenNotRelevant = RecordHzWhenNotRelevant > 0.0f ? 1.0f / RecordHzWhenNotRelevant : 0.5f;
|
|
|
|
TArray<AActor*, TInlineAllocator<128>> ActorsToRemove;
|
|
|
|
FDemoActorPriority DemoActorPriority;
|
|
FActorPriority& ActorPriority = DemoActorPriority.ActorPriority;
|
|
|
|
const bool bDeltaCheckpoint = HasDeltaCheckpoints();
|
|
|
|
const float CurrentTime = GetDemoCurrentTime();
|
|
|
|
int32 ProcessedCount = 0;
|
|
|
|
for (const TSharedPtr<FNetworkObjectInfo>& ObjectInfo : ActiveObjectSet)
|
|
{
|
|
FNetworkObjectInfo* ActorInfo = ObjectInfo.Get();
|
|
|
|
++ProcessedCount;
|
|
|
|
if (GetDemoCurrentTime() > ActorInfo->NextUpdateTime)
|
|
{
|
|
AActor* Actor = ActorInfo->Actor;
|
|
|
|
if (!IsValid(Actor))
|
|
{
|
|
ActorsToRemove.Add(Actor);
|
|
continue;
|
|
}
|
|
|
|
// During client recording, a torn-off actor will already have its remote role set to None, but
|
|
// we still need to replicate it one more time so that the recorded replay knows it's been torn-off as well.
|
|
if (Actor->GetRemoteRole() == ROLE_None && !Actor->GetTearOff())
|
|
{
|
|
ActorsToRemove.Add(Actor);
|
|
continue;
|
|
}
|
|
|
|
if (IsDormInitialStartupActor(Actor))
|
|
{
|
|
ActorsToRemove.Add(Actor);
|
|
continue;
|
|
}
|
|
|
|
if (!Actor->bRelevantForNetworkReplays)
|
|
{
|
|
ActorsToRemove.Add(Actor);
|
|
continue;
|
|
}
|
|
|
|
// We check ActorInfo->LastNetUpdateTime < KINDA_SMALL_NUMBER to force at least one update for each actor
|
|
const bool bWasRecentlyRelevant = (ActorInfo->LastNetUpdateTimestamp < UE_KINDA_SMALL_NUMBER) || ((GetElapsedTime() - ActorInfo->LastNetUpdateTimestamp) < RelevantTimeout);
|
|
|
|
bool bIsRelevant = !bUseNetRelevancy || Actor->bAlwaysRelevant || Actor == ClientConnection->PlayerController || (ActorInfo->ForceRelevantFrame >= ReplicationFrame);
|
|
|
|
if (!bIsRelevant)
|
|
{
|
|
// Assume this actor is relevant as long as *any* viewer says so
|
|
for (const FReplayViewer& ReplayViewer : ReplayViewers)
|
|
{
|
|
if (Actor->IsReplayRelevantFor(ReplayViewer.Viewer, ReplayViewer.ViewTarget, ReplayViewer.Location, CullDistanceOverrideSq))
|
|
{
|
|
bIsRelevant = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bIsRelevant && !bWasRecentlyRelevant)
|
|
{
|
|
// Actor is not relevant (or previously relevant), so skip and set next update time based on demo.RecordHzWhenNotRelevant
|
|
ActorInfo->NextUpdateTime = CurrentTime + UpdateDelayWhenNotRelevant;
|
|
continue;
|
|
}
|
|
|
|
UActorChannel* Channel = nullptr;
|
|
if (bDoFindActorChannelEarly)
|
|
{
|
|
Channel = ClientConnection->FindActorChannelRef(Actor);
|
|
|
|
// Check dormancy
|
|
if (bDoCheckDormancyEarly && Channel && ShouldActorGoDormantForDemo(Actor, Channel))
|
|
{
|
|
// Either shouldn't go dormant, or is already dormant
|
|
Channel->StartBecomingDormant();
|
|
}
|
|
}
|
|
|
|
ActorPriority.ActorInfo = ActorInfo;
|
|
ActorPriority.Channel = Channel;
|
|
DemoActorPriority.Level = Actor->GetOuter();
|
|
|
|
if (bDoPrioritizeActors) // implies bDoFindActorChannelEarly is true
|
|
{
|
|
const double LastReplicationTime = Channel ? (GetElapsedTime() - Channel->LastUpdateTime) : SpawnPrioritySeconds;
|
|
float ReplayPriority = 65536.0f * Actor->GetReplayPriority(ViewLocation, ViewDirection, Viewer, ViewTarget, Channel, LastReplicationTime);
|
|
|
|
if (Actor == ViewTarget)
|
|
{
|
|
ReplayPriority = ReplayPriority * DemoNetDriverRecordingPrivate::CVarDemoViewTargetPriorityScale.GetValueOnAnyThread();
|
|
}
|
|
|
|
// clamp into a valid range prior to rounding to avoid potential undefined behavior
|
|
ActorPriority.Priority = FMath::RoundToInt(FMath::Clamp(ReplayPriority, (float)(MIN_int32 + 10), (float)(MAX_int32 - 10)));
|
|
}
|
|
|
|
PrioritizedActors.Add(DemoActorPriority);
|
|
|
|
ActorInfo->bDirtyForReplay = bDeltaCheckpoint;
|
|
|
|
if (bIsRelevant)
|
|
{
|
|
ActorInfo->LastNetUpdateTimestamp = GetElapsedTime();
|
|
}
|
|
}
|
|
|
|
if (HasConsiderTimeBeenExhausted())
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("Consider time exhaused while iterating the active object list [%d/%d]"), ProcessedCount, ActiveObjectSet.Num());
|
|
break;
|
|
}
|
|
}
|
|
|
|
{
|
|
SCOPED_NAMED_EVENT(UDemoNetDriver_PrioritizeRemoveActors, FColor::Green);
|
|
|
|
// Always remove necessary actors, don't time slice this.
|
|
for (AActor* Actor : ActorsToRemove)
|
|
{
|
|
RemoveNetworkActor(Actor);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bLateDestructionInfos)
|
|
{
|
|
ActorsPrioritized = PrioritizedActors.Num();
|
|
|
|
if (!HasConsiderTimeBeenExhausted())
|
|
{
|
|
PrioritizeDestructionInfos();
|
|
DestructionInfosPrioritized = PrioritizedActors.Num();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("Consider time exhaused without processing destruction infos"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ActorsPrioritized = PrioritizedActors.Num() - DestructionInfosPrioritized;
|
|
}
|
|
}
|
|
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
SCOPED_NAMED_EVENT(UDemoNetDriver_PrioritizeLevelSort, FColor::Green);
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Replay actor level sorting time."), STAT_ReplayLevelSorting, STATGROUP_Net);
|
|
|
|
if (bPrioritizeActors)
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("bPrioritizeActors and HasLevelStreamingFixes are both enabled. This will undo some prioritization work."));
|
|
}
|
|
|
|
// Sort by Level and priority, If the order of levels are relevant we need a second pass on the array to find the intervals of the levels and sort those on "level with netobject with highest priority"
|
|
// but since prioritization is disabled the order is arbitrary so there is really no use to do the extra work
|
|
PrioritizedActors.Sort([](const FDemoActorPriority& A, const FDemoActorPriority& B) { return (B.Level < A.Level) || ((B.Level == A.Level) && (B.ActorPriority.Priority < A.ActorPriority.Priority)); });
|
|
|
|
// Find intervals in sorted priority lists with the same level and sort the intervals based on priority of first Object in each interval.
|
|
// Intervals are then used to determine the order we write out the replicated objects as we write one packet per level.
|
|
BuildSortedLevelPriorityOnLevels(PrioritizedActors, LevelIntervals);
|
|
}
|
|
else if (bPrioritizeActors)
|
|
{
|
|
// Sort on priority
|
|
PrioritizedActors.Sort([](const FDemoActorPriority& A, const FDemoActorPriority& B) { return B.ActorPriority.Priority < A.ActorPriority.Priority; });
|
|
}
|
|
|
|
const double PrioritizeEndTime = FPlatformTime::Seconds();
|
|
const double TotalPrioritizeActorsTime = (PrioritizeEndTime - RecordFrameStartTime);
|
|
const float TotalPrioritizeActorsTimeMS = TotalPrioritizeActorsTime * 1000.f;
|
|
|
|
CSV_CUSTOM_STAT(Demo, DemoPrioritizeTime, TotalPrioritizeActorsTimeMS, ECsvCustomStatOp::Set);
|
|
CSV_CUSTOM_STAT(Demo, DemoNumActiveObjects, NumActiveObjects, ECsvCustomStatOp::Set);
|
|
CSV_CUSTOM_STAT(Demo, DemoPrioritizedActors, ActorsPrioritized, ECsvCustomStatOp::Set);
|
|
CSV_CUSTOM_STAT(Demo, DemoPrioritizedDestInfos, DestructionInfosPrioritized, ECsvCustomStatOp::Set);
|
|
|
|
const int32 NumPrioritizedActors = PrioritizedActors.Num();
|
|
|
|
// Make sure we're under the desired recording time quota, if any.
|
|
// See ReplicatePriorizeActor.
|
|
if (RecordTimeLimit > 0.0f && TotalPrioritizeActorsTime > RecordTimeLimit)
|
|
{
|
|
BudgetLogHelper->MarkFrameOverBudget(
|
|
FDemoBudgetLogHelper::Prioritization,
|
|
TEXT("Exceeded maximum desired recording time (during Prioritization). Max: %.3fms, TimeSpent: %.3fms, Active Actors: %d, Prioritized Actors: %d"),
|
|
MaxDesiredRecordTimeMS, TotalPrioritizeActorsTimeMS, NumActiveObjects, NumPrioritizedActors);
|
|
}
|
|
|
|
float MinRecordHz = CVarDemoMinRecordHz.GetValueOnAnyThread();
|
|
float MaxRecordHz = CVarDemoRecordHz.GetValueOnAnyThread();
|
|
|
|
if (MaxRecordHz < MinRecordHz)
|
|
{
|
|
Swap(MinRecordHz, MaxRecordHz);
|
|
}
|
|
|
|
FRepActorsParams Params
|
|
(
|
|
ClientConnection,
|
|
CVarUseAdaptiveReplayUpdateFrequency.GetValueOnAnyThread() > 0,
|
|
!bDoFindActorChannelEarly,
|
|
!bDoCheckDormancyEarly,
|
|
MinRecordHz,
|
|
MaxRecordHz,
|
|
ServerTickTime,
|
|
RecordFrameStartTime,
|
|
RecordTimeLimit,
|
|
RecordTimeLimit * RecordDestructionInfoReplicationTimeSlice
|
|
);
|
|
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Replay actor replication time"), STAT_ReplayReplicateActors, STATGROUP_Net);
|
|
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
const FDemoActorPriority* Priorities = PrioritizedActors.GetData();
|
|
|
|
// Split per level
|
|
for (const FLevelnterval& Interval : LevelIntervals)
|
|
{
|
|
if (!ReplicatePrioritizedActors(&Priorities[Interval.StartIndex], Interval.Count, Params))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReplicatePrioritizedActors(PrioritizedActors.GetData(), PrioritizedActors.Num(), Params);
|
|
}
|
|
}
|
|
|
|
CSV_CUSTOM_STAT(Demo, DemoNumReplicatedActors, Params.NumActorsReplicated, ECsvCustomStatOp::Set);
|
|
CSV_CUSTOM_STAT(Demo, DemoNumReplicatedDestructionInfos, Params.NumDestructionInfosReplicated, ECsvCustomStatOp::Set);
|
|
|
|
FReplayHelper::FlushNetChecked(*ClientConnection);
|
|
|
|
WriteDemoFrameFromQueuedDemoPackets(*FileAr, ReplayHelper.QueuedDemoPackets, GetDemoCurrentTime(), EWriteDemoFrameFlags::None);
|
|
|
|
const float ReplicatedPercent = NumPrioritizedActors != 0 ? (float)(Params.NumActorsReplicated + Params.NumDestructionInfosReplicated) / (float)NumPrioritizedActors : 1.0f;
|
|
AdjustConsiderTime(ReplicatedPercent);
|
|
LastReplayFrameFidelity = ReplicatedPercent;
|
|
}
|
|
|
|
bool UDemoNetDriver::ReplicatePrioritizedActor(const FActorPriority& ActorPriority, FRepActorsParams& Params)
|
|
{
|
|
FNetworkObjectInfo* ActorInfo = ActorPriority.ActorInfo;
|
|
FActorDestructionInfo* DestructionInfo = ActorPriority.DestructionInfo;
|
|
|
|
const double RecordStartTimeSeconds = FPlatformTime::Seconds();
|
|
|
|
const bool bDoFindActorChannel = Params.bDoFindActorChannel;
|
|
const bool bDoCheckDormancy = Params.bDoCheckDormancy;
|
|
const bool bDestructionInfo = DestructionInfo != nullptr && ActorInfo == nullptr;
|
|
const bool bActorInfo = ActorInfo != nullptr && DestructionInfo == nullptr;
|
|
|
|
// Deletion entry
|
|
if (bDestructionInfo)
|
|
{
|
|
// only process destruction infos if we're below the time limit
|
|
if (Params.TotalDestructionInfoRecordTime < Params.DestructionInfoTimeLimitSeconds)
|
|
{
|
|
++Params.NumDestructionInfosReplicated;
|
|
|
|
UActorChannel* Channel = (UActorChannel*)Params.Connection->CreateChannelByName(NAME_Actor, EChannelCreateFlags::OpenedLocally);
|
|
if (Channel)
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("TickDemoRecord creating destroy channel for NetGUID <%s,%s> Priority: %d"), *DestructionInfo->NetGUID.ToString(), *DestructionInfo->PathName, ActorPriority.Priority);
|
|
|
|
FScopedRepContext LevelContext(Params.Connection, DestructionInfo->Level.Get());
|
|
|
|
// Send a close bunch on the new channel
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
Channel->SetChannelActorForDestroy(DestructionInfo);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
// Remove from connection's to-be-destroyed list (close bunch is reliable, so it will make it there)
|
|
Params.Connection->RemoveDestructionInfo(DestructionInfo);
|
|
|
|
// calling conditional cleanup now allows the channel to be returned to any pools and reused immediately
|
|
Channel->ConditionalCleanUp(false, DestructionInfo->Reason);
|
|
}
|
|
}
|
|
}
|
|
else if (bActorInfo)
|
|
{
|
|
++Params.NumActorsReplicated;
|
|
|
|
AActor* Actor = ActorInfo->Actor;
|
|
|
|
if (bDoCheckDormancy)
|
|
{
|
|
UActorChannel* Channel = (bDoFindActorChannel ? Params.Connection->FindActorChannelRef(Actor) : ActorPriority.Channel);
|
|
if (Channel && ShouldActorGoDormantForDemo(Actor, Channel))
|
|
{
|
|
// Either shouldn't go dormant, or is already dormant
|
|
Channel->StartBecomingDormant();
|
|
}
|
|
}
|
|
|
|
// Use NetUpdateFrequency for this actor, but clamp it to RECORD_HZ.
|
|
const float ClampedNetUpdateFrequency = FMath::Clamp(Actor->NetUpdateFrequency, Params.MinRecordHz, Params.MaxRecordHz);
|
|
const double NetUpdateDelay = 1.0 / ClampedNetUpdateFrequency;
|
|
|
|
// Set defaults if this actor is replicating for first time
|
|
if (ActorInfo->LastNetReplicateTime == 0)
|
|
{
|
|
ActorInfo->LastNetReplicateTime = GetDemoCurrentTime();
|
|
ActorInfo->OptimalNetUpdateDelta = NetUpdateDelay;
|
|
}
|
|
|
|
const float LastReplicateDelta = static_cast<float>(GetDemoCurrentTime() - ActorInfo->LastNetReplicateTime);
|
|
|
|
// Calculate min delta (max rate actor will update), and max delta (slowest rate actor will update)
|
|
const float MinOptimalDelta = NetUpdateDelay; // Don't go faster than NetUpdateFrequency
|
|
const float MinNetUpdateFrequency = (Actor->MinNetUpdateFrequency == 0.0f) ? 2.0f : Actor->MinNetUpdateFrequency;
|
|
const float MaxOptimalDelta = FMath::Max(1.0f / MinNetUpdateFrequency, MinOptimalDelta); // Don't go slower than MinNetUpdateFrequency (or NetUpdateFrequency if it's slower)
|
|
|
|
const float ScaleDownStartTime = 2.0f;
|
|
const float ScaleDownTimeRange = 5.0f;
|
|
|
|
if (LastReplicateDelta > ScaleDownStartTime)
|
|
{
|
|
// Interpolate between MinOptimalDelta/MaxOptimalDelta based on how long it's been since this actor actually sent anything
|
|
const float Alpha = FMath::Clamp((LastReplicateDelta - ScaleDownStartTime) / ScaleDownTimeRange, 0.0f, 1.0f);
|
|
ActorInfo->OptimalNetUpdateDelta = FMath::Lerp(MinOptimalDelta, MaxOptimalDelta, Alpha);
|
|
}
|
|
|
|
const double NextUpdateDelta = Params.bUseAdapativeNetFrequency ? ActorInfo->OptimalNetUpdateDelta : NetUpdateDelay;
|
|
|
|
// Account for being fractionally into the next frame
|
|
// But don't be more than a fraction of a frame behind either (we don't want to do catch-up frames when there is a long delay)
|
|
const double ExtraTime = GetDemoCurrentTime() - ActorInfo->NextUpdateTime;
|
|
const double ClampedExtraTime = FMath::Clamp(ExtraTime, 0.0, NetUpdateDelay);
|
|
|
|
// Try to spread the updates across multiple frames to smooth out spikes.
|
|
ActorInfo->NextUpdateTime = (GetDemoCurrentTime() + NextUpdateDelta - ClampedExtraTime + ((UpdateDelayRandomStream.FRand() - 0.5) * Params.ServerTickTime));
|
|
|
|
const bool bDidReplicateActor = DemoReplicateActor(Actor, Params.Connection, false);
|
|
|
|
const bool bUpdatedExternalData = ReplayHelper.UpdateExternalDataForObject(Params.Connection, Actor);
|
|
|
|
if (bDidReplicateActor || bUpdatedExternalData)
|
|
{
|
|
// Choose an optimal time, we choose 70% of the actual rate to allow frequency to go up if needed
|
|
ActorInfo->OptimalNetUpdateDelta = FMath::Clamp(LastReplicateDelta * 0.7f, MinOptimalDelta, MaxOptimalDelta);
|
|
ActorInfo->LastNetReplicateTime = GetDemoCurrentTime();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("TickDemoRecord: prioritized actor entry should have either an actor or a destruction info"));
|
|
}
|
|
|
|
// Make sure we're under the desired recording time quota, if any.
|
|
if (Params.TimeLimitSeconds > 0.f)
|
|
{
|
|
const double RecordEndTimeSeconds = FPlatformTime::Seconds();
|
|
const double RecordTimeSeconds = RecordEndTimeSeconds - RecordStartTimeSeconds;
|
|
|
|
if (bDestructionInfo)
|
|
{
|
|
Params.TotalDestructionInfoRecordTime += RecordTimeSeconds;
|
|
}
|
|
|
|
if ((ActorInfo && ActorInfo->Actor) && (RecordTimeSeconds > (Params.TimeLimitSeconds * 0.95f)))
|
|
{
|
|
UE_LOG(LogDemo, Verbose, TEXT("Actor %s took more than 95%% of maximum desired recording time. Actor: %.3fms. Max: %.3fms."),
|
|
*ActorInfo->Actor->GetName(), RecordTimeSeconds * 1000.f, MaxDesiredRecordTimeMS);
|
|
}
|
|
|
|
const double TotalRecordTimeSeconds = (RecordEndTimeSeconds - Params.ReplicationStartTimeSeconds);
|
|
|
|
if (TotalRecordTimeSeconds > Params.TimeLimitSeconds)
|
|
{
|
|
BudgetLogHelper->MarkFrameOverBudget(
|
|
FDemoBudgetLogHelper::Replication,
|
|
TEXT("Exceeded maximum desired recording time (during Actor Replication). Max: %.3fms."),
|
|
MaxDesiredRecordTimeMS);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::ReplicatePrioritizedActors(const FDemoActorPriority* ActorsToReplicate, uint32 Count, FRepActorsParams& Params)
|
|
{
|
|
bool bTimeRemaining = true;
|
|
uint32 NumProcessed = 0;
|
|
for (; NumProcessed < Count; ++NumProcessed)
|
|
{
|
|
const FActorPriority& ActorPriority = ActorsToReplicate[NumProcessed].ActorPriority;
|
|
bTimeRemaining = ReplicatePrioritizedActor(ActorPriority, Params);
|
|
if (!bTimeRemaining)
|
|
{
|
|
++NumProcessed;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bTimeRemaining;
|
|
}
|
|
|
|
void UDemoNetDriver::PauseChannels(const bool bPause)
|
|
{
|
|
if (bPause == bChannelsArePaused)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (CVarDemoInternalPauseChannels.GetValueOnAnyThread() > 0)
|
|
{
|
|
// Pause all non player controller actors
|
|
for (int32 i = ServerConnection->OpenChannels.Num() - 1; i >= 0; i--)
|
|
{
|
|
UChannel* OpenChannel = ServerConnection->OpenChannels[i];
|
|
|
|
UActorChannel* ActorChannel = Cast<UActorChannel>(OpenChannel);
|
|
|
|
if (ActorChannel == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ActorChannel->CustomTimeDilation = bPause ? 0.0f : 1.0f;
|
|
|
|
if (ActorChannel->GetActor() == nullptr || SpectatorControllers.Contains(ActorChannel->GetActor()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Better way to pause each actor?
|
|
ActorChannel->GetActor()->CustomTimeDilation = ActorChannel->CustomTimeDilation;
|
|
}
|
|
}
|
|
|
|
bChannelsArePaused = bPause;
|
|
|
|
UE_LOG(LogDemo, Verbose, TEXT("PauseChannels: %d"), bChannelsArePaused);
|
|
FNetworkReplayDelegates::OnPauseChannelsChanged.Broadcast(World, bChannelsArePaused);
|
|
}
|
|
|
|
bool UDemoNetDriver::ReadDemoFrameIntoPlaybackPackets(FArchive& Ar, TArray<FPlaybackPacket>& InPlaybackPackets, const bool bForLevelFastForward, float* OutTime)
|
|
{
|
|
return ReplayHelper.ReadDemoFrame(ServerConnection, Ar, InPlaybackPackets, bForLevelFastForward, MaxArchiveReadPos, OutTime);
|
|
}
|
|
|
|
void UDemoNetDriver::ProcessSeamlessTravel(int32 LevelIndex)
|
|
{
|
|
// Destroy all player controllers since FSeamlessTravelHandler will not destroy them.
|
|
TArray<AController*> Controllers;
|
|
for (FConstControllerIterator Iterator = World->GetControllerIterator(); Iterator; ++Iterator)
|
|
{
|
|
Controllers.Add(Iterator->Get());
|
|
}
|
|
|
|
// Clean up any splitscreen spectators if we have them.
|
|
// Let the destroy below handle deletion of the objects.
|
|
if (SpectatorControllers.Num() > 1)
|
|
{
|
|
CleanUpSplitscreenConnections(false);
|
|
}
|
|
|
|
for (AController* Controller : Controllers)
|
|
{
|
|
if (Controller)
|
|
{
|
|
// bNetForce is true so that the replicated spectator player controller will
|
|
// be destroyed as well.
|
|
Controller->Destroy(true);
|
|
|
|
// If we can, remove the spectator here as well.
|
|
SpectatorControllers.Remove(Cast<APlayerController>(Controller));
|
|
}
|
|
}
|
|
|
|
SpectatorControllers.Empty();
|
|
|
|
// Set this to nullptr since we just destroyed it.
|
|
SpectatorController = nullptr;
|
|
|
|
if (ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes.IsValidIndex(LevelIndex))
|
|
{
|
|
World->SeamlessTravel(ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes[LevelIndex].LevelName, true);
|
|
}
|
|
else
|
|
{
|
|
// If we're watching a live replay, it's probable that the header has been updated with the level added,
|
|
// so we need to download it again before proceeding.
|
|
bIsWaitingForHeaderDownload = true;
|
|
ReplayHelper.ReplayStreamer->DownloadHeader(FDownloadHeaderCallback::CreateUObject(this, &UDemoNetDriver::OnRefreshHeaderCompletePrivate, LevelIndex));
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::OnRefreshHeaderCompletePrivate(const FDownloadHeaderResult& Result, int32 LevelIndex)
|
|
{
|
|
bIsWaitingForHeaderDownload = false;
|
|
|
|
if (Result.WasSuccessful())
|
|
{
|
|
FString Error;
|
|
if (ReplayHelper.ReadPlaybackDemoHeader(Error))
|
|
{
|
|
if (ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes.IsValidIndex(LevelIndex))
|
|
{
|
|
ProcessSeamlessTravel(LevelIndex);
|
|
}
|
|
else
|
|
{
|
|
World->GetGameInstance()->HandleDemoPlaybackFailure(EDemoPlayFailure::Corrupt, FString::Printf(TEXT("UDemoNetDriver::OnDownloadHeaderComplete: LevelIndex %d not in range of level names of size: %d"), LevelIndex, ReplayHelper.PlaybackDemoHeader.LevelNamesAndTimes.Num()));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
World->GetGameInstance()->HandleDemoPlaybackFailure(EDemoPlayFailure::Corrupt, FString::Printf(TEXT("UDemoNetDriver::OnDownloadHeaderComplete: ReadPlaybackDemoHeader header failed with error %s."), *Error));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
World->GetGameInstance()->HandleDemoPlaybackFailure(EDemoPlayFailure::Corrupt, FString::Printf(TEXT("UDemoNetDriver::OnDownloadHeaderComplete: Downloading header failed.")));
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::ConditionallyReadDemoFrameIntoPlaybackPackets(FArchive& Ar)
|
|
{
|
|
if (PlaybackPackets.Num() > 0)
|
|
{
|
|
const float MAX_PLAYBACK_BUFFER_SECONDS = 5.0f;
|
|
|
|
const FPlaybackPacket& LastPacket = PlaybackPackets.Last();
|
|
const float CurrentTime = GetDemoCurrentTime();
|
|
|
|
if ((LastPacket.TimeSeconds > CurrentTime) && ((LastPacket.TimeSeconds - CurrentTime) > MAX_PLAYBACK_BUFFER_SECONDS))
|
|
{
|
|
return false; // Don't buffer more than MAX_PLAYBACK_BUFFER_SECONDS worth of frames
|
|
}
|
|
}
|
|
|
|
if (!ReadDemoFrameIntoPlaybackPackets(Ar))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldSkipPlaybackPacket(const FPlaybackPacket& Packet)
|
|
{
|
|
if (HasLevelStreamingFixes() && Packet.SeenLevelIndex != 0)
|
|
{
|
|
if (ReplayHelper.SeenLevelStatuses.IsValidIndex(Packet.SeenLevelIndex - 1))
|
|
{
|
|
// Flag the status as being seen, since we're potentially going to process it.
|
|
// We need to skip processing if it's not ready (in that case, we'll do a fast-forward).
|
|
FReplayHelper::FLevelStatus& LevelStatus = ReplayHelper.GetLevelStatus(Packet.SeenLevelIndex);
|
|
LevelStatus.bHasBeenSeen = true;
|
|
return !LevelStatus.bIsReady;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("ShouldSkipPlaybackPacket encountered a packet with an invalid seen level index."));
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UDemoNetDriver::ConditionallyProcessPlaybackPackets()
|
|
{
|
|
if (!PlaybackPackets.IsValidIndex(PlaybackPacketIndex))
|
|
{
|
|
PauseChannels(true);
|
|
return false;
|
|
}
|
|
|
|
const FPlaybackPacket& CurPacket = PlaybackPackets[PlaybackPacketIndex];
|
|
if (GetDemoCurrentTime() < CurPacket.TimeSeconds)
|
|
{
|
|
// Not enough time has passed to read another frame
|
|
return false;
|
|
}
|
|
|
|
if (CurPacket.LevelIndex != GetCurrentLevelIndex())
|
|
{
|
|
World->GetGameInstance()->OnSeamlessTravelDuringReplay();
|
|
SetCurrentLevelIndex(CurPacket.LevelIndex);
|
|
ProcessSeamlessTravel(GetCurrentLevelIndex());
|
|
return false;
|
|
}
|
|
|
|
++PlaybackPacketIndex;
|
|
return ProcessPacket(CurPacket);
|
|
}
|
|
|
|
void UDemoNetDriver::ProcessAllPlaybackPackets()
|
|
{
|
|
ProcessPlaybackPackets(PlaybackPackets);
|
|
PlaybackPackets.Empty();
|
|
// this call is used for checkpoint loading, so not dealing with per frame data
|
|
ReplayHelper.PlaybackFrames.Empty();
|
|
}
|
|
|
|
void UDemoNetDriver::ProcessPlaybackPackets(TArrayView<FPlaybackPacket> Packets)
|
|
{
|
|
if (Packets.Num() > 0)
|
|
{
|
|
for (const FPlaybackPacket& PlaybackPacket : Packets)
|
|
{
|
|
ProcessPacket(PlaybackPacket);
|
|
}
|
|
|
|
LastProcessedPacketTime = Packets.Last().TimeSeconds;
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::ProcessPacket(const uint8* Data, int32 Count)
|
|
{
|
|
PauseChannels(false);
|
|
|
|
if (ServerConnection != nullptr )
|
|
{
|
|
// Process incoming packet.
|
|
// ReceivedRawPacket shouldn't change any data, so const_cast should be safe.
|
|
ServerConnection->ReceivedRawPacket(const_cast<uint8*>(Data), Count);
|
|
}
|
|
|
|
if (ServerConnection == nullptr || ServerConnection->GetConnectionState() == USOCK_Closed)
|
|
{
|
|
// Something we received resulted in the demo being stopped
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::ProcessPacket: ReceivedRawPacket closed connection"));
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::Generic);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void UDemoNetDriver::WriteDemoFrameFromQueuedDemoPackets(FArchive& Ar, TArray<FQueuedDemoPacket>& QueuedPackets, float FrameTime, EWriteDemoFrameFlags Flags)
|
|
{
|
|
ReplayHelper.WriteDemoFrame(ClientConnections[0], Ar, QueuedPackets, FrameTime, Flags);
|
|
}
|
|
|
|
void UDemoNetDriver::WritePacket(FArchive& Ar, uint8* Data, int32 Count)
|
|
{
|
|
ReplayHelper.WritePacket(Ar, Data, Count);
|
|
}
|
|
|
|
void UDemoNetDriver::SkipTime(const float InTimeToSkip)
|
|
{
|
|
if (IsNamedTaskInQueue(ReplayTaskNames::SkipTimeInSecondsTask))
|
|
{
|
|
return; // Don't allow time skipping if we already are
|
|
}
|
|
|
|
AddReplayTask(new FSkipTimeInSecondsTask(this, InTimeToSkip));
|
|
}
|
|
|
|
void UDemoNetDriver::SkipTimeInternal(const float SecondsToSkip, const bool InFastForward, const bool InIsForCheckpoint)
|
|
{
|
|
check(!bIsFastForwarding); // Can only do one of these at a time (use tasks to gate this)
|
|
check(!bIsFastForwardingForCheckpoint); // Can only do one of these at a time (use tasks to gate this)
|
|
|
|
SavedSecondsToSkip = SecondsToSkip;
|
|
|
|
SetDemoCurrentTime(FMath::Clamp(GetDemoCurrentTime() + SecondsToSkip, 0.0f, GetDemoTotalTime() - 0.01f));
|
|
|
|
bIsFastForwarding = InFastForward;
|
|
bIsFastForwardingForCheckpoint = InIsForCheckpoint;
|
|
}
|
|
|
|
void UDemoNetDriver::GotoTimeInSeconds(const float TimeInSeconds, const FOnGotoTimeDelegate& InOnGotoTimeDelegate)
|
|
{
|
|
OnGotoTimeDelegate_Transient = InOnGotoTimeDelegate;
|
|
|
|
if (IsNamedTaskInQueue(ReplayTaskNames::GotoTimeInSecondsTask) || bIsFastForwarding)
|
|
{
|
|
NotifyGotoTimeFinished(false);
|
|
return; // Don't allow scrubbing if we already are
|
|
}
|
|
|
|
UE_LOG(LogDemo, Log, TEXT("GotoTimeInSeconds: %2.2f"), TimeInSeconds);
|
|
|
|
AddReplayTask(new FGotoTimeInSecondsTask(this, TimeInSeconds));
|
|
}
|
|
|
|
void UDemoNetDriver::JumpToEndOfLiveReplay()
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("UDemoNetDriver::JumpToEndOfLiveReplay."));
|
|
|
|
const uint32 TotalDemoTimeInMS = GetReplayStreamer()->GetTotalDemoTime();
|
|
|
|
SetDemoTotalTime((float)TotalDemoTimeInMS / 1000.0f);
|
|
|
|
const uint32 BufferInMS = 5 * 1000;
|
|
|
|
const uint32 JoinTimeInMS = FMath::Max((uint32)0, GetReplayStreamer()->GetTotalDemoTime() - BufferInMS);
|
|
|
|
if (JoinTimeInMS > 0)
|
|
{
|
|
GotoTimeInSeconds((float)JoinTimeInMS / 1000.0f);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::AddUserToReplay(const FString& UserString)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
ReplayHelper.ReplayStreamer->AddUserToReplay(UserString);
|
|
}
|
|
}
|
|
|
|
#if DEMO_CSV_PROFILING_HELPERS_ENABLED
|
|
struct FCsvDemoSettings
|
|
{
|
|
FCsvDemoSettings()
|
|
: bCaptureCsv(false)
|
|
, StartTime(-1)
|
|
, EndTime(-1)
|
|
, FrameCount(0)
|
|
, bStopAfterProfile(false)
|
|
, bStopCsvAtReplayEnd(false)
|
|
{}
|
|
bool bCaptureCsv;
|
|
int32 StartTime;
|
|
int32 EndTime;
|
|
int32 FrameCount;
|
|
bool bStopAfterProfile;
|
|
bool bStopCsvAtReplayEnd;
|
|
};
|
|
|
|
static FCsvDemoSettings GetCsvDemoSettings()
|
|
{
|
|
FCsvDemoSettings Settings = {};
|
|
Settings.bCaptureCsv = FParse::Value(FCommandLine::Get(), TEXT("-csvdemostarttime="), Settings.StartTime);
|
|
if (Settings.bCaptureCsv)
|
|
{
|
|
if (!FParse::Value(FCommandLine::Get(), TEXT("-csvdemoendtime="), Settings.EndTime))
|
|
{
|
|
Settings.EndTime = -1.0;
|
|
}
|
|
if (!FParse::Value(FCommandLine::Get(), TEXT("-csvdemoframecount="), Settings.FrameCount))
|
|
{
|
|
Settings.FrameCount = -1;
|
|
}
|
|
}
|
|
Settings.bStopAfterProfile = FParse::Param(FCommandLine::Get(), TEXT("csvDemoStopAfterProfile"));
|
|
Settings.bStopCsvAtReplayEnd = FParse::Param(FCommandLine::Get(), TEXT("csvDemoStopCsvAtReplayEnd"));
|
|
return Settings;
|
|
}
|
|
#endif // DEMO_CSV_PROFILING_HELPERS_ENABLED
|
|
|
|
class FDemoNetDriverReplayPlaylistHelper
|
|
{
|
|
private:
|
|
|
|
friend class UDemoNetDriver;
|
|
|
|
static void RestartPlaylist(FReplayPlaylistTracker& ToRestart)
|
|
{
|
|
ToRestart.Restart();
|
|
}
|
|
};
|
|
|
|
void UDemoNetDriver::TickDemoPlayback(float DeltaSeconds)
|
|
{
|
|
LLM_SCOPE(ELLMTag::Replays);
|
|
SCOPED_NAMED_EVENT(UDemoNetDriver_TickDemoPlayback, FColor::Purple);
|
|
if (World && World->IsInSeamlessTravel())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if DEMO_CSV_PROFILING_HELPERS_ENABLED
|
|
static FCsvDemoSettings CsvDemoSettings = GetCsvDemoSettings();
|
|
{
|
|
if (CsvDemoSettings.bCaptureCsv)
|
|
{
|
|
bool bDoCapture = IsPlaying()
|
|
&& GetDemoCurrentTime() >= CsvDemoSettings.StartTime
|
|
&& ((GetDemoCurrentTime() <= CsvDemoSettings.EndTime) || (CsvDemoSettings.EndTime < 0));
|
|
|
|
static bool bStartedCsvRecording = false;
|
|
if (!bStartedCsvRecording && bDoCapture)
|
|
{
|
|
FCsvProfiler::Get()->BeginCapture(CsvDemoSettings.FrameCount);
|
|
bStartedCsvRecording = true;
|
|
}
|
|
else if (bStartedCsvRecording && !bDoCapture)
|
|
{
|
|
FCsvProfiler::Get()->EndCapture();
|
|
bStartedCsvRecording = false;
|
|
}
|
|
}
|
|
}
|
|
#endif // DEMO_CSV_PROFILING_HELPERS_ENABLED
|
|
|
|
if (!IsPlaying())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This will be true when watching a live replay and we're grabbing an up to date header.
|
|
// In that case, we want to pause playback until we can actually travel.
|
|
if (bIsWaitingForHeaderDownload)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (CVarForceDisableAsyncPackageMapLoading.GetValueOnGameThread() > 0)
|
|
{
|
|
GuidCache->SetAsyncLoadMode(FNetGUIDCache::EAsyncLoadMode::ForceDisable);
|
|
}
|
|
else
|
|
{
|
|
GuidCache->SetAsyncLoadMode(FNetGUIDCache::EAsyncLoadMode::UseCVar);
|
|
}
|
|
|
|
if (CVarGotoTimeInSeconds.GetValueOnGameThread() >= 0.0f)
|
|
{
|
|
GotoTimeInSeconds(CVarGotoTimeInSeconds.GetValueOnGameThread());
|
|
CVarGotoTimeInSeconds.AsVariable()->Set(TEXT( "-1" ), ECVF_SetByConsole);
|
|
}
|
|
|
|
if (FMath::Abs(CVarDemoSkipTime.GetValueOnGameThread()) > 0.0f)
|
|
{
|
|
// Just overwrite existing value, cvar wins in this case
|
|
GotoTimeInSeconds(GetDemoCurrentTime() + CVarDemoSkipTime.GetValueOnGameThread());
|
|
CVarDemoSkipTime.AsVariable()->Set(TEXT("0"), ECVF_SetByConsole);
|
|
}
|
|
|
|
// Before we update tasks or move the demo time forward, see if there are any new sublevels that
|
|
// need to be fast forwarded.
|
|
PrepFastForwardLevels();
|
|
|
|
// Update total demo time
|
|
if (ReplayHelper.ReplayStreamer->GetTotalDemoTime() > 0)
|
|
{
|
|
SetDemoTotalTime((float)ReplayHelper.ReplayStreamer->GetTotalDemoTime() / 1000.0f);
|
|
}
|
|
|
|
if (!ProcessReplayTasks())
|
|
{
|
|
// We're busy processing tasks, return
|
|
return;
|
|
}
|
|
|
|
// If we don't have data on frame 0 wait until we have it
|
|
if (!GetReplayStreamer()->IsDataAvailable() && ReplayHelper.DemoFrameNum == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If the ExitAfterReplay option is set, automatically shut down at the end of the replay.
|
|
// Use AtEnd() of the archive instead of checking DemoCurrentTime/DemoTotalTime, because the DemoCurrentTime may never catch up to DemoTotalTime.
|
|
if (FArchive* const StreamingArchive = ReplayHelper.ReplayStreamer->GetStreamingArchive())
|
|
{
|
|
bool bIsAtEnd = StreamingArchive->AtEnd() && (PlaybackPackets.Num() == 0 || (GetDemoCurrentTime() + DeltaSeconds >= GetDemoTotalTime()));
|
|
#if DEMO_CSV_PROFILING_HELPERS_ENABLED
|
|
bool bCsvIsCapturing = FCsvProfiler::Get()->IsCapturing();
|
|
static bool bCsvProfilingEnabledPreviousTick = bCsvIsCapturing;
|
|
if (CsvDemoSettings.bStopAfterProfile && !bCsvIsCapturing && bCsvProfilingEnabledPreviousTick)
|
|
{
|
|
bIsAtEnd = true;
|
|
}
|
|
if (bIsAtEnd && bCsvIsCapturing && CsvDemoSettings.bStopCsvAtReplayEnd)
|
|
{
|
|
FCsvProfiler::Get()->EndCapture();
|
|
}
|
|
bCsvProfilingEnabledPreviousTick = bCsvIsCapturing;
|
|
#endif
|
|
if (!ReplayHelper.ReplayStreamer->IsLive() && bIsAtEnd)
|
|
{
|
|
FNetworkReplayDelegates::OnReplayPlaybackComplete.Broadcast(World);
|
|
|
|
FReplayPlaylistTracker* LocalPlaylistTracker = PlaylistTracker.Get();
|
|
|
|
CSV_METADATA(TEXT("ReplayID"), nullptr);
|
|
|
|
// checking against 1 so the count will mean total number of playthroughs, not additional loops
|
|
if (GDemoLoopCount > 1)
|
|
{
|
|
if (LocalPlaylistTracker)
|
|
{
|
|
if (LocalPlaylistTracker->IsOnLastReplay())
|
|
{
|
|
--GDemoLoopCount;
|
|
FDemoNetDriverReplayPlaylistHelper::RestartPlaylist(*LocalPlaylistTracker);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
--GDemoLoopCount;
|
|
GotoTimeInSeconds(0.0f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FParse::Param(FCommandLine::Get(), TEXT("ExitAfterReplay")) && (!LocalPlaylistTracker || LocalPlaylistTracker->IsOnLastReplay()))
|
|
{
|
|
FPlatformMisc::RequestExit(false);
|
|
}
|
|
else
|
|
{
|
|
if (CVarLoopDemo.GetValueOnGameThread() > 0)
|
|
{
|
|
if (!LocalPlaylistTracker)
|
|
{
|
|
GotoTimeInSeconds(0.0f);
|
|
}
|
|
else if (LocalPlaylistTracker->IsOnLastReplay())
|
|
{
|
|
FDemoNetDriverReplayPlaylistHelper::RestartPlaylist(*LocalPlaylistTracker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Advance demo time by seconds passed if we're not paused
|
|
if (World && World->GetWorldSettings() && World->GetWorldSettings()->GetPauserPlayerState() == nullptr)
|
|
{
|
|
SetDemoCurrentTime(GetDemoCurrentTime() + DeltaSeconds);
|
|
}
|
|
|
|
// Clamp time
|
|
SetDemoCurrentTime(FMath::Clamp(GetDemoCurrentTime(), 0.0f, GetDemoTotalTime() + 0.01f));
|
|
|
|
ReplayHelper.ReplayStreamer->UpdatePlaybackTime(GetDemoCurrentTimeInMS());
|
|
|
|
bool bProcessAvailableData = (PlaybackPackets.Num() > 0) || GetReplayStreamer()->IsDataAvailable();
|
|
|
|
if (CVarFastForwardLevelsPausePlayback.GetValueOnAnyThread() == 0)
|
|
{
|
|
const uint32 DemoCurrentTimeInMS = GetDemoCurrentTimeInMS();
|
|
bProcessAvailableData = bProcessAvailableData || GetReplayStreamer()->IsDataAvailableForTimeRange(DemoCurrentTimeInMS, DemoCurrentTimeInMS);
|
|
}
|
|
|
|
// Make sure there is data available to read
|
|
// If we're at the end of the demo, just pause channels and return
|
|
if (bProcessAvailableData)
|
|
{
|
|
// we either have packets to process or data available to read
|
|
PauseChannels(false);
|
|
}
|
|
else
|
|
{
|
|
PauseChannels(true);
|
|
return;
|
|
}
|
|
|
|
// Speculatively grab seconds now in case we need it to get the time it took to fast forward
|
|
const double FastForwardStartSeconds = FPlatformTime::Seconds();
|
|
|
|
if (FArchive* const StreamingArchive = GetReplayStreamer()->GetStreamingArchive())
|
|
{
|
|
ReplayHelper.SetPlaybackNetworkVersions(*StreamingArchive);
|
|
}
|
|
|
|
// Buffer up demo frames until we have enough time built-up
|
|
while (ConditionallyReadDemoFrameIntoPlaybackPackets(*GetReplayStreamer()->GetStreamingArchive()))
|
|
{
|
|
}
|
|
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("TickDemoPlayback_ProcessPackets"), TickDemoPlayback_ProcessPackets, STATGROUP_Net);
|
|
|
|
// Process packets until we are caught up (this implicitly handles fast forward if DemoCurrentTime past many frames)
|
|
while (ConditionallyProcessPlaybackPackets())
|
|
{
|
|
ReplayHelper.DemoFrameNum++;
|
|
}
|
|
|
|
if (PlaybackPacketIndex > 0)
|
|
{
|
|
// Remove all packets that were processed
|
|
// At this point, PlaybackPacketIndex will actually be the number of packets we've processed,
|
|
// as it points to the "next" index we would otherwise have processed.
|
|
LastProcessedPacketTime = PlaybackPackets[PlaybackPacketIndex - 1].TimeSeconds;
|
|
|
|
PlaybackPackets.RemoveAt(0, PlaybackPacketIndex);
|
|
PlaybackPacketIndex = 0;
|
|
}
|
|
|
|
// Process playback frames
|
|
for (auto FrameIt = ReplayHelper.PlaybackFrames.CreateIterator(); FrameIt; ++FrameIt)
|
|
{
|
|
if (FrameIt.Key() <= GetDemoCurrentTime())
|
|
{
|
|
if (!bIsFastForwarding)
|
|
{
|
|
FNetworkReplayDelegates::OnProcessGameSpecificFrameData.Broadcast(World, FrameIt.Key(), FrameIt.Value());
|
|
}
|
|
|
|
FrameIt.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finalize any fast forward stuff that needs to happen
|
|
if (bIsFastForwarding)
|
|
{
|
|
FinalizeFastForward(FastForwardStartSeconds);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::FinalizeFastForward(const double StartTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Demo_FinalizeFastForward"), Demo_FinalizeFastForward, STATGROUP_Net);
|
|
|
|
TGuardValue<bool> FinalizingFastForward(bIsFinalizingFastForward, true);
|
|
|
|
// This must be set before we CallRepNotifies or they might be skipped again
|
|
bIsFastForwarding = false;
|
|
|
|
AGameStateBase* const GameState = World != nullptr ? World->GetGameState() : nullptr;
|
|
|
|
// Make sure that we delete any Rewind actors that aren't valid anymore.
|
|
if (bIsFastForwardingForCheckpoint)
|
|
{
|
|
CleanupOutstandingRewindActors();
|
|
}
|
|
|
|
// Correct server world time for fast-forwarding after a checkpoint
|
|
if (GameState != nullptr)
|
|
{
|
|
if (bIsFastForwardingForCheckpoint)
|
|
{
|
|
const float PostCheckpointServerTime = SavedReplicatedWorldTimeSeconds + SavedSecondsToSkip;
|
|
GameState->ReplicatedWorldTimeSeconds = PostCheckpointServerTime;
|
|
}
|
|
|
|
// Correct the ServerWorldTimeSecondsDelta
|
|
GameState->OnRep_ReplicatedWorldTimeSeconds();
|
|
}
|
|
|
|
if (ServerConnection != nullptr && bIsFastForwardingForCheckpoint)
|
|
{
|
|
// Make a pass at OnReps for startup actors, since they were skipped during checkpoint loading.
|
|
// At this point the shadow state of these actors should be the actual state from before the checkpoint,
|
|
// and the current state is the CDO state evolved by any changes that occurred during checkpoint loading and fast-forwarding.
|
|
for (UChannel* Channel : ServerConnection->OpenChannels)
|
|
{
|
|
UActorChannel* const ActorChannel = Cast<UActorChannel>(Channel);
|
|
if (ActorChannel == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const AActor* const Actor = ActorChannel->GetActor();
|
|
if (Actor == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Actor->IsNetStartupActor())
|
|
{
|
|
DiffActorProperties(ActorChannel);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush all pending RepNotifies that were built up during the fast-forward.
|
|
if (ServerConnection != nullptr)
|
|
{
|
|
for (auto& ChannelPair : ServerConnection->ActorChannelMap())
|
|
{
|
|
if (ChannelPair.Value != nullptr)
|
|
{
|
|
for (auto& ReplicatorPair : ChannelPair.Value->ReplicationMap)
|
|
{
|
|
ReplicatorPair.Value->CallRepNotifies(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& DormantPair : ServerConnection->DormantReplicatorMap)
|
|
{
|
|
DormantPair.Value->CallRepNotifies(true);
|
|
}
|
|
}
|
|
|
|
// We may have been fast-forwarding immediately after loading a checkpoint
|
|
// for fine-grained scrubbing. If so, at this point we are no longer loading a checkpoint.
|
|
bIsFastForwardingForCheckpoint = false;
|
|
|
|
// Reset the never-queue GUID list, we'll rebuild it
|
|
NonQueuedGUIDsForScrubbing.Reset();
|
|
|
|
const double FastForwardTotalSeconds = FPlatformTime::Seconds() - StartTime;
|
|
|
|
NotifyGotoTimeFinished(true);
|
|
|
|
UE_LOG(LogDemo, Log, TEXT("Fast forward took %.2f seconds."), FastForwardTotalSeconds);
|
|
}
|
|
|
|
void UDemoNetDriver::SpawnDemoRecSpectator(UNetConnection* Connection, const FURL& ListenURL)
|
|
{
|
|
SpectatorController = ReplayHelper.CreateSpectatorController(Connection);
|
|
if (SpectatorController)
|
|
{
|
|
SpectatorControllers.Add(SpectatorController);
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::SpawnSplitscreenViewer(ULocalPlayer* NewPlayer, UWorld* InWorld)
|
|
{
|
|
if (NewPlayer == nullptr || InWorld == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetDriver::SpawnSplitscreenViewer: Local Player or World is invalid!"));
|
|
return false;
|
|
}
|
|
|
|
if (ClientConnections.Num() == 0 && ServerConnection == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoNetDriver::SpawnSplitscreenViewer: This netdriver has no demo connection data"));
|
|
return false;
|
|
}
|
|
|
|
UNetConnection* ChildConnection = CreateChild((ClientConnections.Num() > 0) ? ClientConnections[0] : ServerConnection);
|
|
|
|
APlayerController* NewSplitscreenController = ReplayHelper.CreateSpectatorController(ChildConnection);
|
|
if (NewSplitscreenController == nullptr)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetDriver::SpawnSplitscreenViewer: Unable to create new splitscreen controller"));
|
|
return false;
|
|
}
|
|
|
|
// Link this spectator to the given local player, as this will facilitate spectator pawn creation
|
|
// (spectator pawns only create if the controller is linked to a local player)
|
|
NewSplitscreenController->Player = NewPlayer;
|
|
NewSplitscreenController->NetPlayerIndex = GEngine->GetGamePlayers(InWorld).Find(NewPlayer);
|
|
|
|
// Create the Pawn
|
|
NewSplitscreenController->ChangeState(NAME_Spectating);
|
|
NewPlayer->CurrentNetSpeed = 0;
|
|
|
|
// Link the local player to the player controller as the local player has been marked as active
|
|
// but without a PlayerController, the player will never be considered "ready" by other systems.
|
|
NewPlayer->PlayerController = NewSplitscreenController;
|
|
|
|
// This would typically be set via SetPlayer, but we need to call SetPlayer with the LocalPlayer
|
|
// and not with the child connection, otherwise we never create the input controls we need.
|
|
ChildConnection->PlayerController = NewSplitscreenController;
|
|
ChildConnection->OwningActor = NewSplitscreenController;
|
|
|
|
// Create input control
|
|
NewSplitscreenController->SetPlayer(NewPlayer);
|
|
|
|
// Add to the list
|
|
SpectatorControllers.Add(NewSplitscreenController);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::RemoveSplitscreenViewer(APlayerController* RemovePlayer, bool bMarkOwnerForDeletion)
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("Attempting to remove splitscreen viewer!"));
|
|
|
|
if (RemovePlayer && SpectatorControllers.Contains(RemovePlayer) && RemovePlayer != SpectatorController)
|
|
{
|
|
SpectatorControllers.Remove(RemovePlayer);
|
|
UNetConnection* RemovedNetConnection = RemovePlayer->NetConnection;
|
|
if (!bMarkOwnerForDeletion)
|
|
{
|
|
RemovedNetConnection->OwningActor = nullptr;
|
|
}
|
|
RemovedNetConnection->Close();
|
|
RemovedNetConnection->CleanUp();
|
|
RemovePlayer->NetConnection = nullptr;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int32 UDemoNetDriver::CleanUpSplitscreenConnections(bool bDeleteOwner)
|
|
{
|
|
int32 NumSplitscreenConnectionsCleaned = 0;
|
|
|
|
for (APlayerController* CurController : SpectatorControllers)
|
|
{
|
|
UNetConnection* ControllerNetConnection = (CurController != nullptr) ? ToRawPtr(CurController->NetConnection) : nullptr;
|
|
if (ControllerNetConnection != nullptr && ControllerNetConnection->IsA(UChildConnection::StaticClass()))
|
|
{
|
|
++NumSplitscreenConnectionsCleaned;
|
|
// With this toggled, this prevents actor deletion (which we don't want to do when scrubbing)
|
|
if (!bDeleteOwner)
|
|
{
|
|
ControllerNetConnection->OwningActor = nullptr;
|
|
}
|
|
ControllerNetConnection->Close();
|
|
ControllerNetConnection->CleanUp();
|
|
CurController->NetConnection = nullptr;
|
|
}
|
|
}
|
|
|
|
FString OwnerDeletionStr(bDeleteOwner ? TEXT("with") : TEXT("without"));
|
|
UE_LOG(LogDemo, Log, TEXT("Cleaned up %d splitscreen connections %s owner deletion"), NumSplitscreenConnectionsCleaned, *OwnerDeletionStr);
|
|
return NumSplitscreenConnectionsCleaned;
|
|
}
|
|
|
|
void UDemoNetDriver::PauseRecording(const bool bInPauseRecording)
|
|
{
|
|
ReplayHelper.bPauseRecording = bInPauseRecording;
|
|
}
|
|
|
|
bool UDemoNetDriver::IsRecordingPaused() const
|
|
{
|
|
return ReplayHelper.bPauseRecording;
|
|
}
|
|
|
|
void UDemoNetDriver::ReplayStreamingReady(const FStartStreamingResult& Result)
|
|
{
|
|
bIsWaitingForStream = false;
|
|
bWasStartStreamingSuccessful = Result.WasSuccessful();
|
|
|
|
if (!bWasStartStreamingSuccessful)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetDriver::ReplayStreamingReady: Failed. %s"), Result.bRecording ? TEXT("") : EDemoPlayFailure::ToString(EDemoPlayFailure::DemoNotFound));
|
|
|
|
if (Result.bRecording)
|
|
{
|
|
StopDemo();
|
|
}
|
|
else
|
|
{
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::DemoNotFound);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!Result.bRecording)
|
|
{
|
|
FString Error;
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
if (!InitConnectInternal(Error))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// InitConnectInternal calls ResetDemoState which will reset this, so restore the value
|
|
bWasStartStreamingSuccessful = Result.WasSuccessful();
|
|
|
|
const TCHAR* const SkipToLevelIndexOption = ReplayHelper.DemoURL.GetOption(TEXT("SkipToLevelIndex="), nullptr);
|
|
if (SkipToLevelIndexOption)
|
|
{
|
|
int32 Index = FCString::Atoi(SkipToLevelIndexOption);
|
|
if (ReplayHelper.LevelNamesAndTimes.IsValidIndex(Index))
|
|
{
|
|
AddReplayTask(new FGotoTimeInSecondsTask(this, (float)ReplayHelper.LevelNamesAndTimes[Index].LevelChangeTimeInMS / 1000.0f));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("ReplayStreamingReady: SkipToLevelIndex was invalid: %d"), Index);
|
|
}
|
|
}
|
|
|
|
if (CVarDemoJumpToEndOfLiveReplay.GetValueOnGameThread() != 0)
|
|
{
|
|
if (ReplayHelper.ReplayStreamer->IsLive() && ReplayHelper.ReplayStreamer->GetTotalDemoTime() > 15 * 1000)
|
|
{
|
|
// If the load time wasn't very long, jump to end now
|
|
// Otherwise, defer it until we have a more recent replay time
|
|
if (FPlatformTime::Seconds() - StartTime < 10)
|
|
{
|
|
JumpToEndOfLiveReplay();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("UDemoNetDriver::ReplayStreamingReady: Deferring checkpoint until next available time."));
|
|
AddReplayTask(new FJumpToLiveReplayTask(this));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (UE_LOG_ACTIVE(LogDemo, Log))
|
|
{
|
|
FString HeaderFlags;
|
|
|
|
for (uint32 i = 0; i < sizeof(EReplayHeaderFlags) * 8; ++i)
|
|
{
|
|
EReplayHeaderFlags Flag = (EReplayHeaderFlags)(1 << i);
|
|
|
|
if (EnumHasAnyFlags(ReplayHelper.PlaybackDemoHeader.HeaderFlags, Flag))
|
|
{
|
|
HeaderFlags += (HeaderFlags.IsEmpty() ? TEXT("") : TEXT("|"));
|
|
HeaderFlags += LexToString(Flag);
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogDemo, Log, TEXT("ReplayStreamingReady: playing back replay [%s] %s, which was recorded on engine version %s with flags [%s]"),
|
|
*ReplayHelper.GetPlaybackGuid().ToString(EGuidFormats::Digits), *ReplayHelper.DemoURL.Map, *ReplayHelper.PlaybackDemoHeader.EngineVersion.ToString(), *HeaderFlags);
|
|
|
|
if (ReplayHelper.PlaybackDemoHeader.Version >= ENetworkVersionHistory::HISTORY_RECORDING_METADATA)
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("ReplayStreamingReady: replay was recorded with: MinHz: %0.2f MaxHz: %0.2f FrameMS: %0.2f CheckpointMS: %0.2f Platform: [%s] Config: [%s] Target: [%s]"),
|
|
ReplayHelper.PlaybackDemoHeader.MinRecordHz, ReplayHelper.PlaybackDemoHeader.MaxRecordHz,
|
|
ReplayHelper.PlaybackDemoHeader.FrameLimitInMS, ReplayHelper.PlaybackDemoHeader.CheckpointLimitInMS,
|
|
*ReplayHelper.PlaybackDemoHeader.Platform, LexToString(ReplayHelper.PlaybackDemoHeader.BuildConfig), LexToString(ReplayHelper.PlaybackDemoHeader.BuildTarget));
|
|
}
|
|
|
|
CSV_METADATA(TEXT("ReplayID"), *ReplayHelper.GetPlaybackGuid().ToString(EGuidFormats::Digits));
|
|
}
|
|
|
|
// Notify all listeners that a demo is starting
|
|
FNetworkReplayDelegates::OnReplayStarted.Broadcast(World);
|
|
}
|
|
}
|
|
|
|
FReplayExternalDataArray* UDemoNetDriver::GetExternalDataArrayForObject(UObject* Object)
|
|
{
|
|
FNetworkGUID NetworkGUID = GuidCache->NetGUIDLookup.FindRef(Object);
|
|
|
|
if (!NetworkGUID.IsValid())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return ReplayHelper.ExternalDataToObjectMap.Find(NetworkGUID);
|
|
}
|
|
|
|
bool UDemoNetDriver::SetExternalDataForObject(UObject* OwningObject, const uint8* Src, const int32 NumBits)
|
|
{
|
|
if (IsRecording())
|
|
{
|
|
// IsRecording verifies that ClientConnections[0] exists
|
|
return ReplayHelper.SetExternalDataForObject(ClientConnections[0], OwningObject, Src, NumBits);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UDemoNetDriver::RespawnNecessaryNetStartupActors(TArray<AActor*>& SpawnedActors, ULevel* Level /* = nullptr */)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_RespawnNecessaryNetStartupActors);
|
|
|
|
TGuardValue<bool> RestoringStartupActors(bIsRestoringStartupActors, true);
|
|
|
|
const FName FilterLevelName = Level ? Level->GetOutermost()->GetFName() : NAME_None;
|
|
|
|
for (auto RollbackIt = RollbackNetStartupActors.CreateIterator(); RollbackIt; ++RollbackIt)
|
|
{
|
|
if (ReplayHelper.PlaybackDeletedNetStartupActors.Contains(RollbackIt.Key()))
|
|
{
|
|
// We don't want to re-create these since they should no longer exist after the current checkpoint
|
|
continue;
|
|
}
|
|
|
|
FRollbackNetStartupActorInfo& RollbackActor = RollbackIt.Value();
|
|
|
|
// filter to a specific level
|
|
if ((Level != nullptr) && (RollbackActor.LevelName != FilterLevelName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
const FString LevelPackageName = UWorld::RemovePIEPrefix(RollbackActor.LevelName.ToString());
|
|
|
|
// skip rollback actors in streamed out levels (pending gc)
|
|
if (!ReplayHelper.LevelStatusesByName.Contains(LevelPackageName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FReplayHelper::FLevelStatus& LevelStatus = ReplayHelper.GetLevelStatus(LevelPackageName);
|
|
if (!LevelStatus.bIsReady)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
ULevel* RollbackActorLevel = ReplayHelper.WeakLevelsByName.FindRef(RollbackActor.LevelName).Get();
|
|
|
|
if (!ensureMsgf(RollbackActorLevel, TEXT("RespawnNecessaryNetStartupActors: Rollback actor level is nullptr: %s"), *RollbackActor.Name.ToString()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AActor* ExistingActor = FindObjectFast<AActor>(RollbackActorLevel, RollbackActor.Name);
|
|
if (ExistingActor)
|
|
{
|
|
ensureMsgf((!IsValidChecked(ExistingActor) || ExistingActor->IsUnreachable()), TEXT("RespawnNecessaryNetStartupActors: Renaming rollback actor that wasn't destroyed: %s"), *GetFullNameSafe(ExistingActor));
|
|
ExistingActor->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors | REN_ForceNoResetLoaders);
|
|
}
|
|
|
|
FActorSpawnParameters SpawnInfo;
|
|
|
|
SpawnInfo.Template = CastChecked<AActor>(RollbackActor.Archetype);
|
|
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
SpawnInfo.bNoFail = true;
|
|
SpawnInfo.Name = RollbackActor.Name;
|
|
SpawnInfo.OverrideLevel = RollbackActorLevel;
|
|
SpawnInfo.bDeferConstruction = true;
|
|
|
|
const FTransform SpawnTransform = FTransform(RollbackActor.Rotation, RollbackActor.Location, RollbackActor.Scale3D);
|
|
|
|
AActor* Actor = World->SpawnActorAbsolute(RollbackActor.Archetype->GetClass(), SpawnTransform, SpawnInfo);
|
|
if (Actor)
|
|
{
|
|
if (!ensure( Actor->GetFullName() == RollbackIt.Key()))
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("RespawnNecessaryNetStartupActors: NetStartupRollbackActor name doesn't match original: %s, %s"), *Actor->GetFullName(), *RollbackIt.Key());
|
|
}
|
|
|
|
bool bSanityCheckReferences = true;
|
|
|
|
for (UObject* ObjRef : RollbackActor.ObjReferences)
|
|
{
|
|
if (ObjRef == nullptr)
|
|
{
|
|
bSanityCheckReferences = false;
|
|
UE_LOG(LogDemo, Warning, TEXT("RespawnNecessaryNetStartupActors: Rollback actor reference was gc'd, skipping state restore: %s"), *GetFullNameSafe(Actor));
|
|
break;
|
|
}
|
|
}
|
|
|
|
TSharedPtr<FRepLayout> RepLayout = GetObjectClassRepLayout(Actor->GetClass());
|
|
FReceivingRepState* ReceivingRepState = RollbackActor.RepState.IsValid() ? RollbackActor.RepState->GetReceivingRepState() : nullptr;
|
|
|
|
if (RepLayout.IsValid() && ReceivingRepState && bSanityCheckReferences)
|
|
{
|
|
const ENetRole SavedRole = Actor->GetLocalRole();
|
|
|
|
FRepObjectDataBuffer ActorData(Actor);
|
|
FConstRepShadowDataBuffer ShadowData(ReceivingRepState->StaticBuffer.GetData());
|
|
|
|
RepLayout->DiffStableProperties(&ReceivingRepState->RepNotifies, nullptr, ActorData, ShadowData);
|
|
|
|
Actor->SetRole(SavedRole);
|
|
}
|
|
|
|
check(Actor->GetRemoteRole() != ROLE_Authority);
|
|
|
|
Actor->bNetStartup = true;
|
|
|
|
if (Actor->GetLocalRole() == ROLE_Authority)
|
|
{
|
|
Actor->SwapRoles();
|
|
}
|
|
|
|
UGameplayStatics::FinishSpawningActor(Actor, SpawnTransform);
|
|
|
|
Actor->PostNetInit();
|
|
|
|
if (RepLayout.IsValid() && ReceivingRepState)
|
|
{
|
|
if (ReceivingRepState->RepNotifies.Num() > 0)
|
|
{
|
|
RepLayout->CallRepNotifies(ReceivingRepState, Actor);
|
|
|
|
Actor->PostRepNotifies();
|
|
}
|
|
}
|
|
|
|
for (UActorComponent* ActorComp : Actor->GetComponents())
|
|
{
|
|
if (ActorComp)
|
|
{
|
|
TSharedPtr<FRepLayout> SubObjLayout = GetObjectClassRepLayout(ActorComp->GetClass());
|
|
if (SubObjLayout.IsValid() && bSanityCheckReferences)
|
|
{
|
|
TSharedPtr<FRepState> RepState = RollbackActor.SubObjRepState.FindRef(ActorComp->GetFullName());
|
|
FReceivingRepState* SubObjReceivingRepState = RepState.IsValid() ? RepState->GetReceivingRepState() : nullptr;
|
|
|
|
if (SubObjReceivingRepState)
|
|
{
|
|
FRepObjectDataBuffer ActorCompData(ActorComp);
|
|
FConstRepShadowDataBuffer ShadowData(SubObjReceivingRepState->StaticBuffer.GetData());
|
|
|
|
SubObjLayout->DiffStableProperties(&SubObjReceivingRepState->RepNotifies, nullptr, ActorCompData, ShadowData);
|
|
|
|
if (SubObjReceivingRepState->RepNotifies.Num() > 0)
|
|
{
|
|
SubObjLayout->CallRepNotifies(SubObjReceivingRepState, ActorComp);
|
|
|
|
ActorComp->PostRepNotifies();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check(Actor->GetRemoteRole() == ROLE_Authority);
|
|
|
|
SpawnedActors.Add(Actor);
|
|
}
|
|
|
|
RollbackIt.RemoveCurrent();
|
|
}
|
|
|
|
RollbackNetStartupActors.Compact();
|
|
}
|
|
|
|
void UDemoNetDriver::PrepFastForwardLevels()
|
|
{
|
|
if (!HasLevelStreamingFixes() || ReplayHelper.NewStreamingLevelsThisFrame.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(!bIsFastForwarding);
|
|
check(!ReplayHelper.bIsLoadingCheckpoint);
|
|
|
|
// Do a quick pass to double check everything is still valid, and that we have data for the levels.
|
|
for (TWeakObjectPtr<UObject>& WeakLevel : ReplayHelper.NewStreamingLevelsThisFrame)
|
|
{
|
|
// For playback, we should only ever see ULevels in this list.
|
|
if (ULevel* Level = CastChecked<ULevel>(WeakLevel.Get()))
|
|
{
|
|
if (!ensure(!ReplayHelper.LevelsPendingFastForward.Contains(Level)))
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FastForwardLevels - NewStreamingLevel found in Pending list! %s"), *GetFullName(Level));
|
|
continue;
|
|
}
|
|
|
|
ReplayHelper.LevelsPendingFastForward.Add(Level);
|
|
}
|
|
}
|
|
|
|
ReplayHelper.NewStreamingLevelsThisFrame.Empty();
|
|
|
|
if (ReplayHelper.LevelsPendingFastForward.Num() == 0 ||
|
|
LastProcessedPacketTime == 0.f ||
|
|
// If there's already a FastForwardLevelsTask or GotoTimeTask, then we don't need
|
|
// to add another (as the levels will get picked up by either of those).
|
|
IsNamedTaskInQueue(ReplayTaskNames::GotoTimeInSecondsTask) ||
|
|
IsNamedTaskInQueue(ReplayTaskNames::FastForwardLevelsTask))
|
|
{
|
|
return;
|
|
}
|
|
|
|
AddReplayTask(new FFastForwardLevelsTask(this));
|
|
}
|
|
|
|
bool UDemoNetDriver::ProcessFastForwardPackets(TArrayView<FPlaybackPacket> Packets, const TSet<int32>& LevelIndices)
|
|
{
|
|
// Process all the packets we need.
|
|
for (FPlaybackPacket& Packet : Packets)
|
|
{
|
|
// Skip packets that aren't associated with levels.
|
|
if (Packet.SeenLevelIndex == 0)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("ProcessFastForwardPackets: Skipping packet with no seen level index"));
|
|
continue;
|
|
}
|
|
|
|
// Don't attempt to go beyond the current demo time.
|
|
// These packets should have been already been filtered out while reading.
|
|
if (!ensureMsgf(Packet.TimeSeconds <= GetDemoCurrentTime(), TEXT("UDemoNetDriver::FastForwardLevels: Read packet beyond DemoCurrentTime DemoTime = %f PacketTime = %f"), GetDemoCurrentTime(), Packet.TimeSeconds))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (ReplayHelper.SeenLevelStatuses.IsValidIndex(Packet.SeenLevelIndex - 1))
|
|
{
|
|
const FReplayHelper::FLevelStatus& LevelStatus = ReplayHelper.GetLevelStatus(Packet.SeenLevelIndex);
|
|
const bool bCareAboutLevel = LevelIndices.Contains(LevelStatus.LevelIndex);
|
|
|
|
if (bCareAboutLevel)
|
|
{
|
|
// If we tried to process the packet, but failed, then the replay will be in a broken state.
|
|
// ProcessPacket will have called StopDemo.
|
|
if (!ProcessPacket(Packet.Data.GetData(), Packet.Data.Num()))
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FastForwardLevel failed to process packet"));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("FastForwardLevel could not process packet with invalid seen level index"));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::FastForwardLevels(const FGotoResult& GotoResult)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FastForwardLevels time"), STAT_FastForwardLevelTime, STATGROUP_Net);
|
|
|
|
FArchive* CheckpointArchive = GetReplayStreamer()->GetCheckpointArchive();
|
|
|
|
PauseChannels(false);
|
|
|
|
// We can skip processing the checkpoint here, because Goto will load one up for us later.
|
|
// We only want to check the very next task, though. Otherwise, we could end processing other
|
|
// tasks in an invalid state.
|
|
if (GetNextQueuedTaskName() == ReplayTaskNames::GotoTimeInSecondsTask)
|
|
{
|
|
// This is a bit hacky, but we don't want to do *any* processing this frame.
|
|
// Therefore, we'll reset the ActiveReplayTask and return false.
|
|
// This will cause us to early out, and then handle the Goto task next frame.
|
|
ActiveReplayTask.Reset();
|
|
return false;
|
|
}
|
|
|
|
// Generate the list of level names, and an uber list of the startup actors.
|
|
// We manually track whenever a level is added and removed from the world, so these should always be valid.
|
|
TSet<int32> LevelIndices;
|
|
TSet<TWeakObjectPtr<AActor>> StartupActors;
|
|
TSet<ULevel*> LocalLevels;
|
|
|
|
// Reserve some default space, and just assume a minimum of at least 4 actors per level (super low estimate).
|
|
LevelIndices.Reserve(ReplayHelper.LevelsPendingFastForward.Num());
|
|
StartupActors.Reserve(ReplayHelper.LevelsPendingFastForward.Num() * 4);
|
|
|
|
struct FLocalReadPacketsHelper
|
|
{
|
|
FLocalReadPacketsHelper(UDemoNetDriver& InDriver, const float InLastPacketTime):
|
|
Driver(InDriver),
|
|
LastPacketTime(InLastPacketTime)
|
|
{
|
|
}
|
|
|
|
// @return True if another read can be attempted, false otherwise.
|
|
bool ReadPackets(FArchive& Ar)
|
|
{
|
|
// Grab the packets, and make sure the stream is OK.
|
|
PreFramePos = Ar.Tell();
|
|
NumPackets = Packets.Num();
|
|
if (!Driver.ReadDemoFrameIntoPlaybackPackets(Ar, Packets, true, &LastReadTime))
|
|
{
|
|
bErrorOccurred = true;
|
|
return false;
|
|
}
|
|
|
|
// In case the archive had more data than we needed, we'll try to leave it where we left off
|
|
// before the level fast forward.
|
|
else if (LastReadTime > LastPacketTime)
|
|
{
|
|
Ar.Seek(PreFramePos);
|
|
if (ensure(NumPackets != 0))
|
|
{
|
|
Packets.RemoveAt(NumPackets, Packets.Num() - NumPackets);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsError() const
|
|
{
|
|
return bErrorOccurred;
|
|
}
|
|
|
|
TArray<FPlaybackPacket> Packets;
|
|
|
|
private:
|
|
|
|
UDemoNetDriver& Driver;
|
|
const float LastPacketTime;
|
|
|
|
// We only want to process packets that are before anything we've currently processed.
|
|
// Further, we want to make sure that we leave the archive in a good state for later use.
|
|
int32 NumPackets = 0;
|
|
float LastReadTime = 0;
|
|
FArchivePos PreFramePos = 0;
|
|
|
|
bool bErrorOccurred = false;
|
|
|
|
} ReadPacketsHelper(*this, LastProcessedPacketTime);
|
|
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Empty();
|
|
|
|
PlaybackDeltaCheckpointData.Empty();
|
|
|
|
TArray<TInterval<int32>> DeltaCheckpointPacketIntervals;
|
|
const bool bDeltaCheckpoint = HasDeltaCheckpoints();
|
|
|
|
{
|
|
auto IgnoreReceivedExportGUIDs = Cast<UPackageMapClient>(ServerConnection->PackageMap)->ScopedIgnoreReceivedExportGUIDs();
|
|
|
|
// First, read in the checkpoint data (if any is available);
|
|
if (CheckpointArchive->TotalSize() != 0)
|
|
{
|
|
ReplayHelper.SetPlaybackNetworkVersions(*CheckpointArchive);
|
|
|
|
CheckpointArchive->ArMaxSerializeSize = FReplayHelper::MAX_DEMO_STRING_SERIALIZATION_SIZE;
|
|
|
|
TGuardValue<bool> LoadingCheckpointGuard(ReplayHelper.bIsLoadingCheckpoint, true);
|
|
|
|
uint32 PlaybackVersion = GetPlaybackDemoVersion();
|
|
|
|
do
|
|
{
|
|
FArchivePos MaxArchivePos = 0;
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
uint32 CheckpointSize = 0;
|
|
*CheckpointArchive << CheckpointSize;
|
|
|
|
MaxArchivePos = CheckpointArchive->Tell() + CheckpointSize;
|
|
}
|
|
|
|
TGuardValue<int64> MaxArchivePosGuard(MaxArchiveReadPos, MaxArchivePos);
|
|
|
|
FArchivePos PacketOffset = 0;
|
|
*CheckpointArchive << PacketOffset;
|
|
|
|
PacketOffset += CheckpointArchive->Tell();
|
|
|
|
int32 LevelIndex = INDEX_NONE;
|
|
*CheckpointArchive << LevelIndex;
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
TUniquePtr<FDeltaCheckpointData>& CheckpointData = PlaybackDeltaCheckpointData.Emplace_GetRef(new FDeltaCheckpointData());
|
|
|
|
ReplayHelper.ReadDeletedStartupActors(ServerConnection, *CheckpointArchive, CheckpointData->DestroyedNetStartupActors);
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Append(CheckpointData->DestroyedNetStartupActors);
|
|
|
|
*CheckpointArchive << CheckpointData->DestroyedDynamicActors;
|
|
*CheckpointArchive << CheckpointData->ChannelsToClose;
|
|
}
|
|
else
|
|
{
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Empty();
|
|
|
|
ReplayHelper.ReadDeletedStartupActors(ServerConnection, *CheckpointArchive, ReplayHelper.PlaybackDeletedNetStartupActors);
|
|
}
|
|
|
|
CheckpointArchive->Seek(PacketOffset);
|
|
|
|
int32 DeltaPacketStartIndex = INDEX_NONE;
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
DeltaPacketStartIndex = ReadPacketsHelper.Packets.Num();
|
|
}
|
|
|
|
if (!ReadPacketsHelper.ReadPackets(*CheckpointArchive) && ReadPacketsHelper.IsError())
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetDriver::FastForwardLevels: Failed to read packets from Checkpoint."));
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::Generic);
|
|
return false;
|
|
}
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
const int32 DeltaPacketEndIndex = ReadPacketsHelper.Packets.Num() - 1;
|
|
if (DeltaPacketEndIndex >= DeltaPacketStartIndex)
|
|
{
|
|
DeltaCheckpointPacketIntervals.Emplace(DeltaPacketStartIndex, DeltaPacketEndIndex);
|
|
}
|
|
}
|
|
}
|
|
while (!CheckpointArchive->IsError() && (CheckpointArchive->Tell() < CheckpointArchive->TotalSize()));
|
|
}
|
|
|
|
// Next, read in streaming data (if any is available)
|
|
FArchive* StreamingAr = GetReplayStreamer()->GetStreamingArchive();
|
|
check(StreamingAr);
|
|
|
|
ReplayHelper.SetPlaybackNetworkVersions(*StreamingAr);
|
|
|
|
int32 StreamPacketStartIndex = INDEX_NONE;
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
StreamPacketStartIndex = ReadPacketsHelper.Packets.Num();
|
|
}
|
|
|
|
while (!StreamingAr->AtEnd() && GetReplayStreamer()->IsDataAvailable() && ReadPacketsHelper.ReadPackets(*StreamingAr));
|
|
|
|
if (ReadPacketsHelper.IsError())
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetDriver::FastForwardLevels: Failed to read packets from Stream."));
|
|
NotifyDemoPlaybackFailure(EDemoPlayFailure::Serialization);
|
|
return false;
|
|
}
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
const int32 StreamPacketEndIndex = ReadPacketsHelper.Packets.Num() - 1;
|
|
if (StreamPacketEndIndex >= StreamPacketStartIndex)
|
|
{
|
|
DeltaCheckpointPacketIntervals.Emplace(StreamPacketStartIndex, StreamPacketEndIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we've gotten this far, it means we should have something to process.
|
|
check(ReadPacketsHelper.Packets.Num() > 0);
|
|
|
|
for (ULevel* Level : ReplayHelper.LevelsPendingFastForward)
|
|
{
|
|
// Track the appropriate level, and mark it as ready.
|
|
FReplayHelper::FLevelStatus& LevelStatus = ReplayHelper.GetLevelStatus(ReplayHelper.GetLevelPackageName(*Level));
|
|
LevelIndices.Add(LevelStatus.LevelIndex);
|
|
LevelStatus.bIsReady = true;
|
|
|
|
TSet<TWeakObjectPtr<AActor>> LevelActors;
|
|
for (AActor* Actor : Level->Actors)
|
|
{
|
|
if (Actor == nullptr || !Actor->IsNetStartupActor())
|
|
{
|
|
continue;
|
|
}
|
|
else if (ReplayHelper.PlaybackDeletedNetStartupActors.Contains(Actor->GetFullName()))
|
|
{
|
|
// Put this actor on the rollback list so we can undelete it during future scrubbing,
|
|
// then delete it.
|
|
QueueNetStartupActorForRollbackViaDeletion(Actor);
|
|
World->DestroyActor(Actor, true);
|
|
}
|
|
else
|
|
{
|
|
if (RollbackNetStartupActors.Contains(Actor->GetFullName()))
|
|
{
|
|
World->DestroyActor(Actor, true);
|
|
}
|
|
else
|
|
{
|
|
StartupActors.Add(Actor);
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<AActor*> SpawnedActors;
|
|
RespawnNecessaryNetStartupActors(SpawnedActors, Level);
|
|
|
|
for (AActor* Actor : SpawnedActors)
|
|
{
|
|
StartupActors.Add(Actor);
|
|
}
|
|
|
|
LocalLevels.Add(Level);
|
|
}
|
|
|
|
ReplayHelper.LevelsPendingFastForward.Reset();
|
|
|
|
{
|
|
TGuardValue<bool> FastForward(bIsFastForwarding, true);
|
|
FScopedAllowExistingChannelIndex ScopedAllowExistingChannelIndex(ServerConnection);
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
UDemoNetConnection* DemoConnection = CastChecked<UDemoNetConnection>(ServerConnection);
|
|
|
|
for (int32 i = 0; i < DeltaCheckpointPacketIntervals.Num(); ++i)
|
|
{
|
|
if (PlaybackDeltaCheckpointData.IsValidIndex(i))
|
|
{
|
|
check(PlaybackDeltaCheckpointData[i].IsValid());
|
|
|
|
for (auto& ChannelPair : PlaybackDeltaCheckpointData[i]->ChannelsToClose)
|
|
{
|
|
if (UActorChannel* ActorChannel = DemoConnection->GetOpenChannelMap().FindRef(ChannelPair.Key))
|
|
{
|
|
if (AActor* Actor = ActorChannel->GetActor())
|
|
{
|
|
if (LocalLevels.Contains(Actor->GetLevel()))
|
|
{
|
|
ActorChannel->ConditionalCleanUp(true, ChannelPair.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check(DeltaCheckpointPacketIntervals[i].IsValid());
|
|
check(ReadPacketsHelper.Packets.IsValidIndex(DeltaCheckpointPacketIntervals[i].Min));
|
|
check(ReadPacketsHelper.Packets.IsValidIndex(DeltaCheckpointPacketIntervals[i].Min + DeltaCheckpointPacketIntervals[i].Size()));
|
|
|
|
ProcessFastForwardPackets(MakeArrayView<FPlaybackPacket>(&ReadPacketsHelper.Packets[DeltaCheckpointPacketIntervals[i].Min], DeltaCheckpointPacketIntervals[i].Size() + 1), LevelIndices);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ProcessFastForwardPackets(ReadPacketsHelper.Packets, LevelIndices);
|
|
}
|
|
}
|
|
|
|
if (ensure(ServerConnection != nullptr))
|
|
{
|
|
// Make a pass at OnReps for startup actors, since they were skipped during checkpoint loading.
|
|
// At this point the shadow state of these actors should be the actual state from before the checkpoint,
|
|
// and the current state is the CDO state evolved by any changes that occurred during checkpoint loading and fast-forwarding.
|
|
|
|
TArray<UActorChannel*> ChannelsToUpdate;
|
|
ChannelsToUpdate.Reserve(StartupActors.Num());
|
|
|
|
for (UChannel* Channel : ServerConnection->OpenChannels)
|
|
{
|
|
// Skip non-actor channels.
|
|
if (Channel == nullptr || Channel->ChName != NAME_Actor)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Since we know this is an actor channel, should be safe to do a static_cast.
|
|
UActorChannel* const ActorChannel = static_cast<UActorChannel*>(Channel);
|
|
if (AActor* Actor = ActorChannel->GetActor())
|
|
{
|
|
const bool bDynamicInLevel = !Actor->IsNetStartupActor() && LocalLevels.Contains(Actor->GetLevel());
|
|
|
|
// We only need to consider startup actors, or dynamic that were spawned and outered
|
|
// to one of our sublevels.
|
|
if (bDynamicInLevel || StartupActors.Contains(Actor))
|
|
{
|
|
ChannelsToUpdate.Add(ActorChannel);
|
|
|
|
DiffActorProperties(ActorChannel);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (UActorChannel* Channel : ChannelsToUpdate)
|
|
{
|
|
for (auto& ReplicatorPair : Channel->ReplicationMap)
|
|
{
|
|
ReplicatorPair.Value->CallRepNotifies(true);
|
|
}
|
|
}
|
|
|
|
for (auto& DormantPair : ServerConnection->DormantReplicatorMap)
|
|
{
|
|
DormantPair.Value->CallRepNotifies( true );
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::LoadCheckpoint(const FGotoResult& GotoResult)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("LoadCheckpoint time"), STAT_ReplayCheckpointLoadTime, STATGROUP_Net);
|
|
|
|
FArchive* GotoCheckpointArchive = GetReplayStreamer()->GetCheckpointArchive();
|
|
|
|
check(GotoCheckpointArchive != nullptr);
|
|
check(!bIsFastForwardingForCheckpoint);
|
|
check(!bIsFastForwarding);
|
|
|
|
ReplayHelper.SetPlaybackNetworkVersions(*GotoCheckpointArchive);
|
|
|
|
GotoCheckpointArchive->ArMaxSerializeSize = FReplayHelper::MAX_DEMO_STRING_SERIALIZATION_SIZE;
|
|
|
|
int32 LevelForCheckpoint = 0;
|
|
|
|
const bool bDeltaCheckpoint = HasDeltaCheckpoints();
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
if (GotoCheckpointArchive->TotalSize() > 0)
|
|
{
|
|
uint32 CheckpointSize = 0;
|
|
*GotoCheckpointArchive << CheckpointSize;
|
|
}
|
|
}
|
|
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
// Make sure to read the packet offset, even though we won't use it here.
|
|
if (GotoCheckpointArchive->TotalSize() > 0)
|
|
{
|
|
FArchivePos PacketOffset = 0;
|
|
*GotoCheckpointArchive << PacketOffset;
|
|
}
|
|
|
|
ReplayHelper.ResetLevelStatuses();
|
|
}
|
|
|
|
ReplayHelper.ResetLevelMap();
|
|
|
|
LastProcessedPacketTime = 0.f;
|
|
ReplayHelper.LatestReadFrameTime = 0.f;
|
|
|
|
uint32 PlaybackVersion = GetPlaybackDemoVersion();
|
|
|
|
if (GotoCheckpointArchive->TotalSize() > 0)
|
|
{
|
|
*GotoCheckpointArchive << LevelForCheckpoint;
|
|
}
|
|
|
|
check(World);
|
|
|
|
if (LevelForCheckpoint != GetCurrentLevelIndex())
|
|
{
|
|
World->GetGameInstance()->OnSeamlessTravelDuringReplay();
|
|
|
|
for (FActorIterator ActorIt(World); ActorIt; ++ActorIt)
|
|
{
|
|
World->DestroyActor(*ActorIt, true);
|
|
}
|
|
|
|
// Clean package map to prepare to restore it to the checkpoint state
|
|
GuidCache->ResetCacheForDemo();
|
|
|
|
// Since we only count the number of sub-spectators, add one more slot for main spectator
|
|
// Very small optimization. We do want to clear this so that we don't end up doing during ProcessSeamlessTravel
|
|
SpectatorControllers.Empty(CleanUpSplitscreenConnections(true) + 1);
|
|
SpectatorController = nullptr;
|
|
|
|
ServerConnection->Close();
|
|
ServerConnection->CleanUp();
|
|
|
|
// Recreate the server connection - this is done so that when we execute the code the below again when we read in the
|
|
// checkpoint again after the server travel is finished, we'll have a clean server connection to work with.
|
|
ServerConnection = NewObject<UNetConnection>(GetTransientPackage(), UDemoNetConnection::StaticClass());
|
|
|
|
FURL ConnectURL;
|
|
ConnectURL.Map = ReplayHelper.DemoURL.Map;
|
|
ServerConnection->InitConnection(this, USOCK_Pending, ConnectURL, 1000000);
|
|
|
|
GEngine->ForceGarbageCollection(true);
|
|
|
|
ProcessSeamlessTravel(LevelForCheckpoint);
|
|
SetCurrentLevelIndex(LevelForCheckpoint);
|
|
|
|
if (GotoCheckpointArchive->TotalSize() != 0 && GotoCheckpointArchive->TotalSize() != INDEX_NONE)
|
|
{
|
|
GotoCheckpointArchive->Seek(0);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Save off the current spectator position
|
|
// Check for nullptr, which can be the case if we haven't played any of the demo yet but want to fast forward (joining live game for example)
|
|
if (SpectatorController != nullptr)
|
|
{
|
|
// Save off the SpectatorController's GUID so that we know not to queue his bunches
|
|
AddNonQueuedActorForScrubbing(SpectatorController);
|
|
}
|
|
|
|
// Remember the spectator controller's view target so we can restore it
|
|
FNetworkGUID ViewTargetGUID;
|
|
|
|
if (SpectatorController && SpectatorController->GetViewTarget())
|
|
{
|
|
ViewTargetGUID = GuidCache->NetGUIDLookup.FindRef(SpectatorController->GetViewTarget());
|
|
|
|
if (ViewTargetGUID.IsValid())
|
|
{
|
|
AddNonQueuedActorForScrubbing(SpectatorController->GetViewTarget());
|
|
}
|
|
}
|
|
|
|
PauseChannels(false);
|
|
|
|
FNetworkReplayDelegates::OnPreScrub.Broadcast(World);
|
|
|
|
ReplayHelper.bIsLoadingCheckpoint = true;
|
|
|
|
struct FPreservedNetworkGUIDEntry
|
|
{
|
|
FPreservedNetworkGUIDEntry(const FNetworkGUID InNetGUID, const AActor* const InActor)
|
|
: NetGUID(InNetGUID), Actor(InActor) {}
|
|
|
|
FNetworkGUID NetGUID;
|
|
const AActor* Actor;
|
|
};
|
|
|
|
// Store GUIDs for the spectator controller and any of its owned actors, so we can find them when we process the checkpoint.
|
|
// For the spectator controller, this allows the state and position to persist.
|
|
TArray<FPreservedNetworkGUIDEntry> NetGUIDsToPreserve;
|
|
|
|
if (!ensureMsgf(TrackedRewindActorsByGUID.Num() == 0, TEXT("LoadCheckpoint: TrackedRewindAcotrsByGUID list not empty!")))
|
|
{
|
|
TrackedRewindActorsByGUID.Empty();
|
|
}
|
|
|
|
#if 1
|
|
TSet<const AActor*> KeepAliveActors;
|
|
|
|
// Determine if an Actor has a reference to a spectator in some way.
|
|
// This prevents garbage collection on splitscreen playercontrollers
|
|
auto HasPlayerSpectatorRef = [this](const AActor* InActor) -> bool
|
|
{
|
|
for (const APlayerController* CurSpectator : SpectatorControllers)
|
|
{
|
|
if (IsValid(CurSpectator) &&
|
|
(InActor == CurSpectator || InActor == CurSpectator->GetSpectatorPawn() || InActor->IsOwnedBy(CurSpectator)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
// Destroy all non startup actors. They will get restored with the checkpoint
|
|
for (FActorIterator ActorIt(World); ActorIt; ++ActorIt)
|
|
{
|
|
AActor* CurrentActor = *ActorIt;
|
|
|
|
// If there are any existing actors that are bAlwaysRelevant, don't queue their bunches.
|
|
// Actors that do queue their bunches might not appear immediately after the checkpoint is loaded,
|
|
// and missing bAlwaysRelevant actors are more likely to cause noticeable artifacts.
|
|
// NOTE - We are adding the actor guid here, under the assumption that the actor will reclaim the same guid when we load the checkpoint
|
|
// This is normally the case, but could break if actors get destroyed and re-created with different guids during recording
|
|
if (CurrentActor->bAlwaysRelevant)
|
|
{
|
|
AddNonQueuedActorForScrubbing(CurrentActor);
|
|
}
|
|
|
|
const bool bShouldPreserveForPlayerController = HasPlayerSpectatorRef(CurrentActor);
|
|
const bool bShouldPreserveForRewindability = (CurrentActor->bReplayRewindable && !CurrentActor->IsNetStartupActor());
|
|
|
|
if (bShouldPreserveForPlayerController || bShouldPreserveForRewindability)
|
|
{
|
|
// If an non-startup actor that we don't destroy has an entry in the GuidCache, preserve that entry so
|
|
// that the object will be re-used after loading the checkpoint. Otherwise, a new copy
|
|
// of the object will be created each time a checkpoint is loaded, causing a leak.
|
|
const FNetworkGUID FoundGUID = GuidCache->NetGUIDLookup.FindRef(CurrentActor);
|
|
|
|
if (FoundGUID.IsValid())
|
|
{
|
|
NetGUIDsToPreserve.Emplace(FoundGUID, CurrentActor);
|
|
|
|
if (bShouldPreserveForRewindability)
|
|
{
|
|
TrackedRewindActorsByGUID.Add(FoundGUID);
|
|
}
|
|
}
|
|
|
|
KeepAliveActors.Add(CurrentActor);
|
|
continue;
|
|
}
|
|
|
|
// Prevent NetStartupActors from being destroyed.
|
|
// NetStartupActors that can't have properties directly re-applied should use QueueNetStartupActorForRollbackViaDeletion.
|
|
if (CurrentActor->IsNetStartupActor())
|
|
{
|
|
// Go ahead and rewind this now, since we won't be destroying it later.
|
|
if (CurrentActor->bReplayRewindable)
|
|
{
|
|
CurrentActor->RewindForReplay();
|
|
}
|
|
KeepAliveActors.Add(CurrentActor);
|
|
continue;
|
|
}
|
|
|
|
World->DestroyActor(CurrentActor, true);
|
|
}
|
|
|
|
// Destroy all particle FX attached to the WorldSettings (the WorldSettings actor persists but the particle FX spawned at runtime shouldn't)
|
|
World->HandleTimelineScrubbed();
|
|
|
|
// Remove references to our KeepAlive actors so that cleaning up the channels won't destroy them.
|
|
for (int32 i = ServerConnection->OpenChannels.Num() - 1; i >= 0; i--)
|
|
{
|
|
UChannel* OpenChannel = ServerConnection->OpenChannels[i];
|
|
if ( OpenChannel != nullptr )
|
|
{
|
|
UActorChannel* ActorChannel = Cast<UActorChannel>(OpenChannel);
|
|
if (ActorChannel != nullptr && KeepAliveActors.Contains(ActorChannel->Actor))
|
|
{
|
|
ActorChannel->Actor = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ServerConnection->OwningActor == SpectatorController)
|
|
{
|
|
ServerConnection->OwningActor = nullptr;
|
|
}
|
|
|
|
#else
|
|
for (int32 i = ServerConnection->OpenChannels.Num() - 1; i >= 0; i--)
|
|
{
|
|
UChannel* OpenChannel = ServerConnection->OpenChannels[i];
|
|
if (OpenChannel != nullptr)
|
|
{
|
|
UActorChannel* ActorChannel = Cast<UActorChannel>(OpenChannel);
|
|
if (ActorChannel != nullptr && ActorChannel->GetActor() != nullptr && !ActorChannel->GetActor()->IsNetStartupActor())
|
|
{
|
|
World->DestroyActor(ActorChannel->GetActor(), true);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
ReplayHelper.ExternalDataToObjectMap.Empty();
|
|
|
|
PlaybackPackets.Empty();
|
|
ReplayHelper.PlaybackFrames.Empty();
|
|
|
|
// Destroy startup actors that need to rollback via being destroyed and re-created
|
|
for (FActorIterator ActorIt(World); ActorIt; ++ActorIt)
|
|
{
|
|
if (RollbackNetStartupActors.Contains(ActorIt->GetFullName()))
|
|
{
|
|
World->DestroyActor(*ActorIt, true);
|
|
}
|
|
}
|
|
|
|
// Going to be recreating the splitscreen connections, but keep around the player controller.
|
|
CleanUpSplitscreenConnections(false);
|
|
ServerConnection->Close();
|
|
ServerConnection->CleanUp();
|
|
|
|
// Optionally collect garbage after the old actors and connection are cleaned up - there could be a lot of pending-kill objects at this point.
|
|
if (CVarDemoLoadCheckpointGarbageCollect.GetValueOnGameThread() != 0)
|
|
{
|
|
GEngine->ForceGarbageCollection(true);
|
|
}
|
|
|
|
FURL ConnectURL;
|
|
ConnectURL.Map = ReplayHelper.DemoURL.Map;
|
|
|
|
ServerConnection = NewObject<UNetConnection>(GetTransientPackage(), UDemoNetConnection::StaticClass());
|
|
ServerConnection->InitConnection( this, USOCK_Pending, ConnectURL, 1000000 );
|
|
|
|
// Set network version on connection
|
|
ReplayHelper.SetPlaybackNetworkVersions(ServerConnection);
|
|
|
|
// Create fake control channel
|
|
CreateInitialClientChannels();
|
|
|
|
// Respawn child connections as the parent connection has been recreated.
|
|
for (APlayerController* CurController : SpectatorControllers)
|
|
{
|
|
if (CurController != SpectatorController)
|
|
{
|
|
RestoreConnectionPostScrub(CurController, CreateChild(ServerConnection));
|
|
}
|
|
}
|
|
|
|
// Catch a rare case where the spectator controller is null, but a valid GUID is
|
|
// found on the GuidCache. The weak pointers in the NetGUIDLookup map are probably
|
|
// going null, and we want catch these cases and investigate further.
|
|
if (!ensure(GuidCache->NetGUIDLookup.FindRef(SpectatorController).IsValid() == (SpectatorController != nullptr)))
|
|
{
|
|
UE_LOG(LogDemo, Log, TEXT("LoadCheckpoint: SpectatorController is null and a valid GUID for null was found in the GuidCache. SpectatorController = %s"),
|
|
*GetFullNameSafe(SpectatorController));
|
|
}
|
|
|
|
// Clean package map to prepare to restore it to the checkpoint state
|
|
FlushAsyncLoading();
|
|
GuidCache->ResetCacheForDemo();
|
|
|
|
// Restore preserved packagemap entries
|
|
for (const FPreservedNetworkGUIDEntry& PreservedEntry : NetGUIDsToPreserve)
|
|
{
|
|
check(PreservedEntry.NetGUID.IsValid());
|
|
|
|
FNetGuidCacheObject& CacheObject = GuidCache->ObjectLookup.FindOrAdd(PreservedEntry.NetGUID);
|
|
|
|
CacheObject.Object = MakeWeakObjectPtr(const_cast<AActor*>(PreservedEntry.Actor));
|
|
check(CacheObject.Object != nullptr);
|
|
CacheObject.bNoLoad = true;
|
|
GuidCache->NetGUIDLookup.Add(CacheObject.Object, PreservedEntry.NetGUID);
|
|
}
|
|
|
|
if (GotoCheckpointArchive->TotalSize() == 0 || GotoCheckpointArchive->TotalSize() == INDEX_NONE)
|
|
{
|
|
// Make sure this is empty so that RespawnNecessaryNetStartupActors will respawn them
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Empty();
|
|
|
|
// Re-create all startup actors that were destroyed but should exist beyond this point
|
|
TArray<AActor*> SpawnedActors;
|
|
RespawnNecessaryNetStartupActors(SpawnedActors);
|
|
|
|
// This is the very first checkpoint, we'll read the stream from the very beginning in this case
|
|
SetDemoCurrentTime(0.0f);
|
|
ReplayHelper.bIsLoadingCheckpoint = false;
|
|
|
|
if (GotoResult.ExtraTimeMS != -1)
|
|
{
|
|
SkipTimeInternal((float)GotoResult.ExtraTimeMS / 1000.0f, true, true);
|
|
}
|
|
else
|
|
{
|
|
// Make sure that we delete any Rewind actors that aren't valid anymore.
|
|
// If there's more data to stream in, we will handle this in FinalizeFastForward.
|
|
CleanupOutstandingRewindActors();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
GotoCheckpointArchive->Seek(0);
|
|
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Empty();
|
|
|
|
PlaybackDeltaCheckpointData.Empty();
|
|
|
|
TArray<TInterval<int32>> DeltaCheckpointPacketIntervals;
|
|
TArray<FName> PathNameTable;
|
|
|
|
do
|
|
{
|
|
FArchivePos MaxArchivePos = 0;
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
uint32 CheckpointSize = 0;
|
|
*GotoCheckpointArchive << CheckpointSize;
|
|
|
|
MaxArchivePos = GotoCheckpointArchive->Tell() + CheckpointSize;
|
|
}
|
|
|
|
TGuardValue<int64> MaxArchivePosGuard(MaxArchiveReadPos, MaxArchivePos);
|
|
|
|
if (HasLevelStreamingFixes())
|
|
{
|
|
FArchivePos PacketOffset = 0;
|
|
*GotoCheckpointArchive << PacketOffset;
|
|
}
|
|
|
|
int32 LevelIndex = INDEX_NONE;
|
|
*GotoCheckpointArchive << LevelIndex;
|
|
|
|
// Load net startup actors that need to be destroyed
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
TUniquePtr<FDeltaCheckpointData>& CheckpointData = PlaybackDeltaCheckpointData.Emplace_GetRef(new FDeltaCheckpointData());
|
|
|
|
ReplayHelper.ReadDeletedStartupActors(ServerConnection, *GotoCheckpointArchive, CheckpointData->DestroyedNetStartupActors);
|
|
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Append(CheckpointData->DestroyedNetStartupActors);
|
|
|
|
*GotoCheckpointArchive << CheckpointData->DestroyedDynamicActors;
|
|
*GotoCheckpointArchive << CheckpointData->ChannelsToClose;
|
|
}
|
|
else
|
|
{
|
|
ReplayHelper.PlaybackDeletedNetStartupActors.Empty();
|
|
|
|
ReplayHelper.ReadDeletedStartupActors(ServerConnection, *GotoCheckpointArchive, ReplayHelper.PlaybackDeletedNetStartupActors);
|
|
}
|
|
|
|
int32 NumValues = 0;
|
|
*GotoCheckpointArchive << NumValues;
|
|
|
|
for (int32 i = 0; i < NumValues; i++)
|
|
{
|
|
FNetworkGUID Guid;
|
|
|
|
*GotoCheckpointArchive << Guid;
|
|
|
|
FNetGuidCacheObject CacheObject;
|
|
|
|
*GotoCheckpointArchive << CacheObject.OuterGUID;
|
|
|
|
FString PathName;
|
|
|
|
if (PlaybackVersion < HISTORY_GUID_NAMETABLE)
|
|
{
|
|
*GotoCheckpointArchive << PathName;
|
|
}
|
|
else
|
|
{
|
|
uint8 bExported = 0;
|
|
*GotoCheckpointArchive << bExported;
|
|
|
|
if (bExported == 1)
|
|
{
|
|
*GotoCheckpointArchive << PathName;
|
|
|
|
PathNameTable.Add(FName(*PathName));
|
|
}
|
|
else
|
|
{
|
|
uint32 PathNameIndex = 0;
|
|
GotoCheckpointArchive->SerializeIntPacked(PathNameIndex);
|
|
|
|
if (PathNameTable.IsValidIndex(PathNameIndex))
|
|
{
|
|
PathName = PathNameTable[PathNameIndex].ToString();
|
|
}
|
|
else
|
|
{
|
|
GotoCheckpointArchive->SetError();
|
|
UE_LOG(LogDemo, Error, TEXT("Invalid guid path table index while serializing checkpoint."));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remap the pathname to handle client-recorded replays
|
|
GEngine->NetworkRemapPath(ServerConnection, PathName, true);
|
|
|
|
CacheObject.PathName = FName(*PathName);
|
|
|
|
if (PlaybackVersion < HISTORY_GUIDCACHE_CHECKSUMS)
|
|
{
|
|
*GotoCheckpointArchive << CacheObject.NetworkChecksum;
|
|
}
|
|
|
|
uint8 Flags = 0;
|
|
*GotoCheckpointArchive << Flags;
|
|
|
|
CacheObject.bNoLoad = (Flags & (1 << 0)) ? true : false;
|
|
CacheObject.bIgnoreWhenMissing = (Flags & (1 << 1)) ? true : false;
|
|
|
|
GuidCache->ObjectLookup.Add(Guid, CacheObject);
|
|
|
|
if (GotoCheckpointArchive->IsError())
|
|
{
|
|
UE_LOG(LogDemo, Error, TEXT("Guid cache serialization error while loading checkpoint."));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
int32 DeltaPacketStartIndex = INDEX_NONE;
|
|
|
|
// Read in the compatible rep layouts in this checkpoint
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
CastChecked<UPackageMapClient>(ServerConnection->PackageMap)->SerializeNetFieldExportDelta(*GotoCheckpointArchive);
|
|
|
|
DeltaPacketStartIndex = PlaybackPackets.Num();
|
|
}
|
|
else
|
|
{
|
|
CastChecked<UPackageMapClient>(ServerConnection->PackageMap)->SerializeNetFieldExportGroupMap(*GotoCheckpointArchive);
|
|
}
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
// each set of checkpoint packets we read will have a full name table, so only keep the last version
|
|
ReplayHelper.SeenLevelStatuses.Reset();
|
|
}
|
|
|
|
ReadDemoFrameIntoPlaybackPackets(*GotoCheckpointArchive);
|
|
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
const int32 DeltaPacketEndIndex = PlaybackPackets.Num() - 1;
|
|
if (DeltaPacketEndIndex >= DeltaPacketStartIndex)
|
|
{
|
|
DeltaCheckpointPacketIntervals.Emplace(DeltaPacketStartIndex, DeltaPacketEndIndex);
|
|
}
|
|
}
|
|
}
|
|
while (!GotoCheckpointArchive->IsError() && (GotoCheckpointArchive->Tell() < GotoCheckpointArchive->TotalSize()));
|
|
|
|
if (World != nullptr)
|
|
{
|
|
// Destroy startup actors that shouldn't exist past this checkpoint
|
|
for (FActorIterator ActorIt( World ); ActorIt; ++ActorIt)
|
|
{
|
|
AActor* CurrentActor = *ActorIt;
|
|
|
|
const FString FullName = CurrentActor->GetFullName();
|
|
|
|
if (ReplayHelper.PlaybackDeletedNetStartupActors.Contains(FullName))
|
|
{
|
|
if (CurrentActor->bReplayRewindable)
|
|
{
|
|
// Log and skip. We can't queue Rewindable actors and we can't destroy them.
|
|
// This actor may still get destroyed during cleanup.
|
|
UE_LOG(LogDemo, Warning, TEXT("Replay Rewindable Actor found in the DeletedNetStartupActors. Replay may show artifacts (%s)"), *FullName);
|
|
continue;
|
|
}
|
|
|
|
// Put this actor on the rollback list so we can undelete it during future scrubbing
|
|
QueueNetStartupActorForRollbackViaDeletion(CurrentActor);
|
|
|
|
UE_LOG(LogDemo, Verbose, TEXT("LoadCheckpoint: deleting startup actor %s"), *FullName);
|
|
|
|
// Delete the actor
|
|
World->DestroyActor(CurrentActor, true);
|
|
}
|
|
}
|
|
|
|
// Re-create all startup actors that were destroyed but should exist beyond this point
|
|
TArray<AActor*> SpawnedActors;
|
|
RespawnNecessaryNetStartupActors(SpawnedActors);
|
|
}
|
|
|
|
SetDemoCurrentTime((PlaybackPackets.Num() > 0) ? PlaybackPackets.Last().TimeSeconds : 0.0f);
|
|
|
|
if (GotoResult.ExtraTimeMS != -1)
|
|
{
|
|
// If we need to skip more time for fine scrubbing, set that up now
|
|
SkipTimeInternal((float)GotoResult.ExtraTimeMS / 1000.0f, true, true);
|
|
}
|
|
else
|
|
{
|
|
// Make sure that we delete any Rewind actors that aren't valid anymore.
|
|
// If there's more data to stream in, we will handle this in FinalizeFastForward.
|
|
CleanupOutstandingRewindActors();
|
|
}
|
|
|
|
{
|
|
if (bDeltaCheckpoint)
|
|
{
|
|
UDemoNetConnection* DemoConnection = Cast<UDemoNetConnection>(ServerConnection);
|
|
|
|
for (int32 i = 0; i < DeltaCheckpointPacketIntervals.Num(); ++i)
|
|
{
|
|
if (DemoConnection && PlaybackDeltaCheckpointData.IsValidIndex(i))
|
|
{
|
|
check(PlaybackDeltaCheckpointData[i].IsValid());
|
|
|
|
for (auto& ChannelPair : PlaybackDeltaCheckpointData[i]->ChannelsToClose)
|
|
{
|
|
if (UActorChannel* ActorChannel = DemoConnection->GetOpenChannelMap().FindRef(ChannelPair.Key))
|
|
{
|
|
ActorChannel->ConditionalCleanUp(true, ChannelPair.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
check(DeltaCheckpointPacketIntervals[i].IsValid());
|
|
check(PlaybackPackets.IsValidIndex(DeltaCheckpointPacketIntervals[i].Min));
|
|
check(PlaybackPackets.IsValidIndex(DeltaCheckpointPacketIntervals[i].Min + DeltaCheckpointPacketIntervals[i].Size()));
|
|
|
|
// + 1 because the interval is inclusive
|
|
ProcessPlaybackPackets(MakeArrayView<FPlaybackPacket>(&PlaybackPackets[DeltaCheckpointPacketIntervals[i].Min], DeltaCheckpointPacketIntervals[i].Size() + 1));
|
|
}
|
|
|
|
PlaybackPackets.Empty();
|
|
ReplayHelper.PlaybackFrames.Empty();
|
|
}
|
|
else
|
|
{
|
|
ProcessAllPlaybackPackets();
|
|
}
|
|
}
|
|
|
|
ReplayHelper.bIsLoadingCheckpoint = false;
|
|
|
|
// Save the replicated server time here
|
|
if (World != nullptr)
|
|
{
|
|
const AGameStateBase* const GameState = World->GetGameState();
|
|
if (GameState != nullptr)
|
|
{
|
|
SavedReplicatedWorldTimeSeconds = GameState->ReplicatedWorldTimeSeconds;
|
|
}
|
|
}
|
|
|
|
if (SpectatorController && ViewTargetGUID.IsValid())
|
|
{
|
|
AActor* ViewTarget = Cast<AActor>(GuidCache->GetObjectFromNetGUID(ViewTargetGUID, false));
|
|
|
|
if (ViewTarget)
|
|
{
|
|
SpectatorController->SetViewTarget(ViewTarget);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UDemoNetDriver::IsSavingCheckpoint() const
|
|
{
|
|
if (ClientConnections.Num() > 0)
|
|
{
|
|
UNetConnection* const NetConnection = ClientConnections[0];
|
|
if (NetConnection)
|
|
{
|
|
return (NetConnection->ResendAllDataState != EResendAllDataState::None);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldQueueBunchesForActorGUID(FNetworkGUID InGUID) const
|
|
{
|
|
if (CVarDemoQueueCheckpointChannels.GetValueOnGameThread() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// While loading a checkpoint, queue most bunches so that we don't process them all on one frame.
|
|
if (bIsFastForwardingForCheckpoint)
|
|
{
|
|
return !NonQueuedGUIDsForScrubbing.Contains(InGUID);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldIgnoreRPCs() const
|
|
{
|
|
return (CVarDemoFastForwardIgnoreRPCs.GetValueOnAnyThread() && (ReplayHelper.bIsLoadingCheckpoint || bIsFastForwarding));
|
|
}
|
|
|
|
FNetworkGUID UDemoNetDriver::GetGUIDForActor(const AActor* InActor) const
|
|
{
|
|
UNetConnection* Connection = ServerConnection;
|
|
|
|
if (ClientConnections.Num() > 0)
|
|
{
|
|
Connection = ClientConnections[0];
|
|
}
|
|
|
|
if (!Connection)
|
|
{
|
|
return FNetworkGUID();
|
|
}
|
|
|
|
FNetworkGUID Guid = Connection->PackageMap->GetNetGUIDFromObject(InActor);
|
|
return Guid;
|
|
}
|
|
|
|
AActor* UDemoNetDriver::GetActorForGUID(FNetworkGUID InGUID) const
|
|
{
|
|
UNetConnection* Connection = ServerConnection;
|
|
|
|
if (ClientConnections.Num() > 0)
|
|
{
|
|
Connection = ClientConnections[0];
|
|
}
|
|
|
|
if (!Connection)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
UObject* FoundObject = Connection->PackageMap->GetObjectFromNetGUID(InGUID, true);
|
|
return Cast<AActor>(FoundObject);
|
|
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldReceiveRepNotifiesForObject(UObject* Object) const
|
|
{
|
|
// Return false for startup actors during checkpoint loading, since they are
|
|
// not destroyed and re-created like dynamic actors. Startup actors will
|
|
// have their properties diffed and RepNotifies called after the checkpoint is loaded.
|
|
|
|
if (!ReplayHelper.bIsLoadingCheckpoint && !bIsFastForwardingForCheckpoint)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const AActor* const Actor = Cast<AActor>(Object);
|
|
const bool bIsStartupActor = Actor != nullptr && Actor->IsNetStartupActor();
|
|
|
|
return !bIsStartupActor;
|
|
}
|
|
|
|
void UDemoNetDriver::AddNonQueuedActorForScrubbing(AActor const* Actor)
|
|
{
|
|
UActorChannel const* const* const FoundChannel = ServerConnection->FindActorChannel(MakeWeakObjectPtr(const_cast<AActor*>(Actor)));
|
|
if (FoundChannel != nullptr && *FoundChannel != nullptr)
|
|
{
|
|
FNetworkGUID const ActorGUID = (*FoundChannel)->ActorNetGUID;
|
|
NonQueuedGUIDsForScrubbing.Add(ActorGUID);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::AddNonQueuedGUIDForScrubbing(FNetworkGUID InGUID)
|
|
{
|
|
if (InGUID.IsValid())
|
|
{
|
|
NonQueuedGUIDsForScrubbing.Add(InGUID);
|
|
}
|
|
}
|
|
|
|
FDemoSavedRepObjectState::FDemoSavedRepObjectState(
|
|
const TWeakObjectPtr<const UObject>& InObject,
|
|
const TSharedRef<const FRepLayout>& InRepLayout,
|
|
FRepStateStaticBuffer&& InPropertyData) :
|
|
|
|
Object(InObject),
|
|
RepLayout(InRepLayout),
|
|
PropertyData(MoveTemp(InPropertyData))
|
|
{
|
|
}
|
|
|
|
FDemoSavedRepObjectState::~FDemoSavedRepObjectState()
|
|
{
|
|
}
|
|
|
|
FDemoSavedPropertyState UDemoNetDriver::SavePropertyState() const
|
|
{
|
|
FDemoSavedPropertyState State;
|
|
|
|
if (IsRecording())
|
|
{
|
|
const UNetConnection* const RecordingConnection = ClientConnections[0];
|
|
for (auto ChannelPair = RecordingConnection->ActorChannelConstIterator(); ChannelPair; ++ChannelPair)
|
|
{
|
|
const UActorChannel* const Channel = ChannelPair.Value();
|
|
if (Channel)
|
|
{
|
|
for (const auto& ReplicatorPair : Channel->ReplicationMap)
|
|
{
|
|
TWeakObjectPtr<UObject> WeakObjectPtr = ReplicatorPair.Value->GetWeakObjectPtr();
|
|
if (const UObject* const RepObject = WeakObjectPtr.Get())
|
|
{
|
|
const TSharedRef<const FRepLayout> RepLayout = ReplicatorPair.Value->RepLayout.ToSharedRef();
|
|
FDemoSavedRepObjectState& SavedObject = State.Emplace_GetRef(WeakObjectPtr, RepLayout, RepLayout->CreateShadowBuffer((const uint8*)RepObject));
|
|
|
|
// TODO: InitShadowData should copy property data, so this seem unnecessary.
|
|
// Store the properties in the new RepState
|
|
FRepShadowDataBuffer ShadowData(SavedObject.PropertyData.GetData());
|
|
FConstRepObjectDataBuffer RepObjectData(RepObject);
|
|
|
|
SavedObject.RepLayout->DiffProperties(nullptr, ShadowData, RepObjectData, EDiffPropertiesFlags::Sync | EDiffPropertiesFlags::IncludeConditionalProperties);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return State;
|
|
}
|
|
|
|
bool UDemoNetDriver::ComparePropertyState(const FDemoSavedPropertyState& State) const
|
|
{
|
|
bool bWasDifferent = false;
|
|
|
|
if (IsRecording())
|
|
{
|
|
for (const FDemoSavedRepObjectState& ObjectState : State)
|
|
{
|
|
const UObject* const RepObject = ObjectState.Object.Get();
|
|
if (RepObject)
|
|
{
|
|
FRepObjectDataBuffer RepObjectData(const_cast<UObject* const>(RepObject));
|
|
FConstRepShadowDataBuffer ShadowData(ObjectState.PropertyData.GetData());
|
|
|
|
if (ObjectState.RepLayout->DiffProperties(nullptr, RepObjectData, ShadowData, EDiffPropertiesFlags::IncludeConditionalProperties))
|
|
{
|
|
bWasDifferent = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("A replicated object was destroyed or marked pending kill since its state was saved!"));
|
|
bWasDifferent = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bWasDifferent;
|
|
}
|
|
|
|
void UDemoNetDriver::RestoreConnectionPostScrub(APlayerController* PC, UNetConnection* NetConnection)
|
|
{
|
|
check(NetConnection != nullptr);
|
|
check(PC != nullptr);
|
|
|
|
PC->SetRole(ROLE_AutonomousProxy);
|
|
PC->NetConnection = NetConnection;
|
|
NetConnection->LastReceiveTime = GetElapsedTime();
|
|
NetConnection->LastReceiveRealtime = FPlatformTime::Seconds();
|
|
NetConnection->LastGoodPacketRealtime = FPlatformTime::Seconds();
|
|
NetConnection->SetConnectionState(USOCK_Open);
|
|
NetConnection->PlayerController = PC;
|
|
NetConnection->OwningActor = PC;
|
|
}
|
|
|
|
void UDemoNetDriver::SetSpectatorController(APlayerController* PC)
|
|
{
|
|
SpectatorController = PC;
|
|
if (PC != nullptr)
|
|
{
|
|
SpectatorControllers.AddUnique(PC);
|
|
}
|
|
}
|
|
|
|
TSharedPtr<FInternetAddr> FInternetAddrDemo::DemoInternetAddr = MakeShareable(new FInternetAddrDemo);
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
UDemoNetConnection.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
UDemoNetConnection::UDemoNetConnection( const FObjectInitializer& ObjectInitializer ) : Super( ObjectInitializer )
|
|
{
|
|
MaxPacket = FReplayHelper::MAX_DEMO_READ_WRITE_BUFFER;
|
|
SetInternalAck(true);
|
|
SetReplay(true);
|
|
SetAutoFlush(true);
|
|
SetUnlimitedBunchSizeAllowed(true);
|
|
}
|
|
|
|
void UDemoNetConnection::InitConnection( UNetDriver* InDriver, EConnectionState InState, const FURL& InURL, int32 InConnectionSpeed, int32 InMaxPacket)
|
|
{
|
|
// default implementation
|
|
Super::InitConnection( InDriver, InState, InURL, InConnectionSpeed );
|
|
|
|
MaxPacket = (InMaxPacket == 0 || InMaxPacket > FReplayHelper::MAX_DEMO_READ_WRITE_BUFFER) ? FReplayHelper::MAX_DEMO_READ_WRITE_BUFFER : InMaxPacket;
|
|
SetInternalAck(true);
|
|
SetReplay(true);
|
|
SetAutoFlush(true);
|
|
SetUnlimitedBunchSizeAllowed(true);
|
|
|
|
InitSendBuffer();
|
|
|
|
// the driver must be a DemoRecording driver (GetDriver makes assumptions to avoid Cast'ing each time)
|
|
check( InDriver->IsA( UDemoNetDriver::StaticClass() ) );
|
|
}
|
|
|
|
FString UDemoNetConnection::LowLevelGetRemoteAddress( bool bAppendPort )
|
|
{
|
|
return TEXT("UDemoNetConnection");
|
|
}
|
|
|
|
void UDemoNetConnection::LowLevelSend(void* Data, int32 CountBits, FOutPacketTraits& Traits)
|
|
{
|
|
uint32 CountBytes = FMath::DivideAndRoundUp(CountBits, 8);
|
|
|
|
if (CountBytes == 0)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetConnection::LowLevelSend: Ignoring empty packet."));
|
|
return;
|
|
}
|
|
|
|
UDemoNetDriver* DemoDriver = GetDriver();
|
|
if (!DemoDriver)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetConnection::LowLevelSend: No driver found."));
|
|
return;
|
|
}
|
|
|
|
if (CountBytes > FReplayHelper::MAX_DEMO_READ_WRITE_BUFFER)
|
|
{
|
|
UE_LOG(LogDemo, Fatal, TEXT("UDemoNetConnection::LowLevelSend: CountBytes > MAX_DEMO_READ_WRITE_BUFFER."));
|
|
}
|
|
|
|
TrackSendForProfiler(Data, CountBytes);
|
|
|
|
TArray<FQueuedDemoPacket>& QueuedPackets = (ResendAllDataState != EResendAllDataState::None) ? DemoDriver->ReplayHelper.QueuedCheckpointPackets : DemoDriver->ReplayHelper.QueuedDemoPackets;
|
|
|
|
int32 NewIndex = QueuedPackets.Emplace((uint8*)Data, CountBits, Traits);
|
|
|
|
if (ULevel* Level = GetRepContextLevel())
|
|
{
|
|
QueuedPackets[NewIndex].SeenLevelIndex = DemoDriver->ReplayHelper.FindOrAddLevelStatus(*Level).LevelIndex + 1;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("UDemoNetConnection::LowLevelSend: Missing rep context."));
|
|
}
|
|
}
|
|
|
|
void UDemoNetConnection::TrackSendForProfiler(const void* Data, int32 NumBytes)
|
|
{
|
|
NETWORK_PROFILER(GNetworkProfiler.FlushOutgoingBunches(this));
|
|
|
|
// Track "socket send" even though we're not technically sending to a socket, to get more accurate information in the profiler.
|
|
NETWORK_PROFILER(GNetworkProfiler.TrackSocketSendToCore(TEXT("Unreal"), Data, NumBytes, NumPacketIdBits, NumBunchBits, NumAckBits, NumPaddingBits, this));
|
|
}
|
|
|
|
FString UDemoNetConnection::LowLevelDescribe()
|
|
{
|
|
return TEXT("Demo recording/playback driver connection");
|
|
}
|
|
|
|
int32 UDemoNetConnection::IsNetReady(bool Saturate)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
void UDemoNetConnection::FlushNet(bool bIgnoreSimulation)
|
|
{
|
|
// in playback, there is no data to send except
|
|
// channel closing if an error occurs.
|
|
if (GetDriver()->ServerConnection != nullptr)
|
|
{
|
|
InitSendBuffer();
|
|
}
|
|
else
|
|
{
|
|
Super::FlushNet(bIgnoreSimulation);
|
|
}
|
|
}
|
|
|
|
void UDemoNetConnection::HandleClientPlayer(APlayerController* PC, UNetConnection* NetConnection)
|
|
{
|
|
UDemoNetDriver* DemoDriver = GetDriver();
|
|
|
|
// If the spectator is the same, assume this is for scrubbing, and we are keeping the old one
|
|
// (so don't set the position, since we want to persist all that)
|
|
if (DemoDriver->SpectatorController == PC)
|
|
{
|
|
DemoDriver->RestoreConnectionPostScrub(PC, NetConnection);
|
|
DemoDriver->SetSpectatorController(PC);
|
|
return;
|
|
}
|
|
|
|
ULocalPlayer* LocalPlayer = nullptr;
|
|
uint8 PlayerIndex = 0;
|
|
// Attempt to find the player that doesn't already have a connection.
|
|
for (FLocalPlayerIterator LocalPlayerIt(GEngine, Driver->GetWorld()); LocalPlayerIt; ++LocalPlayerIt, PlayerIndex++)
|
|
{
|
|
if (PC->NetPlayerIndex == PlayerIndex)
|
|
{
|
|
LocalPlayer = *LocalPlayerIt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (LocalPlayer != nullptr)
|
|
{
|
|
Super::HandleClientPlayer(PC, NetConnection);
|
|
}
|
|
else
|
|
{
|
|
DemoDriver->RestoreConnectionPostScrub(PC, NetConnection);
|
|
}
|
|
|
|
// This is very likely our main demo controller.
|
|
DemoDriver->SetSpectatorController(PC);
|
|
|
|
// Find a player start, if one exists
|
|
for (TActorIterator<APlayerStart> PlayerStartIt(Driver->World); PlayerStartIt; ++PlayerStartIt)
|
|
{
|
|
PC->SetInitialLocationAndRotation(PlayerStartIt->GetActorLocation(), PlayerStartIt->GetActorRotation());
|
|
break;
|
|
}
|
|
}
|
|
|
|
TSharedPtr<const FInternetAddr> UDemoNetConnection::GetRemoteAddr()
|
|
{
|
|
return FInternetAddrDemo::DemoInternetAddr;
|
|
}
|
|
|
|
bool UDemoNetConnection::ClientHasInitializedLevelFor(const AActor* TestActor) const
|
|
{
|
|
// We save all currently streamed levels into the demo stream so we can force the demo playback client
|
|
// to stay in sync with the recording server
|
|
// This may need to be tweaked or re-evaluated when we start recording demos on the client
|
|
return (GetDriver()->GetDemoFrameNum() > 2 || Super::ClientHasInitializedLevelFor(TestActor));
|
|
}
|
|
|
|
TSharedPtr<FObjectReplicator> UDemoNetConnection::CreateReplicatorForNewActorChannel(UObject* Object)
|
|
{
|
|
TSharedPtr<FObjectReplicator> NewReplicator = MakeShareable(new FObjectReplicator());
|
|
|
|
// To handle rewinding net startup actors in replays properly, we need to
|
|
// initialize the shadow state with the object's current state.
|
|
// Afterwards, we will copy the CDO state to object's current state with repnotifies disabled.
|
|
UDemoNetDriver* NetDriver = GetDriver();
|
|
AActor* Actor = Cast<AActor>(Object);
|
|
|
|
const bool bIsCheckpointStartupActor = NetDriver && NetDriver->IsLoadingCheckpoint() && Actor && Actor->IsNetStartupActor();
|
|
const bool bUseDefaultState = !bIsCheckpointStartupActor;
|
|
|
|
NewReplicator->InitWithObject(Object, this, bUseDefaultState);
|
|
|
|
// Now that the shadow state is initialized, copy the CDO state into the actor state.
|
|
if (bIsCheckpointStartupActor && NewReplicator->RepLayout.IsValid() && Object->GetClass())
|
|
{
|
|
FRepObjectDataBuffer ObjectData(Object);
|
|
FConstRepObjectDataBuffer ShadowData(Object->GetClass()->GetDefaultObject());
|
|
|
|
NewReplicator->RepLayout->DiffProperties(nullptr, ObjectData, ShadowData, EDiffPropertiesFlags::Sync);
|
|
|
|
// Need to swap roles for the startup actor since in the CDO they aren't swapped, and the CDO just
|
|
// overwrote the actor state.
|
|
if (Actor && (Actor->GetLocalRole() == ROLE_Authority))
|
|
{
|
|
Actor->SwapRoles();
|
|
}
|
|
}
|
|
|
|
QueueNetStartupActorForRewind(Actor);
|
|
|
|
return NewReplicator;
|
|
}
|
|
|
|
void UDemoNetConnection::DestroyIgnoredActor(AActor* Actor)
|
|
{
|
|
QueueNetStartupActorForRewind(Actor);
|
|
|
|
Super::DestroyIgnoredActor(Actor);
|
|
}
|
|
|
|
void UDemoNetConnection::QueueNetStartupActorForRewind(AActor* Actor)
|
|
{
|
|
UDemoNetDriver* NetDriver = GetDriver();
|
|
|
|
// Handle rewinding initially dormant startup actors that were changed on the client
|
|
const bool bIsStartupActor = NetDriver && Actor && Actor->IsNetStartupActor() && !Actor->bReplayRewindable;
|
|
if (bIsStartupActor)
|
|
{
|
|
NetDriver->QueueNetStartupActorForRollbackViaDeletion(Actor);
|
|
}
|
|
}
|
|
|
|
void UDemoNetConnection::NotifyActorNetGUID(UActorChannel* Channel)
|
|
{
|
|
const UDemoNetDriver* const NetDriver = GetDriver();
|
|
|
|
if (Channel && NetDriver && NetDriver->HasDeltaCheckpoints())
|
|
{
|
|
GetOpenChannelMap().Add(Channel->ActorNetGUID, Channel);
|
|
}
|
|
}
|
|
|
|
void UDemoNetConnection::NotifyActorChannelCleanedUp(UActorChannel* Channel, EChannelCloseReason CloseReason)
|
|
{
|
|
const UDemoNetDriver* const NetDriver = GetDriver();
|
|
|
|
if (Channel && NetDriver && NetDriver->HasDeltaCheckpoints())
|
|
{
|
|
GetOpenChannelMap().Remove(Channel->ActorNetGUID);
|
|
}
|
|
}
|
|
|
|
bool UDemoNetDriver::IsLevelInitializedForActor(const AActor* InActor, const UNetConnection* InConnection) const
|
|
{
|
|
return (GetDemoFrameNum() > 2 || Super::IsLevelInitializedForActor(InActor, InConnection));
|
|
}
|
|
|
|
bool UDemoNetDriver::IsPlayingClientReplay() const
|
|
{
|
|
return IsPlaying() && EnumHasAnyFlags(ReplayHelper.PlaybackDemoHeader.HeaderFlags, EReplayHeaderFlags::ClientRecorded);
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyGotoTimeFinished(bool bWasSuccessful)
|
|
{
|
|
// execute and clear the transient delegate
|
|
OnGotoTimeDelegate_Transient.ExecuteIfBound(bWasSuccessful);
|
|
OnGotoTimeDelegate_Transient.Unbind();
|
|
|
|
// execute and keep the permanent delegate
|
|
// call only when successful
|
|
if (bWasSuccessful)
|
|
{
|
|
FNetworkReplayDelegates::OnReplayScrubComplete.Broadcast(World);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::OnSeamlessTravelStartDuringRecording(const FString& LevelName)
|
|
{
|
|
if (ClientConnections.Num() > 0)
|
|
{
|
|
ReplayHelper.OnSeamlessTravelStart(World, LevelName, ClientConnections[0]);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::InitDestroyedStartupActors()
|
|
{
|
|
Super::InitDestroyedStartupActors();
|
|
|
|
if (World)
|
|
{
|
|
check(ReplayHelper.RecordingDeletedNetStartupActors.Num() == 0);
|
|
check(ReplayHelper.RecordingDeltaCheckpointData.RecordingDeletedNetStartupActors.Num() == 0);
|
|
|
|
// add startup actors destroyed before the creation of this net driver
|
|
for (FConstLevelIterator LevelIt(World->GetLevelIterator()); LevelIt; ++LevelIt)
|
|
{
|
|
if (const ULevel* Level = *LevelIt)
|
|
{
|
|
const TArray<FReplicatedStaticActorDestructionInfo>& DestroyedReplicatedStaticActors = Level->GetDestroyedReplicatedStaticActors();
|
|
for (const FReplicatedStaticActorDestructionInfo& Info : DestroyedReplicatedStaticActors)
|
|
{
|
|
ReplayHelper.RecordingDeletedNetStartupActors.Add(Info.FullName);
|
|
ReplayHelper.RecordingDeltaCheckpointData.RecordingDeletedNetStartupActors.Add(Info.FullName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyActorDestroyed(AActor* Actor, bool IsSeamlessTravel)
|
|
{
|
|
check(Actor != nullptr);
|
|
|
|
const bool bIsRecording = IsRecording();
|
|
const bool bNetStartup = Actor->IsNetStartupActor();
|
|
const bool bActorRewindable = Actor->bReplayRewindable;
|
|
|
|
if (bActorRewindable && !IsSeamlessTravel && !bIsRecording)
|
|
{
|
|
if (bNetStartup || !TrackedRewindActorsByGUID.Contains(GuidCache->NetGUIDLookup.FindRef(Actor)))
|
|
{
|
|
// This may happen during playback due to new versions of code playing captures with old versions.
|
|
// but this should never happen during recording (otherwise it's likely a game code bug).
|
|
// We catch that case below.
|
|
UE_LOG(LogDemo, Warning, TEXT("Replay Rewindable Actor destroyed during playback. Replay may show artifacts (%s)"), *Actor->GetFullName());
|
|
}
|
|
}
|
|
|
|
if (bIsRecording)
|
|
{
|
|
// We don't want to send any destruction info in this case, because the actor should stick around.
|
|
// The Replay will manage deleting this when it performs streaming or travel behavior.
|
|
if (bNetStartup && IsSeamlessTravel)
|
|
{
|
|
// This is a stripped down version of UNetDriver::NotifyActorDestroy and UActorChannel::Close
|
|
// combined, and should be kept up to date with those methods.
|
|
|
|
|
|
if (UNetConnection* Connection = ClientConnections[0])
|
|
{
|
|
if (Actor->bNetTemporary)
|
|
{
|
|
Connection->SentTemporaries.Remove(Actor);
|
|
}
|
|
|
|
if (UActorChannel* Channel = Connection->FindActorChannelRef(Actor))
|
|
{
|
|
check(Channel->OpenedLocally);
|
|
Channel->bClearRecentActorRefs = false;
|
|
Channel->SetClosingFlag();
|
|
Channel->Actor = nullptr;
|
|
Channel->CleanupReplicators(false);
|
|
}
|
|
|
|
Connection->DormantReplicatorMap.Remove(Actor);
|
|
}
|
|
|
|
GetNetworkObjectList().Remove(Actor);
|
|
RenamedStartupActors.Remove(Actor->GetFName());
|
|
return;
|
|
}
|
|
|
|
if (!IsSeamlessTravel)
|
|
{
|
|
ReplayHelper.NotifyActorDestroyed(ClientConnections[0], Actor);
|
|
}
|
|
}
|
|
|
|
Super::NotifyActorDestroyed(Actor, IsSeamlessTravel);
|
|
}
|
|
|
|
void UDemoNetDriver::CleanupOutstandingRewindActors()
|
|
{
|
|
if (World)
|
|
{
|
|
for (const FNetworkGUID& NetGUID : TrackedRewindActorsByGUID)
|
|
{
|
|
if (FNetGuidCacheObject* CacheObject = GuidCache->ObjectLookup.Find(NetGUID))
|
|
{
|
|
if (AActor* Actor = Cast<AActor>(CacheObject->Object))
|
|
{
|
|
// Destroy the actor before removing entries from the GuidCache so its entries are still valid in NotifyActorDestroyed.
|
|
World->DestroyActor(Actor, true);
|
|
|
|
ensureMsgf(GuidCache->NetGUIDLookup.Remove(CacheObject->Object) > 0, TEXT("CleanupOutstandingRewindActors: No entry found for %d in NetGUIDLookup"), NetGUID.Value);
|
|
GuidCache->ObjectLookup.Remove(NetGUID);
|
|
CacheObject->bNoLoad = false;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("CleanupOutstandingRewindActors - Invalid object for %d, skipping."), NetGUID.Value);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("CleanupOutstandingRewindActors - CacheObject not found for %s"), NetGUID.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
TrackedRewindActorsByGUID.Empty();
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyActorChannelOpen(UActorChannel* Channel, AActor* Actor)
|
|
{
|
|
const bool bValidChannel = ensureMsgf(Channel, TEXT("NotifyActorChannelOpen called with invalid channel"));
|
|
const bool bValidActor = ensureMsgf(Actor, TEXT("NotifyActorChannelOpen called with invalid actor"));
|
|
|
|
// Rewind the actor if necessary.
|
|
// This should be called before any other notifications / data reach the Actor.
|
|
if (bValidChannel && bValidActor && TrackedRewindActorsByGUID.Remove(Channel->ActorNetGUID) > 0)
|
|
{
|
|
Actor->RewindForReplay();
|
|
}
|
|
|
|
// Only necessary on clients where dynamic actors can go in and out of relevancy
|
|
if (bValidChannel && bValidActor && IsRecording() && HasDeltaCheckpoints())
|
|
{
|
|
ReplayHelper.RecordingDeltaCheckpointData.DestroyedDynamicActors.Remove(Channel->ActorNetGUID);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyActorClientDormancyChanged(AActor* Actor, ENetDormancy OldDormancyState)
|
|
{
|
|
if (IsRecording() && (Actor->NetDormancy <= DORM_Awake))
|
|
{
|
|
AddNetworkActor(Actor);
|
|
FlushActorDormancy(Actor);
|
|
|
|
GetNetworkObjectList().MarkActive(Actor, ClientConnections[0], this);
|
|
GetNetworkObjectList().ClearRecentlyDormantConnection(Actor, ClientConnections[0], this);
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyActorChannelCleanedUp(UActorChannel* Channel, EChannelCloseReason CloseReason)
|
|
{
|
|
// channels can be cleaned up during the checkpoint record (dormancy), make sure to skip those
|
|
if (IsRecording() && HasDeltaCheckpoints() && (ReplayHelper.GetCheckpointSaveState() == FReplayHelper::ECheckpointSaveState::Idle))
|
|
{
|
|
if (Channel && Channel->bOpenedForCheckpoint)
|
|
{
|
|
ReplayHelper.RecordingDeltaCheckpointData.ChannelsToClose.Add(Channel->ActorNetGUID, CloseReason);
|
|
}
|
|
}
|
|
|
|
Super::NotifyActorChannelCleanedUp(Channel, CloseReason);
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyActorLevelUnloaded(AActor* Actor)
|
|
{
|
|
if (ServerConnection != nullptr)
|
|
{
|
|
// This is a combination of the Client and Server logic for destroying a channel,
|
|
// since we won't actually be sending data back and forth.
|
|
if (UActorChannel* ActorChannel = ServerConnection->FindActorChannelRef(Actor))
|
|
{
|
|
ServerConnection->RemoveActorChannel(Actor);
|
|
ActorChannel->Actor = nullptr;
|
|
ActorChannel->ConditionalCleanUp(false, EChannelCloseReason::LevelUnloaded);
|
|
}
|
|
}
|
|
|
|
Super::NotifyActorLevelUnloaded(Actor);
|
|
}
|
|
|
|
void UDemoNetDriver::QueueNetStartupActorForRollbackViaDeletion(AActor* Actor)
|
|
{
|
|
if (!IsPlaying())
|
|
{
|
|
return; // We should only be doing this at runtime while playing a replay
|
|
}
|
|
|
|
if (bSkipStartupActorRollback)
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(Actor != nullptr);
|
|
|
|
if (!Actor->IsNetStartupActor())
|
|
{
|
|
return; // We only want startup actors
|
|
}
|
|
|
|
if (Actor->bReplayRewindable)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("Attempted to queue a Replay Rewindable Actor for Rollback Via Deletion. Replay may have artifacts (%s)"), *GetFullNameSafe(Actor));
|
|
return;
|
|
}
|
|
|
|
FString ActorFullName = Actor->GetFullName();
|
|
if (RollbackNetStartupActors.Contains(ActorFullName))
|
|
{
|
|
return; // This actor is already queued up
|
|
}
|
|
|
|
FRollbackNetStartupActorInfo& RollbackActor = RollbackNetStartupActors.Add(MoveTemp(ActorFullName));
|
|
|
|
RollbackActor.Name = Actor->GetFName();
|
|
RollbackActor.Archetype = Actor->GetArchetype();
|
|
RollbackActor.Location = Actor->GetActorLocation();
|
|
RollbackActor.Rotation = Actor->GetActorRotation();
|
|
RollbackActor.Scale3D = Actor->GetActorScale3D();
|
|
|
|
if (ULevel* ActorLevel = Actor->GetLevel())
|
|
{
|
|
RollbackActor.LevelName = ActorLevel->GetOutermost()->GetFName();
|
|
}
|
|
|
|
if (GDemoSaveRollbackActorState != 0)
|
|
{
|
|
{
|
|
TSharedPtr<FObjectReplicator> NewReplicator = MakeShared<FObjectReplicator>();
|
|
NewReplicator->InitWithObject(Actor->GetArchetype(), ServerConnection, false);
|
|
|
|
if (NewReplicator->RepLayout.IsValid() && NewReplicator->RepState.IsValid())
|
|
{
|
|
FReceivingRepState* ReceivingRepState = NewReplicator->RepState->GetReceivingRepState();
|
|
FRepShadowDataBuffer ShadowData(ReceivingRepState->StaticBuffer.GetData());
|
|
FConstRepObjectDataBuffer ActorData(Actor);
|
|
|
|
if (NewReplicator->RepLayout->DiffStableProperties(nullptr, &ToRawPtrTArrayUnsafe(RollbackActor.ObjReferences), ShadowData, ActorData))
|
|
{
|
|
RollbackActor.RepState = MakeShareable(NewReplicator->RepState.Release());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (UActorComponent* ActorComp : Actor->GetComponents())
|
|
{
|
|
if (ActorComp)
|
|
{
|
|
TSharedPtr<FObjectReplicator> SubObjReplicator = MakeShared<FObjectReplicator>();
|
|
SubObjReplicator->InitWithObject(ActorComp->GetArchetype(), ServerConnection, false);
|
|
|
|
if (SubObjReplicator->RepLayout.IsValid() && SubObjReplicator->RepState.IsValid())
|
|
{
|
|
FReceivingRepState* ReceivingRepState = SubObjReplicator->RepState->GetReceivingRepState();
|
|
FRepShadowDataBuffer ShadowData(ReceivingRepState->StaticBuffer.GetData());
|
|
FConstRepObjectDataBuffer ActorCompData(ActorComp);
|
|
|
|
if (SubObjReplicator->RepLayout->DiffStableProperties(nullptr, &ToRawPtrTArrayUnsafe(RollbackActor.ObjReferences), ShadowData, ActorCompData))
|
|
{
|
|
RollbackActor.SubObjRepState.Add(ActorComp->GetFullName(), MakeShareable(SubObjReplicator->RepState.Release()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::ForceNetUpdate(AActor* Actor)
|
|
{
|
|
UReplicationDriver* RepDriver = GetReplicationDriver();
|
|
if (RepDriver)
|
|
{
|
|
RepDriver->ForceNetUpdate(Actor);
|
|
}
|
|
else
|
|
{
|
|
if (FNetworkObjectInfo* NetActor = FindNetworkObjectInfo(Actor))
|
|
{
|
|
// replays use update times relative to DemoCurrentTime and not World->TimeSeconds
|
|
NetActor->NextUpdateTime = GetDemoCurrentTime() - 0.01f;
|
|
}
|
|
}
|
|
}
|
|
|
|
UChannel* UDemoNetDriver::InternalCreateChannelByName(const FName& ChName)
|
|
{
|
|
// In case of recording off the game thread with CVarDemoClientRecordAsyncEndOfFrame,
|
|
// we need to clear the async flag on the channel so that it will get cleaned up by GC.
|
|
// This should be safe since channel objects don't interact with async loading, and
|
|
// async recording happens in a very controlled manner.
|
|
UChannel* NewChannel = Super::InternalCreateChannelByName(ChName);
|
|
if (NewChannel)
|
|
{
|
|
NewChannel->ClearInternalFlags(EInternalObjectFlags::Async);
|
|
}
|
|
return NewChannel;
|
|
}
|
|
|
|
void UDemoNetDriver::NotifyDemoPlaybackFailure(EDemoPlayFailure::Type FailureType)
|
|
{
|
|
UE_LOG(LogDemo, Warning, TEXT("Demo playback failure: '%s'"), EDemoPlayFailure::ToString(FailureType));
|
|
|
|
const bool bIsPlaying = IsPlaying();
|
|
|
|
// fire delegate
|
|
FNetworkReplayDelegates::OnReplayStartFailure.Broadcast(World, FailureType);
|
|
|
|
StopDemo();
|
|
|
|
if (bIsPlaying)
|
|
{
|
|
if (World)
|
|
{
|
|
if (UGameInstance* GameInstance = World->GetGameInstance())
|
|
{
|
|
GameInstance->HandleDemoPlaybackFailure(FailureType, FString(EDemoPlayFailure::ToString(FailureType)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FString UDemoNetDriver::GetDemoPath() const
|
|
{
|
|
if (ReplayHelper.ReplayStreamer.IsValid())
|
|
{
|
|
FString DemoPath;
|
|
if (ReplayHelper.ReplayStreamer->GetDemoPath(DemoPath) == EStreamingOperationResult::Success)
|
|
{
|
|
return DemoPath;
|
|
}
|
|
}
|
|
|
|
return FString();
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldReplicateFunction(AActor* Actor, UFunction* Function) const
|
|
{
|
|
// ReplayNetConnection does not currently have this functionality, as it filters fast shared rpcs directly in the rep graph
|
|
bool bShouldRecordMulticast = (Function && Function->FunctionFlags & FUNC_NetMulticast) && IsRecording();
|
|
if (bShouldRecordMulticast)
|
|
{
|
|
const FString FuncPathName = GetPathNameSafe(Function);
|
|
const int32 Idx = MulticastRecordOptions.IndexOfByPredicate([FuncPathName](const FMulticastRecordOptions& Options) { return (Options.FuncPathName == FuncPathName); });
|
|
if (Idx != INDEX_NONE)
|
|
{
|
|
if (World && World->IsRecordingClientReplay())
|
|
{
|
|
bShouldRecordMulticast = bShouldRecordMulticast && !MulticastRecordOptions[Idx].bClientSkip;
|
|
}
|
|
else
|
|
{
|
|
bShouldRecordMulticast = bShouldRecordMulticast && !MulticastRecordOptions[Idx].bServerSkip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bShouldRecordMulticast || Super::ShouldReplicateFunction(Actor, Function);
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldReplicateActor(AActor* Actor) const
|
|
{
|
|
// replicate actors that share the demo net driver name, or actors belonging to the game net driver
|
|
return (Actor && Actor->GetIsReplicated()) && (Super::ShouldReplicateActor(Actor) || (Actor->GetNetDriverName() == NAME_GameNetDriver));
|
|
}
|
|
|
|
/*
|
|
* If a large number of Actors makes it onto the NetworkObjectList, and Demo Recording is limited,
|
|
* then we can easily hit cases where building the Consider List and Sorting it can take up the
|
|
* entire time slice. In that case, we'll have spent a lot of time setting up for replication,
|
|
* but never actually doing it.
|
|
* Further, if dormancy is used, dormant actors need to replicate once before they're removed from
|
|
* the NetworkObjectList. That means in the worst case, we can have a large number of dormant actors
|
|
* artificially driving up consider / sort times.
|
|
*
|
|
* To prevent that, we'll throttle the amount of time we spend prioritize next frame based
|
|
* on how much time it took this frame.
|
|
*
|
|
* @param TimeSlicePercent The current percent of time allocated to building consider lists / prioritizing.
|
|
* @param ReplicatedPercet The percent of actors that were replicated this last frame.
|
|
*/
|
|
void UDemoNetDriver::AdjustConsiderTime(const float ReplicatedPercent)
|
|
{
|
|
if (MaxDesiredRecordTimeMS > 0.f)
|
|
{
|
|
auto ConditionallySwap = [](float& Less, float& More)
|
|
{
|
|
if (More < Less)
|
|
{
|
|
Swap(Less, More);
|
|
}
|
|
};
|
|
|
|
float DecreaseThreshold = CVarDemoDecreaseRepPrioritizeThreshold.GetValueOnAnyThread();
|
|
float IncreaseThreshold = CVarDemoIncreaseRepPrioritizeThreshold.GetValueOnAnyThread();
|
|
ConditionallySwap(DecreaseThreshold, IncreaseThreshold);
|
|
|
|
float MinRepTime = CVarDemoMinimumRepPrioritizeTime.GetValueOnAnyThread();
|
|
float MaxRepTime = CVarDemoMaximumRepPrioritizeTime.GetValueOnAnyThread();
|
|
ConditionallySwap(MinRepTime, MaxRepTime);
|
|
MinRepTime = FMath::Clamp<float>(MinRepTime, 0.1, 1.0);
|
|
MaxRepTime = FMath::Clamp<float>(MaxRepTime, 0.1, 1.0);
|
|
|
|
if (ReplicatedPercent > IncreaseThreshold)
|
|
{
|
|
RecordBuildConsiderAndPrioritizeTimeSlice += 0.1f;
|
|
UE_LOG(LogDemo, Verbose, TEXT("AdjustConsiderTime: RecordBuildConsiderAndPrioritizeTimeSlice is now %0.1f"), RecordBuildConsiderAndPrioritizeTimeSlice)
|
|
}
|
|
else if (ReplicatedPercent < DecreaseThreshold)
|
|
{
|
|
RecordBuildConsiderAndPrioritizeTimeSlice *= (1.f - ReplicatedPercent) * 0.5f;
|
|
UE_LOG(LogDemo, Verbose, TEXT("AdjustConsiderTime: RecordBuildConsiderAndPrioritizeTimeSlice is now %0.1f"), RecordBuildConsiderAndPrioritizeTimeSlice)
|
|
}
|
|
|
|
RecordBuildConsiderAndPrioritizeTimeSlice = FMath::Clamp<float>(RecordBuildConsiderAndPrioritizeTimeSlice, MinRepTime, MaxRepTime);
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
UDemoPendingNetGame.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
UDemoPendingNetGame::UDemoPendingNetGame(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
void UDemoPendingNetGame::Tick(float DeltaTime)
|
|
{
|
|
// Replays don't need to do anything here
|
|
}
|
|
|
|
void UDemoPendingNetGame::SendJoin()
|
|
{
|
|
// Don't send a join request to a replay
|
|
}
|
|
|
|
void UDemoPendingNetGame::LoadMapCompleted(UEngine* Engine, FWorldContext& Context, bool bLoadedMapSuccessfully, const FString& LoadMapError)
|
|
{
|
|
UDemoNetDriver* TheDriver = GetDemoNetDriver();
|
|
|
|
// If we have a demo pending net game we should have a demo net driver
|
|
check(TheDriver);
|
|
|
|
if (!bLoadedMapSuccessfully)
|
|
{
|
|
TheDriver->StopDemo();
|
|
|
|
// If we don't have a world that means we failed loading the new world.
|
|
// Since there is no world, we must free the net driver ourselves
|
|
// Technically the pending net game should handle it, but things aren't quite setup properly to handle that either
|
|
if (Context.World() == nullptr)
|
|
{
|
|
GEngine->DestroyNamedNetDriver(Context.PendingNetGame, TheDriver->NetDriverName);
|
|
}
|
|
|
|
Context.PendingNetGame = nullptr;
|
|
|
|
GEngine->BrowseToDefaultMap(Context);
|
|
|
|
UE_LOG(LogDemo, Error, TEXT("UDemoPendingNetGame::HandlePostLoadMap: LoadMap failed: %s"), *LoadMapError);
|
|
if (Context.OwningGameInstance)
|
|
{
|
|
Context.OwningGameInstance->HandleDemoPlaybackFailure(EDemoPlayFailure::LoadMap, FString(TEXT("LoadMap failed")));
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
void UDemoNetDriver::Serialize(FArchive& Ar)
|
|
{
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_INIT(Ar, "UDemoNetDriver::Serialize");
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("Super", Super::Serialize(Ar));
|
|
|
|
if (Ar.IsCountingMemory())
|
|
{
|
|
// TODO: We don't currently track:
|
|
// Replay Streamers
|
|
// Dynamic Delegate Data
|
|
// QueuedReplayTasks.
|
|
// DemoURL
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("PlaybackPackets",
|
|
PlaybackPackets.CountBytes(Ar);
|
|
for (const FPlaybackPacket& Packet : PlaybackPackets)
|
|
{
|
|
Packet.CountBytes(Ar);
|
|
}
|
|
);
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("NonQueuedGUIDsForScrubbing", NonQueuedGUIDsForScrubbing.CountBytes(Ar));
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("QueuedReplayTasks", QueuedReplayTasks.CountBytes(Ar));
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("DemoSessionID", DemoSessionID.CountBytes(Ar));
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("PrioritizedActors", PrioritizedActors.CountBytes(Ar));
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("LevelInternals", LevelIntervals.CountBytes(Ar));
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("TrackedRewindActorsByGUID", TrackedRewindActorsByGUID.CountBytes(Ar));
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("QueuedPacketsBeforeTravel",
|
|
QueuedPacketsBeforeTravel.CountBytes(Ar);
|
|
for (const FQueuedDemoPacket& QueuedPacket : QueuedPacketsBeforeTravel)
|
|
{
|
|
QueuedPacket.CountBytes(Ar);
|
|
}
|
|
);
|
|
|
|
ReplayHelper.Serialize(Ar);
|
|
}
|
|
}
|
|
|
|
void UDemoNetConnection::Serialize(FArchive& Ar)
|
|
{
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_INIT(Ar, "UDemoNetConnection::Serialize");
|
|
|
|
GRANULAR_NETWORK_MEMORY_TRACKING_TRACK("Super", Super::Serialize(Ar));
|
|
}
|
|
|
|
void UDemoNetDriver::SetAnalyticsProvider(TSharedPtr<IAnalyticsProvider> InProvider)
|
|
{
|
|
Super::SetAnalyticsProvider(InProvider);
|
|
|
|
ReplayHelper.SetAnalyticsProvider(InProvider);
|
|
}
|
|
|
|
void UDemoNetDriver::SetWorld(UWorld* InWorld)
|
|
{
|
|
Super::SetWorld(InWorld);
|
|
|
|
ReplayHelper.World = InWorld;
|
|
}
|
|
|
|
bool UDemoNetDriver::ShouldForwardFunction(AActor* Actor, UFunction* Function, void* Parms) const
|
|
{
|
|
// currently no need to forward replay playback RPCs on to other drivers
|
|
return false;
|
|
}
|
|
|
|
void UDemoNetDriver::RequestCheckpoint()
|
|
{
|
|
if (IsRecording())
|
|
{
|
|
ReplayHelper.RequestCheckpoint();
|
|
}
|
|
}
|