// Copyright Epic Games, Inc. All Rights Reserved. #include "SmartObjectSubsystem.h" #include "Containers/StridedView.h" #include "Math/ColorList.h" #include "SmartObjectComponent.h" #include "SmartObjectUserComponent.h" #include "SmartObjectRequestTypes.h" #include "SmartObjectTypes.h" #include "EngineUtils.h" #include "SmartObjectHashGrid.h" #include "WorldConditionContext.h" #include "VisualLogger/VisualLogger.h" #include "Engine/LevelStreaming.h" #include "NavigationSystem.h" #include "AI/Navigation/NavigationTypes.h" #include "NavFilters/NavigationQueryFilter.h" #include "Annotations/SmartObjectSlotEntranceAnnotation.h" #include "Annotations/SmartObjectAnnotation_SlotUserCollision.h" #include "Misc/EnumerateRange.h" #include "Types/TargetingSystemTypes.h" #include "MassSubsystemBase.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(SmartObjectSubsystem) #if UE_ENABLE_DEBUG_DRAWING #include "SmartObjectSubsystemRenderingActor.h" #endif #if WITH_SMARTOBJECT_DEBUG #endif #if WITH_EDITOR #include "Engine/LevelBounds.h" #include "WorldPartition/WorldPartition.h" #endif #if WITH_EDITORONLY_DATA #include "SmartObjectCollection.h" #endif // WITH_EDITORONLY_DATA #define UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR() #define UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR() #define UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR() #define UE_MT_SCOPED_INSTANCE_WRITE_ACCESS_DETECTOR() #if WITH_SMARTOBJECT_MT_INSTANCE_LOCK #define UE_MT_SCOPED_INSTANCE_READ_LOCK() FScopeLock ScopedLock(&RuntimeInstanceLock) #define UE_MT_SCOPED_INSTANCE_WRITE_LOCK() FScopeLock ScopedLock(&RuntimeInstanceLock) #else #define UE_MT_SCOPED_INSTANCE_READ_LOCK() #define UE_MT_SCOPED_INSTANCE_WRITE_LOCK() #endif // WITH_SMARTOBJECT_MT_INSTANCE_LOCK namespace UE::SmartObject { // Indicates that runtime shouldn't be initialized. // This flag must be set BEFORE launching the game and not toggled after. bool bDisableRuntime = false; FAutoConsoleVariableRef CVarDisableRuntime( TEXT("ai.smartobject.DisableRuntime"), bDisableRuntime, TEXT("If enabled, runtime instances won't be created for baked collection entries or runtime added ones from component registration."), ECVF_Default); #if WITH_SMARTOBJECT_DEBUG static FAutoConsoleCommandWithWorldArgsAndOutputDevice CmdDumpRuntimeEntries( TEXT("ai.smartobject.DumpRuntimeEntries"), TEXT("Logs to the output device the list of all runtime instances created in the subsystem."), FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray& Args, const UWorld* World, FOutputDevice& OutputDevice) { if (const USmartObjectSubsystem* Subsystem = World->GetSubsystem()) { const TMap& RuntimeSmartObjects = Subsystem->DebugGetRuntimeObjects(); for (auto& RuntimeSmartObjectEntry : RuntimeSmartObjects) { OutputDevice.Logf(ELogVerbosity::Log, TEXT("Handle: [%s] Component:'%s'"), *LexToString(RuntimeSmartObjectEntry.Key), *GetNameSafe(RuntimeSmartObjectEntry.Value.GetOwnerComponent(ETrySpawnActorIfDehydrated::No))); } OutputDevice.Logf(ELogVerbosity::Log, TEXT("Total: %d elements"), RuntimeSmartObjects.Num()); } else { OutputDevice.Log(ELogVerbosity::Error, TEXT("Command failed since it was unable to access the SmartObject subsystem")); } }) ); namespace Debug { static FAutoConsoleCommandWithWorld RegisterAllSmartObjectsCmd ( TEXT("ai.debug.so.RegisterAllSmartObjects"), TEXT("Force register all objects registered in the subsystem to simulate & debug runtime flows (will ignore already registered components)."), FConsoleCommandWithWorldDelegate::CreateLambda([](const UWorld* InWorld) { if (USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(InWorld)) { Subsystem->DebugRegisterAllSmartObjects(); } }) ); static FAutoConsoleCommandWithWorld UnregisterAllSmartObjectsCmd ( TEXT("ai.debug.so.UnregisterAllSmartObjects"), TEXT("Force unregister all objects registered in the subsystem to simulate & debug runtime flows (will ignore already unregistered components)."), FConsoleCommandWithWorldDelegate::CreateLambda([](const UWorld* InWorld) { if (USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(InWorld)) { Subsystem->DebugUnregisterAllSmartObjects(); } }) ); } // UE::SmartObject::Debug #endif // WITH_SMARTOBJECT_DEBUG FString DebugGetComponentName(const TNotNull SmartObjectComponent) { const AActor* Owner = SmartObjectComponent->GetOwner(); return SmartObjectComponent->GetFullName(Owner != nullptr ? Owner->GetOwner() // Get path relative to the owner's owner to get a good compromise between not enough and too many details : nullptr); // Get fully qualified pathname } } // UE::SmartObject /** * Internal helper struct for all the data needed for smart object entrance validation. */ struct FSmartObjectValidationContext { const ANavigationData* NavigationData = nullptr; FSharedConstNavQueryFilter NavigationFilter = nullptr; FVector NavigationSearchExtents = FVector::ZeroVector; const USmartObjectSlotValidationFilter* ValidationFilter = nullptr; const FSmartObjectSlotValidationParams* ValidationParams = nullptr; FSmartObjectUserCapsuleParams UserCapsuleParams; FSmartObjectTraceParams GroundTraceParams; FSmartObjectTraceParams TransitionTraceParams; FCollisionQueryParams GroundTraceQueryParams; FCollisionQueryParams TransitionTraceQueryParams; bool Init(const UWorld* World, const FSmartObjectSlotEntranceLocationRequest& Request, const AActor* SmartObjectActor) { const UObject* LogOwner = USmartObjectSubsystem::GetCurrent(World); if (!LogOwner) { LogOwner = World; } TSubclassOf ValidationFilterClass = Request.ValidationFilter; NavigationData = Request.NavigationData; if (Request.UserActor) { // If user actor is present, try to query some data automatically from interfaces and components. if (!ValidationFilterClass.Get()) { if (const USmartObjectUserComponent* UserComponent = Request.UserActor->GetComponentByClass()) { ValidationFilterClass = UserComponent->GetValidationFilter(); } } if (!NavigationData) { NavigationData = UE::SmartObject::Annotations::GetNavDataForActor(*World, Request.UserActor); } } if (!ValidationFilterClass.Get()) { UE_VLOG_UELOG(LogOwner, LogSmartObject, Warning, TEXT("%hs: Invalid validation filter for user actor %s."), __FUNCTION__, *GetNameSafe(Request.UserActor)); return false; } ValidationFilter = ValidationFilterClass.GetDefaultObject(); check(ValidationFilter); ValidationParams = &ValidationFilter->GetValidationParams(Request.LocationType); const bool bRequiresValidUserCapsule = Request.bCheckSlotLocationOverlap || Request.bCheckEntranceLocationOverlap; if (bRequiresValidUserCapsule) { if (Request.UserCapsuleParams.IsValid()) { UserCapsuleParams = ValidationParams->GetUserCapsule(Request.UserCapsuleParams); } else if (Request.UserActor) { if (!ValidationParams->GetUserCapsuleForActor(*Request.UserActor, UserCapsuleParams)) { UE_VLOG_UELOG(LogOwner, LogSmartObject, Error, TEXT("%hs: Could not resolve user capsule size. Failed to access navigation parameters for user actor %s."), __FUNCTION__, *GetNameSafe(Request.UserActor)); return false; } } else { // Fallback to the capsule size from validation params. UserCapsuleParams = ValidationParams->GetUserCapsule(); } } // Navdata must be valid when testing for navigable. if (Request.bProjectNavigationLocation) { if (!NavigationData) { UE_VLOG_UELOG(LogOwner, LogSmartObject, Error, TEXT("%hs: ProjectNavigationLocation is requested, expecting valid navigation data, NavigationData is not set."), __FUNCTION__); return false; } // Filter must be valid if specified. if (ValidationParams->GetNavigationFilter().Get()) { NavigationFilter = UNavigationQueryFilter::GetQueryFilter(*NavigationData, Request.UserActor, ValidationParams->GetNavigationFilter()); if (!NavigationFilter.IsValid()) { UE_VLOG_UELOG(LogOwner, LogSmartObject, Error, TEXT("%hs: Navigation filter was specified was failed to resolve it."), __FUNCTION__); return false; } } } NavigationSearchExtents = FVector(ValidationParams->GetSearchExtents()); GroundTraceParams = ValidationParams->GetGroundTraceParameters(); TransitionTraceParams = ValidationParams->GetTransitionTraceParameters(); GroundTraceQueryParams = FCollisionQueryParams(SCENE_QUERY_STAT(SmartObjectTrace), GroundTraceParams.bTraceComplex); TransitionTraceQueryParams = FCollisionQueryParams(SCENE_QUERY_STAT(SmartObjectTrace), TransitionTraceParams.bTraceComplex); GroundTraceQueryParams.bIgnoreTouches = true; TransitionTraceQueryParams.bIgnoreTouches = true; if (SmartObjectActor) { GroundTraceQueryParams.AddIgnoredActor(SmartObjectActor); TransitionTraceQueryParams.AddIgnoredActor(SmartObjectActor); } if (Request.UserActor) { GroundTraceQueryParams.AddIgnoredActor(Request.UserActor); TransitionTraceQueryParams.AddIgnoredActor(Request.UserActor); } return true; } }; //----------------------------------------------------------------------// // USmartObjectSubsystem //----------------------------------------------------------------------// /*__________________________________________________________________________________________________________________________________________________________________________________ [Registration flows] +----------------------------+ +-------------------------------------+ | CreateSmartObject() |------------------------------------------------------------------------------->| CreateRuntimeInstance() | +----------------------------+ / +-------------------------------------+ +----------------------------+ +----------------------------+ (no comp) / | RegisterCollection() |---->| AddContainerToSimulation() |--------------------- / +----------------------------+ +----------------------------+ \ +----------------------------------+ \ ->| AddCollectionEntryToSimulation() | (comp) \ +------------------------------------+ (1)/ +----------------------------------+ -> | AddComponentToSimulationInternal() |---- (not registered & not in collection) / +------------------------------------+ (2)\ / \ +----------------------------+ +----------------------------+ \ +-------------------------------------+ | RegisterSmartObjectActor() |---->| RegisterSmartObject() |-------------------------------------------->| BindComponentToSimulationInternal() | +----------------------------+ +----------------------------+ (registered | already in collection) +-------------------------------------+ ____________________________________________________________________________________________________________________________________________________________________________________ [Unregistration flows] +------------------------------+ | UnregisterCollection() |------------------------------------------------------------------------------- +------------------------------+ \ +------------------------------+ +-------------------------+ \ | UnregisterSmartObjectActor() |---->| UnregisterSmartObject() | \ +------------------------------+ +-------------------------+ \ \ \ \ +-------------------------------+ (keep runtime) \ +-----------------------------------------+ ->| UnregisterSmartObjectInternal |-----------------------------------------------------> | UnbindComponentFromSimulationInternal() | / +-------------------------------+ / +-----------------------------------------+ / \ (destroy runtime) / +------------------------------+ +-------------------------+ \ +---------------------------------+ / | RemoveSmartObjectActor() |---->| RemoveSmartObject() | -> | RemoveComponentFromSimulation() | / +------------------------------+ / +-------------------------+ +---------------------------------+ / / (comp) \ / +------------------------------+/ \ +-----------------------------------------------+ +-----------------------------------------+ | DestroySmartObject() |--------------------------------------------------> | RemoveRuntimeInstanceFromSimulationInternal() |---->| DestroyRuntimeInstanceInternal() | +------------------------------+ (no comp) +-----------------------------------------------+ +-----------------------------------------+ __________________________________________________________________________________________________________________________________________________________________________________*/ USmartObjectSubsystem::USmartObjectSubsystem() : SmartObjectContainer(this) { } void USmartObjectSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); // Note that we're using GetClass() rather than StaticClass() to work as expected for child-classes as well. // Child class can always override the traits registered this way. UE::Mass::Subsystems::RegisterSubsystemType(Collection, GetClass(), UE::Mass::FSubsystemTypeTraits::Make()); } void USmartObjectSubsystem::OnWorldComponentsUpdated(UWorld& World) { #if WITH_EDITORONLY_DATA bIsPartitionedWorld = World.IsPartitionedWorld(); #endif // WITH_EDITORONLY_DATA // Load class required to instantiate the space partition structure UE_CVLOG_UELOG(!SpacePartitionClassName.IsValid(), this, LogSmartObject, Error, TEXT("A valid space partition class name is required.")); if (SpacePartitionClassName.IsValid()) { SpacePartitionClass = LoadClass(nullptr, *SpacePartitionClassName.ToString()); UE_CVLOG_UELOG(*SpacePartitionClass == nullptr, this, LogSmartObject, Error, TEXT("Unable to load class %s"), *SpacePartitionClassName.ToString()); } // Class not specified or invalid, use some default if (SpacePartitionClass.Get() == nullptr) { SpacePartitionClassName = FSoftClassPath(USmartObjectHashGrid::StaticClass()); SpacePartitionClass = USmartObjectHashGrid::StaticClass(); UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Using default class %s"), *SpacePartitionClassName.ToString()); } #if UE_ENABLE_DEBUG_DRAWING // Spawn the rendering actor if (RenderingActor == nullptr) { FActorSpawnParameters SpawnInfo; SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; RenderingActor = World.SpawnActor(SpawnInfo); } #endif // UE_ENABLE_DEBUG_DRAWING // Register collections that were unable to register since they got loaded before the subsystem got created/initialized. RegisterCollectionInstances(); #if WITH_EDITOR if (!World.IsGameWorld() && bAutoInitializeEditorInstances) { // calculating world bounds first since InitializeRuntime is using that data to create the USmartObjectSpacePartition // instance. Note that we use the World-calculated bounds only for editor worlds, since Runtime SmartObjectContainer's // bounds will rely on existing SmartObjectCollections. In editor we use world's size to not resize the // USmartObjectSpacePartition with SO operations SmartObjectContainer.SetBounds(ComputeBounds(World)); InitializeRuntime(); } #endif // WITH_EDITOR } USmartObjectSubsystem* USmartObjectSubsystem::GetCurrent(const UWorld* World) { return UWorld::GetSubsystem(World); } FSmartObjectRuntime* USmartObjectSubsystem::AddComponentToSimulationInternal( TNotNull SmartObjectComponent, const FSmartObjectCollectionEntry& NewEntry ) { checkf(SmartObjectComponent->GetDefinition() != nullptr, TEXT("Shouldn't reach this point with an invalid definition asset")); FSmartObjectRuntime* SmartObjectRuntime = AddCollectionEntryToSimulationInternal(NewEntry, *SmartObjectComponent->GetDefinition(), SmartObjectComponent); if (SmartObjectRuntime != nullptr) { BindComponentToSimulationInternal(SmartObjectComponent, *SmartObjectRuntime); } return SmartObjectRuntime; } bool USmartObjectSubsystem::UpdateSmartObjectTransform(const FSmartObjectHandle Handle, const FTransform& NewTransform) { return ExecuteOnValidatedMutableRuntime(Handle, [this, &Handle, &NewTransform](FSmartObjectRuntime& SmartObjectRuntime) { check(SpacePartition); // Remove from old location in spatial partition. if (SmartObjectRuntime.SpatialEntryData.IsValid()) { SpacePartition->Remove(Handle, SmartObjectRuntime.SpatialEntryData); } // Set transform and register back to spatial partition. SmartObjectRuntime.SetTransform(NewTransform); const FBox Bounds = SmartObjectRuntime.GetDefinition().GetBounds().TransformBy(NewTransform); SpacePartition->Add(Handle, Bounds, SmartObjectRuntime.SpatialEntryData); #if UE_ENABLE_DEBUG_DRAWING // Refresh debug draw SmartObjectRuntime.Bounds = Bounds; if (RenderingActor != nullptr) { RenderingActor->MarkComponentsRenderStateDirty(); } #endif // UE_ENABLE_DEBUG_DRAWING }, __FUNCTION__); } void USmartObjectSubsystem::BindComponentToSimulationInternal(const TNotNull SmartObjectComponent, FSmartObjectRuntime& SmartObjectRuntime) const { ensureMsgf(SmartObjectComponent->GetRegisteredHandle().IsValid(), TEXT("%hs expects parameter SmartObjectComponent to be already registered."), __FUNCTION__); if (!ensureMsgf(!SmartObjectComponent->IsBoundToSimulation(), TEXT("Component and runtime instance should only bound once"))) { return; } // It is possible that the component is already linked to the runtime instance when the collection entry was initially added. const USmartObjectComponent* CurrentComponent = SmartObjectRuntime.GetOwnerComponent(); ensureMsgf(CurrentComponent == nullptr || CurrentComponent == SmartObjectComponent, TEXT("Different OwnerComponent (was %s) when binding SmartObjectComponent %s. This might indicate multiple objects using the same handle."), *GetFullNameSafe(CurrentComponent), *SmartObjectComponent->GetFullName()); SmartObjectRuntime.OwnerComponent = SmartObjectComponent; // Set the component's owner as the runtime owner if it is not already set (e.g. instance created by an instanced actor) if (!SmartObjectRuntime.OwnerData.IsValid()) { SmartObjectRuntime.OwnerData = FConstStructView::Make(FSmartObjectActorUserData(SmartObjectComponent->GetOwner())); } // Notify the component to bind to its runtime counterpart SmartObjectComponent->OnRuntimeInstanceBound(SmartObjectRuntime); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("'%s' using definition '%s' bound to simulation instance with handle '%s'."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent), *SmartObjectComponent->GetDefinition()->GetPathName(), *LexToString(SmartObjectComponent->GetRegisteredHandle())); if (SmartObjectRuntime.OnEvent.IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnComponentBound; SmartObjectRuntime.OnEvent.Broadcast(Data); } } void USmartObjectSubsystem::UnbindComponentFromSimulationInternal(TNotNull SmartObjectComponent, FSmartObjectRuntime& SmartObjectRuntime) const { if (!ensureMsgf(SmartObjectComponent->IsBoundToSimulation(), TEXT("Component and runtime instance should only bound once"))) { return; } if (SmartObjectRuntime.OnEvent.IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnComponentUnbound; SmartObjectRuntime.OnEvent.Broadcast(Data); } SmartObjectComponent->OnRuntimeInstanceUnbound(SmartObjectRuntime); SmartObjectRuntime.OwnerComponent = nullptr; UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("'%s' unbound from simulation instance '%s'."), *GetNameSafe(SmartObjectComponent->GetOwner()), *LexToString(SmartObjectRuntime.GetRegisteredHandle())); } FSmartObjectRuntime* USmartObjectSubsystem::AddCollectionEntryToSimulationInternal( const FSmartObjectCollectionEntry& Entry, const USmartObjectDefinition& Definition, USmartObjectComponent* OwnerComponent ) { const FSmartObjectHandle Handle = Entry.GetHandle(); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Creating SmartObject using handle '%s' from collection entry using definition '%s'%s."), *LexToString(Handle), *Definition.GetName(), (OwnerComponent != nullptr) ? *FString::Printf(TEXT(" for '%s'"), *GetNameSafe(OwnerComponent->GetOwner())) : TEXT("")); FSmartObjectRuntime* Runtime = CreateRuntimeInstance(Handle, Definition, Entry.GetBounds(), OwnerComponent); if (Runtime != nullptr) { Runtime->SetTransform(Entry.GetTransform()); Runtime->Tags = Entry.GetTags(); } return Runtime; } FSmartObjectRuntime* USmartObjectSubsystem::CreateRuntimeInstance(const FSmartObjectHandle Handle, const USmartObjectDefinition& Definition, const FBox& Bounds, USmartObjectComponent* OwnerComponent) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); ensure(IsInGameThread() || IsInParallelGameThread()); if (!ensureMsgf(Handle.IsValid(), TEXT("SmartObject needs a valid Handle to be added to the simulation"))) { return nullptr; } if (!ensureMsgf(GetRuntimeInstanceInternal(Handle) == nullptr, TEXT("Handle '%s' already registered in runtime simulation"), *LexToString(Handle))) { return nullptr; } FSmartObjectRuntime& Runtime = RuntimeSmartObjects.Emplace(Handle, FSmartObjectRuntime(Definition)); Runtime.SetRegisteredHandle(Handle); Runtime.OwnerComponent = OwnerComponent; #if UE_ENABLE_DEBUG_DRAWING Runtime.Bounds = Bounds; #endif FWorldConditionContextData ConditionContextData(*Definition.GetWorldConditionSchema()); SetupConditionContextCommonDataInternal(ConditionContextData, Runtime); // Always initialize state (handles empty conditions) Runtime.PreconditionState.Initialize(*this, Definition.GetPreconditions()); // Activate preconditions only if associated actor is available, otherwise we wait on hydration since // many world conditions relies on actor at the moment. const bool bActivateConditions = Runtime.GetOwnerActor(ETrySpawnActorIfDehydrated::No) != nullptr; if (bActivateConditions) { ActivateObjectPreconditionsInternal(ConditionContextData, Runtime); } // Create runtime data and entity for each slot Runtime.Slots.Reserve(Definition.GetSlots().Num()); int32 SlotIndex = 0; for (const FSmartObjectSlotDefinition& SlotDefinition : Definition.GetSlots()) { FSmartObjectRuntimeSlot& Slot = Runtime.Slots.AddDefaulted_GetRef(); // Setup initial state from slot definition and current object state Slot.Offset = SlotDefinition.Offset; Slot.Rotation = SlotDefinition.Rotation; Slot.bSlotEnabled = SlotDefinition.bEnabled; Slot.Tags = SlotDefinition.RuntimeTags; Slot.bObjectEnabled = Runtime.IsEnabled(); // Always initialize state (handles empty conditions) Slot.PreconditionState.Initialize(*this, SlotDefinition.SelectionPreconditions); if (bActivateConditions) { ActivateSlotPreconditionsInternal(ConditionContextData, Slot, FSmartObjectSlotHandle(Handle, SlotIndex)); } SlotIndex++; } // Insert to the spatial representation structure and store associated data checkfSlow(SpacePartition != nullptr, TEXT("Space partition is expected to be valid since we use the plugins default in OnWorldComponentsUpdated.")); SpacePartition->Add(Handle, Bounds, Runtime.SpatialEntryData); // Notify that the object became in use. if (Runtime.OnEvent.IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = Runtime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnObjectEnabled; Runtime.OnEvent.Broadcast(Data); } return &Runtime; } bool USmartObjectSubsystem::RemoveRuntimeInstanceFromSimulationInternal(FSmartObjectRuntime& SmartObjectRuntime, USmartObjectComponent* SmartObjectComponent) { const FSmartObjectHandle Handle = SmartObjectRuntime.GetRegisteredHandle(); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Removing SmartObject '%s' using definition '%s' from runtime simulation%s."), *LexToString(Handle), *SmartObjectRuntime.GetDefinition().GetName(), (SmartObjectComponent != nullptr) ? *FString::Printf(TEXT(" for '%s'"), *GetNameSafe(SmartObjectComponent->GetOwner())) : TEXT("")); if (SmartObjectComponent != nullptr) { UnbindComponentFromSimulationInternal(SmartObjectComponent, SmartObjectRuntime); } DestroyRuntimeInstanceInternal(Handle, SmartObjectRuntime); // Remove object runtime data RuntimeSmartObjects.Remove(Handle); return true; } void USmartObjectSubsystem::DestroyRuntimeInstanceInternal( const FSmartObjectHandle Handle, FSmartObjectRuntime& SmartObjectRuntime ) { // Abort everything before removing since abort flow may require access to runtime data AbortAllInternal(Handle, SmartObjectRuntime); // Notify that the object is not in use anymore. if (SmartObjectRuntime.OnEvent.IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnObjectDisabled; SmartObjectRuntime.OnEvent.Broadcast(Data); } // Remove from space partition checkfSlow(SpacePartition != nullptr, TEXT("Space partition is expected to be valid since we use the plugins default in OnWorldComponentsUpdated.")); SpacePartition->Remove(Handle, SmartObjectRuntime.SpatialEntryData); if (SmartObjectRuntime.PreconditionState.AreConditionsActivated()) { FWorldConditionContextData ConditionContextData(*SmartObjectRuntime.GetDefinition().GetWorldConditionSchema()); SetupConditionContextCommonDataInternal(ConditionContextData, SmartObjectRuntime); // Deactivate object and slot Preconditions const FWorldConditionContext ObjectContext(SmartObjectRuntime.PreconditionState, ConditionContextData); ObjectContext.Deactivate(); const USmartObjectWorldConditionSchema* DefaultWorldConditionSchema = GetDefault(); for (TConstEnumerateRef RuntimeSlot : EnumerateRange(SmartObjectRuntime.Slots)) { const FSmartObjectSlotHandle SlotHandle(Handle, RuntimeSlot.GetIndex()); ensureMsgf(ConditionContextData.SetContextData(DefaultWorldConditionSchema->GetSlotHandleRef(), &SlotHandle), TEXT("Expecting USmartObjectWorldConditionSchema::SlotHandleRef to be valid.")); // Deactivate slot Preconditions (if successfully initialized) const FWorldConditionContext SlotContext(RuntimeSlot->PreconditionState, ConditionContextData); SlotContext.Deactivate(); } } } bool USmartObjectSubsystem::RemoveCollectionEntryFromSimulation(const FSmartObjectCollectionEntry& Entry) { return DestroySmartObject(Entry.GetHandle()); } void USmartObjectSubsystem::RemoveComponentFromSimulation(TNotNull SmartObjectComponent) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); const FSmartObjectHandle Handle = SmartObjectComponent->GetRegisteredHandle(); if (FSmartObjectRuntime* SmartObjectRuntime = GetRuntimeInstanceInternal(Handle)) { if (RemoveRuntimeInstanceFromSimulationInternal(*SmartObjectRuntime, SmartObjectComponent)) { UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("%hs call succeeded for %s"), __FUNCTION__, *GetNameSafe(SmartObjectComponent->GetOwner())); } else { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("%hs call failed for %s"), __FUNCTION__, *GetNameSafe(SmartObjectComponent->GetOwner())); } } else { #if WITH_SMARTOBJECT_DEBUG ensureAlwaysMsgf(false, TEXT("RemoveComponentFromSimulation is an internal call and should only be used for objects still part of the simulation")); #endif // WITH_SMARTOBJECT_DEBUG UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("%hs called with %s handle and no corresponding SmartObjectRuntime") , __FUNCTION__ , Handle.IsValid() ? *FString::Printf(TEXT("a VALID '%s'"), *LexToString(Handle)) : TEXT("an INVALID")); } } void USmartObjectSubsystem::AbortAllInternal(const FSmartObjectHandle Handle, FSmartObjectRuntime& SmartObjectRuntime) const { for (TEnumerateRef RuntimeSlot : EnumerateRange(SmartObjectRuntime.Slots)) { const FSmartObjectSlotHandle SlotHandle(Handle, RuntimeSlot.GetIndex()); switch (RuntimeSlot->State) { case ESmartObjectSlotState::Claimed: case ESmartObjectSlotState::Occupied: { const FSmartObjectClaimHandle ClaimHandle(SmartObjectRuntime.GetRegisteredHandle(), SlotHandle, RuntimeSlot->User); // Keep user data to be used as payload in the notification event // since it will be released by the following call to Slot.Release const FInstancedStruct Payload(MoveTemp(RuntimeSlot->UserData)); if (RuntimeSlot->Release(ClaimHandle, /* bAborted */true)) { OnSlotChangedInternal(SmartObjectRuntime, *RuntimeSlot, SlotHandle, ESmartObjectChangeReason::OnReleased, Payload); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Slot %s released by an abort"), *LexToString(ClaimHandle.SlotHandle)); UE_VLOG_LOCATION(this, LogSmartObject, Display, SmartObjectRuntime.Transform.TransformPosition(FVector(RuntimeSlot->Offset)), /*Radius*/50, FColor::Red, TEXT("Released by abort")); } break; } case ESmartObjectSlotState::Free: // falling through on purpose default: UE_CVLOG_UELOG(RuntimeSlot->User.IsValid(), this, LogSmartObject, Warning, TEXT("SmartObject '%s' using definition '%s' used by %s while the slot it's assigned to is not marked Claimed nor Occupied"), *LexToString(Handle), *LexToString(SmartObjectRuntime.GetDefinition()), *LexToString(RuntimeSlot->User)); break; } RuntimeSlot->State = ESmartObjectSlotState::Free; } } bool USmartObjectSubsystem::RegisterSmartObject(const TNotNull SmartObjectComponent) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); const USmartObjectDefinition* Definition = SmartObjectComponent->GetDefinition(); if (Definition == nullptr) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Attempting to register '%s' while its DefinitionAsset is not set. Bailing out."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); return false; } if (Definition->HasBeenValidated() == false) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Attempting to register '%s' while its DefinitionAsset has not been Validated. Validating now."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); Definition->Validate(); } if (Definition->IsDefinitionValid() == false) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Attempting to register '%s' while its DefinitionAsset fails validation test. Bailing out." " Resave asset '%s' to see the errors and fix the problem."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent), *GetPathNameSafe(Definition)); return false; } if (Definition->GetSlots().IsEmpty()) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Attempting to register '%s' while its DefinitionAsset doesn't contain any slots. Bailing out." " Resave asset '%s' to see the errors and fix the problem."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent), *GetPathNameSafe(Definition)); return false; } if (RegisteredSOComponents.Contains(SmartObjectComponent)) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Failed to register '%s'. Already registered"), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); return false; } // until the runtime is initialized we're not ready to register SmartObject. We collect them in PendingSmartObjectRegistration // and process them in InitializeRuntime call. if (bRuntimeInitialized) { if (SmartObjectComponent->GetRegisteredHandle().IsValid()) { // Components associated to persistent collection entries might be already bound // to simulation from the registration of the collection container. In that case, we don't // need to bind again from the component self registration flow. if (!SmartObjectComponent->IsBoundToSimulation()) { const bool bInstanceFound = ExecuteOnValidatedMutableRuntime(SmartObjectComponent->GetRegisteredHandle(), [this, &SmartObjectComponent](FSmartObjectRuntime& SmartObjectRuntime) { BindComponentToSimulationInternal(SmartObjectComponent, SmartObjectRuntime); }, __FUNCTION__); ensureAlwaysMsgf(bInstanceFound, TEXT("Unable to bind %s using handle '%s' since an associated runtime doesn't exist."), *SmartObjectComponent->GetFullName(), *LexToString(SmartObjectComponent->GetRegisteredHandle())); } } else { bool bAlreadyInCollection = false; if (const FSmartObjectCollectionEntry* Entry = SmartObjectContainer.AddSmartObject(SmartObjectComponent, bAlreadyInCollection)) { if (bAlreadyInCollection) { SmartObjectComponent->SetRegisteredHandle(Entry->GetHandle(), ESmartObjectRegistrationType::BindToExistingInstance); const bool bInstanceFound = ExecuteOnValidatedMutableRuntime(SmartObjectComponent->GetRegisteredHandle(), [this, &SmartObjectComponent](FSmartObjectRuntime& SmartObjectRuntime) { BindComponentToSimulationInternal(SmartObjectComponent, SmartObjectRuntime); }, __FUNCTION__); ensureAlwaysMsgf(bInstanceFound, TEXT("Unable to bind %s using handle '%s' since an associated runtime doesn't exist."), *SmartObjectComponent->GetFullName(), *LexToString(SmartObjectComponent->GetRegisteredHandle())); } else { SmartObjectComponent->SetRegisteredHandle(Entry->GetHandle(), ESmartObjectRegistrationType::Dynamic); AddComponentToSimulationInternal(SmartObjectComponent, *Entry); #if WITH_EDITOR OnMainCollectionDirtied.Broadcast(); #endif } } } ensureMsgf(RegisteredSOComponents.Find(SmartObjectComponent) == INDEX_NONE , TEXT("Adding '%s' to list of registered components, but it has already been added. Missing unregister call?"), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); RegisteredSOComponents.Add(SmartObjectComponent); #if UE_ENABLE_DEBUG_DRAWING // Refresh debug draw if (RenderingActor != nullptr) { RenderingActor->MarkComponentsRenderStateDirty(); } #endif // UE_ENABLE_DEBUG_DRAWING } else { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("'%s' not registered since InitializeRuntime has not been called yet. Storing component for registration during InitializeRuntime call.") , *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); PendingSmartObjectRegistration.Add(SmartObjectComponent); } return true; } FSmartObjectHandle USmartObjectSubsystem::CreateSmartObject(const USmartObjectDefinition& Definition, const FTransform& Transform, const FConstStructView OwnerData) { if (!ensureMsgf(bRuntimeInitialized, TEXT(""))) { return FSmartObjectHandle::Invalid; } FSmartObjectHandle Handle = FSmartObjectHandleFactory::CreateHandleForDynamicObject(); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Creating SmartObject '%s' using definition '%s'."), *LexToString(Handle), *Definition.GetName()); if (FSmartObjectRuntime* Runtime = CreateRuntimeInstance(Handle, Definition, Definition.GetBounds().TransformBy(Transform))) { Runtime->SetTransform(Transform); Runtime->OwnerData = OwnerData; } else { Handle.Invalidate(); } return Handle; } bool USmartObjectSubsystem::DestroySmartObject(const FSmartObjectHandle Handle) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Destroying SmartObject using handle '%s'."), *LexToString(Handle)); if (FSmartObjectRuntime* SmartObjectRuntime = GetRuntimeInstanceInternal(Handle)) { if (USmartObjectComponent* Component = SmartObjectRuntime->GetOwnerComponent()) { RemoveSmartObject(Component); } else { return RemoveRuntimeInstanceFromSimulationInternal(*SmartObjectRuntime); } } return false; } bool USmartObjectSubsystem::RemoveSmartObject(const TNotNull SmartObjectComponent) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); if (RegisteredSOComponents.Contains(SmartObjectComponent)) { return UnregisterSmartObjectInternal(SmartObjectComponent, /*bDestroyRuntimeState=*/true); } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Failed to remove '%s' since it doesn't seem registered or has already been unregistered."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); return false; } bool USmartObjectSubsystem::UnregisterSmartObject(const TNotNull SmartObjectComponent) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); if (RegisteredSOComponents.Contains(SmartObjectComponent)) { return UnregisterSmartObjectInternal(SmartObjectComponent, /*bDestroyRuntimeState=*/SmartObjectComponent->GetRegistrationType() == ESmartObjectRegistrationType::Dynamic); } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Failed to unregister '%s' since it doesn't seem registered or has already been unregistered."), *UE::SmartObject::DebugGetComponentName(SmartObjectComponent)); return false; } bool USmartObjectSubsystem::UnregisterSmartObjectInternal(const TNotNull SmartObjectComponent, const bool bDestroyRuntimeState) { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("Unregistering '%s' using definition '%s' associated to '%s'."), *LexToString(SmartObjectComponent->GetRegisteredHandle()), *GetNameSafe(SmartObjectComponent->GetDefinition()), *GetNameSafe(SmartObjectComponent->GetOwner())); if (bRuntimeInitialized) { ensure(SmartObjectComponent->GetRegisteredHandle().IsValid()); if (SmartObjectComponent->IsBoundToSimulation()) { if (bDestroyRuntimeState) { RemoveComponentFromSimulation(SmartObjectComponent); SmartObjectContainer.RemoveSmartObject(SmartObjectComponent); } // otherwise we keep all the runtime entries in place - those will be removed along with the collection that has added them else { FSmartObjectRuntime* SmartObjectRuntime = GetRuntimeInstanceInternal(SmartObjectComponent->GetRegisteredHandle()); if (ensureAlwaysMsgf(SmartObjectRuntime != nullptr, TEXT("Unable to unbind '%s' using handle '%s' since an associated runtime doesn't exist."), *SmartObjectComponent->GetFullName(), *LexToString(SmartObjectComponent->GetRegisteredHandle()))) { // Unbind the component from its associated runtime instance UnbindComponentFromSimulationInternal(SmartObjectComponent, *SmartObjectRuntime); } } } RegisteredSOComponents.Remove(SmartObjectComponent); } else { PendingSmartObjectRegistration.RemoveSingleSwap(SmartObjectComponent); } return true; } bool USmartObjectSubsystem::RegisterSmartObjectActor(const AActor& SmartObjectActor) { TArray Components; SmartObjectActor.GetComponents(Components); UE_CVLOG_UELOG(Components.Num() == 0, &SmartObjectActor, LogSmartObject, Log, TEXT("Failed to register SmartObject components for '%s'. No components found."), *SmartObjectActor.GetFullName(SmartObjectActor.GetOwner())); int32 NumSuccess = 0; for (USmartObjectComponent* SOComponent : Components) { if (RegisterSmartObject(SOComponent)) { NumSuccess++; } } return NumSuccess > 0 && NumSuccess == Components.Num(); } bool USmartObjectSubsystem::UnregisterSmartObjectActor(const AActor& SmartObjectActor) { TArray Components; SmartObjectActor.GetComponents(Components); UE_CVLOG_UELOG(Components.Num() == 0, &SmartObjectActor, LogSmartObject, Log, TEXT("Failed to unregister SmartObject components for '%s'. No components found."), *SmartObjectActor.GetFullName(SmartObjectActor.GetOwner())); int32 NumSuccess = 0; for (USmartObjectComponent* SOComponent : Components) { if (UnregisterSmartObject(SOComponent)) { NumSuccess++; } } return NumSuccess > 0 && NumSuccess == Components.Num(); } bool USmartObjectSubsystem::RemoveSmartObjectActor(const AActor& SmartObjectActor) { TArray Components; SmartObjectActor.GetComponents(Components); UE_CVLOG_UELOG(Components.Num() == 0, &SmartObjectActor, LogSmartObject, Log, TEXT("Failed to remove SmartObject components runtime data for '%s'. No components found."), *SmartObjectActor.GetFullName()); int32 NumSuccess = 0; for (USmartObjectComponent* SOComponent : Components) { if (RemoveSmartObject(SOComponent)) { NumSuccess++; } } return NumSuccess > 0 && NumSuccess == Components.Num(); } bool USmartObjectSubsystem::SetSmartObjectActorEnabled(const AActor& SmartObjectActor, const bool bEnabled) { TArray Components; SmartObjectActor.GetComponents(Components); UE_CVLOG_UELOG(Components.Num() == 0, this, LogSmartObject, Log, TEXT("Failed to change SmartObject components enabled state for '%s'. No components found."), *SmartObjectActor.GetFullName()); int32 NumSuccess = 0; for (const USmartObjectComponent* SOComponent : Components) { if (SetEnabledForReason(SOComponent->GetRegisteredHandle(), UE::SmartObject::EnabledReason::Gameplay, bEnabled)) { NumSuccess++; } } return NumSuccess > 0 && NumSuccess == Components.Num(); } bool USmartObjectSubsystem::SetEnabled(const FSmartObjectHandle Handle, const bool bEnabled) { return SetEnabledForReason(Handle, UE::SmartObject::EnabledReason::Gameplay, bEnabled); } bool USmartObjectSubsystem::SetEnabledForReason(const FSmartObjectHandle Handle, const FGameplayTag ReasonTag, const bool bEnabled) { if (!ensureMsgf(ReasonTag.IsValid(), TEXT("All code paths are expected to provide a specific reason tag."))) { return false; } return ExecuteOnValidatedMutableRuntime(Handle, [this, Handle, ReasonTag, bEnabled](FSmartObjectRuntime& SmartObjectRuntime) { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s Tag %s"), bEnabled ? TEXT("Removing") : TEXT("Adding"), *ReasonTag.ToString()); // Keep track of our previous state const uint16 OldFlags = SmartObjectRuntime.DisableFlags; const uint16 ReasonFlag = UE::SmartObject::GetMaskForEnabledReasonTag(ReasonTag); const bool bWasEnabled = !(OldFlags & ReasonFlag); if (bWasEnabled == bEnabled) { // Already in the proper state, nothing to notify UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Object is already in the desired state for Tag %s. That might indicates asymmetrical calls to SetEnabledForReason(..., ReasonX, true|false)"), *ReasonTag.ToString()); return; } // Apply the mask SmartObjectRuntime.SetEnabled(bEnabled, ReasonFlag); if (!OldFlags == !SmartObjectRuntime.DisableFlags) { // Already in the proper state for other reasons, nothing to notify return; } // Notify if needed if (SmartObjectRuntime.OnEvent.IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = bEnabled ? ESmartObjectChangeReason::OnObjectEnabled : ESmartObjectChangeReason::OnObjectDisabled; SmartObjectRuntime.OnEvent.Broadcast(Data); } // Propagate object enabled state to slots and notify if needed. for (TEnumerateRef RuntimeSlot : EnumerateRange(SmartObjectRuntime.Slots)) { const FSmartObjectSlotHandle SlotHandle(Handle, RuntimeSlot.GetIndex()); // Using 'IsEnabled' to combine slot enable and smart object enable const bool bSlotPreviousValue = RuntimeSlot->IsEnabled(); // Always set object enabled state even if combined result might not be affected RuntimeSlot->bObjectEnabled = bEnabled; // Using new combined value to detect changes if (RuntimeSlot->IsEnabled() != bSlotPreviousValue) { OnSlotChangedInternal( SmartObjectRuntime, *RuntimeSlot, SlotHandle, RuntimeSlot->IsEnabled() ? ESmartObjectChangeReason::OnSlotEnabled : ESmartObjectChangeReason::OnSlotDisabled, RuntimeSlot->UserData); } } }, __FUNCTION__); } bool USmartObjectSubsystem::IsEnabled(const FSmartObjectHandle Handle) const { bool bOutIsEnabled = false; ExecuteOnValidatedRuntime(Handle, [&bOutIsEnabled](const FSmartObjectRuntime& SmartObjectRuntime) { bOutIsEnabled = SmartObjectRuntime.IsEnabled(); }, __FUNCTION__); return bOutIsEnabled; } bool USmartObjectSubsystem::IsEnabledForReason(const FSmartObjectHandle Handle, const FGameplayTag ReasonTag) const { bool bOutIsEnabled = false; ExecuteOnValidatedRuntime(Handle, [&bOutIsEnabled, ReasonTag](const FSmartObjectRuntime& SmartObjectRuntime) { bOutIsEnabled = SmartObjectRuntime.IsEnabledForReason(ReasonTag); }, __FUNCTION__); return bOutIsEnabled; } void USmartObjectSubsystem::SetupConditionContextCommonDataInternal(FWorldConditionContextData& ContextData, const FSmartObjectRuntime& SmartObjectRuntime) const { const USmartObjectWorldConditionSchema* DefaultSchema = GetDefault(); ensureMsgf(ContextData.SetContextData(DefaultSchema->GetSmartObjectActorRef(), SmartObjectRuntime.GetOwnerActor()), TEXT("Expecting USmartObjectWorldConditionSchema::GetSmartObjectActorRef to be valid.")); ensureMsgf(ContextData.SetContextData(DefaultSchema->GetSmartObjectHandleRef(), &SmartObjectRuntime.RegisteredHandle), TEXT("Expecting USmartObjectWorldConditionSchema::SmartObjectHandleRef to be valid.")); ensureMsgf(ContextData.SetContextData(DefaultSchema->GetSubsystemRef(), this), TEXT("Expecting USmartObjectWorldConditionSchema::SubsystemRef to be valid.")); } void USmartObjectSubsystem::BindPropertiesFromStructInternal(FWorldConditionContextData& ContextData, const FConstStructView& UserData) const { const UWorldConditionSchema* Schema = ContextData.GetSchema(); check(Schema); // @todo SO: could create a cache of layouts since user data types shouldn't vary much // @todo SO: consider moving this into FWorldConditionContextData for (TFieldIterator It(UserData.GetScriptStruct()); It; ++It) { const FProperty* Property = *It; if (const FStructProperty* StructProperty = CastField(Property)) { const FWorldConditionContextDataRef Ref = Schema->GetContextDataRefByName(Property->GetFName(), StructProperty->Struct); if (Ref.IsValid()) { const FConstStructView StructView(StructProperty->Struct, UserData.GetMemory() + Property->GetOffset_ForInternal()); ContextData.SetContextData(Ref, StructView); } } else if (const FObjectPropertyBase* ObjectProperty = CastField(Property)) { const FWorldConditionContextDataRef Ref = Schema->GetContextDataRefByName(Property->GetFName(), ObjectProperty->PropertyClass); if (Ref.IsValid()) { const UObject* Object = ObjectProperty->GetObjectPropertyValue(UserData.GetMemory() + Property->GetOffset_ForInternal()); ContextData.SetContextData(Ref, Object); } } } } bool USmartObjectSubsystem::ActivateObjectPreconditionsInternal(const FWorldConditionContextData& ContextData, const FSmartObjectRuntime& SmartObjectRuntime) const { if (SmartObjectRuntime.PreconditionState.GetNumConditions() == 0) { // Nothing to activate is considered a success SmartObjectRuntime.PreconditionState.SetConditionsActivated(true); return true; } const FWorldConditionContext ObjectContext(SmartObjectRuntime.PreconditionState, ContextData); if (!ObjectContext.Activate()) { UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Failed to activate Preconditions on SmartObject '%s'."), *LexToString(SmartObjectRuntime.GetRegisteredHandle())); return false; } return true; } bool USmartObjectSubsystem::ActivateSlotPreconditionsInternal(FWorldConditionContextData& ContextData, const FSmartObjectRuntimeSlot& Slot, const FSmartObjectSlotHandle& SlotHandle) const { if (Slot.PreconditionState.GetNumConditions() == 0) { // Nothing to activate is considered a success Slot.PreconditionState.SetConditionsActivated(true); return true; } // Activate slot Preconditions if any ensureMsgf(ContextData.SetContextData(CastChecked(ContextData.GetSchema())->GetSlotHandleRef(), &SlotHandle), TEXT("Expecting USmartObjectWorldConditionSchema::SlotHandleRef to be valid.")); const FWorldConditionContext SlotContext(Slot.PreconditionState, ContextData); if (!SlotContext.Activate()) { UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Failed to activate Preconditions on SmartObject '%s' slot '%s'."), *LexToString(SlotHandle.GetSmartObjectHandle()), *LexToString(SlotHandle)); return false; } return true; } bool USmartObjectSubsystem::TryActivatePreconditionsInternal(const FSmartObjectRuntime& SmartObjectRuntime) const { if (SmartObjectRuntime.PreconditionState.AreConditionsActivated()) { return true; } if (!SmartObjectRuntime.ResolveOwnerActor()) { UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Preconditions for owning SmartObject '%s' can't be activated: no owner actor"), *LexToString(SmartObjectRuntime.GetRegisteredHandle())); return false; } FWorldConditionContextData ContextData(*SmartObjectRuntime.GetDefinition().GetWorldConditionSchema()); SetupConditionContextCommonDataInternal(ContextData, SmartObjectRuntime); if (!ActivateObjectPreconditionsInternal(ContextData, SmartObjectRuntime)) { // No need to continue with slot preconditions, we already failed. Errors are reported by ActivateObjectPreconditions. return false; } int32 SlotIndex = 0; for (const FSmartObjectRuntimeSlot& Slot : SmartObjectRuntime.Slots) { if (!ActivateSlotPreconditionsInternal(ContextData, Slot, FSmartObjectSlotHandle(SmartObjectRuntime.GetRegisteredHandle(), SlotIndex))) { // No need to continue with other slots preconditions, we already failed. Errors are reported by ActivateSlotPreconditions. return false; } SlotIndex++; } return true; } bool USmartObjectSubsystem::EvaluateObjectConditionsInternal(const FWorldConditionContextData& ConditionContextData, const FSmartObjectRuntime& SmartObjectRuntime) const { // Evaluate object conditions. Note that unsuccessfully initialized conditions is supported (i.e. error during activation) // We only want to evaluate the world condition on the server because, even if a client evaluates a false positive world condition, // the server will reconcile that failure when the replication data gets updated anyway. At the moment it isn't worth the cost // of replicating the world condition across clients to make it work. // The world condition context's FWorldConditionQueryState will never be initialized on the client (bIsInitialized) will always be false // because FWorldConditionQueryState::InitializeInternal is always going to be called with a null InSharedDefinition param. if (!IsRunningOnServer() || SmartObjectRuntime.PreconditionState.GetNumConditions() == 0) { return true; } // Preconditions activation might have been delayed for dehydrated actors if (!TryActivatePreconditionsInternal(SmartObjectRuntime)) { // Errors are reported by TryActivatePreconditions. return false; } const FWorldConditionContext Context(SmartObjectRuntime.PreconditionState, ConditionContextData); if (!Context.IsTrue()) { UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Preconditions for owning SmartObject '%s' failed."), *LexToString(SmartObjectRuntime.GetRegisteredHandle())); return false; } return true; } bool USmartObjectSubsystem::EvaluateSlotConditionsInternal( FWorldConditionContextData& ConditionContextData, const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectSlotHandle& SlotHandle ) const { FWorldConditionQueryState& QueryState = SmartObjectRuntime.Slots[SlotHandle.GetSlotIndex()].PreconditionState; if (!IsRunningOnServer() || QueryState.GetNumConditions() == 0) { return true; } // Preconditions activation might have been delayed for dehydrated actors // We try activate also for slots since the object might not have preconditions so it didn't need to activate any. if (!TryActivatePreconditionsInternal(SmartObjectRuntime)) { UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Preconditions for owning SmartObject '%s' can't be activated."), *LexToString(SmartObjectRuntime.GetRegisteredHandle())); return false; } // Add slot data to the context const USmartObjectWorldConditionSchema* DefaultSchema = GetDefault(); ensureMsgf(ConditionContextData.SetContextData(DefaultSchema->GetSlotHandleRef(), &SlotHandle), TEXT("Expecting USmartObjectWorldConditionSchema::SlotHandleRef to be valid.")); // Evaluate slot conditions. Note that unsuccessfully initialized conditions is supported (i.e. error during activation) const FWorldConditionContext Context(QueryState, ConditionContextData); if (!Context.IsTrue()) { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("Preconditions for slot '%s' failed."), *LexToString(SlotHandle)); return false; } return true; } FSmartObjectClaimHandle USmartObjectSubsystem::MarkSlotAsClaimed(const FSmartObjectSlotHandle& SlotHandle, ESmartObjectClaimPriority ClaimPriority, const FConstStructView UserData) { if (!SlotHandle.IsValid()) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Claiming using an unset SmartObject slot handle. Returning invalid FSmartObjectClaimHandle.")); return FSmartObjectClaimHandle::InvalidHandle; } FSmartObjectClaimHandle OutClaimHandle(FSmartObjectClaimHandle::InvalidHandle); ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&OutClaimHandle, this, SlotHandle, ClaimPriority, UserData](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { // Fast test to see if slot can be claimed (Parent smart object is enabled AND slot is free and enabled) if (!Slot.CanBeClaimed(ClaimPriority)) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Can't claim slot handle '%s' since it is, or its owning SmartObject '%s', disabled or not free."), *LexToString(SlotHandle), *LexToString(SlotHandle.GetSmartObjectHandle())); return; } // We're overriding a claim, notify current listeners about the release. bool bIsClaimOverridden = false; if (Slot.GetState() == ESmartObjectSlotState::Claimed) { const FInstancedStruct Payload(MoveTemp(Slot.UserData)); const FSmartObjectClaimHandle ExistingClaim(SlotHandle.SmartObjectHandle, SlotHandle, Slot.User); ensureMsgf(Slot.Release(ExistingClaim, /*bAborted*/ true), TEXT("Expecting the release to always succeed, since the slot can be claimed based on earlier check.")); UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Released using handle '%s' due to claim override"), *LexToString(ExistingClaim)); UE_VLOG_LOCATION(this, LogSmartObject, Display, GetSlotLocation(ExistingClaim).GetValue(), 50, FColor::White, TEXT("Released (Override)")); OnSlotChangedInternal(SmartObjectRuntime, Slot, ExistingClaim.SlotHandle, ESmartObjectChangeReason::OnReleased, Payload); bIsClaimOverridden = true; } const FSmartObjectUserHandle User(NextFreeUserID++); const bool bClaimed = Slot.Claim(User, ClaimPriority); const FSmartObjectClaimHandle ClaimHandle(SlotHandle.GetSmartObjectHandle(), SlotHandle, User); UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Claim %s for handle '%s'. Slot State is '%s'"), bClaimed ? TEXT("SUCCEEDED") : TEXT("FAILED"), *LexToString(ClaimHandle), *UEnum::GetValueAsString(Slot.GetState())); UE_CVLOG_LOCATION(bClaimed, this, LogSmartObject, Display, GetSlotLocation(ClaimHandle).GetValue(), 50, FColor::Yellow, TEXT("Claim %s"), bIsClaimOverridden ? TEXT("[Override]") : TEXT("")); if (bClaimed) { Slot.UserData = UserData; OnSlotChangedInternal(SmartObjectRuntime, Slot, SlotHandle, ESmartObjectChangeReason::OnClaimed, Slot.UserData); OutClaimHandle = ClaimHandle; } }, __FUNCTION__); return OutClaimHandle; } bool USmartObjectSubsystem::CanBeClaimed(const FSmartObjectSlotHandle& SlotHandle, ESmartObjectClaimPriority ClaimPriority) const { bool bOutCanBeClaimed = false; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&bOutCanBeClaimed, ClaimPriority](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { bOutCanBeClaimed = Slot.CanBeClaimed(ClaimPriority); }, __FUNCTION__); return bOutCanBeClaimed; } bool USmartObjectSubsystem::IsSmartObjectValid(const FSmartObjectHandle SmartObjectHandle) const { bool bOutIsValid = false; ExecuteOnValidatedRuntime(SmartObjectHandle, [&bOutIsValid](const FSmartObjectRuntime& SmartObjectRuntime) { bOutIsValid = true; }, __FUNCTION__); return bOutIsValid; } bool USmartObjectSubsystem::IsClaimedSmartObjectValid(const FSmartObjectClaimHandle& ClaimHandle) const { return ClaimHandle.IsValid() && IsSmartObjectValid(ClaimHandle.SmartObjectHandle); } bool USmartObjectSubsystem::IsSlotValidVerbose(const FSmartObjectSlotHandle& SlotHandle, const ANSICHAR* CallingFunctionName) const { UE_CVLOG_UELOG(!SlotHandle.IsValid(), this, LogSmartObject, Log, TEXT("%hs failed. SlotHandle is not set."), CallingFunctionName); return IsSmartObjectSlotValid(SlotHandle); } bool USmartObjectSubsystem::IsSmartObjectSlotValid(const FSmartObjectSlotHandle& SlotHandle) const { if (!SlotHandle.IsValid()) { return false; } bool bOutIsValid = false; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&bOutIsValid](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { bOutIsValid = true; }, __FUNCTION__); return bOutIsValid; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinition( const FSmartObjectClaimHandle& ClaimHandle, TSubclassOf DefinitionClass ) { const USmartObjectBehaviorDefinition* OutDefinition = nullptr; ExecuteOnValidatedRuntime(ClaimHandle.SmartObjectHandle, [&OutDefinition, &ClaimHandle, &DefinitionClass](const FSmartObjectRuntime& SmartObjectRuntime) { OutDefinition = GetBehaviorDefinitionInternal(SmartObjectRuntime, ClaimHandle.SlotHandle, DefinitionClass); }, __FUNCTION__); return OutDefinition; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinitionByRequestResult( const FSmartObjectRequestResult& RequestResult, TSubclassOf DefinitionClass ) { const USmartObjectBehaviorDefinition* OutDefinition = nullptr; ExecuteOnValidatedRuntime(RequestResult.SmartObjectHandle, [&OutDefinition, &RequestResult, &DefinitionClass](const FSmartObjectRuntime& SmartObjectRuntime) { OutDefinition = GetBehaviorDefinitionInternal(SmartObjectRuntime, RequestResult.SlotHandle, DefinitionClass); }, __FUNCTION__); return OutDefinition; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinitionInternal( const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectSlotHandle& SlotHandle, TSubclassOf DefinitionClass ) { const USmartObjectDefinition& Definition = SmartObjectRuntime.GetDefinition(); return Definition.GetBehaviorDefinition(SlotHandle.GetSlotIndex(), DefinitionClass); } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::MarkSlotAsOccupied( const FSmartObjectClaimHandle& ClaimHandle, TSubclassOf DefinitionClass ) { const USmartObjectBehaviorDefinition* OutDefinition = nullptr; ExecuteOnValidatedMutableRuntime(ClaimHandle.SmartObjectHandle, [this, &OutDefinition, &ClaimHandle, &DefinitionClass](FSmartObjectRuntime& SmartObjectRuntime) { OutDefinition = MarkSlotAsOccupiedInternal(SmartObjectRuntime, ClaimHandle, DefinitionClass); }, __FUNCTION__); return OutDefinition; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::MarkSlotAsOccupiedInternal( FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectClaimHandle& ClaimHandle, TSubclassOf DefinitionClass ) { checkf(ClaimHandle.IsValid(), TEXT("This is an internal method that should only be called with an assigned claim handle")); if (!SmartObjectRuntime.IsEnabled()) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Can't use handle '%s' since associated object is disabled."), *LexToString(ClaimHandle)); return nullptr; } const USmartObjectBehaviorDefinition* BehaviorDefinition = GetBehaviorDefinitionInternal(SmartObjectRuntime, ClaimHandle.SlotHandle, DefinitionClass); if (BehaviorDefinition == nullptr) { const UClass* ClassPtr = DefinitionClass.Get(); UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Unable to find a behavior definition of type '%s' in '%s'"), ClassPtr != nullptr ? *ClassPtr->GetName(): TEXT("Null"), *SmartObjectRuntime.GetDefinition().GetPathName()); return nullptr; } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Start using handle '%s'"), *LexToString(ClaimHandle)); UE_VLOG_LOCATION(this, LogSmartObject, Display, GetSlotLocation(ClaimHandle).GetValue(), 50, FColor::Green, TEXT("Use")); FSmartObjectRuntimeSlot& Slot = SmartObjectRuntime.Slots[ClaimHandle.SlotHandle.GetSlotIndex()]; if (Slot.GetState() == ESmartObjectSlotState::Claimed) { if (Slot.User == ClaimHandle.UserHandle) { Slot.State = ESmartObjectSlotState::Occupied; OnSlotChangedInternal(SmartObjectRuntime, Slot, ClaimHandle.SlotHandle, ESmartObjectChangeReason::OnOccupied, Slot.UserData); return BehaviorDefinition; } UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Fail to occupy slot '%s' for handle '%s': slot is s already assigned to '%s'"), *LexToString(Slot), *LexToString(ClaimHandle), *LexToString(Slot.User)); } else { UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Fail to occupy slot '%s' for handle '%s': state is expected to be 'Claimed', but it is currently '%s'"), *LexToString(Slot), *LexToString(ClaimHandle), *UEnum::GetValueAsString(Slot.GetState())); } return nullptr; } bool USmartObjectSubsystem::MarkSlotAsFree(const FSmartObjectClaimHandle& ClaimHandle) { bool bOutReleased = false; ExecuteOnValidatedMutableRuntimeAndSlot(ClaimHandle.SlotHandle, [&bOutReleased, this, &ClaimHandle](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { // Keep user data to be used as payload in the notification event // since it will be released by the following call to Slot.Release const FInstancedStruct Payload(MoveTemp(Slot.UserData)); bOutReleased = Slot.Release(ClaimHandle, /*bAborted*/ false); if (bOutReleased) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Released using handle '%s'"), *LexToString(ClaimHandle)); UE_VLOG_LOCATION(this, LogSmartObject, Display, GetSlotLocation(ClaimHandle).GetValue(), 50, FColor::White, TEXT("Released")); OnSlotChangedInternal(SmartObjectRuntime, Slot, ClaimHandle.SlotHandle, ESmartObjectChangeReason::OnReleased, Payload); } }, __FUNCTION__); return bOutReleased; } ESmartObjectSlotState USmartObjectSubsystem::GetSlotState(const FSmartObjectSlotHandle SlotHandle) const { ESmartObjectSlotState OutSlotState = ESmartObjectSlotState::Invalid; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&OutSlotState](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { OutSlotState = Slot.GetState(); }, __FUNCTION__); return OutSlotState; } bool USmartObjectSubsystem::GetSlotLocation(const FSmartObjectClaimHandle& ClaimHandle, FVector& OutSlotLocation) const { const TOptional OptionalLocation = GetSlotLocation(ClaimHandle); OutSlotLocation = OptionalLocation.Get(FVector::ZeroVector); return OptionalLocation.IsSet(); } TOptional USmartObjectSubsystem::GetSlotLocation(const FSmartObjectRequestResult& Result) const { return GetSlotLocation(Result.SlotHandle); } TOptional USmartObjectSubsystem::GetSlotLocation(const FSmartObjectSlotHandle& SlotHandle) const { TOptional OutLocation; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&OutLocation](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { OutLocation = SmartObjectRuntime.Transform.TransformPosition(FVector(Slot.Offset)); }, __FUNCTION__); return OutLocation; } bool USmartObjectSubsystem::GetSlotTransform(const FSmartObjectClaimHandle& ClaimHandle, FTransform& OutSlotTransform) const { const TOptional OptionalTransform = GetSlotTransform(ClaimHandle); OutSlotTransform = OptionalTransform.Get(FTransform::Identity); return OptionalTransform.IsSet(); } TOptional USmartObjectSubsystem::GetSlotTransform(const FSmartObjectRequestResult& Result) const { return GetSlotTransform(Result.SlotHandle); } bool USmartObjectSubsystem::GetSlotTransformFromRequestResult(const FSmartObjectRequestResult& RequestResult, FTransform& OutSlotTransform) const { const TOptional OptionalTransform = GetSlotTransform(RequestResult); OutSlotTransform = OptionalTransform.Get(FTransform::Identity); return OptionalTransform.IsSet(); } TOptional USmartObjectSubsystem::GetSlotTransform(const FSmartObjectSlotHandle& SlotHandle) const { TOptional OutTransform; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&OutTransform](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { OutTransform = Slot.GetSlotWorldTransform(SmartObjectRuntime.Transform); }, __FUNCTION__); return OutTransform; } FTransform USmartObjectSubsystem::GetSlotTransformChecked(const FSmartObjectSlotHandle& SlotHandle) const { FTransform OutTransform; verify(ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&OutTransform](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { OutTransform = Slot.GetSlotWorldTransform(SmartObjectRuntime.Transform); }, __FUNCTION__)); return OutTransform; } FConstStructView USmartObjectSubsystem::GetOwnerData(const FSmartObjectHandle Handle) const { // Note that returning a view on the owner data is currently thread safe since the // runtime instance lifetime is still single threaded so the data can't be destroyed while // the returned view gets read by the caller. // If that assumption changes, another version returning a FInstancedStruct will be required. FConstStructView OutOwnerData; verify(ExecuteOnValidatedRuntime(Handle, [&OutOwnerData](const FSmartObjectRuntime& SmartObjectRuntime) { OutOwnerData = SmartObjectRuntime.OwnerData; }, __FUNCTION__)); return OutOwnerData; } bool USmartObjectSubsystem::GetValidatedMutableRuntimeAndSlotInternal(const FSmartObjectSlotHandle& SlotHandle, FSmartObjectRuntime*& OutSmartObjectRuntime, FSmartObjectRuntimeSlot*& OutSlot, const ANSICHAR* CallingFunctionName) const { const FSmartObjectRuntime* ConstSmartObjectRuntime = nullptr; const FSmartObjectRuntimeSlot* ConstSlot = nullptr; if (GetValidatedRuntimeAndSlotInternal(SlotHandle, ConstSmartObjectRuntime, ConstSlot, CallingFunctionName)) { OutSmartObjectRuntime = const_cast(ConstSmartObjectRuntime); OutSlot = const_cast(ConstSlot); return true; } OutSmartObjectRuntime = nullptr; OutSlot = nullptr; return false; } bool USmartObjectSubsystem::ExecuteOnValidatedMutableRuntimeAndSlot(const FSmartObjectSlotHandle& SlotHandle, TFunctionRef ExecFunction, const ANSICHAR* CallingFunctionName) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); FSmartObjectRuntime* SmartObjectRuntime = nullptr; FSmartObjectRuntimeSlot* Slot = nullptr; if (GetValidatedMutableRuntimeAndSlotInternal(SlotHandle, SmartObjectRuntime, Slot, CallingFunctionName)) { UE_MT_SCOPED_INSTANCE_WRITE_LOCK(); UE_MT_SCOPED_INSTANCE_WRITE_ACCESS_DETECTOR(); ExecFunction(*SmartObjectRuntime, *Slot); return true; } return false; } bool USmartObjectSubsystem::GetValidatedRuntimeAndSlotInternal(const FSmartObjectSlotHandle& SlotHandle, const FSmartObjectRuntime*& OutSmartObjectRuntime, const FSmartObjectRuntimeSlot*& OutSlot, const ANSICHAR* CallingFunctionName) const { if (SlotHandle.IsValid()) { if (const FSmartObjectRuntime* SmartObjectRuntime = GetRuntimeInstanceInternal(SlotHandle.GetSmartObjectHandle())) { if (SmartObjectRuntime->Slots.IsValidIndex(SlotHandle.GetSlotIndex())) { OutSmartObjectRuntime = SmartObjectRuntime; OutSlot = SmartObjectRuntime != nullptr ? &SmartObjectRuntime->Slots[SlotHandle.GetSlotIndex()] : nullptr; return true; } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("%hs Invalid slot index %d (%d slots)."), CallingFunctionName, SlotHandle.GetSlotIndex(), SmartObjectRuntime->Slots.Num()); } else { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("%hs failed using handle '%s'. SmartObject is no longer part of the simulation."), CallingFunctionName, *LexToString(SlotHandle)); } } else { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("%hs failed. Handle is not set."), CallingFunctionName); } OutSmartObjectRuntime = nullptr; OutSlot = nullptr; return false; } bool USmartObjectSubsystem::ExecuteOnValidatedRuntimeAndSlot(const FSmartObjectSlotHandle& SlotHandle, TFunctionRef ExecFunction, const ANSICHAR* CallingFunctionName) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); const FSmartObjectRuntime* SmartObjectRuntime = nullptr; const FSmartObjectRuntimeSlot* Slot = nullptr; if (GetValidatedRuntimeAndSlotInternal(SlotHandle, SmartObjectRuntime, Slot, CallingFunctionName)) { UE_MT_SCOPED_INSTANCE_READ_LOCK(); UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); ExecFunction(*SmartObjectRuntime, *Slot); return true; } return false; } FSmartObjectRuntime* USmartObjectSubsystem::GetValidatedMutableRuntimeInternal(const FSmartObjectHandle Handle, const ANSICHAR* CallingFunctionName) const { return const_cast(GetValidatedRuntimeInternal(Handle, CallingFunctionName)); } bool USmartObjectSubsystem::ExecuteOnValidatedMutableRuntime(const FSmartObjectHandle Handle, TFunctionRef ExecFunction, const ANSICHAR* CallingFunctionName) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); if (FSmartObjectRuntime* SmartObjectRuntime = GetValidatedMutableRuntimeInternal(Handle, CallingFunctionName)) { UE_MT_SCOPED_INSTANCE_WRITE_LOCK(); UE_MT_SCOPED_INSTANCE_WRITE_ACCESS_DETECTOR(); ExecFunction(*SmartObjectRuntime); return true; } return false; } const FSmartObjectRuntime* USmartObjectSubsystem::GetValidatedRuntimeInternal(const FSmartObjectHandle Handle, const ANSICHAR* CallingFunctionName) const { const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Handle); UE_CVLOG_UELOG(!Handle.IsValid(), this, LogSmartObject, Log, TEXT("%hs failed. Handle is not set."), CallingFunctionName); UE_CVLOG_UELOG(Handle.IsValid() && SmartObjectRuntime == nullptr, this, LogSmartObject, Log, TEXT("%hs failed using handle '%s'. SmartObject is no longer part of the simulation."), CallingFunctionName, *LexToString(Handle)); return SmartObjectRuntime; } bool USmartObjectSubsystem::ExecuteOnValidatedRuntime(const FSmartObjectHandle Handle, TFunctionRef ExecFunction, const ANSICHAR* CallingFunctionName) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); if (const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntimeInternal(Handle, CallingFunctionName)) { UE_MT_SCOPED_INSTANCE_READ_LOCK(); UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); ExecFunction(*SmartObjectRuntime); return true; } return false; } FOnSmartObjectEvent* USmartObjectSubsystem::GetEventDelegate(const FSmartObjectHandle SmartObjectHandle) { FOnSmartObjectEvent* OutEvent = nullptr; ExecuteOnValidatedMutableRuntime(SmartObjectHandle, [&OutEvent](FSmartObjectRuntime& SmartObjectRuntime) { OutEvent = &SmartObjectRuntime.GetMutableEventDelegate(); }, __FUNCTION__); return OutEvent; } const FGameplayTagContainer& USmartObjectSubsystem::GetInstanceTags(const FSmartObjectHandle Handle) const { const FGameplayTagContainer* OutTagContainer = nullptr; ExecuteOnValidatedRuntime(Handle, [&OutTagContainer](const FSmartObjectRuntime& SmartObjectRuntime) { OutTagContainer = &SmartObjectRuntime.GetTags(); }, __FUNCTION__); return OutTagContainer ? *OutTagContainer : FGameplayTagContainer::EmptyContainer; } void USmartObjectSubsystem::AddTagToInstance(const FSmartObjectHandle Handle, const FGameplayTag& Tag) { ExecuteOnValidatedMutableRuntime(Handle, [&Tag](FSmartObjectRuntime& SmartObjectRuntime) { AddTagToInstanceInternal(SmartObjectRuntime, Tag); }, __FUNCTION__); } void USmartObjectSubsystem::RemoveTagFromInstance(const FSmartObjectHandle Handle, const FGameplayTag& Tag) { ExecuteOnValidatedMutableRuntime(Handle, [&Tag](FSmartObjectRuntime& SmartObjectRuntime) { RemoveTagFromInstanceInternal(SmartObjectRuntime, Tag); }, __FUNCTION__); } const FGameplayTagContainer& USmartObjectSubsystem::GetSlotTags(const FSmartObjectSlotHandle SlotHandle) const { const FGameplayTagContainer* OutTagContainer = nullptr; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&OutTagContainer](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { OutTagContainer = &Slot.Tags; }, __FUNCTION__); return OutTagContainer ? *OutTagContainer : FGameplayTagContainer::EmptyContainer; } void USmartObjectSubsystem::AddTagToSlot(const FSmartObjectSlotHandle SlotHandle, const FGameplayTag& Tag) { if (!Tag.IsValid()) { return; } ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&Tag, &SlotHandle](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { if (!Slot.Tags.HasTag(Tag)) { Slot.Tags.AddTagFast(Tag); OnSlotChangedInternal(SmartObjectRuntime, Slot, SlotHandle, ESmartObjectChangeReason::OnTagAdded, Slot.GetUserData(), Tag); } }, __FUNCTION__); } bool USmartObjectSubsystem::RemoveTagFromSlot(const FSmartObjectSlotHandle SlotHandle, const FGameplayTag& Tag) { if (!Tag.IsValid()) { return false; } bool bOutTagRemoved = false; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&bOutTagRemoved, Tag, &SlotHandle](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { if (Slot.Tags.RemoveTag(Tag)) { OnSlotChangedInternal(SmartObjectRuntime, Slot, SlotHandle, ESmartObjectChangeReason::OnTagRemoved, Slot.GetUserData(), Tag); bOutTagRemoved = true; } }, __FUNCTION__); return bOutTagRemoved; } bool USmartObjectSubsystem::SetSlotEnabled(const FSmartObjectSlotHandle SlotHandle, const bool bEnabled) { bool bOutPreviousValue = false; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&bOutPreviousValue, bEnabled, &SlotHandle](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { // Using 'IsEnabled' that combines both slot and smart object enabled state bOutPreviousValue = Slot.IsEnabled(); // Always set slot enabled state even if combined result might not be affected Slot.bSlotEnabled = bEnabled; // Using new combined value to detect changes if (Slot.IsEnabled() != bOutPreviousValue) { OnSlotChangedInternal( SmartObjectRuntime, Slot, SlotHandle, Slot.IsEnabled() ? ESmartObjectChangeReason::OnSlotEnabled : ESmartObjectChangeReason::OnSlotDisabled, Slot.UserData); } }, __FUNCTION__); return bOutPreviousValue; } bool USmartObjectSubsystem::SendSlotEvent(const FSmartObjectSlotHandle& SlotHandle, const FGameplayTag EventTag, const FConstStructView Payload) { bool bOutEventSent = false; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&bOutEventSent, &SlotHandle, &EventTag, Payload](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { // Runtime slot lifetime is bound to the runtime smart object, so it should always be available. if (SmartObjectRuntime.GetEventDelegate().IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SlotHandle.GetSmartObjectHandle(); Data.SlotHandle = SlotHandle; Data.Reason = ESmartObjectChangeReason::OnEvent; Data.Tag = EventTag; Data.EventPayload = Payload; SmartObjectRuntime.GetEventDelegate().Broadcast(Data); bOutEventSent = true; } }, __FUNCTION__); return bOutEventSent; } void USmartObjectSubsystem::AddTagToInstanceInternal(FSmartObjectRuntime& SmartObjectRuntime, const FGameplayTag& Tag) { if (!SmartObjectRuntime.Tags.HasTag(Tag)) { SmartObjectRuntime.Tags.AddTagFast(Tag); FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnTagAdded; Data.Tag = Tag; SmartObjectRuntime.OnEvent.Broadcast(Data); } } void USmartObjectSubsystem::RemoveTagFromInstanceInternal(FSmartObjectRuntime& SmartObjectRuntime, const FGameplayTag& Tag) { if (SmartObjectRuntime.Tags.RemoveTag(Tag)) { FSmartObjectEventData Data; Data.SmartObjectHandle = SmartObjectRuntime.GetRegisteredHandle(); Data.Reason = ESmartObjectChangeReason::OnTagRemoved; Data.Tag = Tag; SmartObjectRuntime.OnEvent.Broadcast(Data); } } void USmartObjectSubsystem::OnSlotChangedInternal( const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot, const FSmartObjectSlotHandle& SlotHandle, const ESmartObjectChangeReason Reason, const FConstStructView Payload, const FGameplayTag ChangedTag ) { if (SmartObjectRuntime.GetEventDelegate().IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = SlotHandle.GetSmartObjectHandle(); Data.SlotHandle = SlotHandle; Data.Reason = Reason; Data.Tag = ChangedTag; Data.EventPayload = Payload; SmartObjectRuntime.GetEventDelegate().Broadcast(Data); } } void USmartObjectSubsystem::RegisterSlotInvalidationCallback(const FSmartObjectClaimHandle& ClaimHandle, const FOnSlotInvalidated& Callback) { ExecuteOnValidatedMutableRuntimeAndSlot(ClaimHandle.SlotHandle, [&Callback](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { Slot.OnSlotInvalidatedDelegate = Callback; }, __FUNCTION__); } void USmartObjectSubsystem::UnregisterSlotInvalidationCallback(const FSmartObjectClaimHandle& ClaimHandle) { ExecuteOnValidatedMutableRuntimeAndSlot(ClaimHandle.SlotHandle, [](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { Slot.OnSlotInvalidatedDelegate.Unbind(); }, __FUNCTION__); } FOnSmartObjectEvent* USmartObjectSubsystem::GetSlotEventDelegate(const FSmartObjectSlotHandle& SlotHandle) { FOnSmartObjectEvent* OutEventDelegate = nullptr; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&OutEventDelegate](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { OutEventDelegate = &SmartObjectRuntime.GetMutableEventDelegate(); }, __FUNCTION__); return OutEventDelegate; } #if UE_ENABLE_DEBUG_DRAWING void USmartObjectSubsystem::DebugDraw(FDebugRenderSceneProxy* DebugProxy) const { if (!bRuntimeInitialized) { return; } checkfSlow(SpacePartition != nullptr, TEXT("Space partition is expected to be valid since we use the plugins default in OnWorldComponentsUpdated.")); SpacePartition->Draw(DebugProxy); for (auto It(RuntimeSmartObjects.CreateConstIterator()); It; ++It) { const FSmartObjectRuntime& Runtime = It.Value(); DebugProxy->Boxes.Emplace(Runtime.Bounds, GColorList.Blue); } } #endif // UE_ENABLE_DEBUG_DRAWING void USmartObjectSubsystem::AddSlotData(const FSmartObjectClaimHandle& ClaimHandle, const FConstStructView InData) { ExecuteOnValidatedMutableRuntimeAndSlot(ClaimHandle.SlotHandle, [InData](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { // If we have a data of same type, override, else add. bool bFound = false; for (FStructView Data : Slot.StateData) { if (Data.GetScriptStruct() == InData.GetScriptStruct()) { Data.GetScriptStruct()->CopyScriptStruct(Data.GetMemory(), InData.GetMemory()); bFound = true; break; } } if (!bFound) { Slot.StateData.Append({ InData }); } }, __FUNCTION__); } FSmartObjectSlotView USmartObjectSubsystem::GetSlotView(const FSmartObjectSlotHandle& SlotHandle) const { FSmartObjectSlotView OutSlotView; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [&OutSlotView, &SlotHandle](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { OutSlotView = FSmartObjectSlotView(SlotHandle, SmartObjectRuntime, Slot); }, __FUNCTION__); return OutSlotView; } bool USmartObjectSubsystem::ReadSlotData(const FSmartObjectSlotHandle& SlotHandle, TFunctionRef Function) const { bool bExecuted = false; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [Function, &SlotHandle, &bExecuted](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { FConstSmartObjectSlotView ConstSlotView(SlotHandle, SmartObjectRuntime, Slot); if (ConstSlotView.IsValid()) { Function(ConstSlotView); bExecuted = true; } }, __FUNCTION__); return bExecuted; } bool USmartObjectSubsystem::MutateSlotData(const FSmartObjectSlotHandle& SlotHandle, TFunctionRef Function) const { bool bExecuted = false; ExecuteOnValidatedMutableRuntimeAndSlot(SlotHandle, [Function, &SlotHandle, &bExecuted](FSmartObjectRuntime& SmartObjectRuntime, FSmartObjectRuntimeSlot& Slot) { FSmartObjectSlotView SlotView(SlotHandle, SmartObjectRuntime, Slot); if (SlotView.IsValid()) { Function(SlotView); bExecuted = true; } }, __FUNCTION__); return bExecuted; } void USmartObjectSubsystem::FindSlots(const FSmartObjectHandle Handle, const FSmartObjectRequestFilter& Filter, TArray& OutSlots, const FConstStructView UserData) const { ExecuteOnValidatedRuntime(Handle, [this, &Handle, &Filter, &OutSlots, UserData](const FSmartObjectRuntime& SmartObjectRuntime) { FindSlotsInternal(Handle, SmartObjectRuntime, Filter, OutSlots, UserData); }, __FUNCTION__); } void USmartObjectSubsystem::GetAllSlots(const FSmartObjectHandle Handle, TArray& OutSlots) const { TRACE_CPUPROFILER_EVENT_SCOPE_STR("SmartObject_FilterSlots"); ExecuteOnValidatedRuntime(Handle, [&OutSlots, &Handle](const FSmartObjectRuntime& SmartObjectRuntime) { OutSlots.Reserve(SmartObjectRuntime.Slots.Num()); for (int32 Index = 0; Index < SmartObjectRuntime.Slots.Num(); Index++) { OutSlots.Add(FSmartObjectSlotHandle(Handle, Index)); } }, __FUNCTION__); } bool USmartObjectSubsystem::EvaluateConditionsForFilteringInternal( const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectSlotHandle& SlotHandle, FWorldConditionContextData& ContextData, const FConstStructView UserData, TPair& LastEvaluatedRuntime ) const { // Evaluate preconditions on the parent object only once if all slots have the same one (usual case) if (&SmartObjectRuntime != LastEvaluatedRuntime.Key) { LastEvaluatedRuntime.Key = &SmartObjectRuntime; // Set context schema and bind user data only if not set or changed const UWorldConditionSchema* PrevSchema = ContextData.GetSchema(); if (PrevSchema == nullptr || PrevSchema != SmartObjectRuntime.GetDefinition().GetWorldConditionSchema()) { ContextData.SetSchema(*SmartObjectRuntime.GetDefinition().GetWorldConditionSchema()); // Setup some context data using user data BindPropertiesFromStructInternal(ContextData, UserData); } // Setup system related data (object runtime, slot, subsystem, etc.) SetupConditionContextCommonDataInternal(ContextData, SmartObjectRuntime); // Evaluate object conditions. LastEvaluatedRuntime.Value = EvaluateObjectConditionsInternal(ContextData, SmartObjectRuntime); } // Evaluate slot conditions only if parent runtime passed its own selection conditions return LastEvaluatedRuntime.Value ? EvaluateSlotConditionsInternal(ContextData, SmartObjectRuntime, SlotHandle) : false; } void USmartObjectSubsystem::ExecuteOnSlotFilteredBySelectionConditions( const TConstStridedView SlotsToFilter, const FConstStructView UserData, TFunctionRef ExecFunction ) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); FWorldConditionContextData ContextData; TPair LastEvaluatedSmartObjectRuntime = {nullptr, false}; const FSmartObjectRuntime* CurrentRuntime = nullptr; FSmartObjectHandle CurrentRuntimeHandle = {}; // Using explicit index since can't use TEnumerateRef with StridedView int32 Index = 0; for (const FSmartObjectSlotHandle& SlotHandle : SlotsToFilter) { const FSmartObjectHandle ObjectHandle = SlotHandle.GetSmartObjectHandle(); if (!CurrentRuntime || CurrentRuntimeHandle != ObjectHandle) { CurrentRuntimeHandle = ObjectHandle; CurrentRuntime = GetValidatedRuntimeInternal(CurrentRuntimeHandle, __FUNCTION__); } if (CurrentRuntime && CurrentRuntime->Slots.IsValidIndex(SlotHandle.GetSlotIndex())) { UE_MT_SCOPED_INSTANCE_READ_LOCK(); UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); if (EvaluateConditionsForFilteringInternal(*CurrentRuntime, SlotHandle, ContextData, UserData, LastEvaluatedSmartObjectRuntime)) { ExecFunction(Index); } } else { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("%hs failed using handle '%s'. Slot is no longer part of the simulation. Consider calling IsSmartObjectSlotValid to avoid this message."), __FUNCTION__, *LexToString(SlotHandle)); } Index++; } } TArray USmartObjectSubsystem::FilterSlotsBySelectionConditions( const TConstArrayView& SlotsToFilter, const FConstStructView UserData ) const { TArray OutResults; OutResults.Reserve(SlotsToFilter.Num()); ExecuteOnSlotFilteredBySelectionConditions(MakeConstStridedView(SlotsToFilter), UserData, [&OutResults, SlotsToFilter](const int32 Index) { OutResults.Add(SlotsToFilter[Index]); }); OutResults.Shrink(); return MoveTemp(OutResults); } TArray USmartObjectSubsystem::FilterResultsBySelectionConditions( const TConstArrayView& ResultsToFilter, const FConstStructView UserData ) const { TArray OutResults; OutResults.Reserve(ResultsToFilter.Num()); ExecuteOnSlotFilteredBySelectionConditions(MakeConstStridedView(ResultsToFilter, &FSmartObjectRequestResult::SlotHandle), UserData, [&OutResults, ResultsToFilter](const int32 Index) { OutResults.Add(ResultsToFilter[Index]); }); OutResults.Shrink(); return MoveTemp(OutResults); } bool USmartObjectSubsystem::EvaluateSelectionConditions(const FSmartObjectSlotHandle& SlotHandle, const FConstStructView UserData) const { FWorldConditionContextData ContextData; TPair LastEvaluatedSmartObjectRuntime = {nullptr, false}; bool bOutConditionsPassed = false; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&bOutConditionsPassed, this, &SlotHandle, &ContextData, UserData, &LastEvaluatedSmartObjectRuntime] (const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { bOutConditionsPassed = EvaluateConditionsForFilteringInternal(SmartObjectRuntime, SlotHandle, ContextData, UserData, LastEvaluatedSmartObjectRuntime); }, __FUNCTION__); return bOutConditionsPassed; } bool USmartObjectSubsystem::FindEntranceLocationForSlot(const FSmartObjectSlotHandle& SlotHandle, const FSmartObjectSlotEntranceLocationRequest& Request, FSmartObjectSlotEntranceLocationResult& Result) const { return FindEntranceLocationInternal(SlotHandle, FSmartObjectSlotEntranceHandle(), Request, Result); } bool USmartObjectSubsystem::UpdateEntranceLocation(const FSmartObjectSlotEntranceHandle& EntranceHandle, const FSmartObjectSlotEntranceLocationRequest& Request, FSmartObjectSlotEntranceLocationResult& Result) const { return FindEntranceLocationInternal(EntranceHandle.GetSlotHandle(), EntranceHandle, Request, Result); } bool USmartObjectSubsystem::FindEntranceLocationInternal( const FSmartObjectSlotHandle& SlotHandle, const FSmartObjectSlotEntranceHandle& SlotEntranceHandle, const FSmartObjectSlotEntranceLocationRequest& Request, FSmartObjectSlotEntranceLocationResult& OutResult ) const { OutResult = {}; bool bOutHasResult = false; ExecuteOnValidatedRuntimeAndSlot(SlotHandle, [&bOutHasResult, this, &Request, &SlotHandle, &SlotEntranceHandle, &OutResult](const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot) { UWorld* World = GetWorld(); FSmartObjectValidationContext ValidationContext; if (!ValidationContext.Init(World, Request, SmartObjectRuntime.GetOwnerActor())) { return; } const FSmartObjectSlotDefinition& SlotDefinition = SmartObjectRuntime.GetDefinition().GetSlot(SlotHandle.GetSlotIndex()); const FTransform& SlotTransform = Slot.GetSlotWorldTransform(SmartObjectRuntime.Transform); QueryValidatedSlotEntranceLocationsInternal( World, ValidationContext, Request, SlotHandle, SlotDefinition, SlotTransform, SlotEntranceHandle, [&OutResult = OutResult, &bOutHasResult](const FSmartObjectSlotEntranceLocationResult& Result) { if (Result.bIsValid) { OutResult = Result; bOutHasResult = true; return false; // Stop iterating } return true; // Continue }); }, __FUNCTION__); return bOutHasResult; } bool USmartObjectSubsystem::QueryAllValidatedEntranceLocations( const UWorld* World, const USmartObjectDefinition& SmartObjectDefinition, const FTransform& SmartObjectTransform, const AActor* SkipActor, const FSmartObjectSlotEntranceLocationRequest& Request, TArray& Results ) { FSmartObjectValidationContext ValidationContext; if (!ValidationContext.Init(World, Request, SkipActor)) { return false; } TConstArrayView SlotDefinitions = SmartObjectDefinition.GetSlots(); for (TConstEnumerateRef SlotDefinition : EnumerateRange(SlotDefinitions)) { const FTransform& SlotTransform = SmartObjectDefinition.GetSlotWorldTransform(SlotDefinition.GetIndex(), SmartObjectTransform); const FSmartObjectSlotHandle SlotHandle({}, SlotDefinition.GetIndex()); QueryValidatedSlotEntranceLocationsInternal( World, ValidationContext, Request, SlotHandle, *SlotDefinition, SlotTransform, {}, [&Results](const FSmartObjectSlotEntranceLocationResult& Result) { Results.Add(Result); return true; // Continue }); } return Results.Num() > 0; } void USmartObjectSubsystem::QueryValidatedSlotEntranceLocationsInternal( const UWorld* World, FSmartObjectValidationContext& ValidationContext, const FSmartObjectSlotEntranceLocationRequest& Request, const FSmartObjectSlotHandle& SlotHandle, const FSmartObjectSlotDefinition& SlotDefinition, const FTransform& SlotTransform, const FSmartObjectSlotEntranceHandle& SlotEntranceHandle, TFunctionRef ResultFunc ) { struct FSmartObjectSlotEntranceCandidate { FVector Location; FRotator Rotation; NavNodeRef NodeRef; FVector::FReal DistanceSqr = 0.0; const FSmartObjectSlotEntranceAnnotation* EntranceAnnotation = nullptr; ESmartObjectEntrancePriority SelectionPriority = ESmartObjectEntrancePriority::Normal; bool bTraceGroundLocation = false; bool bCheckTransitionTrajectory = false; FSmartObjectSlotEntranceHandle Handle; }; TArray SlotColliders; TArray> Candidates; const bool bIncludeEntries = Request.LocationType == ESmartObjectSlotNavigationLocationType::Entry; const bool bIncludeExits = Request.LocationType == ESmartObjectSlotNavigationLocationType::Exit; for (TConstEnumerateRef DataProxy : EnumerateRange(SlotDefinition.DefinitionData)) { if (const FSmartObjectSlotEntranceAnnotation* EntranceAnnotation = DataProxy->Data.GetPtr()) { // If specific entry location was requested and this is not the one, skip it. if (SlotEntranceHandle.Type == FSmartObjectSlotEntranceHandle::EType::Entrance && SlotEntranceHandle.Index != DataProxy.GetIndex()) { continue; } if ((EntranceAnnotation->bIsEntry == bIncludeEntries || EntranceAnnotation->bIsExit == bIncludeExits) && EntranceAnnotation->HasTransform()) { const FTransform EntryTransform = EntranceAnnotation->GetAnnotationWorldTransform(SlotTransform); FSmartObjectSlotEntranceCandidate& Candidate = Candidates.AddDefaulted_GetRef(); Candidate.Location = EntryTransform.GetLocation(); Candidate.Rotation = EntryTransform.GetRotation().Rotator(); Candidate.EntranceAnnotation = EntranceAnnotation; Candidate.bTraceGroundLocation = EntranceAnnotation->bTraceGroundLocation; Candidate.bCheckTransitionTrajectory = EntranceAnnotation->bCheckTransitionTrajectory; Candidate.SelectionPriority = EntranceAnnotation->SelectionPriority; Candidate.Handle = FSmartObjectSlotEntranceHandle(SlotHandle, FSmartObjectSlotEntranceHandle::EType::Entrance, DataProxy.GetIndex()); } } else if (const FSmartObjectAnnotation_SlotUserCollision* UserCollisionAnnotation = DataProxy->Data.GetPtr()) { UserCollisionAnnotation->GetColliders(ValidationContext.UserCapsuleParams, SlotTransform, SlotColliders); } } if ((Candidates.IsEmpty() && Request.bUseSlotLocationAsFallback) || SlotEntranceHandle.Type == FSmartObjectSlotEntranceHandle::EType::Slot) { FSmartObjectSlotEntranceCandidate& Candidate = Candidates.AddDefaulted_GetRef(); Candidate.Location = SlotTransform.GetLocation(); Candidate.Rotation = SlotTransform.GetRotation().Rotator(); Candidate.bTraceGroundLocation = true; // Use ground project by default on slots (this seems to match the users expectation). Entrances have specific bool to turn it off. Candidate.Handle = FSmartObjectSlotEntranceHandle(SlotHandle, FSmartObjectSlotEntranceHandle::EType::Slot); } // Early out if nothing to report. if (Candidates.IsEmpty()) { return; } // Sort candidates so that the best candidate is first. if (Candidates.Num() > 1) { if (Request.SelectMethod == FSmartObjectSlotEntrySelectionMethod::NearestToSearchLocation) { for (FSmartObjectSlotEntranceCandidate& Candidate : Candidates) { Candidate.DistanceSqr = FVector::DistSquared(Request.SearchLocation, Candidate.Location); } Candidates.Sort([](const FSmartObjectSlotEntranceCandidate& A, const FSmartObjectSlotEntranceCandidate& B) { if (A.SelectionPriority == B.SelectionPriority) { return A.DistanceSqr < B.DistanceSqr; } return A.SelectionPriority > B.SelectionPriority; }); } else { // Use stable sort to keep initial order. Candidates.StableSort([](const FSmartObjectSlotEntranceCandidate& A, const FSmartObjectSlotEntranceCandidate& B) { return A.SelectionPriority > B.SelectionPriority; }); } } check(Candidates.Num() > 0); // If the slot location should be free of collisions, check it now since it's shared for all entries. bool bIsSlotCollisionsValid = true; if (Request.bCheckSlotLocationOverlap && !SlotColliders.IsEmpty()) { if (UE::SmartObject::Annotations::TestCollidersOverlap(*World, SlotColliders, ValidationContext.TransitionTraceParams, ValidationContext.TransitionTraceQueryParams)) { bIsSlotCollisionsValid = false; } } // Candidates are now in order of preference, validate each for hard requirements. // In order to save performance, we stop validating as soon as the first hard test fails. // Results are generated for both valid and invalid results, which allows the callback to decide // to pick first valid result or all results (e.g. for visualization). for (FSmartObjectSlotEntranceCandidate& Candidate : Candidates) { const FBox SearchBounds(Candidate.Location - ValidationContext.NavigationSearchExtents, Candidate.Location + ValidationContext.NavigationSearchExtents); bool bIsValid = bIsSlotCollisionsValid; // Check and adjust the location on navigable space. if (bIsValid && Request.bProjectNavigationLocation) { FNavLocation NavLocation; if (!UE::SmartObject::Annotations::ProjectNavigationLocation(*ValidationContext.NavigationData, Candidate.Location, SearchBounds, ValidationContext.NavigationFilter, Request.UserActor, NavLocation)) { // If no navigable area found, skip the candidate. bIsValid = false; } else { Candidate.Location = NavLocation.Location; Candidate.NodeRef = NavLocation.NodeRef; } } // Check and adjust the location on ground. if (bIsValid && Request.bTraceGroundLocation && Candidate.bTraceGroundLocation) { FVector GroundLocation; if (!UE::SmartObject::Annotations::TraceGroundLocation(*World, Candidate.Location, SearchBounds, ValidationContext.GroundTraceParams, ValidationContext.GroundTraceQueryParams, GroundLocation)) { // If not ground location found, skip the candidate. bIsValid = false; } else { Candidate.Location = GroundLocation; } } // Check that the entry location is free of collisions if requested. // This is done after ground location adjustments so that we avoid doing a physics collision under the terrain if the navmesh has such an error // Note: the latter fix requires bTraceGroundLocation to be true on the candidate and the request and applies only if bProjectNavigationLocation is true if (bIsValid && Request.bCheckEntranceLocationOverlap) { const FSmartObjectAnnotationCollider Collider = ValidationContext.UserCapsuleParams.GetAsCollider(Candidate.Location, Candidate.Rotation.Quaternion()); if (UE::SmartObject::Annotations::TestCollidersOverlap(*World, { Collider }, ValidationContext.TransitionTraceParams, ValidationContext.TransitionTraceQueryParams)) { // If the colliders overlap, skip the candidate. bIsValid = false; } } // Check that there's no collision during transition to slot location. if (bIsValid && Request.bCheckTransitionTrajectory && Candidate.bCheckTransitionTrajectory && Candidate.EntranceAnnotation) { // @todo: we're currently _not_ using the adjusted location (Candidate.Location), consider if we should. TArray Colliders; Candidate.EntranceAnnotation->GetTrajectoryColliders(SlotTransform, Colliders); if (UE::SmartObject::Annotations::TestCollidersOverlap(*World, Colliders, ValidationContext.TransitionTraceParams, ValidationContext.TransitionTraceQueryParams)) { // If the colliders overlap, skip the candidate. bIsValid = false; } } // Make result for the validated data, the callback will decide to use the data or not, or to keep on validating the next entrances. FSmartObjectSlotEntranceLocationResult Result; Result.Location = Candidate.Location; Result.Rotation = Candidate.Rotation; Result.NodeRef = INVALID_NAVNODEREF; if (Candidate.EntranceAnnotation) { Result.Tags = Candidate.EntranceAnnotation->Tags; PRAGMA_DISABLE_DEPRECATION_WARNINGS // ReSharper disable once CppDeprecatedEntity Result.Tag = Result.Tags.First(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } if (Request.LocationType == ESmartObjectSlotNavigationLocationType::Exit) { // Reverse direction for exits. Result.Rotation = Result.Rotation.Add(0.0, 180.0, 0.0).Clamp(); } Result.EntranceHandle = Candidate.Handle; Result.bIsValid = bIsValid; const bool bShouldContinue = ResultFunc(Result); if (!bShouldContinue) { break; } } } void USmartObjectSubsystem::FindSlotsInternal(const FSmartObjectHandle Handle, const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRequestFilter& Filter, TArray& OutResults, const FConstStructView UserData) const { TRACE_CPUPROFILER_EVENT_SCOPE_STR("SmartObject_FilterSlots"); // Use the high level flag, no need to dig into each slot state since they are also all disabled. if (!SmartObjectRuntime.IsEnabled()) { return; } const USmartObjectDefinition& Definition = SmartObjectRuntime.GetDefinition(); const int32 NumSlots = Definition.GetSlots().Num(); checkf(NumSlots > 0, TEXT("Definition should contain slot definitions at this point")); checkf(SmartObjectRuntime.Slots.Num() == NumSlots, TEXT("Number of runtime slot handles should match number of slot definitions")); // Applying caller's predicate if (Filter.Predicate && !Filter.Predicate(SmartObjectRuntime.GetRegisteredHandle())) { return; } // Apply definition level filtering (Tags and BehaviorDefinition) // This could be improved to cache results between a single query against multiple instances of the same definition TArray ValidSlotIndices; FindMatchingSlotDefinitionIndicesInternal(Definition, Filter, ValidSlotIndices); FWorldConditionContextData ConditionContextData; ConditionContextData.SetSchema(*Definition.GetWorldConditionSchema()); // Setup default data SetupConditionContextCommonDataInternal(ConditionContextData, SmartObjectRuntime); // Setup additional data related to requester BindPropertiesFromStructInternal(ConditionContextData, UserData); // Check object conditions. if (Filter.bShouldEvaluateConditions && !EvaluateObjectConditionsInternal(ConditionContextData, SmartObjectRuntime)) { return; } // Build list of available slot indices (filter out occupied or reserved slots or disabled slots) for (const int32 SlotIndex : ValidSlotIndices) { const FSmartObjectRuntimeSlot& RuntimeSlot = SmartObjectRuntime.GetSlot(SlotIndex); if (!Filter.bShouldIncludeDisabledSlots && !RuntimeSlot.IsEnabled()) { continue; } if (Filter.bShouldIncludeClaimedSlots || RuntimeSlot.State == ESmartObjectSlotState::Free || (RuntimeSlot.State == ESmartObjectSlotState::Claimed && RuntimeSlot.ClaimedPriority < Filter.ClaimPriority)) { const FSmartObjectSlotHandle SlotHandle(Handle, SlotIndex); // Check slot conditions. if (Filter.bShouldEvaluateConditions && !EvaluateSlotConditionsInternal(ConditionContextData, SmartObjectRuntime, SlotHandle)) { continue; } OutResults.Add(SlotHandle); } } } void USmartObjectSubsystem::FindMatchingSlotDefinitionIndicesInternal(const USmartObjectDefinition& Definition, const FSmartObjectRequestFilter& Filter, TArray& OutValidIndices) { const ESmartObjectTagFilteringPolicy UserTagsFilteringPolicy = Definition.GetUserTagsFilteringPolicy(); // Define our Tags filtering predicate auto MatchesTagQueryFunc = [](const FGameplayTagQuery& Query, const FGameplayTagContainer& Tags){ return Query.IsEmpty() || Query.Matches(Tags); }; // When filter policy is to use combined we can validate the user tag query of the parent object first // since they can't be merge so we need to apply them one after the other. // For activity requirements we have to merge parent and slot tags together before testing. if (UserTagsFilteringPolicy == ESmartObjectTagFilteringPolicy::Combine && !MatchesTagQueryFunc(Definition.GetUserTagFilter(), Filter.UserTags)) { return; } // Apply filter to individual slots const TConstArrayView SlotDefinitions = Definition.GetSlots(); OutValidIndices.Reserve(SlotDefinitions.Num()); for (int i = 0; i < SlotDefinitions.Num(); ++i) { const FSmartObjectSlotDefinition& Slot = SlotDefinitions[i]; // Filter out mismatching behavior type (if specified) if (!Filter.BehaviorDefinitionClasses.IsEmpty()) { bool bMatchesAny = false; for (const TSubclassOf& BehaviorDefinitionClass : Filter.BehaviorDefinitionClasses) { if (Definition.GetBehaviorDefinition(i, BehaviorDefinitionClass) != nullptr) { bMatchesAny = true; break; } } if (!bMatchesAny) { continue; } } // Filter out slots based on their activity tags FGameplayTagContainer ActivityTags; Definition.GetSlotActivityTags(Slot, ActivityTags); if (!MatchesTagQueryFunc(Filter.ActivityRequirements, ActivityTags)) { continue; } // Filter out slots based on their TagQuery applied on provided User Tags // - override: we only run query from the slot if provided otherwise we run the one from the parent object // - combine: we run slot query (parent query was applied before processing individual slots) if (UserTagsFilteringPolicy == ESmartObjectTagFilteringPolicy::Combine && !MatchesTagQueryFunc(Slot.UserTagFilter, Filter.UserTags)) { continue; } if (UserTagsFilteringPolicy == ESmartObjectTagFilteringPolicy::Override && !MatchesTagQueryFunc((Slot.UserTagFilter.IsEmpty() ? Definition.GetUserTagFilter() : Slot.UserTagFilter), Filter.UserTags)) { continue; } OutValidIndices.Add(i); } } FSmartObjectRequestResult USmartObjectSubsystem::FindSmartObject(const FSmartObjectRequest& Request, const FConstStructView UserData) const { TArray Results; FindSmartObjects(Request, Results, UserData); return Results.Num() ? Results.Top() : FSmartObjectRequestResult(); } FSmartObjectRequestResult USmartObjectSubsystem::FindSmartObject(const FSmartObjectRequest& Request, const AActor* UserActor) const { return FindSmartObject(Request, FConstStructView::Make(FSmartObjectActorUserData(UserActor))); } bool USmartObjectSubsystem::FindSmartObjects(const FSmartObjectRequest& Request, TArray& OutResults, const FConstStructView UserData) const { TRACE_CPUPROFILER_EVENT_SCOPE_STR("SmartObject_FindAllResults"); UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); // For now we want to enforce users to not use Find methods from a multithreaded context. // Instead they should use async requests to batch them (see MassSmartObject). // This is why we use the thread access detector for individual instance without locking. UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); if (!bRuntimeInitialized) { // Do not report warning if runtime was explicitly disabled by CVar UE_CVLOG_UELOG(!UE::SmartObject::bDisableRuntime, this, LogSmartObject, Warning, TEXT("Can't find smart objet before runtime gets initialized (i.e. InitializeRuntime gets called).")); return false; } const FSmartObjectRequestFilter& Filter = Request.Filter; TArray QueryResults; checkf(SpacePartition != nullptr, TEXT("Space partition is expected to be valid since we use the plugins default in OnWorldComponentsUpdated.")); SpacePartition->Find(Request.QueryBox, QueryResults); for (const FSmartObjectHandle SmartObjectHandle : QueryResults) { const FSmartObjectRuntime* SmartObjectRuntime = GetRuntimeInstanceInternal(SmartObjectHandle); checkf(SmartObjectRuntime != nullptr, TEXT("Results returned by the space partition are expected to be valid.")); if (!Request.QueryBox.IsInside(SmartObjectRuntime->GetTransform().GetLocation())) { continue; } TArray SlotHandles; FindSlotsInternal(SmartObjectHandle, *SmartObjectRuntime, Filter, SlotHandles, UserData); OutResults.Reserve(OutResults.Num() + SlotHandles.Num()); for (FSmartObjectSlotHandle SlotHandle: SlotHandles) { OutResults.Emplace(SmartObjectHandle, SlotHandle); } } return (OutResults.Num() > 0); } bool USmartObjectSubsystem::FindSmartObjectsInList(const FSmartObjectRequestFilter& Filter, TConstArrayView ActorList, TArray& OutResults, const FConstStructView UserData) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); // For now we want to enforce users to not use Find methods from a multithreaded context. // Instead they should use async requests to batch them (see MassSmartObject). // This is why we use the thread access detector for individual instance without locking. UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); // Iterate the actor list, if it has a Smart Object Component in it, then find all the slots and populate our results // We don't want to use a Query Box here because that could include smart objects from outside of this ActorList. for (const AActor* SearchActor : ActorList) { if (!SearchActor) { continue; } const USmartObjectComponent* FoundComponent = SearchActor->GetComponentByClass(); if (!FoundComponent) { continue; } const FSmartObjectHandle SmartObjectHandle = FoundComponent->GetRegisteredHandle(); const FSmartObjectRuntime* SmartObjectRuntime = SmartObjectHandle.IsValid() ? GetRuntimeInstanceInternal(SmartObjectHandle) : nullptr; if (!SmartObjectRuntime) { continue; } // We found a valid smart object runtime, populate our results with it's slots TArray SlotHandles; FindSlotsInternal(SmartObjectHandle, *SmartObjectRuntime, Filter, SlotHandles, UserData); OutResults.Reserve(OutResults.Num() + SlotHandles.Num()); for (FSmartObjectSlotHandle SlotHandle : SlotHandles) { OutResults.Emplace(SmartObjectHandle, SlotHandle); } } // Successful if we found some smart objects return (OutResults.Num() > 0); } bool USmartObjectSubsystem::FindSmartObjectsInTargetingRequest(const FSmartObjectRequestFilter& Filter, const FTargetingRequestHandle TargetingHandle, TArray& OutResults, const FConstStructView UserData) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); // For now we want to enforce users to not use Find methods from a multithreaded context. // Instead they should use async requests to batch them (see MassSmartObject). // This is why we use the thread access detector for individual instance without locking. UE_MT_SCOPED_INSTANCE_READ_ACCESS_DETECTOR(); if (FTargetingDefaultResultsSet* Results = FTargetingDefaultResultsSet::Find(TargetingHandle)) { for (const FTargetingDefaultResultData& Data : Results->TargetResults) { AActor* ResultActor = Data.HitResult.GetActor(); if (!ResultActor) { continue; } const USmartObjectComponent* FoundComponent = ResultActor->GetComponentByClass(); if (!FoundComponent) { continue; } const FSmartObjectHandle SmartObjectHandle = FoundComponent->GetRegisteredHandle(); const FSmartObjectRuntime* SmartObjectRuntime = SmartObjectHandle.IsValid() ? GetRuntimeInstanceInternal(SmartObjectHandle) : nullptr; if (!SmartObjectRuntime) { continue; } // We found a valid smart object runtime, populate our results with it's slots TArray SlotHandles; FindSlotsInternal(SmartObjectHandle, *SmartObjectRuntime, Filter, SlotHandles, UserData); OutResults.Reserve(OutResults.Num() + SlotHandles.Num()); for (FSmartObjectSlotHandle SlotHandle : SlotHandles) { OutResults.Emplace(SmartObjectHandle, SlotHandle); } } } // Successful if we found some smart objects return (OutResults.Num() > 0); } void USmartObjectSubsystem::RegisterCollectionInstances() { for (TActorIterator It(GetWorld()); It; ++It) { ASmartObjectPersistentCollection* Collection = (*It); if (IsValid(Collection) && Collection->IsRegistered() == false) { const ESmartObjectCollectionRegistrationResult Result = RegisterCollection(*Collection); UE_VLOG_UELOG(Collection, LogSmartObject, Log, TEXT("Collection '%s' registration from USmartObjectSubsystem initialization - %s"), *Collection->GetPathName(), *UEnum::GetValueAsString(Result)); } } } ESmartObjectCollectionRegistrationResult USmartObjectSubsystem::RegisterCollection(ASmartObjectPersistentCollection& InCollection) { if (!IsValid(&InCollection)) { return ESmartObjectCollectionRegistrationResult::Failed_InvalidCollection; } if (InCollection.IsRegistered()) { UE_VLOG_UELOG(&InCollection, LogSmartObject, Error, TEXT("Trying to register collection '%s' more than once"), *InCollection.GetPathName()); return ESmartObjectCollectionRegistrationResult::Failed_AlreadyRegistered; } ESmartObjectCollectionRegistrationResult Result = ESmartObjectCollectionRegistrationResult::Succeeded; UE_VLOG_UELOG(&InCollection, LogSmartObject, Log, TEXT("Adding collection '%s' registered with %d entries"), *InCollection.GetName(), InCollection.GetEntries().Num()); InCollection.GetMutableSmartObjectContainer().ValidateDefinitions(); SmartObjectContainer.Append(InCollection.GetSmartObjectContainer()); RegisteredCollections.Add(&InCollection); // We want to add the new collection to the "simulation" only if the Runtime part of the subsystem has been initialized. // SmartObjectContainer is added to simulation in one go in InitializeRuntime. if (bRuntimeInitialized) { AddContainerToSimulation(InCollection.GetSmartObjectContainer()); } #if WITH_EDITOR // Broadcast after rebuilding so listeners will be able to access up-to-date data OnMainCollectionChanged.Broadcast(); #endif // WITH_EDITOR InCollection.OnRegistered(); Result = ESmartObjectCollectionRegistrationResult::Succeeded; return Result; } void USmartObjectSubsystem::UnregisterCollection(ASmartObjectPersistentCollection& InCollection) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); if (RegisteredCollections.Remove(&InCollection)) { SmartObjectContainer.Remove(InCollection.GetSmartObjectContainer()); for (const FSmartObjectCollectionEntry& Entry : InCollection.GetSmartObjectContainer().GetEntries()) { FSmartObjectRuntime SORuntime; // even though we did add this entry to RuntimeSmartObjects at some point it could have been removed // when the smart object in question got disabled or removed if (RuntimeSmartObjects.RemoveAndCopyValue(Entry.GetHandle(), SORuntime)) { if (USmartObjectComponent* SOComponent = SORuntime.GetOwnerComponent(ETrySpawnActorIfDehydrated::No)) { UnbindComponentFromSimulationInternal(SOComponent, SORuntime); } DestroyRuntimeInstanceInternal(Entry.GetHandle(), SORuntime); } } InCollection.OnUnregistered(); } else { UE_VLOG_UELOG(&InCollection, LogSmartObject, Verbose, TEXT("Ignoring unregistration of collection '%s' since this is not one of the previously registered collections."), *InCollection.GetPathName()); return; } } void USmartObjectSubsystem::AddContainerToSimulation(const FSmartObjectContainer& InSmartObjectContainer) { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); if (!ensureMsgf(bRuntimeInitialized, TEXT("%hs called before InitializeRuntime, this is not expected to happen."), __FUNCTION__)) { return; } for (const FSmartObjectCollectionEntry& Entry : InSmartObjectContainer.GetEntries()) { const USmartObjectDefinition* Definition = InSmartObjectContainer.GetDefinitionForEntry(Entry, GetWorld()); USmartObjectComponent* Component = Entry.GetComponent(); if (Definition == nullptr || Definition->IsDefinitionValid() == false) { UE_CVLOG_UELOG(Component != nullptr, Component->GetOwner(), LogSmartObject, Error, TEXT("Skipped runtime data creation for SmartObject %s: Invalid definition"), *GetNameSafe(Component->GetOwner())); continue; } if (Component != nullptr) { if (const USmartObjectDefinition* ComponentDefinition = Component->GetDefinition()) { UE_CVLOG_UELOG(ComponentDefinition != Definition, this, LogSmartObject, Warning, TEXT("Definition '%s' specified in component for '%s' differs from '%s' specified in the collection entry. Collection should be rebuild."), *ComponentDefinition->GetPathName(), *UE::SmartObject::DebugGetComponentName(Component), *Definition->GetFullName()); // When component is available we add it to the simulation along with its collection entry to create the runtime instance and bound them together. Component->SetRegisteredHandle(Entry.GetHandle(), ESmartObjectRegistrationType::BindToExistingInstance); AddComponentToSimulationInternal(Component, Entry); continue; } UE_VLOG_UELOG(Component->GetOwner(), LogSmartObject, Error, TEXT("Component in '%s' doesn't have a valid definition. Adding based on the collection entry but collection should be rebuild."), *UE::SmartObject::DebugGetComponentName(Component)); } // Otherwise we create the runtime instance based on the information from the collection and component will be bound later (e.g. on load) AddCollectionEntryToSimulationInternal(Entry, *Definition, nullptr); } } USmartObjectComponent* USmartObjectSubsystem::GetSmartObjectComponent(const FSmartObjectClaimHandle& ClaimHandle, const ETrySpawnActorIfDehydrated TrySpawnActorIfDehydrated) const { USmartObjectComponent* OutComponent = nullptr; ExecuteOnValidatedRuntime(ClaimHandle.SmartObjectHandle, [&OutComponent, TrySpawnActorIfDehydrated](const FSmartObjectRuntime& SmartObjectRuntime) { OutComponent = SmartObjectRuntime.GetOwnerComponent(TrySpawnActorIfDehydrated); }, __FUNCTION__); return OutComponent; } USmartObjectComponent* USmartObjectSubsystem::GetSmartObjectComponentByRequestResult(const FSmartObjectRequestResult& Result, const ETrySpawnActorIfDehydrated TrySpawnActorIfDehydrated) const { USmartObjectComponent* OutComponent = nullptr; ExecuteOnValidatedRuntime(Result.SmartObjectHandle, [&OutComponent, TrySpawnActorIfDehydrated](const FSmartObjectRuntime& SmartObjectRuntime) { OutComponent = SmartObjectRuntime.GetOwnerComponent(TrySpawnActorIfDehydrated); }, __FUNCTION__); return OutComponent; } void USmartObjectSubsystem::InitializeRuntime() { if (UE::SmartObject::bDisableRuntime) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Runtime explicitly disabled by CVar. Initialization skipped in %hs."), __FUNCTION__); return; } // Initialize spatial representation structure checkfSlow(*SpacePartitionClass != nullptr, TEXT("Partition class is expected to be valid since we use the plugins default in OnWorldComponentsUpdated.")); SpacePartition = NewObject(this, SpacePartitionClass); SpacePartition->SetBounds(SmartObjectContainer.GetBounds()); // Note that we use our own flag instead of relying on World.HasBegunPlay() since world might not be marked // as BegunPlay immediately after subsystem OnWorldBeingPlay gets called (e.g. waiting game mode to be ready on clients) // Setting bRuntimeInitialized at this point since the following code assumes the SpatialPartition has been created // and EntityManager cached. bRuntimeInitialized = true; AddContainerToSimulation(SmartObjectContainer); UE_CVLOG_UELOG(PendingSmartObjectRegistration.Num() > 0, this, LogSmartObject, VeryVerbose, TEXT("SmartObjectSubsystem: Handling %d pending registrations during runtime initialization."), PendingSmartObjectRegistration.Num()); for (TObjectPtr& SOComponent : PendingSmartObjectRegistration) { // ensure the SOComponent is still valid - things could have happened to it between adding to PendingSmartObjectRegistration and it being processed here if (SOComponent && IsValid(SOComponent)) { RegisterSmartObject(SOComponent); } } PendingSmartObjectRegistration.Empty(); #if UE_ENABLE_DEBUG_DRAWING // Refresh debug draw if (RenderingActor != nullptr) { RenderingActor->MarkComponentsRenderStateDirty(); } #endif // UE_ENABLE_DEBUG_DRAWING } void USmartObjectSubsystem::CleanupRuntime() { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); // Process component list first so they can be notified before we destroy their associated runtime instance for (USmartObjectComponent* Component : RegisteredSOComponents) { // Make sure component was registered to simulation (e.g. Valid associated definition) if (Component != nullptr && Component->IsBoundToSimulation()) { RemoveComponentFromSimulation(Component); } } // Cleanup all remaining entries (e.g. associated to unloaded SmartObjectComponents) for (auto It(RuntimeSmartObjects.CreateIterator()); It; ++It) { DestroyRuntimeInstanceInternal(It.Key(), It.Value()); } RuntimeSmartObjects.Reset(); bRuntimeInitialized = false; RegisteredCollections.Reset(); #if UE_ENABLE_DEBUG_DRAWING // Refresh debug draw if (RenderingActor != nullptr) { RenderingActor->MarkComponentsRenderStateDirty(); } #endif // UE_ENABLE_DEBUG_DRAWING } void USmartObjectSubsystem::OnWorldBeginPlay(UWorld& World) { Super::OnWorldBeginPlay(World); InitializeRuntime(); } void USmartObjectSubsystem::Deinitialize() { CleanupRuntime(); Super::Deinitialize(); } bool USmartObjectSubsystem::ShouldCreateSubsystem(UObject* Outer) const { if (Super::ShouldCreateSubsystem(Outer)) { if (const UWorld* OuterWorld = Cast(Outer)) { return OuterWorld->IsNetMode(NM_Client) == false; } } return false; } bool USmartObjectSubsystem::IsRunningOnServer() const { if (const UWorld* World = GetWorld()) { return World->GetNetMode() < NM_Client; } return false; } #if WITH_EDITOR FBox USmartObjectSubsystem::ComputeBounds(const UWorld& World) const { FBox Bounds(ForceInitToZero); if (const UWorldPartition* WorldPartition = World.GetWorldPartition()) { Bounds = WorldPartition->GetRuntimeWorldBounds(); } else if (const ULevel* PersistentLevel = World.PersistentLevel.Get()) { if (PersistentLevel->LevelBoundsActor.IsValid()) { Bounds = PersistentLevel->LevelBoundsActor.Get()->GetComponentsBoundingBox(); } else { Bounds = ALevelBounds::CalculateLevelBounds(PersistentLevel); } } else { UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("Unable to determine world bounds: no world partition or persistent level.")); } return Bounds; } void USmartObjectSubsystem::PopulateCollection(ASmartObjectPersistentCollection& InCollection) const { TArray RelevantComponents; if (GetRegisteredSmartObjectsCompatibleWithCollection(InCollection, RelevantComponents) > 0) { InCollection.AppendToCollection(RelevantComponents); } } int32 USmartObjectSubsystem::GetRegisteredSmartObjectsCompatibleWithCollection( const ASmartObjectPersistentCollection& InCollection, TArray& OutRelevantComponents ) const { UE_MT_SCOPED_INSTANCES_LIST_READ_ACCESS_DETECTOR(); ensureMsgf(IsInGameThread(), TEXT("%hs expected to be called from the game thread."), __FUNCTION__); const int32 InitialCount = OutRelevantComponents.Num(); if (bIsPartitionedWorld == false) { const ULevel* MyLevel = InCollection.GetLevel(); const ULevelStreaming* MyLevelStreaming = ULevelStreaming::FindStreamingLevel(MyLevel); const bool bCollectionShouldAlwaysBeLoaded = (MyLevelStreaming == nullptr) || MyLevelStreaming->ShouldBeAlwaysLoaded(); const ULevel* PreviousLevel = nullptr; bool bPreviousLevelValid = false; for (const TObjectPtr& Component : RegisteredSOComponents) { check(Component); if (Component->GetCanBePartOfCollection() == false) { continue; } const ULevel* OwnerLevel = Component->GetComponentLevel(); bool bValid = bPreviousLevelValid; if (OwnerLevel != PreviousLevel) { const ULevelStreaming* LevelStreaming = ULevelStreaming::FindStreamingLevel(OwnerLevel); bValid = (MyLevelStreaming == LevelStreaming) || (bCollectionShouldAlwaysBeLoaded && LevelStreaming && LevelStreaming->ShouldBeAlwaysLoaded()); } if (bValid) { OutRelevantComponents.Add(Component); } bPreviousLevelValid = bValid; PreviousLevel = OwnerLevel; } } else { TArray DataLayers = InCollection.GetDataLayerInstances(); const bool bPersistentLevelCollection = (DataLayers.Num() == 0); for (const TObjectPtr& Component : RegisteredSOComponents) { check(Component); if (Component->GetCanBePartOfCollection() == false) { continue; } if (const AActor* Owner = Component->GetOwner()) { const bool bInPersistentLayer = (Owner->HasDataLayers() == false); if (bPersistentLevelCollection == bInPersistentLayer) { if (bPersistentLevelCollection) { OutRelevantComponents.Add(Component); } else { for (const UDataLayerInstance* DataLayerInstance : DataLayers) { if (Owner->ContainsDataLayer(DataLayerInstance)) { OutRelevantComponents.Add(Component); // breaking here since at the moment we only support registering smart objects only // with a single collection break; } } } } } } } return (OutRelevantComponents.Num() - InitialCount); } void USmartObjectSubsystem::IterativelyBuildCollections() { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); ensureMsgf(bIsPartitionedWorld, TEXT("%hs expected to be called in World Partitioned worlds"), __FUNCTION__); ensureMsgf(IsInGameThread(), TEXT("%hs expected to be called from the game thread."), __FUNCTION__); if (RegisteredSOComponents.Num() == 0) { return; } TArray ComponentsToRestore = RegisteredSOComponents; TArray RelevantComponents; for (TWeakObjectPtr& WeakCollection : RegisteredCollections) { if (ASmartObjectPersistentCollection* Collection = WeakCollection.Get()) { RelevantComponents.Reset(); if (GetRegisteredSmartObjectsCompatibleWithCollection(*Collection, RelevantComponents) > 0) { Collection->AppendToCollection(RelevantComponents); // A component can belong to only a single collection. // We remove objects added to the collection so that they do not get added to another collection. // Also, the subsequent GetRegisteredSmartObjectsCompatibleWithCollection calls get less data to consider. for (USmartObjectComponent* SOComponent : RelevantComponents) { RegisteredSOComponents.RemoveSingleSwap(SOComponent); } } } } // Restore registered components so they can be unregistered properly by the normal streaming flow (i.e. not reporting any warnings/errors) RegisteredSOComponents = MoveTemp(ComponentsToRestore); } #endif // WITH_EDITOR #if WITH_EDITORONLY_DATA PRAGMA_DISABLE_DEPRECATION_WARNINGS void USmartObjectSubsystem::CreatePersistentCollectionFromDeprecatedData(UWorld& World, const ADEPRECATED_SmartObjectCollection& DeprecatedCollection) { if (DeprecatedCollection.CollectionEntries.Num() == 0) { // we ignore the empty deprecated collections - we used to always create these even if no smart objects were being used // and an empty collection is an indication of such a case. No point in creating a replacement for such a collection. return; } FActorSpawnParameters SpawnParams; SpawnParams.OverrideLevel = DeprecatedCollection.GetLevel(); if (ASmartObjectPersistentCollection* NewCollection = World.SpawnActor(SpawnParams)) { NewCollection->SmartObjectContainer.Bounds = DeprecatedCollection.Bounds; NewCollection->SmartObjectContainer.CollectionEntries = DeprecatedCollection.CollectionEntries; NewCollection->SmartObjectContainer.RegisteredIdToObjectMap_DEPRECATED = DeprecatedCollection.RegisteredIdToObjectMap; NewCollection->SmartObjectContainer.DefinitionReferences.Reserve(DeprecatedCollection.Definitions.Num()); for (const USmartObjectDefinition* SmartObjectDefinition : DeprecatedCollection.Definitions) { NewCollection->SmartObjectContainer.DefinitionReferences.Add(FSmartObjectDefinitionReference(SmartObjectDefinition)); } NewCollection->bUpdateCollectionOnSmartObjectsChange = DeprecatedCollection.bBuildCollectionAutomatically; NewCollection->SmartObjectContainer.ConvertDeprecatedDefinitionsToReferences(); NewCollection->SmartObjectContainer.ConvertDeprecatedEntries(); } } PRAGMA_ENABLE_DEPRECATION_WARNINGS #endif // WITH_EDITORONLY_DATA #if WITH_SMARTOBJECT_DEBUG void USmartObjectSubsystem::DebugUnregisterAllSmartObjects() { for (USmartObjectComponent* Cmp : RegisteredSOComponents) { if (Cmp != nullptr && GetRuntimeInstanceInternal(Cmp->GetRegisteredHandle()) != nullptr) { RemoveComponentFromSimulation(Cmp); } } } void USmartObjectSubsystem::DebugRegisterAllSmartObjects() { UE_MT_SCOPED_INSTANCES_LIST_WRITE_ACCESS_DETECTOR(); for (USmartObjectComponent* Cmp : RegisteredSOComponents) { if (Cmp != nullptr) { const FSmartObjectCollectionEntry* Entry = SmartObjectContainer.GetEntries().FindByPredicate( [Handle=Cmp->GetRegisteredHandle()](const FSmartObjectCollectionEntry& CollectionEntry) { return CollectionEntry.GetHandle() == Handle; }); // In this debug command we register back components that were already part of the simulation but // removed using debug command 'ai.debug.so.UnregisterAllSmartObjects'. // We need to find associated collection entry and pass it back so the callbacks can be bound properly if (Entry && GetRuntimeInstanceInternal(Entry->GetHandle()) == nullptr) { AddComponentToSimulationInternal(Cmp, *Entry); } } } } void USmartObjectSubsystem::DebugInitializeRuntime() { // do not initialize more than once or on a GameWorld if (bRuntimeInitialized || GetWorldRef().IsGameWorld()) { return; } InitializeRuntime(); } void USmartObjectSubsystem::DebugCleanupRuntime() { // do not cleanup more than once or on a GameWorld if (!bRuntimeInitialized || GetWorldRef().IsGameWorld()) { return; } CleanupRuntime(); } #endif // WITH_SMARTOBJECT_DEBUG //----------------------------------------------------------------------// // deprecated functions implementations //----------------------------------------------------------------------// PRAGMA_DISABLE_DEPRECATION_WARNINGS void USmartObjectSubsystem::BindComponentToSimulation(USmartObjectComponent& SmartObjectComponent) { const bool bInstanceFound = ExecuteOnValidatedMutableRuntime(SmartObjectComponent.GetRegisteredHandle(), [this, &SmartObjectComponent](FSmartObjectRuntime& SmartObjectRuntime) { // Simply bind the newly available component to its active runtime instance BindComponentToSimulationInternal(&SmartObjectComponent, SmartObjectRuntime); }, __FUNCTION__); ensureAlwaysMsgf(bInstanceFound, TEXT("Unable to bind %s using handle '%s' since an associated runtime doesn't exist."), *UE::SmartObject::DebugGetComponentName(&SmartObjectComponent), *LexToString(SmartObjectComponent.GetRegisteredHandle())); } void USmartObjectSubsystem::UnbindComponentFromSimulation(USmartObjectComponent& SmartObjectComponent) { const bool bInstanceFound = ExecuteOnValidatedMutableRuntime(SmartObjectComponent.GetRegisteredHandle(), [this, &SmartObjectComponent](FSmartObjectRuntime& SmartObjectRuntime) { UnbindComponentFromSimulationInternal(&SmartObjectComponent, SmartObjectRuntime); }, __FUNCTION__); ensureAlwaysMsgf(bInstanceFound, TEXT("Unable to unbind %s using handle '%s' since an associated runtime doesn't exist."), *UE::SmartObject::DebugGetComponentName(&SmartObjectComponent), *LexToString(SmartObjectComponent.GetRegisteredHandle())); } PRAGMA_ENABLE_DEPRECATION_WARNINGS