You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
The fix involved an extensive rework of how the testing environment is being set up, to limit loaded UWorld's influence on the results, and in turn limit how tests affect SmartObjectSubsystem instantiated for the current world. #preflight 6368c59e63037c1026540f38 [CL 23010255 by mieszko zielinski in ue5-main branch]
399 lines
13 KiB
C++
399 lines
13 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SmartObjectCollection.h"
|
|
|
|
#include "GameplayTagAssetInterface.h"
|
|
#include "SmartObjectTypes.h"
|
|
#include "SmartObjectSubsystem.h"
|
|
#include "SmartObjectComponent.h"
|
|
#include "Engine/Level.h"
|
|
#include "Engine/World.h"
|
|
#include "VisualLogger/VisualLogger.h"
|
|
#include "Engine/LevelStreaming.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(SmartObjectCollection)
|
|
|
|
//----------------------------------------------------------------------//
|
|
// FSmartObjectCollectionEntry
|
|
//----------------------------------------------------------------------//
|
|
FSmartObjectCollectionEntry::FSmartObjectCollectionEntry(const FSmartObjectHandle SmartObjectHandle, const USmartObjectComponent& SmartObjectComponent, const uint32 DefinitionIndex)
|
|
: Path(&SmartObjectComponent)
|
|
, Transform(SmartObjectComponent.GetComponentTransform())
|
|
, Bounds(SmartObjectComponent.GetSmartObjectBounds())
|
|
, Handle(SmartObjectHandle)
|
|
, DefinitionIdx(DefinitionIndex)
|
|
{
|
|
if (const IGameplayTagAssetInterface* TagInterface = Cast<IGameplayTagAssetInterface>(SmartObjectComponent.GetOwner()))
|
|
{
|
|
TagInterface->GetOwnedGameplayTags(Tags);
|
|
}
|
|
}
|
|
|
|
USmartObjectComponent* FSmartObjectCollectionEntry::GetComponent() const
|
|
{
|
|
return CastChecked<USmartObjectComponent>(Path.ResolveObject(), ECastCheckedType::NullAllowed);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------//
|
|
// ASmartObjectCollection
|
|
//----------------------------------------------------------------------//
|
|
ASmartObjectCollection::ASmartObjectCollection(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
bActorLabelEditable = false;
|
|
#endif
|
|
|
|
PrimaryActorTick.bCanEverTick = false;
|
|
bNetLoadOnClient = false;
|
|
SetCanBeDamaged(false);
|
|
}
|
|
|
|
void ASmartObjectCollection::PostLoad()
|
|
{
|
|
Super::PostLoad();
|
|
#if WITH_EDITORONLY_DATA
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
bBuildCollectionAutomatically = !bBuildOnDemand_DEPRECATED;
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
#endif // WITH_EDITORONLY_DATA
|
|
}
|
|
|
|
void ASmartObjectCollection::Destroyed()
|
|
{
|
|
// Handle editor delete.
|
|
UnregisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
Super::Destroyed();
|
|
}
|
|
|
|
void ASmartObjectCollection::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
// Handle Level unload, PIE end, SIE end, game end.
|
|
UnregisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
Super::EndPlay(EndPlayReason);
|
|
}
|
|
|
|
void ASmartObjectCollection::PostActorCreated()
|
|
{
|
|
// Register after being initially spawned.
|
|
Super::PostActorCreated();
|
|
RegisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
|
|
void ASmartObjectCollection::PreRegisterAllComponents()
|
|
{
|
|
Super::PreRegisterAllComponents();
|
|
|
|
// Handle UWorld::AddToWorld(), i.e. turning on level visibility
|
|
if (const ULevel* Level = GetLevel())
|
|
{
|
|
// This function gets called in editor all the time, we're only interested the case where level is being added to world.
|
|
if (Level->bIsAssociatingLevel)
|
|
{
|
|
RegisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ASmartObjectCollection::PostUnregisterAllComponents()
|
|
{
|
|
// Handle UWorld::RemoveFromWorld(), i.e. turning off level visibility
|
|
if (const ULevel* Level = GetLevel())
|
|
{
|
|
// This function gets called in editor all the time, we're only interested the case where level is being removed from world.
|
|
if (Level->bIsDisassociatingLevel)
|
|
{
|
|
UnregisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
}
|
|
|
|
Super::PostUnregisterAllComponents();
|
|
}
|
|
|
|
bool ASmartObjectCollection::RegisterWithSubsystem(const FString& Context)
|
|
{
|
|
if (bRegistered)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Failed: already registered"), *GetFullName(), *Context);
|
|
return false;
|
|
}
|
|
|
|
if (HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Failed: ignoring default object"), *GetFullName(), *Context);
|
|
return false;
|
|
}
|
|
|
|
USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
|
|
if (SmartObjectSubsystem == nullptr)
|
|
{
|
|
// Collection might attempt to register before the subsystem is created. At its initialization the subsystem gathers
|
|
// all collections and registers them. For this reason we use a log instead of an error.
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Failed: unable to find smart object subsystem"), *GetFullName(), *Context);
|
|
return false;
|
|
}
|
|
|
|
const ESmartObjectCollectionRegistrationResult Result = SmartObjectSubsystem->RegisterCollection(*this);
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - %s"), *GetFullName(), *Context, *UEnum::GetValueAsString(Result));
|
|
return true;
|
|
}
|
|
|
|
bool ASmartObjectCollection::UnregisterWithSubsystem(const FString& Context)
|
|
{
|
|
if (!bRegistered)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Failed: not registered"), *GetFullName(), *Context);
|
|
return false;
|
|
}
|
|
|
|
USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(GetWorld());
|
|
if (SmartObjectSubsystem == nullptr)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Failed: unable to find smart object subsystem"), *GetFullName(), *Context);
|
|
return false;
|
|
}
|
|
|
|
SmartObjectSubsystem->UnregisterCollection(*this);
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("'%s' %s - Succeeded"), *GetFullName(), *Context);
|
|
return true;
|
|
}
|
|
|
|
FSmartObjectCollectionEntry* ASmartObjectCollection::AddSmartObject(USmartObjectComponent& SOComponent, bool& bAlreadyInCollection)
|
|
{
|
|
FSmartObjectCollectionEntry* Entry = nullptr;
|
|
bAlreadyInCollection = false;
|
|
|
|
const UWorld* World = GetWorld();
|
|
if (World == nullptr)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("'%s' can't be registered to collection '%s': no associated world"),
|
|
*GetNameSafe(SOComponent.GetOwner()), *GetFullName());
|
|
return Entry;
|
|
}
|
|
|
|
const FSoftObjectPath ObjectPath = &SOComponent;
|
|
FString AssetPathString = ObjectPath.GetAssetPathString();
|
|
|
|
// We are not using asset path for partitioned world since they are not stable between editor and runtime.
|
|
// SubPathString should be enough since all actors are part of the main level.
|
|
if (World->IsPartitionedWorld())
|
|
{
|
|
AssetPathString.Reset();
|
|
}
|
|
#if WITH_EDITOR
|
|
else if (World->WorldType == EWorldType::PIE)
|
|
{
|
|
AssetPathString = UWorld::RemovePIEPrefix(ObjectPath.GetAssetPathString());
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
// Compute hash manually from strings since GetTypeHash(FSoftObjectPath) relies on a FName which implements run-dependent hash computations.
|
|
FSmartObjectHandle Handle = FSmartObjectHandle(HashCombine(GetTypeHash(AssetPathString), GetTypeHash(ObjectPath.GetSubPathString())));
|
|
SOComponent.SetRegisteredHandle(Handle);
|
|
|
|
Entry = CollectionEntries.FindByPredicate([Handle](const FSmartObjectCollectionEntry& ExistingEntry)
|
|
{
|
|
return ExistingEntry.Handle == Handle;
|
|
});
|
|
|
|
if (Entry != nullptr)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("'%s[%s]' already registered to collection '%s'"),
|
|
*GetNameSafe(SOComponent.GetOwner()), *LexToString(Handle), *GetFullName());
|
|
bAlreadyInCollection = true;
|
|
return Entry;
|
|
}
|
|
|
|
const USmartObjectDefinition* Definition = SOComponent.GetDefinition();
|
|
ensureMsgf(Definition != nullptr, TEXT("Shouldn't reach this point with an invalid definition asset"));
|
|
uint32 DefinitionIndex = Definitions.AddUnique(Definition);
|
|
|
|
UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Adding '%s[%s]' to collection '%s'"), *GetNameSafe(SOComponent.GetOwner()), *LexToString(Handle), *GetFullName());
|
|
const int32 NewEntryIndex = CollectionEntries.Emplace(Handle, SOComponent, DefinitionIndex);
|
|
RegisteredIdToObjectMap.Add(Handle, ObjectPath);
|
|
|
|
return &CollectionEntries[NewEntryIndex];
|
|
}
|
|
|
|
bool ASmartObjectCollection::RemoveSmartObject(USmartObjectComponent& SOComponent)
|
|
{
|
|
FSmartObjectHandle Handle = SOComponent.GetRegisteredHandle();
|
|
if (!Handle.IsValid())
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Skipped removal of '%s[%s]' from collection '%s'. Handle is not valid"),
|
|
*GetNameSafe(SOComponent.GetOwner()), *LexToString(Handle), *GetFullName());
|
|
return false;
|
|
}
|
|
|
|
UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Removing '%s[%s]' from collection '%s'"), *GetNameSafe(SOComponent.GetOwner()), *LexToString(Handle), *GetFullName());
|
|
const int32 Index = CollectionEntries.IndexOfByPredicate(
|
|
[&Handle](const FSmartObjectCollectionEntry& Entry)
|
|
{
|
|
return Entry.GetHandle() == Handle;
|
|
});
|
|
|
|
if (Index != INDEX_NONE)
|
|
{
|
|
CollectionEntries.RemoveAt(Index);
|
|
RegisteredIdToObjectMap.Remove(Handle);
|
|
}
|
|
|
|
SOComponent.SetRegisteredHandle(FSmartObjectHandle::Invalid);
|
|
|
|
return Index != INDEX_NONE;
|
|
}
|
|
|
|
USmartObjectComponent* ASmartObjectCollection::GetSmartObjectComponent(const FSmartObjectHandle SmartObjectHandle) const
|
|
{
|
|
const FSoftObjectPath* Path = RegisteredIdToObjectMap.Find(SmartObjectHandle);
|
|
return Path != nullptr ? CastChecked<USmartObjectComponent>(Path->ResolveObject(), ECastCheckedType::NullAllowed) : nullptr;
|
|
}
|
|
|
|
const USmartObjectDefinition* ASmartObjectCollection::GetDefinitionForEntry(const FSmartObjectCollectionEntry& Entry) const
|
|
{
|
|
const bool bIsValidIndex = Definitions.IsValidIndex(Entry.GetDefinitionIndex());
|
|
if (!bIsValidIndex)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Using invalid index (%d) to retrieve definition from collection '%s'"), Entry.GetDefinitionIndex(), *GetFullName());
|
|
return nullptr;
|
|
}
|
|
|
|
const USmartObjectDefinition* Definition = Definitions[Entry.GetDefinitionIndex()];
|
|
ensureMsgf(Definition != nullptr, TEXT("Collection is expected to contain only valid definition entries"));
|
|
return Definition;
|
|
}
|
|
|
|
void ASmartObjectCollection::OnRegistered()
|
|
{
|
|
bRegistered = true;
|
|
}
|
|
|
|
void ASmartObjectCollection::OnUnregistered()
|
|
{
|
|
bRegistered = false;
|
|
}
|
|
|
|
void ASmartObjectCollection::ValidateDefinitions()
|
|
{
|
|
for (const USmartObjectDefinition* Definition : Definitions)
|
|
{
|
|
|
|
UE_CVLOG_UELOG(Definition == nullptr, this, LogSmartObject, Warning
|
|
, TEXT("Null definition found at index (%d) in collection '%s'. Collection needs to be rebuilt and saved.")
|
|
, Definitions.IndexOfByKey(Definition)
|
|
,*GetFullName());
|
|
if (Definition != nullptr)
|
|
{
|
|
Definition->Validate();
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void ASmartObjectCollection::PostEditUndo()
|
|
{
|
|
Super::PostEditUndo();
|
|
|
|
if (IsPendingKillPending())
|
|
{
|
|
UnregisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
else
|
|
{
|
|
RegisterWithSubsystem(ANSI_TO_TCHAR(__FUNCTION__));
|
|
}
|
|
}
|
|
|
|
void ASmartObjectCollection::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
|
{
|
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
|
|
|
if (PropertyChangedEvent.Property)
|
|
{
|
|
const FName PropName = PropertyChangedEvent.Property->GetFName();
|
|
if (PropName == GET_MEMBER_NAME_CHECKED(ASmartObjectCollection, bBuildCollectionAutomatically))
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
bBuildOnDemand_DEPRECATED = !bBuildCollectionAutomatically;
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
if (bBuildCollectionAutomatically)
|
|
{
|
|
RebuildCollection();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ASmartObjectCollection::ClearCollection()
|
|
{
|
|
ResetCollection();
|
|
}
|
|
|
|
void ASmartObjectCollection::RebuildCollection()
|
|
{
|
|
if (USmartObjectSubsystem* SmartObjectSubsystem = USmartObjectSubsystem::GetCurrent(GetWorld()))
|
|
{
|
|
SmartObjectSubsystem->RebuildCollection(*this);
|
|
|
|
// Dirty package since this is an explicit user action
|
|
MarkPackageDirty();
|
|
}
|
|
}
|
|
|
|
void ASmartObjectCollection::RebuildCollection(const TConstArrayView<USmartObjectComponent*> Components)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Rebuilding collection '%s' from component list"), *GetFullName());
|
|
|
|
ResetCollection(Components.Num());
|
|
|
|
// the incoming Components collection represents all the smart objects currently registered with the SmartObjectsSubsystem,
|
|
// but we don't want to register all of them - some of the SmartObjectComponents in the collection come from levels
|
|
// added to the editor world, but at runtime will be procedurally loaded with a BP function call, which means those
|
|
// components might never get loaded, so we don't really want to store them in the persistent collection.
|
|
// The collection we're building here represents all the smart objects the level will start with, not the ones that will
|
|
// eventually get added.
|
|
auto LevelTester = [](const ULevel* Level) -> bool
|
|
{
|
|
const ULevelStreaming* LevelStreaming = ULevelStreaming::FindStreamingLevel(Level);
|
|
return (LevelStreaming && LevelStreaming->ShouldBeAlwaysLoaded()) || Level->IsPersistentLevel();
|
|
};
|
|
|
|
ULevel* PreviousLevel = nullptr;
|
|
bool bPreviousLevelValid = false;
|
|
bool bAlreadyInCollection = false;
|
|
for (USmartObjectComponent* const Component : Components)
|
|
{
|
|
if (Component != nullptr)
|
|
{
|
|
ULevel* OwnerLevel = Component->GetComponentLevel();
|
|
const bool bValid = (OwnerLevel == PreviousLevel) ? bPreviousLevelValid : LevelTester(OwnerLevel);
|
|
|
|
// @todo the GetShouldIgnoreLevelTesting is a temporary mean to support SmartObject unit testing.
|
|
// The whole RebuildCollection mechanic is scheduled for rewrite.
|
|
if (bValid || GetShouldIgnoreLevelTesting())
|
|
{
|
|
AddSmartObject(*Component, bAlreadyInCollection);
|
|
}
|
|
|
|
bPreviousLevelValid = bValid;
|
|
PreviousLevel = OwnerLevel;
|
|
}
|
|
}
|
|
|
|
CollectionEntries.Shrink();
|
|
RegisteredIdToObjectMap.Shrink();
|
|
Definitions.Shrink();
|
|
}
|
|
|
|
void ASmartObjectCollection::ResetCollection(const int32 ExpectedNumElements)
|
|
{
|
|
UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Reseting collection '%s'"), *GetFullName());
|
|
|
|
CollectionEntries.Reset(ExpectedNumElements);
|
|
RegisteredIdToObjectMap.Empty(ExpectedNumElements);
|
|
Definitions.Reset();
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|