// Copyright Epic Games, Inc. All Rights Reserved. #include "Evaluation/MovieSceneEvaluationState.h" #include "Algo/Sort.h" #include "Algo/Unique.h" #include "Engine/World.h" #include "EntitySystem/MovieSceneEntitySystemLinker.h" #include "EntitySystem/MovieSceneInstanceRegistry.h" #include "EntitySystem/MovieSceneSequenceInstance.h" #include "IMovieScenePlaybackClient.h" #include "IMovieScenePlayer.h" #include "MovieScene.h" #include "MovieSceneObjectBindingID.h" #include "MovieSceneSequence.h" #include "UniversalObjectLocatorResolveParams.h" DECLARE_CYCLE_STAT(TEXT("Find Bound Objects"), MovieSceneEval_FindBoundObjects, STATGROUP_MovieSceneEval); DECLARE_CYCLE_STAT(TEXT("Iterate Bound Objects"), MovieSceneEval_IterateBoundObjects, STATGROUP_MovieSceneEval); namespace UE::MovieScene { UE_DEFINE_MOVIESCENE_PLAYBACK_CAPABILITY(IObjectBindingNotifyPlaybackCapability) UE_DEFINE_MOVIESCENE_PLAYBACK_CAPABILITY(IStaticBindingOverridesPlaybackCapability) FMovieSceneEvaluationOperand* FStaticBindingOverrides::GetBindingOverride(const FMovieSceneEvaluationOperand& InOperand) { return BindingOverrides.Find(InOperand); } void FStaticBindingOverrides::AddBindingOverride(const FMovieSceneEvaluationOperand& InOperand, const FMovieSceneEvaluationOperand& InOverrideOperand) { BindingOverrides.Add(InOperand, InOverrideOperand); } void FStaticBindingOverrides::RemoveBindingOverride(const FMovieSceneEvaluationOperand& InOperand) { BindingOverrides.Remove(InOperand); } } // namespace UE::MovieScene FMovieSceneSharedDataId FMovieSceneSharedDataId::Allocate() { static uint32 Counter = 0; FMovieSceneSharedDataId Value; Value.UniqueId = ++Counter; check(Counter != -1); return Value; } TArrayView> FMovieSceneObjectCache::FindBoundObjects(const FGuid& InBindingID, TSharedRef InSharedPlaybackState) { MOVIESCENE_DETAILED_SCOPE_CYCLE_COUNTER(MovieSceneEval_FindBoundObjects) // Fast route - where everything's cached FBoundObjects* Bindings = BoundObjects.Find(InBindingID); if (Bindings && Bindings->bUpToDate) { return TArrayView>( Bindings->Objects.GetData(), Bindings->Objects.Num() ); } // Attempt to update the bindings UpdateBindings(InBindingID, InSharedPlaybackState); Bindings = BoundObjects.Find(InBindingID); if (Bindings) { return TArrayView>(Bindings->Objects.GetData(), Bindings->Objects.Num()); } // Just return nothing return TArrayView>(); } TArrayView> FMovieSceneObjectCache::IterateBoundObjects(const FGuid& InBindingID) const { MOVIESCENE_DETAILED_SCOPE_CYCLE_COUNTER(MovieSceneEval_IterateBoundObjects) const FBoundObjects* Bindings = BoundObjects.Find(InBindingID); if (Bindings && Bindings->bUpToDate) { return TArrayView>( Bindings->Objects.GetData(), Bindings->Objects.Num() ); } // Just return nothing return TArrayView>(); } FGuid FMovieSceneObjectCache::FindObjectId(UObject& InObject, TSharedRef SharedPlaybackState) { UMovieSceneSequence* Sequence = WeakSequence.Get(); UMovieScene* MovieScene = Sequence ? Sequence->GetMovieScene() : nullptr; if (!MovieScene) { return FGuid(); } if (!bReentrantUpdate) { // @todo: Currently we delete the entire object cache when attempting to find an object's ID to ensure that we do a // complete lookup from scratch. This is required for UMG as it interchanges content slots without notifying sequencer. Clear(SharedPlaybackState); } return FindCachedObjectId(InObject, SharedPlaybackState); } FGuid FMovieSceneObjectCache::FindCachedObjectId(UObject& InObject, TSharedRef SharedPlaybackState) { UMovieSceneSequence* Sequence = WeakSequence.Get(); UMovieScene* MovieScene = Sequence ? Sequence->GetMovieScene() : nullptr; if (!MovieScene) { return FGuid(); } TWeakObjectPtr<> ObjectToFind(&InObject); // Search all possessables for (int32 Index = 0; Index < MovieScene->GetPossessableCount(); ++Index) { FGuid ThisGuid = MovieScene->GetPossessable(Index).GetGuid(); if (FindBoundObjects(ThisGuid, SharedPlaybackState).Contains(ObjectToFind)) { return ThisGuid; } } // Search all spawnables for (int32 Index = 0; Index < MovieScene->GetSpawnableCount(); ++Index) { FGuid ThisGuid = MovieScene->GetSpawnable(Index).GetGuid(); if (FindBoundObjects(ThisGuid, SharedPlaybackState).Contains(ObjectToFind)) { return ThisGuid; } } return FGuid(); } void FMovieSceneObjectCache::FilterObjectBindings(UObject* PredicateObject, TSharedRef SharedPlaybackState, TArray* OutBindings) { check(OutBindings); TArray> OutOfDateBindings; for (const TTuple& Pair : BoundObjects) { if (Pair.Value.bUpToDate) { for (TWeakObjectPtr<> WeakObject : Pair.Value.Objects) { UObject* Object = WeakObject.Get(); if (Object && Object == PredicateObject) { OutBindings->Add(UE::MovieScene::FFixedObjectBindingID(Pair.Key, SequenceID)); break; } } } else { OutOfDateBindings.Add(Pair.Key); } } for (const FGuid& DirtyBinding : OutOfDateBindings) { UpdateBindings(DirtyBinding, SharedPlaybackState); const FBoundObjects& Bindings = BoundObjects.FindChecked(DirtyBinding); for (TWeakObjectPtr<> WeakObject : Bindings.Objects) { UObject* Object = WeakObject.Get(); if (Object && Object == PredicateObject) { OutBindings->Add(UE::MovieScene::FFixedObjectBindingID(DirtyBinding, SequenceID)); break; } } } } void FMovieSceneObjectCache::InvalidateExpiredObjects() { for (auto& Pair : BoundObjects) { if (!Pair.Value.bUpToDate) { continue; } for (TWeakObjectPtr<>& Ptr : Pair.Value.Objects) { if (!Ptr.Get()) { InvalidateInternal(Pair.Key); break; } } } if (UMovieSceneSequence* Sequence = WeakSequence.Get()) { TArray InvalidObjectIDs; Sequence->GatherExpiredObjects(*this, InvalidObjectIDs); for (const FGuid& ObjectID : InvalidObjectIDs) { InvalidateInternal(ObjectID); } } UpdateSerialNumber(); } void FMovieSceneObjectCache::InvalidateIfValid(const FGuid& InGuid) { const bool bInvalidated = InvalidateIfValidInternal(InGuid); if (bInvalidated) { UpdateSerialNumber(); } } bool FMovieSceneObjectCache::GetBindingActivation(const FGuid& InGuid) const { return !InactiveBindingIds.Contains(InGuid); } void FMovieSceneObjectCache::SetBindingActivation(const FGuid& InGuid, bool bActive) { if (bActive) { InactiveBindingIds.Remove(InGuid); } else { InactiveBindingIds.Add(InGuid); } InvalidateInternal(InGuid); } bool FMovieSceneObjectCache::InvalidateIfValidInternal(const FGuid& InGuid) { // Don't manipulate the actual map structure, since this can be called from inside an iterator FBoundObjects* Cache = BoundObjects.Find(InGuid); if (Cache && Cache->bUpToDate == true) { Cache->bUpToDate = false; auto* Children = ChildBindings.Find(InGuid); if (Children) { for (const FGuid& Child : *Children) { InvalidateIfValidInternal(Child); } } OnBindingInvalidated.Broadcast(InGuid); return true; } return false; } void FMovieSceneObjectCache::Invalidate(const FGuid& InGuid) { InvalidateInternal(InGuid); UpdateSerialNumber(); } void FMovieSceneObjectCache::Invalidate(const FGuid& InGuid, FMovieSceneSequenceIDRef InSequenceID) { if (InSequenceID == SequenceID) { Invalidate(InGuid); } else { FMovieSceneObjectBindingID BindingID(UE::MovieScene::FFixedObjectBindingID(InGuid, InSequenceID)); if (FGuidArray* ReferencedGuids = ReverseMappedBindings.Find(BindingID)) { for (FGuid ReferencedGuid : *ReferencedGuids) { Invalidate(ReferencedGuid); } ReverseMappedBindings.Remove(BindingID); } } } bool FMovieSceneObjectCache::InvalidateInternal(const FGuid& InGuid) { // Don't manipulate the actual map structure, since this can be called from inside an iterator FBoundObjects* Cache = BoundObjects.Find(InGuid); if (Cache) { Cache->bUpToDate = false; auto* Children = ChildBindings.Find(InGuid); if (Children) { for (const FGuid& Child : *Children) { InvalidateInternal(Child); } } } OnBindingInvalidated.Broadcast(InGuid); return true; } void FMovieSceneObjectCache::Clear(TSharedRef SharedPlaybackState) { using namespace UE::MovieScene; BoundObjects.Reset(); ChildBindings.Reset(); ReverseMappedBindings.Reset(); UpdateSerialNumber(); if (IObjectBindingNotifyPlaybackCapability* Notify = SharedPlaybackState->FindCapability()) { Notify->NotifyBindingsChanged(); } OnBindingInvalidated.Broadcast(FGuid()); } void FMovieSceneObjectCache::SetSequence(UMovieSceneSequence& InSequence, FMovieSceneSequenceIDRef InSequenceID, TSharedRef SharedPlaybackState) { if (WeakSequence != &InSequence) { Clear(SharedPlaybackState); } WeakSequence = &InSequence; SequenceID = InSequenceID; } void FMovieSceneObjectCache::UpdateBindings(const FGuid& InGuid, TSharedRef SharedPlaybackState) { using namespace UE::MovieScene; TGuardValue ReentrancyGuard(bReentrantUpdate, true); // Invalidate existing bindings, we're going to rebuild them. FBoundObjects* Bindings = &BoundObjects.FindOrAdd(InGuid); Bindings->Objects.Reset(); // Update our serial now so it's done before any early returns. UpdateSerialNumber(); if (auto* Children = ChildBindings.Find(InGuid)) { for (const FGuid& Child : *Children) { InvalidateIfValidInternal(Child); } } // Find the sequence for this cache. UMovieSceneSequence* Sequence = WeakSequence.Get(); if (!Sequence) { return; } // Binding is inactive, do not resolve. if (InactiveBindingIds.Contains(InGuid)) { return; } // If we have overrides for this binding, ask the player to find it for us (most probably in a different cache // for a different sequence). // TODO-lchabant: we could technically end up in a circular override that creates an infinite loop... const FMovieSceneEvaluationOperand Operand(SequenceID, InGuid); FMovieSceneEvaluationState* State = SharedPlaybackState->FindCapability(); IStaticBindingOverridesPlaybackCapability* StaticOverrides = SharedPlaybackState->FindCapability(); if (const FMovieSceneEvaluationOperand* OverrideOperand = StaticOverrides ? StaticOverrides->GetBindingOverride(Operand) : nullptr) { const TArrayView> OverrideBoundObjects = State->FindBoundObjects(*OverrideOperand, SharedPlaybackState); Bindings->Objects.Append(OverrideBoundObjects.GetData(), OverrideBoundObjects.Num()); } else { const bool bUseParentsAsContext = Sequence->AreParentContextsSignificant(); UObject* Context = SharedPlaybackState->GetPlaybackContext(); IMovieScenePlayer* Player = FPlayerIndexPlaybackCapability::GetPlayer(SharedPlaybackState); const FMovieScenePossessable* Possessable = Sequence->GetMovieScene()->FindPossessable(InGuid); if (Possessable) { UObject* ResolutionContext = Context; // Because these are ordered parent-first, the parent must have already been bound, if it exists if (Possessable->GetParent().IsValid()) { TArrayView> ParentBoundObjects = FindBoundObjects(Possessable->GetParent(), SharedPlaybackState); ChildBindings.FindOrAdd(Possessable->GetParent()).AddUnique(InGuid); // Refresh bindings in case of map changes Bindings = BoundObjects.Find(InGuid); for (TWeakObjectPtr<> Parent : ParentBoundObjects) { if (bUseParentsAsContext) { ResolutionContext = Parent.Get(); if (!ResolutionContext) { continue; } } TArray> FoundObjects; if (Possessable->GetSpawnableObjectBindingID().IsValid()) { for (TWeakObjectPtr<> BoundObject : Possessable->GetSpawnableObjectBindingID().ResolveBoundObjects(SequenceID, SharedPlaybackState)) { if (BoundObject.IsValid()) { FoundObjects.Add(BoundObject.Get()); } } } else { UE::UniversalObjectLocator::FResolveParams ResolveParams(ResolutionContext); if (Player) { Player->ResolveBoundObjects(ResolveParams, InGuid, SequenceID, *Sequence, FoundObjects); } else { Sequence->LocateBoundObjects(InGuid, ResolveParams, SharedPlaybackState, FoundObjects); } } Bindings = BoundObjects.Find(InGuid); for (UObject* Object : FoundObjects) { Bindings->Objects.Add(Object); } } } else { TArray> FoundObjects; if (Possessable->GetSpawnableObjectBindingID().IsValid()) { // We resolve this binding to fixed here, as we conveniently have a Player pointer already, and when being invalidated, // the binding ID passed down will be relative to the root. FMovieSceneObjectBindingID SpawnableFixedBindingID = Possessable->GetSpawnableObjectBindingID().ResolveToFixed(SequenceID, SharedPlaybackState); ReverseMappedBindings.FindOrAdd(SpawnableFixedBindingID).AddUnique(InGuid); for (TWeakObjectPtr<> BoundObject : Possessable->GetSpawnableObjectBindingID().ResolveBoundObjects(SequenceID, SharedPlaybackState)) { if (BoundObject.IsValid()) { FoundObjects.Add(BoundObject.Get()); } } } else { UE::UniversalObjectLocator::FResolveParams ResolveParams(ResolutionContext); if (Player) { Player->ResolveBoundObjects(ResolveParams, InGuid, SequenceID, *Sequence, FoundObjects); } else { Sequence->LocateBoundObjects(InGuid, ResolveParams, SharedPlaybackState, FoundObjects); } } Bindings = BoundObjects.Find(InGuid); for (UObject* Object : FoundObjects) { Bindings->Objects.Add(Object); } } } else { // Probably a FMovieSceneSpawnable then (or an phantom) bool bUseDefault = true; // Allow external overrides for spawnables const IMovieScenePlaybackClient* DynamicOverrides = SharedPlaybackState->FindCapability(); if (DynamicOverrides) { TArray> FoundObjects; bUseDefault = DynamicOverrides->RetrieveBindingOverrides(InGuid, SequenceID, FoundObjects); for (UObject* Object : FoundObjects) { Bindings->Objects.Add(Object); } } // If we have no overrides, or they want to allow the default spawnable, do that now if (bUseDefault) { const FMovieSceneSpawnRegister* SpawnRegister = SharedPlaybackState->FindCapability(); UObject* SpawnedObject = SpawnRegister ? SpawnRegister->FindSpawnedObject(InGuid, SequenceID, 0).Get() : nullptr; if (SpawnedObject) { Bindings->Objects.Add(SpawnedObject); } } } } const int32 NumBoundObjects = Bindings->Objects.Num(); // Remove duplicates from bound objects if (NumBoundObjects > 1) { Algo::Sort(Bindings->Objects, [](const TWeakObjectPtr<>& A, const TWeakObjectPtr<>& B) { return A.Get() < B.Get(); }); const int32 EndIndex = Algo::Unique(Bindings->Objects); if (EndIndex < NumBoundObjects) { FMovieSceneBinding* Binding = Sequence->GetMovieScene()->FindBinding(InGuid); FString BindingName = Binding ? Binding->GetName() : TEXT(""); UE_LOG(LogMovieScene, Warning, TEXT("Found %d duplicate object(s) while resolving binding %s (%s) in %s"), NumBoundObjects - EndIndex, *BindingName, *LexToString(InGuid), *Sequence->GetPathName()); Bindings->Objects.SetNum(EndIndex); } } if (NumBoundObjects > 0) { Bindings->bUpToDate = true; if (IObjectBindingNotifyPlaybackCapability* Notify = SharedPlaybackState->FindCapability()) { Notify->NotifyBindingUpdate(InGuid, SequenceID, Bindings->Objects); } if (auto* Children = ChildBindings.Find(InGuid)) { for (const FGuid& Child : *Children) { InvalidateIfValidInternal(Child); } } ChildBindings.Remove(InGuid); } } void FMovieSceneObjectCache::UpdateSerialNumber() { // Ok to overflow. ++SerialNumber; } TArrayView> FMovieSceneObjectCache::FindBoundObjects(const FGuid& InBindingID, IMovieScenePlayer& Player) { return FindBoundObjects(InBindingID, Player.GetSharedPlaybackState()); } void FMovieSceneObjectCache::SetSequence(UMovieSceneSequence& InSequence, FMovieSceneSequenceIDRef InSequenceID, IMovieScenePlayer& Player) { SetSequence(InSequence, InSequenceID, Player.GetSharedPlaybackState()); } FGuid FMovieSceneObjectCache::FindObjectId(UObject& InObject, IMovieScenePlayer& Player) { return FindObjectId(InObject, Player.GetSharedPlaybackState()); } FGuid FMovieSceneObjectCache::FindCachedObjectId(UObject& InObject, IMovieScenePlayer& Player) { return FindCachedObjectId(InObject, Player.GetSharedPlaybackState()); } void FMovieSceneObjectCache::Clear(IMovieScenePlayer& Player) { Clear(Player.GetSharedPlaybackState()); } void FMovieSceneObjectCache::FilterObjectBindings(UObject* PredicateObject, IMovieScenePlayer& Player, TArray* OutBindings) { FilterObjectBindings(PredicateObject, Player.GetSharedPlaybackState(), OutBindings); } UE_DEFINE_MOVIESCENE_PLAYBACK_CAPABILITY(FMovieSceneEvaluationState) void FMovieSceneEvaluationState::InvalidateExpiredObjects() { for (auto& Pair : ObjectCaches) { Pair.Value.ObjectCache.InvalidateExpiredObjects(); } } void FMovieSceneEvaluationState::Invalidate(const FGuid& InGuid, FMovieSceneSequenceIDRef SequenceID) { // We need to send the invalidation method to all of the caches, as there may be other bindings in other sequences referencing this one that is being invalidated for (auto& Pair : ObjectCaches) { Pair.Value.ObjectCache.Invalidate(InGuid, SequenceID); } } bool FMovieSceneEvaluationState::GetBindingActivation(const FGuid& InGuid, FMovieSceneSequenceIDRef InSequenceID) const { if (const FMovieSceneObjectCache* Cache = FindObjectCache(InSequenceID)) { return Cache->GetBindingActivation(InGuid); } return true; } void FMovieSceneEvaluationState::SetBindingActivation(const FGuid& InGuid, FMovieSceneSequenceIDRef InSequenceID, bool bActive) { GetObjectCache(InSequenceID).SetBindingActivation(InGuid, bActive); } void FMovieSceneEvaluationState::ClearObjectCaches(TSharedRef SharedPlaybackState) { for (auto& Pair : ObjectCaches) { Pair.Value.ObjectCache.Clear(SharedPlaybackState); } } void FMovieSceneEvaluationState::AssignSequence(FMovieSceneSequenceIDRef InSequenceID, UMovieSceneSequence& InSequence, TSharedRef SharedPlaybackState) { GetObjectCache(InSequenceID).SetSequence(InSequence, InSequenceID, SharedPlaybackState); } UMovieSceneSequence* FMovieSceneEvaluationState::FindSequence(FMovieSceneSequenceIDRef InSequenceID) const { const FVersionedObjectCache* Cache = ObjectCaches.Find(InSequenceID); return Cache ? Cache->ObjectCache.GetSequence() : nullptr; } FMovieSceneSequenceID FMovieSceneEvaluationState::FindSequenceId(const UMovieSceneSequence* InSequence) const { return FindSequenceId(const_cast(InSequence)); } FMovieSceneSequenceID FMovieSceneEvaluationState::FindSequenceId(UMovieSceneSequence* InSequence) const { for (auto& Pair : ObjectCaches) { if (Pair.Value.ObjectCache.GetSequence() == InSequence) { return Pair.Key; } } return FMovieSceneSequenceID(); } FGuid FMovieSceneEvaluationState::FindObjectId(UObject& Object, FMovieSceneSequenceIDRef InSequenceID, TSharedRef SharedPlaybackState) { FVersionedObjectCache* Cache = ObjectCaches.Find(InSequenceID); return Cache ? Cache->ObjectCache.FindObjectId(Object, SharedPlaybackState) : FGuid(); } FGuid FMovieSceneEvaluationState::FindCachedObjectId(UObject& Object, FMovieSceneSequenceIDRef InSequenceID, TSharedRef SharedPlaybackState) { FVersionedObjectCache* Cache = ObjectCaches.Find(InSequenceID); return Cache ? Cache->ObjectCache.FindCachedObjectId(Object, SharedPlaybackState) : FGuid(); } void FMovieSceneEvaluationState::FilterObjectBindings(UObject* PredicateObject, TSharedRef SharedPlaybackState, TArray* OutBindings) { check(OutBindings); for (TTuple& Cache : ObjectCaches) { Cache.Value.ObjectCache.FilterObjectBindings(PredicateObject, SharedPlaybackState, OutBindings); } } uint32 FMovieSceneEvaluationState::GetSerialNumber() { bool bUpdateSerial = false; for (TTuple& Cache : ObjectCaches) { const uint32 CurrentCacheSerial = Cache.Value.ObjectCache.GetSerialNumber(); bUpdateSerial = bUpdateSerial || (CurrentCacheSerial != Cache.Value.LastKnownSerial); Cache.Value.LastKnownSerial = CurrentCacheSerial; } if (bUpdateSerial) { // Ok to overflow. ++SerialNumber; } return SerialNumber; } void FMovieSceneEvaluationState::Initialize(TSharedRef Owner) { UMovieSceneEntitySystemLinker* Linker = Owner->GetLinker(); RegisterObjectCacheEvents(Linker, Owner->GetRootInstanceHandle(), MovieSceneSequenceID::Root); } void FMovieSceneEvaluationState::OnSubInstanceCreated(TSharedRef Owner, const UE::MovieScene::FInstanceHandle InstanceHandle) { UMovieSceneEntitySystemLinker* Linker = Owner->GetLinker(); const UE::MovieScene::FSequenceInstance& SubInstance = Linker->GetInstanceRegistry()->GetInstance(InstanceHandle); RegisterObjectCacheEvents(Linker, InstanceHandle, SubInstance.GetSequenceID()); } void FMovieSceneEvaluationState::RegisterObjectCacheEvents(UMovieSceneEntitySystemLinker* Linker, const UE::MovieScene::FInstanceHandle& InstanceHandle, const FMovieSceneSequenceID SequenceID) { FMovieSceneObjectCache& ObjectCache = GetObjectCache(SequenceID); // Make sure the cache is created... FVersionedObjectCache& VersionedObjectCache = ObjectCaches.FindChecked(SequenceID); VersionedObjectCache.OnInvalidateObjectBindingHandle = ObjectCache.OnBindingInvalidated.AddUObject( Linker, &UMovieSceneEntitySystemLinker::InvalidateObjectBinding, InstanceHandle); } void FMovieSceneEvaluationState::AssignSequence(FMovieSceneSequenceIDRef InSequenceID, UMovieSceneSequence& InSequence, IMovieScenePlayer& Player) { AssignSequence(InSequenceID, InSequence, Player.GetSharedPlaybackState()); } FGuid FMovieSceneEvaluationState::FindObjectId(UObject& Object, FMovieSceneSequenceIDRef InSequenceID, IMovieScenePlayer& Player) { return FindObjectId(Object, InSequenceID, Player.GetSharedPlaybackState()); } FGuid FMovieSceneEvaluationState::FindCachedObjectId(UObject& Object, FMovieSceneSequenceIDRef InSequenceID, IMovieScenePlayer& Player) { return FindCachedObjectId(Object, InSequenceID, Player.GetSharedPlaybackState()); } void FMovieSceneEvaluationState::FilterObjectBindings(UObject* PredicateObject, IMovieScenePlayer& Player, TArray* OutBindings) { FilterObjectBindings(PredicateObject, Player.GetSharedPlaybackState(), OutBindings); } void FMovieSceneEvaluationState::ClearObjectCaches(IMovieScenePlayer& Player) { ClearObjectCaches(Player.GetSharedPlaybackState()); }