// Copyright Epic Games, Inc. All Rights Reserved. #include "SmartObjectSubsystem.h" #include "SmartObjectDefinition.h" #include "SmartObjectComponent.h" #include "SmartObjectCollection.h" #include "EngineUtils.h" #include "MassCommandBuffer.h" #include "MassEntityManager.h" #include "MassEntitySubsystem.h" #include "SmartObjectHashGrid.h" #include "VisualLogger/VisualLogger.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(SmartObjectSubsystem) #if UE_ENABLE_DEBUG_DRAWING #include "SmartObjectSubsystemRenderingActor.h" #endif #if WITH_SMARTOBJECT_DEBUG #include "MassExecutor.h" #endif #if WITH_EDITOR #include "Engine/LevelBounds.h" #include "WorldPartition/WorldPartition.h" #endif 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 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 } // UE::SmartObject //----------------------------------------------------------------------// // USmartObjectSubsystem //----------------------------------------------------------------------// void USmartObjectSubsystem::OnWorldComponentsUpdated(UWorld& World) { // 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(this, *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 SpawnMissingCollection(); if (!World.IsGameWorld()) { ComputeBounds(World, *MainCollection); } #endif // WITH_EDITOR } USmartObjectSubsystem* USmartObjectSubsystem::GetCurrent(const UWorld* World) { return UWorld::GetSubsystem(World); } FSmartObjectRuntime* USmartObjectSubsystem::AddComponentToSimulation(USmartObjectComponent& SmartObjectComponent, const FSmartObjectCollectionEntry& NewEntry) { checkf(SmartObjectComponent.GetDefinition() != nullptr, TEXT("Shouldn't reach this point with an invalid definition asset")); FSmartObjectRuntime* SmartObjectRuntime = AddCollectionEntryToSimulation(NewEntry, *SmartObjectComponent.GetDefinition()); if (SmartObjectRuntime != nullptr) { SmartObjectComponent.OnRuntimeInstanceCreated(*SmartObjectRuntime); } return SmartObjectRuntime; } void USmartObjectSubsystem::BindComponentToSimulation(USmartObjectComponent& SmartObjectComponent) { // Notify the component to bind to its runtime counterpart FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(SmartObjectComponent.GetRegisteredHandle()); if (ensureMsgf(SmartObjectRuntime != nullptr, TEXT("Binding a component should only be used when an associated runtime instance exists."))) { SmartObjectComponent.OnRuntimeInstanceBound(*SmartObjectRuntime); } } void USmartObjectSubsystem::UnbindComponentFromSimulation(USmartObjectComponent& SmartObjectComponent) { // Notify the component to unbind from its runtime counterpart FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(SmartObjectComponent.GetRegisteredHandle()); if (ensureMsgf(SmartObjectRuntime != nullptr, TEXT("Unbinding a component should only be used when an associated runtime instance exists."))) { SmartObjectComponent.OnRuntimeInstanceUnbound(*SmartObjectRuntime); } } FSmartObjectRuntime* USmartObjectSubsystem::AddCollectionEntryToSimulation(const FSmartObjectCollectionEntry& Entry, const USmartObjectDefinition& Definition) { const FSmartObjectHandle Handle = Entry.GetHandle(); const FTransform& Transform = Entry.GetTransform(); const FBox& Bounds = Entry.GetBounds(); const FGameplayTagContainer& Tags = Entry.GetTags(); if (!ensureMsgf(Handle.IsValid(), TEXT("SmartObject needs a valid Handle to be added to the simulation"))) { return nullptr; } if (!ensureMsgf(RuntimeSmartObjects.Find(Handle) == nullptr, TEXT("Handle '%s' already registered in runtime simulation"), *LexToString(Handle))) { return nullptr; } if (!ensureMsgf(EntityManager, TEXT("Entity subsystem required to add a smartobject to the simulation"))) { return nullptr; } UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Adding SmartObject '%s' to runtime simulation."), *LexToString(Handle)); FSmartObjectRuntime& Runtime = RuntimeSmartObjects.Emplace(Handle, FSmartObjectRuntime(Definition)); Runtime.SetRegisteredHandle(Handle); Runtime.Tags = Tags; #if UE_ENABLE_DEBUG_DRAWING Runtime.Bounds = Bounds; #endif // Create runtime data and entity for each slot int32 SlotIndex = 0; for (const FSmartObjectSlotDefinition& SlotDefinition : Definition.GetSlots()) { // Build our shared fragment FMassArchetypeSharedFragmentValues SharedFragmentValues; const uint32 Hash = HashCombine(Definition.GetUniqueID(), SlotIndex); FConstSharedStruct& SharedFragment = EntityManager->GetOrCreateSharedFragmentByHash(Hash, Definition, SlotDefinition); SharedFragmentValues.AddConstSharedFragment(SharedFragment); FSmartObjectSlotTransform TransformFragment; TOptional OptionalTransform = Definition.GetSlotTransform(Transform, FSmartObjectSlotIndex(SlotIndex)); TransformFragment.SetTransform(OptionalTransform.Get(Transform)); const FMassEntityHandle EntityHandle = EntityManager->ReserveEntity(); EntityManager->Defer().PushCommand(EntityHandle, MoveTemp(SharedFragmentValues), TransformFragment); FSmartObjectSlotHandle SlotHandle(EntityHandle); FSmartObjectRuntimeSlot& Slot = RuntimeSlots.Add(SlotHandle, FSmartObjectRuntimeSlot(Handle, SlotIndex)); // Setup initial state from slot definition Slot.bEnabled = SlotDefinition.bEnabled; Slot.Tags = SlotDefinition.RuntimeTags; Runtime.SlotHandles[SlotIndex] = SlotHandle; SlotIndex++; } // For objects added to simulation after initial collection, we need to flush the command buffer if (bInitialCollectionAddedToSimulation) { // This is the temporary way to force our commands to be processed until MassEntitySubsystem // offers a threadsafe solution to push and flush commands in our own execution context. EntityManager->FlushCommands(); } // Transfer spatial information to the runtime instance Runtime.SetTransform(Transform); // 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.")); Runtime.SpatialEntryData = SpacePartition->Add(Handle, Bounds); return &Runtime; } bool USmartObjectSubsystem::RemoveRuntimeInstanceFromSimulation(const FSmartObjectHandle Handle) { UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Removing SmartObject '%s' from runtime simulation."), *LexToString(Handle)); FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Handle); #if WITH_SMARTOBJECT_DEBUG ensureMsgf(SmartObjectRuntime != nullptr, TEXT("RemoveFromSimulation is an internal call and should only be used for objects still part of the simulation")); #endif // WITH_SMARTOBJECT_DEBUG if (SmartObjectRuntime == nullptr) { // temporary debug data logging to help fixing FORT-515024 UE_LOG(LogSmartObject, Error, TEXT("%s called with %s SO Handle and no corresponding SmartObjectRuntime") , ANSI_TO_TCHAR(__FUNCTION__) , Handle.IsValid() ? TEXT("a VALID") : TEXT("an INVALID")); return false; } if (!ensureMsgf(EntityManager, TEXT("Entity subsystem required to remove a smartobject from the simulation"))) { return false; } DestroyRuntimeInstanceInternal(Handle, *SmartObjectRuntime, *EntityManager.Get()); // Remove object runtime data RuntimeSmartObjects.Remove(Handle); return true; } void USmartObjectSubsystem::DestroyRuntimeInstanceInternal(const FSmartObjectHandle Handle, const FSmartObjectRuntime& SmartObjectRuntime, FMassEntityManager& EntityManagerRef) { // Abort everything before removing since abort flow may require access to runtime data AbortAll(SmartObjectRuntime); // 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); // Destroy entities associated to slots TArray EntitiesToDestroy; EntitiesToDestroy.Reserve(SmartObjectRuntime.SlotHandles.Num()); for (const FSmartObjectSlotHandle SlotHandle : SmartObjectRuntime.SlotHandles) { RuntimeSlots.Remove(SlotHandle); EntitiesToDestroy.Add(SlotHandle); } EntityManagerRef.Defer().DestroyEntities(EntitiesToDestroy); } bool USmartObjectSubsystem::RemoveCollectionEntryFromSimulation(const FSmartObjectCollectionEntry& Entry) { return RemoveRuntimeInstanceFromSimulation(Entry.GetHandle()); } void USmartObjectSubsystem::RemoveComponentFromSimulation(USmartObjectComponent& SmartObjectComponent) { if (RemoveRuntimeInstanceFromSimulation(SmartObjectComponent.GetRegisteredHandle())) { SmartObjectComponent.OnRuntimeInstanceDestroyed(); } else { UE_LOG(LogSmartObject, Warning, TEXT("%s call failed for %s") , ANSI_TO_TCHAR(__FUNCTION__) , *GetFullNameSafe(&SmartObjectComponent)); } } void USmartObjectSubsystem::AbortAll(const FSmartObjectRuntime& SmartObjectRuntime) { for (const FSmartObjectSlotHandle SlotHandle : SmartObjectRuntime.SlotHandles) { FSmartObjectRuntimeSlot& Slot = RuntimeSlots.FindChecked(SlotHandle); switch (Slot.State) { case ESmartObjectSlotState::Claimed: case ESmartObjectSlotState::Occupied: { const FSmartObjectClaimHandle ClaimHandle(SmartObjectRuntime.GetRegisteredHandle(), SlotHandle, Slot.User); if (Slot.Release(ClaimHandle, /* bAborted */true)) { OnSlotChanged(SmartObjectRuntime, Slot, SlotHandle, ESmartObjectChangeReason::OnReleased); } break; } case ESmartObjectSlotState::Free: // falling through on purpose default: UE_CVLOG_UELOG(Slot.User.IsValid(), this, LogSmartObject, Warning, TEXT("Smart object %s used by %s while the slot it's assigned to is not marked Claimed nor Occupied"), *LexToString(SmartObjectRuntime.GetDefinition()), *LexToString(Slot.User)); break; } Slot.State = ESmartObjectSlotState::Free; } } bool USmartObjectSubsystem::RegisterSmartObject(USmartObjectComponent& SmartObjectComponent) { if (SmartObjectComponent.GetDefinition() == nullptr) { UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Attempting to register %s while its DefinitionAsset is not set. Bailing out."), *GetFullNameSafe(&SmartObjectComponent)); return false; } TOptional bIsValid = SmartObjectComponent.GetDefinition()->IsValid(); if (bIsValid.IsSet() == false) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Attempting to register %s while its DefinitionAsset has not been Validated. Validating now."), *GetFullNameSafe(&SmartObjectComponent)); bIsValid = SmartObjectComponent.GetDefinition()->Validate(); } if (bIsValid.Get(false) == false) { UE_VLOG_UELOG(this, LogSmartObject, Warning, TEXT("Attempting to register %s while its DefinitionAsset fails validation test. Bailing out."), *GetFullNameSafe(&SmartObjectComponent)); return false; } if (!RegisteredSOComponents.Contains(&SmartObjectComponent)) { return RegisterSmartObjectInternal(SmartObjectComponent); } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Failed to register %s. Already registered"), *GetFullNameSafe(SmartObjectComponent.GetOwner()), *GetFullNameSafe(SmartObjectComponent.GetDefinition())); return false; } bool USmartObjectSubsystem::RegisterSmartObjectInternal(USmartObjectComponent& SmartObjectComponent) { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("Registering %s using definition %s."), *GetFullNameSafe(SmartObjectComponent.GetOwner()), *GetFullNameSafe(SmartObjectComponent.GetDefinition())); // Main collection may not assigned until world components are updated (active level set and actors registered) // In this case objects will be part of the loaded collection or collection will be rebuilt from registered components if (IsValid(MainCollection)) { const UWorld& World = GetWorldRef(); bool bAddToCollection = true; #if WITH_EDITOR if (!World.IsGameWorld()) { // For collections not built automatically we wait an explicit build request to clear and repopulate if (MainCollection->ShouldBuildCollectionAutomatically() == false) { bAddToCollection = false; UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s not added to collection that is built on demand only."), *GetNameSafe(SmartObjectComponent.GetOwner())); } // For partition world we don't alter the collection unless we are explicitly building the collection else if(World.IsPartitionedWorld() && !MainCollection->IsBuildingForWorldPartition()) { bAddToCollection = false; UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s not added to collection that is owned by partitioned world."), *GetNameSafe(SmartObjectComponent.GetOwner())); } } #endif // WITH_EDITOR if (bAddToCollection) { bool bAlreadyInCollection = false; const FSmartObjectCollectionEntry* Entry = MainCollection->AddSmartObject(SmartObjectComponent, bAlreadyInCollection); #if WITH_EDITOR if (Entry != nullptr && !bAlreadyInCollection) { OnMainCollectionDirtied.Broadcast(); } #endif // At runtime we only consider registrations after collection was pushed to the simulation. All existing entries were added on WorldBeginPlay if (World.IsGameWorld() && bInitialCollectionAddedToSimulation && Entry != nullptr) { if (bAlreadyInCollection) { // Simply bind the newly available component to its active runtime instance BindComponentToSimulation(SmartObjectComponent); } else { // This is a new entry added after runtime initialization, mark it as a runtime entry (lifetime is tied to the component) AddComponentToSimulation(SmartObjectComponent, *Entry); RuntimeCreatedEntries.Add(SmartObjectComponent.GetRegisteredHandle()); } } } ensureMsgf(RegisteredSOComponents.Find(&SmartObjectComponent) == INDEX_NONE , TEXT("Adding %s to RegisteredSOColleciton, but it has already been added. Missing unregister call?"), *SmartObjectComponent.GetName()); RegisteredSOComponents.Add(&SmartObjectComponent); } else { if (bInitialCollectionAddedToSimulation) { // Report error regarding missing Collection only when SmartObjects are registered (i.e. Collection is required) UE_VLOG_UELOG(this, LogSmartObject, Error, TEXT("%s not added to collection since Main Collection doesn't exist in world '%s'. You need to open and save that world to create the missing collection."), *GetNameSafe(SmartObjectComponent.GetOwner()), *GetWorldRef().GetFullName()); } else { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s not added to collection since Main Collection is not set yet. Storing SOComponent instance for registration once a collection is set."), *GetNameSafe(SmartObjectComponent.GetOwner())); PendingSmartObjectRegistration.Add(&SmartObjectComponent); } } return true; } bool USmartObjectSubsystem::UnregisterSmartObject(USmartObjectComponent& SmartObjectComponent) { if (RegisteredSOComponents.Contains(&SmartObjectComponent)) { return UnregisterSmartObjectInternal(SmartObjectComponent, ESmartObjectUnregistrationMode::DestroyRuntimeInstance); } UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Failed to unregister %s. Already unregistered"), *GetFullNameSafe(SmartObjectComponent.GetOwner()), *GetFullNameSafe(SmartObjectComponent.GetDefinition())); return false; } bool USmartObjectSubsystem::UnregisterSmartObjectInternal(USmartObjectComponent& SmartObjectComponent, const ESmartObjectUnregistrationMode UnregistrationMode) { UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("Unregistering %s using definition %s."), *GetFullNameSafe(SmartObjectComponent.GetOwner()), *GetFullNameSafe(SmartObjectComponent.GetDefinition())); if (IsValid(MainCollection)) { const UWorld& World = GetWorldRef(); bool bRemoveFromCollection = true; #if WITH_EDITOR if (!World.IsGameWorld()) { // For collections not built automatically we wait an explicit build request to clear and repopulate if (MainCollection->ShouldBuildCollectionAutomatically() == false) { bRemoveFromCollection = false; UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s not removed from collection that is built on demand only."), *GetNameSafe(SmartObjectComponent.GetOwner())); } // For partition world we never remove from the collection since it is built incrementally else if (World.IsPartitionedWorld()) { bRemoveFromCollection = false; UE_VLOG_UELOG(this, LogSmartObject, VeryVerbose, TEXT("%s not removed from collection that is owned by partitioned world."), *GetNameSafe(SmartObjectComponent.GetOwner())); } } #endif // WITH_EDITOR // At runtime, only entries created outside the initial collection are removed from simulation and collection if (World.IsGameWorld() && bInitialCollectionAddedToSimulation && SmartObjectComponent.GetRegisteredHandle().IsValid()) // Make sure component was registered to simulation (e.g. Valid associated definition) { bRemoveFromCollection = RuntimeCreatedEntries.Remove(SmartObjectComponent.GetRegisteredHandle()) != 0 || UnregistrationMode == ESmartObjectUnregistrationMode::DestroyRuntimeInstance; if (bRemoveFromCollection) { RemoveComponentFromSimulation(SmartObjectComponent); } else { // Unbind the component from its associated runtime instance UnbindComponentFromSimulation(SmartObjectComponent); } } if (bRemoveFromCollection) { MainCollection->RemoveSmartObject(SmartObjectComponent); } } RegisteredSOComponents.Remove(&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.GetName()); 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.GetName()); int32 NumSuccess = 0; for (USmartObjectComponent* SOComponent : Components) { if (UnregisterSmartObject(*SOComponent)) { NumSuccess++; } } return NumSuccess > 0 && NumSuccess == Components.Num(); } FSmartObjectClaimHandle USmartObjectSubsystem::Claim(const FSmartObjectHandle Handle, const FSmartObjectRequestFilter& Filter) { const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__)); if (SmartObjectRuntime == nullptr) { return FSmartObjectClaimHandle::InvalidHandle; } TArray SlotHandles; FindSlots(*SmartObjectRuntime, Filter, SlotHandles); if (SlotHandles.IsEmpty()) { return FSmartObjectClaimHandle::InvalidHandle; } return Claim(Handle, SlotHandles.Top()); } FSmartObjectClaimHandle USmartObjectSubsystem::Claim(const FSmartObjectHandle Handle, const FSmartObjectSlotHandle SlotHandle) { if (!Handle.IsValid()) { UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Claiming using an unset smart object handle. Returning invalid FSmartObjectClaimHandle.")); return FSmartObjectClaimHandle::InvalidHandle; } FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__)); if (Slot == nullptr) { return FSmartObjectClaimHandle::InvalidHandle; } const FSmartObjectUserHandle User(NextFreeUserID++); const bool bClaimed = Slot->Claim(User); const FSmartObjectClaimHandle ClaimHandle(Handle, SlotHandle, User); UE_VLOG_UELOG(this, LogSmartObject, Verbose, 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.f, FColor::Yellow, TEXT("Claim")); if (bClaimed) { if (const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Slot->GetOwnerRuntimeObject())) { OnSlotChanged(*SmartObjectRuntime, *Slot, SlotHandle, ESmartObjectChangeReason::OnClaimed); } return ClaimHandle; } return FSmartObjectClaimHandle::InvalidHandle; } bool USmartObjectSubsystem::CanBeClaimed(FSmartObjectSlotHandle SlotHandle) const { const FSmartObjectRuntimeSlot* Slot = GetSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__)); return Slot != nullptr && Slot->CanBeClaimed(); } bool USmartObjectSubsystem::IsSmartObjectValid(const FSmartObjectHandle SmartObjectHandle) const { return SmartObjectHandle.IsValid() && RuntimeSmartObjects.Find(SmartObjectHandle) != nullptr; } bool USmartObjectSubsystem::IsClaimedSmartObjectValid(const FSmartObjectClaimHandle& ClaimHandle) const { return ClaimHandle.IsValid() && RuntimeSmartObjects.Find(ClaimHandle.SmartObjectHandle) != nullptr; } bool USmartObjectSubsystem::IsSlotValidVerbose(const FSmartObjectSlotHandle SlotHandle, const TCHAR* LogContext) const { UE_CVLOG_UELOG(!SlotHandle.IsValid(), this, LogSmartObject, Log, TEXT("%s failed. SlotHandle is not set."), LogContext); UE_CVLOG_UELOG(SlotHandle.IsValid() && RuntimeSlots.Find(SlotHandle) == nullptr, this, LogSmartObject, Log, TEXT("%s failed using handle '%s'. Slot is no longer part of the simulation."), LogContext, *LexToString(SlotHandle)); return IsSmartObjectSlotValid(SlotHandle); } FSmartObjectRuntimeSlot* USmartObjectSubsystem::GetMutableSlotVerbose(const FSmartObjectSlotHandle SlotHandle, const TCHAR* LogContext) { UE_CVLOG_UELOG(!SlotHandle.IsValid(), this, LogSmartObject, Log, TEXT("%s failed. SlotHandle is not set."), LogContext); UE_CVLOG_UELOG(SlotHandle.IsValid() && RuntimeSlots.Find(SlotHandle) == nullptr, this, LogSmartObject, Log, TEXT("%s failed using handle '%s'. Slot is no longer part of the simulation."), LogContext, *LexToString(SlotHandle)); return SlotHandle.IsValid() ? RuntimeSlots.Find(SlotHandle) : nullptr; } const FSmartObjectRuntimeSlot* USmartObjectSubsystem::GetSlotVerbose(const FSmartObjectSlotHandle SlotHandle, const TCHAR* LogContext) const { UE_CVLOG_UELOG(!SlotHandle.IsValid(), this, LogSmartObject, Log, TEXT("%s failed. SlotHandle is not set."), LogContext); UE_CVLOG_UELOG(SlotHandle.IsValid() && RuntimeSlots.Find(SlotHandle) == nullptr, this, LogSmartObject, Log, TEXT("%s failed using handle '%s'. Slot is no longer part of the simulation."), LogContext, *LexToString(SlotHandle)); return SlotHandle.IsValid() ? RuntimeSlots.Find(SlotHandle) : nullptr; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinition(const FSmartObjectClaimHandle& ClaimHandle, const TSubclassOf& DefinitionClass) { const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(ClaimHandle.SmartObjectHandle, ANSI_TO_TCHAR(__FUNCTION__)); return SmartObjectRuntime != nullptr ? GetBehaviorDefinition(*SmartObjectRuntime, ClaimHandle.SlotHandle, DefinitionClass) : nullptr; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinitionByRequestResult(const FSmartObjectRequestResult& RequestResult, const TSubclassOf& DefinitionClass) { const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(RequestResult.SmartObjectHandle, ANSI_TO_TCHAR(__FUNCTION__)); return SmartObjectRuntime != nullptr ? GetBehaviorDefinition(*SmartObjectRuntime, RequestResult.SlotHandle, DefinitionClass) : nullptr; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::GetBehaviorDefinition(const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectSlotHandle SlotHandle, const TSubclassOf& DefinitionClass) { const USmartObjectDefinition& Definition = SmartObjectRuntime.GetDefinition(); const FSmartObjectSlotIndex SlotIndex(SmartObjectRuntime.SlotHandles.IndexOfByKey(SlotHandle)); return Definition.GetBehaviorDefinition(SlotIndex, DefinitionClass); } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::Use(const FSmartObjectClaimHandle& ClaimHandle, const TSubclassOf& DefinitionClass) { const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(ClaimHandle.SmartObjectHandle, ANSI_TO_TCHAR(__FUNCTION__)); return SmartObjectRuntime != nullptr ? Use(*SmartObjectRuntime, ClaimHandle, DefinitionClass) : nullptr; } const USmartObjectBehaviorDefinition* USmartObjectSubsystem::Use(const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectClaimHandle& ClaimHandle, const 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 = GetBehaviorDefinition(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"), *LexToString(SmartObjectRuntime.GetDefinition())); return nullptr; } UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Start using handle %s"), *LexToString(ClaimHandle)); UE_VLOG_LOCATION(this, LogSmartObject, Display, SmartObjectRuntime.GetTransform().GetLocation(), 50.f, FColor::Green, TEXT("Use")); FSmartObjectRuntimeSlot& Slot = RuntimeSlots.FindChecked(ClaimHandle.SlotHandle); if (ensureMsgf(Slot.GetState() == ESmartObjectSlotState::Claimed, TEXT("Should have been claimed first: %s"), *LexToString(ClaimHandle)) && ensureMsgf(Slot.User == ClaimHandle.UserHandle, TEXT("Attempt to use slot %s from handle %s but already assigned to %s"), *LexToString(Slot), *LexToString(ClaimHandle), *LexToString(Slot.User))) { Slot.State = ESmartObjectSlotState::Occupied; return BehaviorDefinition; } return nullptr; } bool USmartObjectSubsystem::Release(const FSmartObjectClaimHandle& ClaimHandle) { FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(ClaimHandle.SlotHandle, ANSI_TO_TCHAR(__FUNCTION__)); if (!Slot) { return false; } const bool bSuccess = Slot->Release(ClaimHandle, /*bAborted*/ false); UE_CVLOG_UELOG(bSuccess, this, LogSmartObject, Verbose, TEXT("Released using handle %s"), *LexToString(ClaimHandle)); UE_CVLOG_LOCATION(bSuccess, this, LogSmartObject, Display, GetSlotLocation(ClaimHandle).GetValue(), 50.f, FColor::Red, TEXT("Release")); if (bSuccess) { if (const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Slot->GetOwnerRuntimeObject())) { OnSlotChanged(*SmartObjectRuntime, *Slot, ClaimHandle.SlotHandle, ESmartObjectChangeReason::OnReleased); } } return bSuccess; } ESmartObjectSlotState USmartObjectSubsystem::GetSlotState(const FSmartObjectSlotHandle SlotHandle) const { const FSmartObjectRuntimeSlot* Slot = RuntimeSlots.Find(SlotHandle); return Slot != nullptr ? Slot->GetState() : ESmartObjectSlotState::Invalid; } 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 FSmartObjectSlotHandle SlotHandle) const { TOptional Transform = GetSlotTransform(SlotHandle); return (Transform.IsSet() ? Transform.GetValue().GetLocation() : TOptional()); } bool USmartObjectSubsystem::GetSlotTransform(const FSmartObjectClaimHandle& ClaimHandle, FTransform& OutSlotTransform) const { const TOptional OptionalTransform = GetSlotTransform(ClaimHandle); OutSlotTransform = OptionalTransform.Get(FTransform::Identity); return OptionalTransform.IsSet(); } 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 Transform; if (const FSmartObjectRuntimeSlot* Slot = GetSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (ensureMsgf(EntityManager, TEXT("Entity subsystem required to retrieve slot transform"))) { const FSmartObjectSlotView View(*EntityManager.Get(), SlotHandle, Slot); const FSmartObjectSlotTransform& SlotTransform = View.GetStateData(); Transform = SlotTransform.GetTransform(); } } return Transform; } const FTransform& USmartObjectSubsystem::GetSlotTransformChecked(const FSmartObjectSlotHandle SlotHandle) const { check(EntityManager); const FSmartObjectSlotView View(*EntityManager.Get(), SlotHandle, nullptr); const FSmartObjectSlotTransform& SlotTransform = View.GetStateData(); return SlotTransform.GetTransform(); } FSmartObjectRuntime* USmartObjectSubsystem::GetValidatedMutableRuntime(const FSmartObjectHandle Handle, const TCHAR* Context) const { return const_cast(GetValidatedRuntime(Handle, Context)); } const FSmartObjectRuntime* USmartObjectSubsystem::GetValidatedRuntime(const FSmartObjectHandle Handle, const TCHAR* Context) const { const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Handle); UE_CVLOG_UELOG(!Handle.IsValid(), this, LogSmartObject, Log, TEXT("%s failed. Handle is not set."), Context); UE_CVLOG_UELOG(Handle.IsValid() && SmartObjectRuntime == nullptr, this, LogSmartObject, Log, TEXT("%s failed using handle '%s'. SmartObject is no longer part of the simulation."), Context, *LexToString(Handle)); return SmartObjectRuntime; } const FGameplayTagContainer& USmartObjectSubsystem::GetInstanceTags(const FSmartObjectHandle Handle) const { const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__)); return SmartObjectRuntime != nullptr ? SmartObjectRuntime->GetTags() : FGameplayTagContainer::EmptyContainer; } void USmartObjectSubsystem::AddTagToInstance(const FSmartObjectHandle Handle, const FGameplayTag& Tag) { if (FSmartObjectRuntime* SmartObjectRuntime = GetValidatedMutableRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__))) { AddTagToInstance(*SmartObjectRuntime, Tag); } } void USmartObjectSubsystem::RemoveTagFromInstance(const FSmartObjectHandle Handle, const FGameplayTag& Tag) { if (FSmartObjectRuntime* SmartObjectRuntime = GetValidatedMutableRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__))) { RemoveTagFromInstance(*SmartObjectRuntime, Tag); } } const FGameplayTagContainer& USmartObjectSubsystem::GetSlotTags(const FSmartObjectSlotHandle SlotHandle) const { static const FGameplayTagContainer EmptyTags; if (const FSmartObjectRuntimeSlot* Slot = GetSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { return Slot->Tags; } return EmptyTags; } void USmartObjectSubsystem::AddTagToSlot(const FSmartObjectSlotHandle SlotHandle, const FGameplayTag& Tag) { if (!Tag.IsValid()) { return; } if (FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (!Slot->Tags.HasTag(Tag)) { Slot->Tags.AddTagFast(Tag); if (const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Slot->GetOwnerRuntimeObject())) { OnSlotChanged(*SmartObjectRuntime, *Slot, SlotHandle, ESmartObjectChangeReason::OnTagAdded, Tag); } } } } bool USmartObjectSubsystem::RemoveTagFromSlot(const FSmartObjectSlotHandle SlotHandle, const FGameplayTag& Tag) { if (!Tag.IsValid()) { return false; } if (FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (Slot->Tags.RemoveTag(Tag)) { if (const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Slot->GetOwnerRuntimeObject())) { OnSlotChanged(*SmartObjectRuntime, *Slot, SlotHandle, ESmartObjectChangeReason::OnTagRemoved, Tag); } return true; } } return false; } bool USmartObjectSubsystem::SetSlotEnabled(const FSmartObjectSlotHandle SlotHandle, const bool bEnabled) { bool bPreviousValue = false; if (FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { bPreviousValue = Slot->bEnabled; if (Slot->bEnabled != bEnabled) { Slot->bEnabled = bEnabled; if (const FSmartObjectRuntime* SmartObjectRuntime = RuntimeSmartObjects.Find(Slot->GetOwnerRuntimeObject())) { OnSlotChanged(*SmartObjectRuntime, *Slot, SlotHandle, Slot->bEnabled ? ESmartObjectChangeReason::OnEnabled : ESmartObjectChangeReason::OnDisabled); } } } return bPreviousValue; } bool USmartObjectSubsystem::SendSlotEvent(const FSmartObjectSlotHandle SlotHandle, const FGameplayTag EventTag, const FConstStructView Payload) { if (const FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (Slot->GetEventDelegate().IsBound() && RuntimeSmartObjects.Contains(Slot->GetOwnerRuntimeObject())) { FSmartObjectEventData Data; Data.SmartObjectHandle = Slot->GetOwnerRuntimeObject(); Data.SlotHandle = SlotHandle; Data.Reason = ESmartObjectChangeReason::OnEvent; Data.Tag = EventTag; Data.EventPayload = Payload; Slot->GetEventDelegate().Broadcast(Data); return true; } } return false; } void USmartObjectSubsystem::AddTagToInstance(FSmartObjectRuntime& SmartObjectRuntime, const FGameplayTag& Tag) { if (!SmartObjectRuntime.Tags.HasTag(Tag)) { SmartObjectRuntime.Tags.AddTagFast(Tag); SmartObjectRuntime.OnTagChangedDelegate.ExecuteIfBound(Tag, 1); UpdateRuntimeInstanceStatus(SmartObjectRuntime, ESmartObjectChangeReason::OnTagAdded); } } void USmartObjectSubsystem::RemoveTagFromInstance(FSmartObjectRuntime& SmartObjectRuntime, const FGameplayTag& Tag) { if (SmartObjectRuntime.Tags.RemoveTag(Tag)) { SmartObjectRuntime.OnTagChangedDelegate.ExecuteIfBound(Tag, 0); UpdateRuntimeInstanceStatus(SmartObjectRuntime, ESmartObjectChangeReason::OnTagRemoved); } } void USmartObjectSubsystem::UpdateRuntimeInstanceStatus(FSmartObjectRuntime& SmartObjectRuntime, const ESmartObjectChangeReason Reason) { const FGameplayTagQuery& Requirements = SmartObjectRuntime.GetDefinition().GetObjectTagFilter(); const bool bEnabled = (Requirements.IsEmpty() || Requirements.Matches(SmartObjectRuntime.Tags)); if (SmartObjectRuntime.bEnabled != bEnabled) { // Update slot enable states. for (const FSmartObjectSlotHandle SlotHandle : SmartObjectRuntime.SlotHandles) { FSmartObjectRuntimeSlot& Slot = RuntimeSlots.FindChecked(SlotHandle); Slot.bEnabled = bEnabled; OnSlotChanged(SmartObjectRuntime, Slot, SlotHandle, bEnabled ? ESmartObjectChangeReason::OnEnabled : ESmartObjectChangeReason::OnDisabled); } SmartObjectRuntime.bEnabled = bEnabled; } } void USmartObjectSubsystem::OnSlotChanged(const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRuntimeSlot& Slot, const FSmartObjectSlotHandle SlotHandle, const ESmartObjectChangeReason Reason, const FGameplayTag ChangedTag) const { if (Slot.GetEventDelegate().IsBound()) { FSmartObjectEventData Data; Data.SmartObjectHandle = Slot.GetOwnerRuntimeObject(); Data.SlotHandle = SlotHandle; Data.Reason = Reason; Data.Tag = ChangedTag; Slot.GetEventDelegate().Broadcast(Data); } } FSmartObjectRuntimeSlot* USmartObjectSubsystem::GetMutableSlot(const FSmartObjectClaimHandle& ClaimHandle) { return RuntimeSlots.Find(ClaimHandle.SlotHandle); } void USmartObjectSubsystem::RegisterSlotInvalidationCallback(const FSmartObjectClaimHandle& ClaimHandle, const FOnSlotInvalidated& Callback) { FSmartObjectRuntimeSlot* Slot = GetMutableSlot(ClaimHandle); if (Slot != nullptr) { Slot->OnSlotInvalidatedDelegate = Callback; } } void USmartObjectSubsystem::UnregisterSlotInvalidationCallback(const FSmartObjectClaimHandle& ClaimHandle) { FSmartObjectRuntimeSlot* Slot = GetMutableSlot(ClaimHandle); if (Slot != nullptr) { Slot->OnSlotInvalidatedDelegate.Unbind(); } } FOnSmartObjectEvent* USmartObjectSubsystem::GetSlotEventDelegate(const FSmartObjectSlotHandle SlotHandle) { if (FSmartObjectRuntimeSlot* Slot = GetMutableSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { return &Slot->GetMutableEventDelegate(); } return nullptr; } #if UE_ENABLE_DEBUG_DRAWING void USmartObjectSubsystem::DebugDraw(FDebugRenderSceneProxy* DebugProxy) const { if (!bInitialCollectionAddedToSimulation) { 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::AddSlotDataDeferred(const FSmartObjectClaimHandle& ClaimHandle, const FConstStructView InData) const { if (IsSlotValidVerbose(ClaimHandle.SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (ensureMsgf(EntityManager, TEXT("Entity subsystem required to add slot data")) && ensureMsgf(InData.GetScriptStruct()->IsChildOf(FSmartObjectSlotStateData::StaticStruct()), TEXT("Given struct doesn't represent a valid runtime data type. Make sure to inherit from FSmartObjectSlotState or one of its child-types."))) { EntityManager->Defer().PushCommand( [EntityHandle = FMassEntityHandle(ClaimHandle.SlotHandle), DataView = InData](FMassEntityManager& System) { FInstancedStruct Struct = DataView; System.AddFragmentInstanceListToEntity(EntityHandle, MakeArrayView(&Struct, 1)); }); // @todo: This is temporary solution to make the added data immediately accessible. ensureMsgf(!EntityManager->IsProcessing(), TEXT("EntityManager is processing, which prevents immediate data change.")); EntityManager->FlushCommands(); } } } FSmartObjectSlotView USmartObjectSubsystem::GetSlotView(const FSmartObjectSlotHandle SlotHandle) const { if (const FSmartObjectRuntimeSlot* Slot = GetSlotVerbose(SlotHandle, ANSI_TO_TCHAR(__FUNCTION__))) { if (ensureMsgf(EntityManager, TEXT("Entity subsystem required to create slot view"))) { return FSmartObjectSlotView(*EntityManager.Get(), SlotHandle, Slot); } } return FSmartObjectSlotView(); } void USmartObjectSubsystem::FindSlots(const FSmartObjectHandle Handle, const FSmartObjectRequestFilter& Filter, TArray& OutSlots) const { if (const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__))) { FindSlots(*SmartObjectRuntime, Filter, OutSlots); } } void USmartObjectSubsystem::GetAllSlots(const FSmartObjectHandle Handle, TArray& OutSlots) const { TRACE_CPUPROFILER_EVENT_SCOPE_STR("SmartObject_FilterSlots"); OutSlots.Reset(); if (const FSmartObjectRuntime* SmartObjectRuntime = GetValidatedRuntime(Handle, ANSI_TO_TCHAR(__FUNCTION__))) { OutSlots = SmartObjectRuntime->SlotHandles; } } void USmartObjectSubsystem::FindSlots(const FSmartObjectRuntime& SmartObjectRuntime, const FSmartObjectRequestFilter& Filter, TArray& OutResults) 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.SlotHandles.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 predicate on runtime instance tags (affecting all slots and not affected by the filtering policy) if (!Definition.GetObjectTagFilter().IsEmpty() && !Definition.GetObjectTagFilter().Matches(SmartObjectRuntime.GetTags())) { 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; FindMatchingSlotDefinitionIndices(Definition, Filter, ValidSlotIndices); // Build list of available slot indices (filter out occupied or reserved slots or disabled slots) for (const int32 SlotIndex : ValidSlotIndices) { const FSmartObjectRuntimeSlot& RuntimeSlot = RuntimeSlots.FindChecked(SmartObjectRuntime.SlotHandles[SlotIndex]); if (RuntimeSlot.IsEnabled() && RuntimeSlot.GetState() == ESmartObjectSlotState::Free) { OutResults.Add(SmartObjectRuntime.SlotHandles[SlotIndex]); } } } void USmartObjectSubsystem::FindMatchingSlotDefinitionIndices(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]; PRAGMA_DISABLE_DEPRECATION_WARNINGS // (Deprecated property handling) Filter out mismatching behavior type (if specified) if (Filter.BehaviorDefinitionClass != nullptr && Definition.GetBehaviorDefinition(FSmartObjectSlotIndex(i), Filter.BehaviorDefinitionClass) == nullptr) { continue; } PRAGMA_ENABLE_DEPRECATION_WARNINGS // Filter out mismatching behavior type (if specified) if (!Filter.BehaviorDefinitionClasses.IsEmpty()) { bool bMatchesAny = false; for (const TSubclassOf& BehaviorDefinitionClass : Filter.BehaviorDefinitionClasses) { if (Definition.GetBehaviorDefinition(FSmartObjectSlotIndex(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 { TArray Results; FindSmartObjects(Request, Results); return Results.Num() ? Results.Top() : FSmartObjectRequestResult(); } bool USmartObjectSubsystem::FindSmartObjects(const FSmartObjectRequest& Request, TArray& OutResults) const { TRACE_CPUPROFILER_EVENT_SCOPE_STR("SmartObject_FindAllResults"); if (!bInitialCollectionAddedToSimulation) { // 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; checkfSlow(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 = RuntimeSmartObjects.Find(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; FindSlots(*SmartObjectRuntime, Filter, SlotHandles); OutResults.Reserve(OutResults.Num() + SlotHandles.Num()); for (FSmartObjectSlotHandle SlotHandle: SlotHandles) { OutResults.Emplace(SmartObjectHandle, SlotHandle); } } return (OutResults.Num() > 0); } void USmartObjectSubsystem::RegisterCollectionInstances() { for (TActorIterator It(GetWorld()); It; ++It) { ASmartObjectCollection* 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->GetName(), *UEnum::GetValueAsString(Result)); } } } ESmartObjectCollectionRegistrationResult USmartObjectSubsystem::RegisterCollection(ASmartObjectCollection& 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.GetName()); return ESmartObjectCollectionRegistrationResult::Failed_AlreadyRegistered; } ESmartObjectCollectionRegistrationResult Result = ESmartObjectCollectionRegistrationResult::Succeeded; if (InCollection.GetLevel()->IsPersistentLevel()) { ensureMsgf(!IsValid(MainCollection), TEXT("Not expecting to set the main collection more than once")); UE_VLOG_UELOG(&InCollection, LogSmartObject, Log, TEXT("Main collection '%s' registered with %d entries"), *InCollection.GetName(), InCollection.GetEntries().Num()); MainCollection = &InCollection; for (TObjectPtr& SOComponent : PendingSmartObjectRegistration) { // ensure the SOComponent is still valid - things could have happened to it between adding to PendingSmartObjectRegistration and it beind processed here if (SOComponent && IsValid(SOComponent)) { RegisterSmartObject(*SOComponent); } } PendingSmartObjectRegistration.Empty(); #if WITH_EDITOR // For a collection that is automatically updated, it gets rebuilt on registration in the Edition world. const UWorld& World = GetWorldRef(); if (!World.IsGameWorld() && !World.IsPartitionedWorld() && MainCollection->ShouldBuildCollectionAutomatically()) { RebuildCollection(InCollection); } // Broadcast after rebuilding so listeners will be able to access up-to-date data OnMainCollectionChanged.Broadcast(); #endif // WITH_EDITOR InCollection.OnRegistered(); Result = ESmartObjectCollectionRegistrationResult::Succeeded; } else { InCollection.Destroy(); Result = ESmartObjectCollectionRegistrationResult::Failed_NotFromPersistentLevel; } return Result; } void USmartObjectSubsystem::UnregisterCollection(ASmartObjectCollection& InCollection) { if (MainCollection != &InCollection) { UE_VLOG_UELOG(&InCollection, LogSmartObject, Verbose, TEXT("Ignoring unregistration of collection '%s' since this is not the main collection."), *InCollection.GetName()); return; } if (bInitialCollectionAddedToSimulation) { CleanupRuntime(); } MainCollection = nullptr; InCollection.OnUnregistered(); } USmartObjectComponent* USmartObjectSubsystem::GetSmartObjectComponent(const FSmartObjectClaimHandle& ClaimHandle) const { return (IsValid(MainCollection) ? MainCollection->GetSmartObjectComponent(ClaimHandle.SmartObjectHandle) : nullptr); } USmartObjectComponent* USmartObjectSubsystem::GetSmartObjectComponentByRequestResult(const FSmartObjectRequestResult& Result) const { return (IsValid(MainCollection) ? MainCollection->GetSmartObjectComponent(Result.SmartObjectHandle) : nullptr); } void USmartObjectSubsystem::InitializeRuntime() { const UWorld& World = GetWorldRef(); UMassEntitySubsystem* EntitySubsystem = World.GetSubsystem(); if (!ensureMsgf(EntitySubsystem != nullptr, TEXT("Entity subsystem required to use SmartObjects"))) { return; } InitializeRuntime(EntitySubsystem->GetMutableEntityManager().AsShared()); } void USmartObjectSubsystem::InitializeRuntime(const TSharedPtr& InEntityManager) { check(InEntityManager); const UWorld& World = GetWorldRef(); EntityManager = InEntityManager; if (UE::SmartObject::bDisableRuntime) { UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Runtime explicitly disabled by CVar. Initialization skipped in %s."), ANSI_TO_TCHAR(__FUNCTION__)); return; } if (!IsValid(MainCollection)) { if (MainCollection != nullptr && !MainCollection->bNetLoadOnClient && World.IsNetMode(NM_Client)) { UE_VLOG_UELOG(this, LogSmartObject, Verbose, TEXT("Collection not loaded on client. Initialization skipped in %s."), ANSI_TO_TCHAR(__FUNCTION__)); } else { // Report error regarding missing Collection only when SmartObjects are present in the world (i.e. Collection is required) UE_CVLOG_UELOG(RegisteredSOComponents.Num(), this, LogSmartObject, Error, TEXT("Missing collection in world '%s' during %s. You need to open and save that world to create the missing collection."), *GetWorldRef().GetFullName(), ANSI_TO_TCHAR(__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(MainCollection->GetBounds()); // Build all runtime from collection // Perform all validations at once since multiple entries can share the same definition MainCollection->ValidateDefinitions(); for (const FSmartObjectCollectionEntry& Entry : MainCollection->GetEntries()) { const USmartObjectDefinition* Definition = MainCollection->GetDefinitionForEntry(Entry); USmartObjectComponent* Component = Entry.GetComponent(); if (Definition == nullptr || Definition->IsValid() == 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) { // 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()); AddComponentToSimulation(*Component, Entry); } else { // Otherwise we create the runtime instance based on the information from the collection and component will be bound later (e.g. on load) AddCollectionEntryToSimulation(Entry, *Definition); } } // Until this point all runtime entries were created from the collection, start tracking newly created RuntimeCreatedEntries.Reset(); // 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) bInitialCollectionAddedToSimulation = true; // Flush all entity subsystem commands pushed while adding collection entries to the simulation // This is the temporary way to force our commands to be processed until MassEntitySubsystem // offers a threadsafe solution to push and flush commands in our own execution context. EntityManager->FlushCommands(); #if UE_ENABLE_DEBUG_DRAWING // Refresh debug draw if (RenderingActor != nullptr) { RenderingActor->MarkComponentsRenderStateDirty(); } #endif // UE_ENABLE_DEBUG_DRAWING } void USmartObjectSubsystem::CleanupRuntime() { EntityManager = UE::Mass::Utils::GetEntityManagerChecked(GetWorldRef()).AsShared(); if (!ensureMsgf(EntityManager, TEXT("Entity manager required to use SmartObjects"))) { return; } // 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->GetRegisteredHandle().IsValid()) { RemoveComponentFromSimulation(*Component); } } // Cleanup all remaining entries (e.g. associated to unloaded SmartObjectComponents) for (auto It(RuntimeSmartObjects.CreateIterator()); It; ++It) { DestroyRuntimeInstanceInternal(It.Key(), It.Value(), *EntityManager); } RuntimeSmartObjects.Reset(); RuntimeCreatedEntries.Reset(); bInitialCollectionAddedToSimulation = false; // Flush all entity subsystem commands pushed while stopping the simulation // This is the temporary way to force our commands to be processed until MassEntitySubsystem // offers a threadsafe solution to push and flush commands in our own execution context. EntityManager->FlushCommands(); #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() { EntityManager.Reset(); Super::Deinitialize(); } #if WITH_EDITOR void USmartObjectSubsystem::ComputeBounds(const UWorld& World, ASmartObjectCollection& Collection) 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.")); } Collection.SetBounds(Bounds); } void USmartObjectSubsystem::RebuildCollection(ASmartObjectCollection& InCollection) { InCollection.RebuildCollection(RegisteredSOComponents); } void USmartObjectSubsystem::SpawnMissingCollection() { if (IsValid(MainCollection)) { return; } UWorld& World = GetWorldRef(); FActorSpawnParameters SpawnInfo; SpawnInfo.OverrideLevel = World.PersistentLevel; SpawnInfo.bAllowDuringConstructionScript = true; UE_VLOG_UELOG(this, LogSmartObject, Log, TEXT("Spawning missing collection for world '%s'."), *World.GetName()); World.SpawnActor(ASmartObjectCollection::StaticClass(), SpawnInfo); checkf(IsValid(MainCollection), TEXT("MainCollection must be assigned after spawning")); } #endif // WITH_EDITOR #if WITH_SMARTOBJECT_DEBUG void USmartObjectSubsystem::DebugUnregisterAllSmartObjects() { for (USmartObjectComponent* Cmp : RegisteredSOComponents) { if (Cmp != nullptr && RuntimeSmartObjects.Find(Cmp->GetRegisteredHandle()) != nullptr) { RemoveComponentFromSimulation(*Cmp); } } } void USmartObjectSubsystem::DebugRegisterAllSmartObjects() { if (MainCollection == nullptr) { return; } for (USmartObjectComponent* Cmp : RegisteredSOComponents) { if (Cmp != nullptr) { const FSmartObjectCollectionEntry* Entry = MainCollection->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 && RuntimeSmartObjects.Find(Entry->GetHandle()) == nullptr) { AddComponentToSimulation(*Cmp, *Entry); } } } } void USmartObjectSubsystem::DebugInitializeRuntime() { // do not initialize more than once or on a GameWorld if (bInitialCollectionAddedToSimulation || GetWorldRef().IsGameWorld()) { return; } InitializeRuntime(); } void USmartObjectSubsystem::DebugRebuildCollection() { #if WITH_EDITOR if (MainCollection != nullptr) { RebuildCollection(*MainCollection); } #endif // WITH_EDITOR } void USmartObjectSubsystem::DebugCleanupRuntime() { // do not cleanup more than once or on a GameWorld if (!bInitialCollectionAddedToSimulation || GetWorldRef().IsGameWorld()) { return; } CleanupRuntime(); } #endif // WITH_SMARTOBJECT_DEBUG