// Copyright Epic Games, Inc. All Rights Reserved. #include "MassLookAtSubsystem.h" #include "GameFramework/Pawn.h" #include "MassActorSubsystem.h" #include "MassAIBehaviorTypes.h" #include "MassEntitySubsystem.h" #include "MassEntityView.h" #include "MassSignalSubsystem.h" #include "MassStateTreeTypes.h" namespace UE::Mass::LookAt::Private { void FlushCommands(FMassEntityManager& EntityManager, const TSharedPtr& CommandBuffer) { ExecuteOnGameThread(UE_SOURCE_LOCATION, [WeakEntityManager = EntityManager.AsWeak(), CommandBuffer]() { if (const TSharedPtr SharedEntityManager = WeakEntityManager.Pin()) { SharedEntityManager->FlushCommands(CommandBuffer); } else { CommandBuffer->CancelCommands(); } }); } bool CreateRequests(FMassEntityManager& InEntityManager , const FMassArchetypeHandle& RequestArchetype , const TConstArrayView Viewers , const TConstArrayView Requests , const FMassEntityHandle TargetEntity , const FMassLookAtPriority Priority , const EMassLookAtInterpolationSpeed InterpolationSpeed , const float CustomInterpolationSpeed = DefaultCustomInterpolationSpeed) { if (Viewers.IsEmpty()) { return false; } checkf(Viewers.Num() == Requests.Num(), TEXT("Number of reserved entities for requests must match the number of provided viewer entities.")); const TSharedRef CreationContext = InEntityManager.BatchCreateReservedEntities(RequestArchetype, /*SharedFragmentValues*/{}, Requests); for (int Index = 0; Index < Requests.Num(); ++Index) { InEntityManager.SetEntityFragmentValues(Requests[Index], { FInstancedStruct::Make( FMassLookAtRequestFragment { Viewers[Index] , Priority , EMassLookAtMode::LookAtEntity , TargetEntity , InterpolationSpeed , CustomInterpolationSpeed })}); UE_VLOG_UELOG(InEntityManager.GetOwner(), LogMassBehavior, Log, TEXT("Created LookAt Request '%s', Target '%s', Priority=%s") , *LexToString(Requests[Index]) , *LexToString(TargetEntity) , *LexToString(Priority.Get())) } return true; } bool CreateTargets(FMassEntityManager& InEntityManager , const FMassArchetypeHandle& TargetArchetype , const TConstArrayView Targets , const TConstArrayView Transforms , const FMassLookAtPriority Priority) { if (Targets.IsEmpty()) { return false; } checkf(Targets.Num() == Transforms.Num(), TEXT("Number of reserved entities for targets must match the number of provided transforms.")); // This needs to stay in sync with 'TargetArchetype' created on subsystem initialization TArray> FragmentInstanceList; FragmentInstanceList.Add(FInstancedStruct::Make(FMassLookAtTargetFragment{ .Offset = FVector::ZeroVector, .Priority = Priority })); FTransformFragment& TransformFragment = FragmentInstanceList.Add_GetRef({ FInstancedStruct::Make(FTransformFragment{})}).GetMutable(); const TSharedRef CreationContext = InEntityManager.BatchCreateReservedEntities(TargetArchetype, /*SharedFragmentValues*/{}, Targets); for (int Index = 0; Index < Targets.Num(); ++Index) { TransformFragment.SetTransform(Transforms[Index]); InEntityManager.SetEntityFragmentValues(Targets[Index], FragmentInstanceList); UE_VLOG_UELOG(InEntityManager.GetOwner(), LogMassBehavior, Log, TEXT("Created LookAtTarget '%s' at '%s'") , *LexToString(Targets[Index]) , *LexToString(TransformFragment.GetTransform().ToString())) UE_VLOG_LOCATION(InEntityManager.GetOwner(), LogMassBehavior, Display , TransformFragment.GetTransform().GetLocation(), /*Thickness*/50, FColor::Yellow, TEXT("")); } return true; } TArray DebugRequests; static FAutoConsoleCommandWithWorldArgsAndOutputDevice CmdSendLookAtPlayerRequestToAll( TEXT("ai.debug.mass.SendLookAtPlayerRequestToAll"), TEXT("Creates, or removed, LookAt requests toward the player for all mass entities with a LookAt fragment" "(optional)<0|1> to indicate if the requests must be created (1, default) or deleted (0)." "(optional) to indicate the priority of the request where a lower value represents a higher priority (default is 5)"), FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray& Args, const UWorld* World, FOutputDevice& OutputDevice) { bool bCreateRequest = true; uint8 PriorityLevel = static_cast(EMassLookAtPriorities::LowestPriority); if (Args.Num() > 2) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: invalid number of arguments")); return; } if (Args.Num() > 0) { if (!LexTryParseString(bCreateRequest, *Args[0])) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Unable to parse the first argument: expecting 0|1 or true|false")); return; } if (Args.Num() > 1) { if (!LexTryParseString(PriorityLevel, *Args[1])) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Unable to parse the second argument: expecting an [0-255] integer to represent the priority")); return; } // Clamp priority in valid range (highest value being the lowest priority) if (constexpr uint8 LastPriority = static_cast(EMassLookAtPriorities::LowestPriority); PriorityLevel > LastPriority) { OutputDevice.Log(ELogVerbosity::Warning, FString::Printf(TEXT("Clamped priority level to the lowest priority %d"), LastPriority)); PriorityLevel = FMath::Clamp(PriorityLevel, 0, LastPriority); } } } UMassActorSubsystem* ActorSubsystem = World->GetSubsystem(); if (ActorSubsystem == nullptr) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find MassActorSubsystem")); return; } const APlayerController* PlayerController = World->GetFirstPlayerController(); if (PlayerController == nullptr) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find the player controller")); return; } const APawn* PlayerPawn = PlayerController->GetPawn(); if (PlayerPawn == nullptr) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find the player pawn")); return; } UMassEntitySubsystem* EntitySubsystem = World->GetSubsystem(); if (EntitySubsystem == nullptr) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find UMassEntitySubsystem")); return; } const FMassEntityHandle PlayerEntity = ActorSubsystem->GetEntityHandleFromActor(PlayerPawn); if (!PlayerEntity.IsSet()) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find a MassEntity associated to the player")); return; } const UMassLookAtSubsystem* LookAtSubsystem = World->GetSubsystem(); if (LookAtSubsystem == nullptr) { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed: unable to find UMassLookAtSubsystem")); return; } TSharedPtr CommandBuffer = MakeShareable(new FMassCommandBuffer()); if (bCreateRequest) { // Create requests for all entities with a MassLookAtFragment CommandBuffer->PushCommand([PlayerEntity, PriorityLevel, RequestArchetype = LookAtSubsystem->DebugGetRequestArchetype()](FMassEntityManager& InEntityManager) { FMassEntityQuery EntityQuery(InEntityManager.AsShared()); EntityQuery.AddRequirement(EMassFragmentAccess::ReadOnly); TArray Viewers = EntityQuery.GetMatchingEntityHandles(); CreateRequests(InEntityManager , RequestArchetype , Viewers , InEntityManager.BatchReserveEntities(Viewers.Num(), DebugRequests) , PlayerEntity , FMassLookAtPriority{ PriorityLevel } , EMassLookAtInterpolationSpeed::Regular); }); } else { // Delete all entities created for debug requests CommandBuffer->PushCommand([](FMassEntityManager& InEntityManager) { InEntityManager.BatchDestroyEntities(DebugRequests); DebugRequests.Reset(); }); } FlushCommands(EntitySubsystem->GetMutableEntityManager(), CommandBuffer); } )); } // UE::Mass::LookAt::Private FMassLookAtRequestHandle UMassLookAtSubsystem::CreateLookAtPositionRequest( AActor* ViewerActor , const FMassLookAtPriority Priority , const FVector TargetLocation , const EMassLookAtInterpolationSpeed InterpolationSpeed , const float CustomInterpolationSpeed) const { const UWorld* World = GetWorld(); UMassActorSubsystem* ActorSubsystem = World->GetSubsystem(); if (ActorSubsystem == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find MassActorSubsystem"), __FUNCTION__); return {}; } UMassEntitySubsystem* EntitySubsystem = World->GetSubsystem(); if (EntitySubsystem == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find UMassEntitySubsystem"), __FUNCTION__); return {}; } const FMassEntityHandle ViewerEntity = ActorSubsystem->GetEntityHandleFromActor(ViewerActor); if (!ViewerEntity.IsSet()) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find a MassEntity associated to '%s'"), __FUNCTION__, *GetNameSafe(ViewerActor)); return {}; } // @todo: once available, consider using incoming EntityBuilder improvements for this whole creation step FMassEntityManager& EntityManager = EntitySubsystem->GetMutableEntityManager(); FMassEntityHandle TargetEntity = EntityManager.ReserveEntity(); FMassEntityHandle RequestEntity = EntityManager.ReserveEntity(); FTransform TargetTransform(TargetLocation); // Push command to create a new entity representing a LookAt target along with a request to look at it const TSharedPtr CommandBuffer = MakeShareable(new FMassCommandBuffer()); CommandBuffer->PushCommand( [ TargetArchetype = TargetArchetype , RequestArchetype = RequestArchetype , RequestEntity , ViewerEntity , TargetEntity , TargetTransform , Priority , InterpolationSpeed , CustomInterpolationSpeed ] (FMassEntityManager& InEntityManager) { UE::Mass::LookAt::Private::CreateTargets(InEntityManager, TargetArchetype, { TargetEntity }, { TargetTransform }, Priority); UE::Mass::LookAt::Private::CreateRequests( InEntityManager , RequestArchetype , { ViewerEntity } , { RequestEntity } , TargetEntity , Priority , InterpolationSpeed , CustomInterpolationSpeed); }); UE::Mass::LookAt::Private::FlushCommands(EntityManager, CommandBuffer); return FMassLookAtRequestHandle{RequestEntity, TargetEntity, /*bTargetEntityOwnedByRequest*/true}; } FMassLookAtRequestHandle UMassLookAtSubsystem::CreateLookAtActorRequest( AActor* ViewerActor , const FMassLookAtPriority Priority , AActor* TargetActor , const EMassLookAtInterpolationSpeed InterpolationSpeed , const float CustomInterpolationSpeed) const { const UWorld* World = GetWorld(); if (TargetActor == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Log, TEXT("%hs failed: invalid target actor"), __FUNCTION__); return {}; } UMassActorSubsystem* ActorSubsystem = World->GetSubsystem(); if (ActorSubsystem == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find MassActorSubsystem"), __FUNCTION__); return {}; } const FMassEntityHandle TargetEntity = ActorSubsystem->GetEntityHandleFromActor(TargetActor); if (!TargetEntity.IsSet()) { UE_VLOG_UELOG(this, LogMassBehavior, Log, TEXT("%hs: using static target location since no MassEntity is associated to '%s'"), __FUNCTION__, *GetNameSafe(TargetActor)); return CreateLookAtPositionRequest(ViewerActor, Priority, TargetActor->GetActorLocation(), InterpolationSpeed, CustomInterpolationSpeed); } UMassEntitySubsystem* EntitySubsystem = World->GetSubsystem(); if (EntitySubsystem == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find UMassEntitySubsystem"), __FUNCTION__); return {}; } const FMassEntityHandle ViewerEntity = ActorSubsystem->GetEntityHandleFromActor(ViewerActor); if (!ViewerEntity.IsSet()) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find a MassEntity associated to '%s'"), __FUNCTION__, *GetNameSafe(ViewerActor)); return {}; } // @todo: once available, consider using incoming EntityBuilder improvements for this whole creation step FMassEntityManager& EntityManager = EntitySubsystem->GetMutableEntityManager(); FMassEntityHandle RequestEntity = EntityManager.ReserveEntity(); // Push command to create a new entity representing a LookAt target along with a request to look at it const TSharedPtr CommandBuffer = MakeShareable(new FMassCommandBuffer()); CommandBuffer->PushCommand( [ RequestArchetype = RequestArchetype , RequestEntity , ViewerEntity , TargetEntity , Priority , InterpolationSpeed , CustomInterpolationSpeed ] (FMassEntityManager& InEntityManager) { UE::Mass::LookAt::Private::CreateRequests( InEntityManager , RequestArchetype , { ViewerEntity } , { RequestEntity } , TargetEntity , Priority , InterpolationSpeed , CustomInterpolationSpeed); }); UE::Mass::LookAt::Private::FlushCommands(EntityManager, CommandBuffer); return FMassLookAtRequestHandle{RequestEntity, TargetEntity, /*bTargetEntityOwnedByRequest*/false}; } void UMassLookAtSubsystem::DeleteRequest(const FMassLookAtRequestHandle RequestHandle) const { // Simple validation when none of the entities are set since it is probably due to a bad data setup. // Otherwise, the EntityManager can process gracefully the handles, valid or not. if (!RequestHandle.Request.IsSet() && !RequestHandle.Target.IsSet()) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: invalid request handle"), __FUNCTION__); return; } const UWorld* World = GetWorld(); if (World == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find World"), __FUNCTION__); return; } UMassEntitySubsystem* EntitySubsystem = World->GetSubsystem(); if (EntitySubsystem == nullptr) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("%hs failed: unable to find UMassEntitySubsystem"), __FUNCTION__); return; } const TSharedPtr CommandBuffer = MakeShareable(new FMassCommandBuffer()); CommandBuffer->PushCommand([RequestHandle](FMassEntityManager& InEntityManager) { if (RequestHandle.bTargetEntityOwnedByRequest) { InEntityManager.BatchDestroyEntities({ RequestHandle.Request, RequestHandle.Target }); } else { InEntityManager.DestroyEntity(RequestHandle.Request); } }); UE::Mass::LookAt::Private::FlushCommands(EntitySubsystem->GetMutableEntityManager(), CommandBuffer); } //----------------------------------------------------------------------// // UMassLookAtSubsystem //----------------------------------------------------------------------// void UMassLookAtSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); FMassEntityManager& EntityManager = Collection.InitializeDependency()->GetMutableEntityManager(); // Create Mass archetype for entities representing requests const FMassArchetypeCompositionDescriptor RequestComposition{ FMassFragmentBitSet(*FMassLookAtRequestFragment::StaticStruct()) }; RequestArchetype = EntityManager.CreateArchetype(RequestComposition); // Create Mass archetype for entities representing targets TArray> TargetFragmentTypes{FMassLookAtTargetFragment::StaticStruct(), FTransformFragment::StaticStruct()}; const FMassArchetypeCompositionDescriptor TargetComposition{ FMassFragmentBitSet(TargetFragmentTypes) }; TargetArchetype = EntityManager.CreateArchetype(TargetComposition); OverrideSubsystemTraits(Collection); } TStatId UMassLookAtSubsystem::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(UMassLookAtSubsystem, STATGROUP_Tickables); } void UMassLookAtSubsystem::RegisterRequests(const FMassExecutionContext& InContext, TArray&& InRequests) { TArray DirtyViewers; DirtyViewers.Reserve(InRequests.Num()); { UE_MT_SCOPED_WRITE_ACCESS(RequestsAccessDetector); TRACE_CPUPROFILER_EVENT_SCOPE_STR("MassLookAt_RegisterRequests") RegisteredRequests.Reserve(RegisteredRequests.Num() + (InRequests.Num() - ActiveRequestsFreeList.Num())); for (const FRequest& NewRequest : InRequests) { if (NewRequest.Parameters.LookAtMode == EMassLookAtMode::LookAtEntity && !NewRequest.Parameters.TargetEntity.IsSet()) { UE_VLOG_UELOG(this, LogMassBehavior, Error, TEXT("Ignoring LookAtEntity request: invalid target entity")); continue; } int32 NewRequestIndex = INDEX_NONE; if (ActiveRequestsFreeList.Num()) { NewRequestIndex = ActiveRequestsFreeList.Pop(); RegisteredRequests[NewRequestIndex] = NewRequest; } else { NewRequestIndex = RegisteredRequests.Add(NewRequest); } RequestHandleToIndexMap.Add(NewRequest.RequestHandle, NewRequestIndex); int32& ViewerDataIndex = ViewerHandleToIndexMap.FindOrAdd(NewRequest.Parameters.ViewerEntity, INDEX_NONE); if (ViewerDataIndex == INDEX_NONE) { ViewerDataIndex = PerViewerRequests.Num(); PerViewerRequests.AddDefaulted_GetRef().Viewer = NewRequest.Parameters.ViewerEntity; } DirtyViewers.Add(ViewerDataIndex); PerViewerRequests[ViewerDataIndex].RequestIndices.Add(NewRequestIndex); } } UpdateLookAts(InContext, DirtyViewers); } void UMassLookAtSubsystem::UnregisterRequests(const FMassExecutionContext& InContext, const TConstArrayView InRequests) { TArray DirtyViewers; DirtyViewers.Reserve(InRequests.Num()); { UE_MT_SCOPED_WRITE_ACCESS(RequestsAccessDetector); TRACE_CPUPROFILER_EVENT_SCOPE_STR("MassLookAt_UnregisterRequests") for (const FMassEntityHandle& RemovedRequest : InRequests) { int32 InvalidatedIndex = INDEX_NONE; RequestHandleToIndexMap.RemoveAndCopyValue(RemovedRequest, InvalidatedIndex); if (ensureMsgf(InvalidatedIndex != INDEX_NONE, TEXT("Trying to remove a request that was never registered"))) { // Invalidate entry and add to the free list const FMassEntityHandle ViewerEntity = RegisteredRequests[InvalidatedIndex].Parameters.ViewerEntity; RegisteredRequests[InvalidatedIndex].RequestHandle.Reset(); ActiveRequestsFreeList.Push(InvalidatedIndex); const int32* ViewerDataIndex = ViewerHandleToIndexMap.Find(ViewerEntity); if (ensureMsgf(ViewerDataIndex, TEXT("Unable to find the per viewer data; looks like a bookkeeping issue"))) { checkf(*ViewerDataIndex != INDEX_NONE, TEXT("INDEX_NONEs are not expected to be stored in ViewerHandleToIndexMap")); DirtyViewers.Add(*ViewerDataIndex); PerViewerRequests[*ViewerDataIndex].RequestIndices.Remove(InvalidatedIndex); } } } } UpdateLookAts(InContext, DirtyViewers); } void UMassLookAtSubsystem::UpdateLookAts(const FMassExecutionContext& Context, const TConstArrayView DirtyViewers) { TRACE_CPUPROFILER_EVENT_SCOPE_STR("MassLookAt_UpdateLookAts") TArray> Updated; for (const int32 DirtyViewerIndex : DirtyViewers) { check(PerViewerRequests.IsValidIndex(DirtyViewerIndex)); int32 SelectedRequestIndex = INDEX_NONE; for (const int32 RequestIndex : PerViewerRequests[DirtyViewerIndex].RequestIndices) { check(RegisteredRequests.IsValidIndex(RequestIndex)); RegisteredRequests[RequestIndex].bActive = false; if (SelectedRequestIndex == INDEX_NONE) { SelectedRequestIndex = RequestIndex; continue; } // Higher priority is represented by the lowest value if (RegisteredRequests[RequestIndex].Parameters.Priority.Get() < RegisteredRequests[SelectedRequestIndex].Parameters.Priority.Get()) { SelectedRequestIndex = RequestIndex; } } FMassLookAtRequestFragment RequestFragment; if (SelectedRequestIndex != INDEX_NONE) { RegisteredRequests[SelectedRequestIndex].bActive = true; RequestFragment = RegisteredRequests[SelectedRequestIndex].Parameters; } Updated.Add({ PerViewerRequests[DirtyViewerIndex].Viewer, RequestFragment }); } Context.Defer().PushCommand([Updated = MoveTemp(Updated)](const FMassEntityManager& Manager) { TArray EntitiesToSignal; EntitiesToSignal.Reserve(Updated.Num()); for (const TPair& Pair : Updated) { const FMassEntityHandle Entity = Pair.Key; if (FMassLookAtFragment* LookAtFragment = Manager.IsEntityValid(Entity) ? Manager.GetFragmentDataPtr(Entity) : nullptr) { const FMassLookAtRequestFragment& Request = Pair.Value; // Default request is used to clear the override const bool bIsClearOverrideRequest = !Request.ViewerEntity.IsSet(); switch (LookAtFragment->OverrideState) { case FMassLookAtFragment::EOverrideState::AllDisabled: if (bIsClearOverrideRequest) { // Already disabled no need to change the fragment continue; } LookAtFragment->OverrideState = FMassLookAtFragment::EOverrideState::ActiveOverrideOnly; break; case FMassLookAtFragment::EOverrideState::ActiveOverrideOnly: if (bIsClearOverrideRequest) { LookAtFragment->OverrideState = FMassLookAtFragment::EOverrideState::AllDisabled; } // In both cases we set values from the request to apply the override or to clear the active one break; case FMassLookAtFragment::EOverrideState::ActiveSystemicOnly: if (bIsClearOverrideRequest) { // Override already cleared, do not change the fragment (systemic is active) continue; } // Switch to 'OverridenSystemic' state and apply request parameters LookAtFragment->OverrideState = FMassLookAtFragment::EOverrideState::OverridenSystemic; break; case FMassLookAtFragment::EOverrideState::OverridenSystemic: if (bIsClearOverrideRequest) { // Mark as pending reactivation and signal the entity to wake up the task to retry activation of the systemic LookAt LookAtFragment->OverrideState = FMassLookAtFragment::EOverrideState::PendingSystemicReactivation; EntitiesToSignal.Add(Pair.Key); continue; } // Stay in 'OverridenSystemic' state and apply new request parameters break; case FMassLookAtFragment::EOverrideState::PendingSystemicReactivation: if (bIsClearOverrideRequest) { // Already pending reactivation and signaled, no need to signal again continue; } // Switch back to 'OverridenSystemic' so task will not apply its values when processing the signal LookAtFragment->OverrideState = FMassLookAtFragment::EOverrideState::OverridenSystemic; break; } // Only update main LookAt information and don't modify gaze related one LookAtFragment->InterpolationSpeed = Request.InterpolationSpeed; LookAtFragment->CustomInterpolationSpeed = Request.CustomInterpolationSpeed; LookAtFragment->LookAtMode = Request.LookAtMode; LookAtFragment->TrackedEntity = Request.TargetEntity; } } // Signal all entities if (EntitiesToSignal.Num()) { if (UMassSignalSubsystem* SignalSubsystem = Manager.GetWorld()->GetSubsystem()) { SignalSubsystem->SignalEntities(UE::Mass::Signals::LookAtFinished, EntitiesToSignal); } } }); } #if WITH_MASSGAMEPLAY_DEBUG FString UMassLookAtSubsystem::DebugGetRequestsString(const FMassEntityHandle InEntity) const { UE_MT_SCOPED_READ_ACCESS(RequestsAccessDetector); const int32* ViewerDataIndex = ViewerHandleToIndexMap.Find(InEntity); TStringBuilder<256> Builder; if (ViewerDataIndex && *ViewerDataIndex != INDEX_NONE) { for (const int32 RequestIndex : PerViewerRequests[*ViewerDataIndex].RequestIndices) { if (Builder.Len() > 0) { Builder << TEXT("\n"); } Builder << (RegisteredRequests[RequestIndex].bActive ? TEXT(">") : TEXT(" ")); Builder << LexToString(RegisteredRequests[RequestIndex].Parameters); } } return Builder.ToString(); } #endif // WITH_MASSGAMEPLAY_DEBUG