// Copyright Epic Games, Inc. All Rights Reserved. #include "MovieSceneEventUtils.h" #include "Algo/Find.h" #include "UObject/StrongObjectPtr.h" #include "UObject/Package.h" #include "MovieScene.h" #include "MovieSceneSequence.h" #include "Sections/MovieSceneEventSectionBase.h" #include "Tracks/MovieSceneEventTrack.h" #include "MovieSceneEventBlueprintExtension.h" #include "ScopedTransaction.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "GameFramework/Actor.h" #include "Engine/Blueprint.h" #include "KismetCompiler.h" #include "K2Node_CustomEvent.h" #include "K2Node_VariableGet.h" #include "K2Node_CallFunction.h" #include "K2Node_FunctionEntry.h" #include "K2Node_EditablePinBase.h" #include "K2Node_DynamicCast.h" #include "Kismet/KismetSystemLibrary.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/Kismet2NameValidators.h" #define LOCTEXT_NAMESPACE "MovieSceneEventUtils" FMovieSceneEventEndpointParameters FMovieSceneEventEndpointParameters::Generate(UMovieSceneEventTrack* Track) { check(Track); UMovieScene* MovieScene = Track->GetTypedOuter(); FGuid ObjectBindingID; if (MovieScene->FindTrackBinding(*Track, ObjectBindingID)) { return Generate(MovieScene, ObjectBindingID); } FMovieSceneEventEndpointParameters Params; Params.SanitizedObjectName = TEXT("None"); Params.SanitizedEventName = TEXT("SequenceEvent"); return Params; } FMovieSceneEventEndpointParameters FMovieSceneEventEndpointParameters::Generate(UMovieScene* MovieScene, const FGuid& ObjectBindingID) { check(MovieScene); FMovieSceneEventEndpointParameters Params; if (ObjectBindingID.IsValid()) { Params.SanitizedObjectName = MovieScene->GetObjectDisplayName(ObjectBindingID).ToString(); for (TCHAR& Char : Params.SanitizedObjectName) { if (FCString::Strchr(INVALID_NAME_CHARACTERS, Char) != nullptr) { Char = '_'; } } Params.SanitizedEventName = Params.SanitizedObjectName + TEXT("_Event"); if (FMovieScenePossessable* Possessable = MovieScene->FindPossessable(ObjectBindingID)) { Params.BoundObjectPinClass = const_cast(Possessable->GetPossessedObjectClass()); } else if(FMovieSceneSpawnable* Spawnable = MovieScene->FindSpawnable(ObjectBindingID)) { Params.BoundObjectPinClass = Spawnable->GetObjectTemplate()->GetClass(); } } else { Params.BoundObjectPinClass = nullptr; Params.SanitizedObjectName = TEXT("None"); Params.SanitizedEventName = TEXT("SequenceEvent"); } return Params; } UK2Node_CustomEvent* FMovieSceneEventUtils::BindNewUserFacingEvent(FMovieSceneEvent* EntryPoint, UMovieSceneEventSectionBase* EventSection, UBlueprint* Blueprint) { check(EntryPoint && EventSection && Blueprint); UMovieSceneEventTrack* Track = EventSection->GetTypedOuter(); // Modify necessary objects EventSection->Modify(); Blueprint->Modify(); // Ensure the section is bound to the blueprint function generation event FMovieSceneEventUtils::BindEventSectionToBlueprint(EventSection, Blueprint); // Create the new user-facing event node FMovieSceneEventEndpointParameters Params = FMovieSceneEventEndpointParameters::Generate(Track); UK2Node_CustomEvent* NewEventNode = CreateUserFacingEvent(Blueprint, Params); if (NewEventNode) { // Bind the node to the event entry point UEdGraphPin* BoundObjectPin = FMovieSceneEventUtils::FindBoundObjectPin(NewEventNode, Params.BoundObjectPinClass); FMovieSceneEventUtils::SetEndpoint(EntryPoint, EventSection, NewEventNode, BoundObjectPin); } return NewEventNode; } UK2Node_CustomEvent* FMovieSceneEventUtils::CreateUserFacingEvent(UBlueprint* Blueprint, const FMovieSceneEventEndpointParameters& Parameters) { static const TCHAR* const EventGraphName = TEXT("Sequencer Events"); UEdGraph* SequenceEventGraph = FindObject(Blueprint, EventGraphName); if (!SequenceEventGraph) { SequenceEventGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, EventGraphName, UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass()); SequenceEventGraph->GraphGuid = FGuid::NewGuid(); Blueprint->UbergraphPages.Add(SequenceEventGraph); } // Create a custom event node to replace the original event node imported from text UK2Node_CustomEvent* CustomEventNode = NewObject(SequenceEventGraph); check(Parameters.SanitizedEventName.Len() != 0); CustomEventNode->CustomFunctionName = FBlueprintEditorUtils::FindUniqueKismetName(Blueprint, Parameters.SanitizedEventName); // Ensure that it is editable CustomEventNode->bIsEditable = true; CustomEventNode->CreateNewGuid(); CustomEventNode->PostPlacedNewNode(); CustomEventNode->AllocateDefaultPins(); const FVector2D NewPosition = SequenceEventGraph->GetGoodPlaceForNewNode(); CustomEventNode->NodePosX = NewPosition.X; CustomEventNode->NodePosY = NewPosition.Y; if (Parameters.BoundObjectPinClass != nullptr) { FEdGraphPinType PinType; PinType.PinCategory = UEdGraphSchema_K2::PC_Object; PinType.PinSubCategoryObject = Parameters.BoundObjectPinClass; CustomEventNode->CreateUserDefinedPin(*Parameters.SanitizedObjectName, PinType, EGPD_Output, true); } SequenceEventGraph->AddNode(CustomEventNode, false, false); FBlueprintEditorUtils::ValidateBlueprintChildVariables(Blueprint, CustomEventNode->CustomFunctionName); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); return CustomEventNode; } UK2Node* FMovieSceneEventUtils::FindEndpoint(FMovieSceneEvent* EntryPoint, UMovieSceneEventSectionBase* EventSection, UBlueprint* OwnerBlueprint) { check(OwnerBlueprint); check(EntryPoint); if (EntryPoint->WeakEndpoint.IsStale()) { return nullptr; } if (UK2Node* Node = Cast(EntryPoint->WeakEndpoint.Get())) { return Node; } if (!EntryPoint->GraphGuid_DEPRECATED.IsValid()) { return nullptr; } if (EntryPoint->NodeGuid_DEPRECATED.IsValid()) { for (UEdGraph* Graph : OwnerBlueprint->UbergraphPages) { if (Graph->GraphGuid == EntryPoint->GraphGuid_DEPRECATED) { for (UEdGraphNode* Node : Graph->Nodes) { if (Node->NodeGuid == EntryPoint->NodeGuid_DEPRECATED) { UK2Node_CustomEvent* CustomEvent = Cast(Node); if (ensureMsgf(CustomEvent, TEXT("Encountered an event entry point node that is bound to something other than a custom event"))) { CustomEvent->OnUserDefinedPinRenamed().AddUObject(EventSection, &UMovieSceneEventSectionBase::OnUserDefinedPinRenamed); EntryPoint->WeakEndpoint = CustomEvent; return CustomEvent; } } } } } } // If the node guid is invalid, this must be a function graph on the BP else for (UEdGraph* Graph : OwnerBlueprint->FunctionGraphs) { if (Graph->GraphGuid == EntryPoint->GraphGuid_DEPRECATED) { for (UEdGraphNode* Node : Graph->Nodes) { if (UK2Node_FunctionEntry* FunctionEntry = Cast(Node)) { FunctionEntry->OnUserDefinedPinRenamed().AddUObject(EventSection, &UMovieSceneEventSectionBase::OnUserDefinedPinRenamed); EntryPoint->WeakEndpoint = FunctionEntry; return FunctionEntry; } } } } return nullptr; } UEdGraphPin* FMovieSceneEventUtils::FindBoundObjectPin(UK2Node* InEndpoint, UClass* BoundObjectPinClass) { if (!BoundObjectPinClass) { return nullptr; } for (UEdGraphPin* Pin : InEndpoint->Pins) { if (Pin->Direction == EGPD_Output && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object && Pin->PinType.PinSubCategoryObject == BoundObjectPinClass) { return Pin; } } return nullptr; } void FMovieSceneEventUtils::SetEndpoint(FMovieSceneEvent* EntryPoint, UMovieSceneEventSectionBase* EventSection, UK2Node* InNewEndpoint, UEdGraphPin* BoundObjectPin) { check(EntryPoint); UK2Node* ExistingEndpoint = CastChecked(EntryPoint->WeakEndpoint.Get(), ECastCheckedType::NullAllowed); if (ExistingEndpoint) { ExistingEndpoint->OnUserDefinedPinRenamed().RemoveAll(EventSection); } if (InNewEndpoint) { const bool bIsFunction = InNewEndpoint->IsA(); const bool bIsCustomEvent = InNewEndpoint->IsA(); checkf(bIsFunction || bIsCustomEvent, TEXT("Only functions and custom events are supported as event endpoints")); if (BoundObjectPin) { EntryPoint->BoundObjectPinName = BoundObjectPin->GetFName(); } else { EntryPoint->BoundObjectPinName = NAME_None; } InNewEndpoint->OnUserDefinedPinRenamed().AddUObject(EventSection, &UMovieSceneEventSectionBase::OnUserDefinedPinRenamed); EntryPoint->WeakEndpoint = InNewEndpoint; } else { EntryPoint->WeakEndpoint = nullptr; EntryPoint->BoundObjectPinName = NAME_None; } } UK2Node_FunctionEntry* FMovieSceneEventUtils::GenerateEntryPoint(UMovieSceneEventSectionBase* EventSection, FMovieSceneEvent* EntrypointDefinition, FKismetCompilerContext* Compiler, UEdGraphNode* Endpoint) { check(EventSection && Compiler && Endpoint); UBlueprint* Blueprint = Compiler->Blueprint; UFunction* EndpointFunction = nullptr; // Find the function that we need to call on the skeleton class { if (UK2Node_Event* Event = Cast(Endpoint)) { EndpointFunction = Blueprint->SkeletonGeneratedClass ? Blueprint->SkeletonGeneratedClass->FindFunctionByName(Event->GetFunctionName()) : nullptr; } else if (UK2Node_FunctionEntry* EndpointEntry = Cast(Endpoint)) { EndpointFunction = EndpointEntry->FindSignatureFunction(); } else { Compiler->MessageLog.Error(*LOCTEXT("InvalidEndpoint_Error", "Sequencer event is bound to an invalid endpoint node @@").ToString(), Endpoint); } } if (EndpointFunction == nullptr) { return nullptr; } // @todo: use a more destriptive name for event entry points? static FString DefaultEventEntryName = TEXT("SequenceEvent__ENTRYPOINT"); UEdGraph* EntryPointGraph = Compiler->SpawnIntermediateFunctionGraph(DefaultEventEntryName + Blueprint->GetName()); check(EntryPointGraph->Nodes.Num() == 1); const UEdGraphSchema* Schema = EntryPointGraph->GetSchema(); UK2Node_FunctionEntry* FunctionEntry = CastChecked(EntryPointGraph->Nodes[0]); // ------------------------------------------------------------------------------------- // Locate and initialize the function entry node { int32 ExtraFunctionFlags = ( FUNC_BlueprintEvent | FUNC_Public ); FunctionEntry->AddExtraFlags(ExtraFunctionFlags); FunctionEntry->bIsEditable = false; FunctionEntry->MetaData.Category = LOCTEXT("DefaultCategory", "Sequencer Event Endpoints"); FunctionEntry->MetaData.bCallInEditor = EndpointFunction->GetBoolMetaData(FBlueprintMetadata::MD_CallInEditor); } // ------------------------------------------------------------------------------------- // Create a function call node to invoke the endpoint function itself UK2Node_CallFunction* CallFunctionNode = NewObject(EntryPointGraph); { CallFunctionNode->CreateNewGuid(); CallFunctionNode->PostPlacedNewNode(); CallFunctionNode->FunctionReference.SetSelfMember(EndpointFunction->GetFName()); CallFunctionNode->ReconstructNode(); CallFunctionNode->NodePosX = FunctionEntry->NodePosX + 400.f; CallFunctionNode->NodePosY = FunctionEntry->NodePosY - 16.f; EntryPointGraph->AddNode(CallFunctionNode, false, false); } // ------------------------------------------------------------------------------------- // Create a pin for the bound object if possible if (EntrypointDefinition->BoundObjectPinName != NAME_None) { UEdGraphPin* BoundObjectPin = CallFunctionNode->FindPin(EntrypointDefinition->BoundObjectPinName, EGPD_Input); if (BoundObjectPin) { UEdGraphPin* BoundObjectPinInput = FunctionEntry->CreateUserDefinedPin(BoundObjectPin->PinName, BoundObjectPin->PinType, EGPD_Output, true); if (BoundObjectPinInput) { BoundObjectPinInput->MakeLinkTo(BoundObjectPin); } } } // ------------------------------------------------------------------------------------- // Wire up the function entry 'then' pin to the call function 'execute' pin { // Wire up the exec nodes UEdGraphPin* ThenPin = FunctionEntry->FindPin(UEdGraphSchema_K2::PN_Then, EGPD_Output); UEdGraphPin* ExecPin = CallFunctionNode->FindPin(UEdGraphSchema_K2::PN_Execute, EGPD_Input); if (ensure(ThenPin && ExecPin)) { ThenPin->MakeLinkTo(ExecPin); } } // ------------------------------------------------------------------------------------- // Set pin defaults for each of the relevant pins on the call function node according to the payload { TArray> ValidPinNames; // Gather all input pins for the function call for (UEdGraphPin* Pin : CallFunctionNode->Pins) { // Only consider input pins that are data, and not already connected if (Pin->Direction != EGPD_Input || Pin->PinName == UEdGraphSchema_K2::PN_Execute || Pin->PinName == UEdGraphSchema_K2::PN_Self || Pin->LinkedTo.Num() != 0) { continue; } const FMovieSceneEventPayloadVariable* PayloadVariable = EntrypointDefinition->PayloadVariables.Find(Pin->PinName); if (!PayloadVariable) { continue; } UClass* PinObjectType = Cast(Pin->PinType.PinSubCategoryObject.Get()); const bool bIsRawActorPin = Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object && PinObjectType && PinObjectType->IsChildOf(AActor::StaticClass()); if (bIsRawActorPin) { // Raw actor properties are represented as soft object ptrs under the hood so that we can still serialize them // To make this work, we have to make the following graph: // MakeSoftObjectPath('') -> Conv_SoftObjPathToSoftObjRef() -> Conv_SoftObjectReferenceToObject() -> Cast() UK2Node_CallFunction* MakeSoftObjectPathNode = Compiler->SpawnIntermediateNode(CallFunctionNode, EntryPointGraph); MakeSoftObjectPathNode->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, MakeSoftObjectPath), UKismetSystemLibrary::StaticClass()); MakeSoftObjectPathNode->AllocateDefaultPins(); UK2Node_CallFunction* ConvertToSoftObjectRef = Compiler->SpawnIntermediateNode(CallFunctionNode, EntryPointGraph); ConvertToSoftObjectRef->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Conv_SoftObjPathToSoftObjRef), UKismetSystemLibrary::StaticClass()); ConvertToSoftObjectRef->AllocateDefaultPins(); UK2Node_CallFunction* ConvertToObjectFunc = Compiler->SpawnIntermediateNode(CallFunctionNode, EntryPointGraph); ConvertToObjectFunc->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Conv_SoftObjectReferenceToObject), UKismetSystemLibrary::StaticClass()); ConvertToObjectFunc->AllocateDefaultPins(); UK2Node_DynamicCast* CastNode = Compiler->SpawnIntermediateNode(CallFunctionNode, EntryPointGraph); CastNode->SetPurity(true); CastNode->TargetType = PinObjectType; CastNode->AllocateDefaultPins(); UEdGraphPin* PathInput = MakeSoftObjectPathNode->FindPin(FName("PathString")); UEdGraphPin* PathOutput = MakeSoftObjectPathNode->GetReturnValuePin(); UEdGraphPin* SoftRefInput = ConvertToSoftObjectRef->FindPin(FName("SoftObjectPath")); UEdGraphPin* SoftRefOutput = ConvertToSoftObjectRef->GetReturnValuePin(); UEdGraphPin* ConvertInput = ConvertToObjectFunc->FindPin(FName("SoftObject")); UEdGraphPin* ConvertOutput = ConvertToObjectFunc->GetReturnValuePin(); UEdGraphPin* CastInput = CastNode->GetCastSourcePin(); UEdGraphPin* CastOutput = CastNode->GetCastResultPin(); if (!PathInput || !PathOutput || !SoftRefInput || !SoftRefOutput || !ConvertInput || !ConvertOutput || !CastInput || !CastOutput) { Compiler->MessageLog.Error(*LOCTEXT("ActorFacadeError", "GenerateEntryPoint: Failed to generate soft-ptr facade for AActor payload property property @@. @@").ToString(), *Pin->PinName.ToString(), Endpoint); continue; } // Set the default value for the path string const bool bMarkAsModified = false; Schema->TrySetDefaultValue(*PathInput, PayloadVariable->Value, bMarkAsModified); bool bSuccess = true; bSuccess &= Schema->TryCreateConnection(PathOutput, SoftRefInput); bSuccess &= Schema->TryCreateConnection(SoftRefOutput, ConvertInput); bSuccess &= Schema->TryCreateConnection(ConvertOutput, CastInput); bSuccess &= Schema->TryCreateConnection(CastOutput, Pin); if (!bSuccess) { Compiler->MessageLog.Error(*LOCTEXT("ActorFacadeConnectionError", "GenerateEntryPoint: Failed to connect nodes for soft-ptr facade in AActor payload property @@. @@").ToString(), *Pin->PinName.ToString(), Endpoint); } ValidPinNames.Add(Pin->PinName); } else { bool bMarkAsModified = false; Schema->TrySetDefaultValue(*Pin, PayloadVariable->Value, bMarkAsModified); ValidPinNames.Add(Pin->PinName); } } // Remove any invalid payload variable names if (ValidPinNames.Num() != EntrypointDefinition->PayloadVariables.Num()) { for (auto It = EntrypointDefinition->PayloadVariables.CreateIterator(); It; ++It) { if (!ValidPinNames.Contains(It->Key)) { Compiler->MessageLog.Note(*FText::Format(LOCTEXT("PayloadParameterRemoved", "Stale Sequencer event payload parameter %s has been removed."), FText::FromName(It->Key)).ToString()); It.RemoveCurrent(); } } } } return FunctionEntry; } void FMovieSceneEventUtils::BindEventSectionToBlueprint(UMovieSceneEventSectionBase* EventSection, UBlueprint* DirectorBP) { check(EventSection && DirectorBP); for (TObjectPtr Extension : DirectorBP->GetExtensions()) { UMovieSceneEventBlueprintExtension* EventExtension = Cast(Extension); if (EventExtension) { EventExtension->Add(EventSection); return; } } UMovieSceneEventBlueprintExtension* EventExtension = NewObject(DirectorBP); EventExtension->Add(EventSection); DirectorBP->AddExtension(EventExtension); } void FMovieSceneEventUtils::RemoveEndpointsForEventSection(UMovieSceneEventSectionBase* EventSection, UBlueprint* DirectorBP) { check(EventSection && DirectorBP); static const TCHAR* const EventGraphName = TEXT("Sequencer Events"); UEdGraph* SequenceEventGraph = FindObject(DirectorBP, EventGraphName); if (SequenceEventGraph) { for (FMovieSceneEvent& EntryPoint : EventSection->GetAllEntryPoints()) { UEdGraphNode* Endpoint = FMovieSceneEventUtils::FindEndpoint(&EntryPoint, EventSection, DirectorBP); if (Endpoint) { UE_LOG(LogMovieScene, Display, TEXT("Removing event: %s from: %s"), *GetNameSafe(Endpoint), *GetNameSafe(DirectorBP)); SequenceEventGraph->RemoveNode(Endpoint); } } } } void FMovieSceneEventUtils::RemoveUnusedCustomEvents(const TArray>& EventSections, UBlueprint* DirectorBP) { check(DirectorBP); static const TCHAR* const EventGraphName = TEXT("Sequencer Events"); UEdGraph* SequenceEventGraph = FindObject(DirectorBP, EventGraphName); if (SequenceEventGraph) { TArray ExistingNodes; SequenceEventGraph->GetNodesOfClass(ExistingNodes); TSet Endpoints; for (TWeakObjectPtr WeakEventSection : EventSections) { UMovieSceneEventSectionBase* EventSection = WeakEventSection.Get(); if (!EventSection) { continue; } for (FMovieSceneEvent& EntryPoint : EventSection->GetAllEntryPoints()) { if (UEdGraphNode* Endpoint = FMovieSceneEventUtils::FindEndpoint(&EntryPoint, EventSection, DirectorBP)) { Endpoints.Add(Endpoint); } } } FScopedTransaction RemoveUnusedCustomEvents(LOCTEXT("RemoveUnusedCustomEvents", "Remove Unused Custom Events")); for (UK2Node_CustomEvent* ExistingNode : ExistingNodes) { const bool bHasEntryPoint = Endpoints.Contains(ExistingNode); if (!bHasEntryPoint) { FNotificationInfo Info(FText::Format(LOCTEXT("RemoveUnusedCustomEventNotify", "Remove unused custom event {0} from {1}"), FText::FromString(GetNameSafe(ExistingNode)), FText::FromString(GetNameSafe(DirectorBP)))); Info.ExpireDuration = 3.f; FSlateNotificationManager::Get().AddNotification(Info); UE_LOG(LogMovieScene, Display, TEXT("Remove unused custom event %s from %s"), *GetNameSafe(ExistingNode), *GetNameSafe(DirectorBP)); DirectorBP->Modify(); SequenceEventGraph->RemoveNode(ExistingNode); } } } } #undef LOCTEXT_NAMESPACE