Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class FunnyGirlTarget : TargetRules
{
public FunnyGirlTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V6;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
ExtraModuleNames.Add("FunnyGirl");
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class FunnyGirl : ModuleRules
{
public FunnyGirl(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "FunnyGirl.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, FunnyGirl, "FunnyGirl" );
+6
View File
@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
@@ -0,0 +1,3 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Identity/PersonalityTraitDataAsset.h"
@@ -0,0 +1,408 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Input/TouchInputComponent.h"
#include "Engine/World.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "InputCoreTypes.h"
DEFINE_LOG_CATEGORY_STATIC(LogFunnyGirlTouchInput, Log, All);
UTouchInputComponent::UTouchInputComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.TickInterval = 0.0f;
}
void UTouchInputComponent::BeginPlay()
{
Super::BeginPlay();
if (!ResolvePlayerController() && bLogWarnings)
{
UE_LOG(LogFunnyGirlTouchInput, Warning, TEXT("TouchInputComponent on %s could not find a PlayerController. Put it on a PlayerController, PlayerState-owned actor, or local player pawn."), *GetNameSafe(GetOwner()));
}
}
void UTouchInputComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bEnabled)
{
ResetTouchState();
return;
}
PollTouches(DeltaTime);
}
void UTouchInputComponent::ResetTouchState()
{
for (FTrackedTouch& Touch : Touches)
{
Touch = FTrackedTouch();
}
ResetGestureTracking();
}
APlayerController* UTouchInputComponent::ResolvePlayerController() const
{
if (APlayerController* PlayerController = Cast<APlayerController>(GetOwner()))
{
return PlayerController;
}
if (const APawn* Pawn = Cast<APawn>(GetOwner()))
{
return Cast<APlayerController>(Pawn->GetController());
}
if (const UWorld* World = GetWorld())
{
return World->GetFirstPlayerController();
}
return nullptr;
}
void UTouchInputComponent::PollTouches(float DeltaTime)
{
APlayerController* PlayerController = ResolvePlayerController();
if (!PlayerController)
{
ResetTouchState();
return;
}
const double CurrentTime = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0;
for (int32 TouchIndex = 0; TouchIndex < MaxTouchCount; ++TouchIndex)
{
float LocationX = 0.0f;
float LocationY = 0.0f;
bool bPressed = false;
PlayerController->GetInputTouchState(static_cast<ETouchIndex::Type>(TouchIndex), LocationX, LocationY, bPressed);
FTrackedTouch& Touch = Touches[TouchIndex];
const FVector2D Location(LocationX, LocationY);
if (bPressed)
{
if (!Touch.bPressed)
{
Touch.StartLocation = Location;
Touch.StartTime = CurrentTime;
}
Touch.Location = Location;
}
Touch.bPressed = bPressed;
}
const int32 PressedCount = ReadPressedTouches();
if (PressedCount <= 0)
{
if (bGestureActive)
{
EndGesture(CurrentTime);
}
CurrentFingerCount = 0;
return;
}
const FVector2D Centroid = GetCentroid();
if (!bGestureActive)
{
BeginGesture(PressedCount, Centroid, CurrentTime);
}
else if (PressedCount != CurrentFingerCount)
{
HandleFingerCountChanged(PressedCount, Centroid, CurrentTime);
}
HandleActiveGesture(PressedCount, Centroid, DeltaTime, CurrentTime);
PreviousCentroid = Centroid;
CurrentFingerCount = PressedCount;
}
int32 UTouchInputComponent::ReadPressedTouches()
{
int32 PressedCount = 0;
for (const FTrackedTouch& Touch : Touches)
{
if (Touch.bPressed)
{
++PressedCount;
}
}
return PressedCount;
}
void UTouchInputComponent::BeginGesture(int32 FingerCount, const FVector2D& Centroid, double CurrentTime)
{
bGestureActive = true;
GestureState = ETouchGestureState::None;
bMoveStarted = false;
CurrentFingerCount = FingerCount;
GestureFingerCount = FingerCount;
GestureStartTime = CurrentTime;
GestureStartCentroid = Centroid;
PreviousCentroid = Centroid;
InitialSpan = GetAverageSpan(Centroid);
PreviousSpan = InitialSpan;
MaxDistanceFromStart = 0.0f;
}
void UTouchInputComponent::HandleFingerCountChanged(int32 FingerCount, const FVector2D& Centroid, double CurrentTime)
{
if (GestureState != ETouchGestureState::None)
{
return;
}
CurrentFingerCount = FingerCount;
GestureFingerCount = FMath::Max(GestureFingerCount, FingerCount);
GestureStartTime = CurrentTime;
GestureStartCentroid = Centroid;
PreviousCentroid = Centroid;
InitialSpan = GetAverageSpan(Centroid);
PreviousSpan = InitialSpan;
MaxDistanceFromStart = 0.0f;
bMoveStarted = false;
}
void UTouchInputComponent::HandleActiveGesture(int32 FingerCount, const FVector2D& Centroid, float DeltaTime, double CurrentTime)
{
const FVector2D Delta = Centroid - PreviousCentroid;
const FVector2D TotalDelta = Centroid - GestureStartCentroid;
MaxDistanceFromStart = FMath::Max(MaxDistanceFromStart, FVector2D::Distance(Centroid, GestureStartCentroid));
const float Duration = static_cast<float>(CurrentTime - GestureStartTime);
const float Distance = TotalDelta.Size();
const bool bCanBeTap = Duration <= TapMaxDuration && MaxDistanceFromStart <= TapMaxMovement;
if ((FingerCount == 2 || FingerCount == 4) && InitialSpan > KINDA_SMALL_NUMBER)
{
const float CurrentSpan = GetAverageSpan(Centroid);
const float PreviousScale = PreviousSpan > KINDA_SMALL_NUMBER ? CurrentSpan / PreviousSpan : 1.0f;
const float ScaleDelta = PreviousScale - 1.0f;
if (GestureState == ETouchGestureState::ZoomOrPinch)
{
BroadcastPinchOrZoom(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime), ScaleDelta);
}
else if (GestureState == ETouchGestureState::None && FMath::Abs(ScaleDelta) >= PinchMinScaleDelta)
{
GestureState = ETouchGestureState::ZoomOrPinch;
BroadcastPinchOrZoom(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime), ScaleDelta);
}
PreviousSpan = CurrentSpan;
}
if (GestureState != ETouchGestureState::None)
{
return;
}
if (GestureState == ETouchGestureState::None && FingerCount >= 1 && FingerCount <= 3 && Duration >= HoldDuration && MaxDistanceFromStart <= HoldMaxMovement)
{
GestureState = ETouchGestureState::Hold;
BroadcastHold(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime));
return;
}
const bool bShouldStartMove = !bCanBeTap && Duration >= MoveStartDelay;
if (FingerCount == 1 && Delta.Size() >= MoveMinDistancePerFrame && (bMoveStarted || bShouldStartMove))
{
bMoveStarted = true;
BroadcastMove(BuildGestureData(FingerCount, Centroid, Delta, CurrentTime));
}
}
void UTouchInputComponent::EndGesture(double CurrentTime)
{
const FVector2D EndCentroid = PreviousCentroid;
const FVector2D TotalDelta = EndCentroid - GestureStartCentroid;
const float Distance = TotalDelta.Size();
const float Duration = static_cast<float>(CurrentTime - GestureStartTime);
const int32 FinalFingerCount = FMath::Clamp(GestureFingerCount, 1, 5);
const FTouchGestureData Gesture = BuildGestureData(FinalFingerCount, EndCentroid, FVector2D::ZeroVector, CurrentTime);
if (GestureState == ETouchGestureState::None && !bMoveStarted && FinalFingerCount >= 1 && FinalFingerCount <= 3)
{
if (Duration <= TapMaxDuration && Distance <= TapMaxMovement && MaxDistanceFromStart <= TapMaxMovement)
{
BroadcastTap(Gesture);
}
else if (FinalFingerCount == 1 && IsStableSwipe(TotalDelta, Duration))
{
GestureState = ETouchGestureState::Swipe;
BroadcastSwipe(Gesture);
}
}
ResetGestureTracking();
}
void UTouchInputComponent::ResetGestureTracking()
{
bGestureActive = false;
GestureState = ETouchGestureState::None;
bMoveStarted = false;
CurrentFingerCount = 0;
GestureFingerCount = 0;
GestureStartTime = 0.0;
GestureStartCentroid = FVector2D::ZeroVector;
PreviousCentroid = FVector2D::ZeroVector;
InitialSpan = 0.0f;
PreviousSpan = 0.0f;
MaxDistanceFromStart = 0.0f;
}
FVector2D UTouchInputComponent::GetCentroid() const
{
FVector2D Sum = FVector2D::ZeroVector;
int32 Count = 0;
for (const FTrackedTouch& Touch : Touches)
{
if (Touch.bPressed)
{
Sum += Touch.Location;
++Count;
}
}
return Count > 0 ? Sum / static_cast<float>(Count) : FVector2D::ZeroVector;
}
float UTouchInputComponent::GetAverageSpan(const FVector2D& Centroid) const
{
float Sum = 0.0f;
int32 Count = 0;
for (const FTrackedTouch& Touch : Touches)
{
if (Touch.bPressed)
{
Sum += FVector2D::Distance(Touch.Location, Centroid);
++Count;
}
}
return Count > 1 ? Sum / static_cast<float>(Count) : 0.0f;
}
EFunnyGirlTouchSwipeDirection UTouchInputComponent::GetSwipeDirection(const FVector2D& TotalDelta) const
{
const float AbsX = FMath::Abs(TotalDelta.X);
const float AbsY = FMath::Abs(TotalDelta.Y);
if (AbsX >= AbsY * SwipeDirectionBias)
{
return TotalDelta.X >= 0.0f ? EFunnyGirlTouchSwipeDirection::Right : EFunnyGirlTouchSwipeDirection::Left;
}
if (AbsY >= AbsX * SwipeDirectionBias)
{
return TotalDelta.Y >= 0.0f ? EFunnyGirlTouchSwipeDirection::Down : EFunnyGirlTouchSwipeDirection::Up;
}
return EFunnyGirlTouchSwipeDirection::None;
}
bool UTouchInputComponent::IsStableSwipe(const FVector2D& TotalDelta, float Duration) const
{
return Duration > KINDA_SMALL_NUMBER
&& Duration <= SwipeMaxDuration
&& TotalDelta.Size() >= SwipeMinDistance
&& GetSwipeDirection(TotalDelta) != EFunnyGirlTouchSwipeDirection::None;
}
FTouchGestureData UTouchInputComponent::BuildGestureData(int32 FingerCount, const FVector2D& Location, const FVector2D& Delta, double CurrentTime) const
{
FTouchGestureData Gesture;
Gesture.FingerCount = FingerCount;
Gesture.StartLocation = GestureStartCentroid;
Gesture.Location = Location;
Gesture.Delta = Delta;
Gesture.TotalDelta = Location - GestureStartCentroid;
Gesture.SwipeDirection = GetSwipeDirection(Gesture.TotalDelta);
Gesture.Distance = Gesture.TotalDelta.Size();
Gesture.Duration = static_cast<float>(CurrentTime - GestureStartTime);
return Gesture;
}
void UTouchInputComponent::BroadcastTap(const FTouchGestureData& Gesture)
{
switch (Gesture.FingerCount)
{
case 1:
OnOneFingerTap.Broadcast(Gesture);
break;
case 2:
OnTwoFingerTap.Broadcast(Gesture);
break;
default:
break;
}
}
void UTouchInputComponent::BroadcastSwipe(const FTouchGestureData& Gesture)
{
switch (Gesture.FingerCount)
{
case 1:
OnOneFingerSwipe.Broadcast(Gesture);
break;
default:
break;
}
}
void UTouchInputComponent::BroadcastMove(const FTouchGestureData& Gesture)
{
switch (Gesture.FingerCount)
{
case 1:
OnOneFingerMove.Broadcast(Gesture);
break;
default:
break;
}
}
void UTouchInputComponent::BroadcastHold(const FTouchGestureData& Gesture)
{
switch (Gesture.FingerCount)
{
case 1:
OnOneFingerHold.Broadcast(Gesture);
break;
case 2:
OnTwoFingerHold.Broadcast(Gesture);
break;
default:
break;
}
}
void UTouchInputComponent::BroadcastPinchOrZoom(const FTouchGestureData& Gesture, float ZoomDelta)
{
switch (Gesture.FingerCount)
{
case 2:
OnZoom.Broadcast(ZoomDelta);
break;
case 4:
OnFourFingerPinch.Broadcast(Gesture);
break;
default:
break;
}
}
@@ -0,0 +1,355 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/BrainComponentBase.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Interfaces/IHttpRequest.h"
#include "Kismet/GameplayStatics.h"
#include "LLM/BrainToolBase.h"
#include "LLM/LLMProviderDataAsset.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "HAL/FileManager.h"
#include "World/WorldContextActor.h"
UBrainComponentBase::UBrainComponentBase()
: bAutoFindWorldContextActor(true)
, bIsThinking(false)
{
PrimaryComponentTick.bCanEverTick = false;
}
void UBrainComponentBase::BeginPlay()
{
Super::BeginPlay();
AutoFindWorldContextActor();
}
void UBrainComponentBase::CancelRequest()
{
if (ActiveRequest.IsValid())
{
ActiveRequest->CancelRequest();
ActiveRequest.Reset();
}
SetIsThinking(false);
}
bool UBrainComponentBase::IsThinking() const
{
return bIsThinking;
}
FString UBrainComponentBase::GetChatIdentityId() const
{
BroadcastBrainError(FString::Printf(TEXT("%s does not provide a chat identity id."), *GetBrainDebugName()));
return FString();
}
void UBrainComponentBase::BroadcastBrainError(const FString& ErrorMessage) const
{
UE_LOG(LogTemp, Warning, TEXT("%s error: %s"), *GetBrainDebugName(), *ErrorMessage);
}
FString UBrainComponentBase::GetBrainDebugName() const
{
return GetNameSafe(GetClass());
}
void UBrainComponentBase::SetIsThinking(bool bNewIsThinking)
{
bIsThinking = bNewIsThinking;
}
void UBrainComponentBase::AutoFindWorldContextActor()
{
if (WorldContextActor || !bAutoFindWorldContextActor)
{
return;
}
WorldContextActor = Cast<AWorldContextActor>(UGameplayStatics::GetActorOfClass(this, AWorldContextActor::StaticClass()));
}
bool UBrainComponentBase::ValidateProvider(const ULLMProviderDataAsset* EffectiveProvider) const
{
if (!EffectiveProvider)
{
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider is not set."), *GetBrainDebugName()));
return false;
}
if (EffectiveProvider->ApiBaseUrl.IsEmpty() || EffectiveProvider->ChatCompletionsPath.IsEmpty())
{
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider endpoint is not configured."), *GetBrainDebugName()));
return false;
}
if (EffectiveProvider->Model.IsEmpty())
{
BroadcastBrainError(FString::Printf(TEXT("%s LLM provider model is not configured."), *GetBrainDebugName()));
return false;
}
return true;
}
bool UBrainComponentBase::ConfigureRequestWithBody(
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request,
const ULLMProviderDataAsset* EffectiveProvider,
const FString& RequestBody,
const FString& AcceptHeader) const
{
if (!ValidateProvider(EffectiveProvider))
{
return false;
}
Request->SetURL(EffectiveProvider->GetChatCompletionsUrl());
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
Request->SetHeader(TEXT("Accept"), AcceptHeader);
if (!EffectiveProvider->ApiKey.IsEmpty())
{
Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *EffectiveProvider->ApiKey));
}
for (const TPair<FString, FString>& Header : EffectiveProvider->ExtraHeaders)
{
Request->SetHeader(Header.Key, Header.Value);
}
Request->SetTimeout(EffectiveProvider->TimeoutSeconds);
Request->SetContentAsString(RequestBody);
return true;
}
void UBrainComponentBase::AddSystemMessage(TArray<TSharedPtr<FJsonValue>>& Messages, const FString& Content) const
{
const FString TrimmedContent = Content.TrimStartAndEnd();
if (TrimmedContent.IsEmpty())
{
return;
}
Messages.Add(MakeShared<FJsonValueObject>(MakeMessageObject(TEXT("system"), TrimmedContent)));
}
TSharedRef<FJsonObject> UBrainComponentBase::MakeMessageObject(const FString& Role, const FString& Content) const
{
TSharedRef<FJsonObject> MessageObject = MakeShared<FJsonObject>();
MessageObject->SetStringField(TEXT("role"), Role);
MessageObject->SetStringField(TEXT("content"), Content);
return MessageObject;
}
FString UBrainComponentBase::SerializeJsonObject(const TSharedRef<FJsonObject>& RootObject) const
{
FString OutputJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
FJsonSerializer::Serialize(RootObject, Writer);
return OutputJson;
}
void UBrainComponentBase::AppendAllowedTools(TSharedRef<FJsonObject> RootObject, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const
{
TArray<TSharedPtr<FJsonValue>> Tools;
const TArray<TSharedPtr<FJsonValue>>* ExistingTools = nullptr;
if (RootObject->TryGetArrayField(TEXT("tools"), ExistingTools) && ExistingTools)
{
Tools = *ExistingTools;
}
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
{
if (!ToolClass)
{
continue;
}
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
TSharedPtr<FJsonObject> ToolDefinition = BuildToolDefinition(ToolDefaultObject);
if (ToolDefinition.IsValid())
{
Tools.Add(MakeShared<FJsonValueObject>(ToolDefinition.ToSharedRef()));
}
}
if (!Tools.IsEmpty())
{
RootObject->SetArrayField(TEXT("tools"), Tools);
RootObject->SetStringField(TEXT("tool_choice"), TEXT("auto"));
}
}
TSharedPtr<FJsonObject> UBrainComponentBase::BuildToolDefinition(const UBrainToolBase* ToolDefaultObject) const
{
if (!ToolDefaultObject)
{
return nullptr;
}
const FString ToolName = ToolDefaultObject->GetResolvedToolName();
if (ToolName.TrimStartAndEnd().IsEmpty())
{
BroadcastBrainError(FString::Printf(TEXT("Tool has empty ToolName: %s"), *GetNameSafe(ToolDefaultObject->GetClass())));
return nullptr;
}
TSharedRef<FJsonObject> ParametersObject = MakeShared<FJsonObject>();
if (ToolDefaultObject->ParametersJsonSchema.IsEmpty())
{
BroadcastBrainError(FString::Printf(TEXT("Tool %s has empty ParametersJsonSchema."), *ToolName));
return nullptr;
}
{
TSharedPtr<FJsonObject> ParsedParametersObject;
TSharedRef<TJsonReader<>> ParametersReader = TJsonReaderFactory<>::Create(ToolDefaultObject->ParametersJsonSchema.ToString());
if (FJsonSerializer::Deserialize(ParametersReader, ParsedParametersObject) && ParsedParametersObject.IsValid())
{
ParametersObject = ParsedParametersObject.ToSharedRef();
}
else
{
BroadcastBrainError(FString::Printf(TEXT("Tool %s has invalid ParametersJsonSchema."), *ToolName));
return nullptr;
}
}
if (ParametersObject->Values.IsEmpty())
{
BroadcastBrainError(FString::Printf(TEXT("Tool %s has empty ParametersJsonSchema object."), *ToolName));
return nullptr;
}
TSharedRef<FJsonObject> FunctionObject = MakeShared<FJsonObject>();
FunctionObject->SetStringField(TEXT("name"), ToolName);
FunctionObject->SetStringField(TEXT("description"), ToolDefaultObject->Description.ToString());
FunctionObject->SetObjectField(TEXT("parameters"), ParametersObject);
TSharedRef<FJsonObject> ToolObject = MakeShared<FJsonObject>();
ToolObject->SetStringField(TEXT("type"), TEXT("function"));
ToolObject->SetObjectField(TEXT("function"), FunctionObject);
return ToolObject;
}
const UBrainToolBase* UBrainComponentBase::FindAllowedToolDefaultObject(const FString& ToolName, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const
{
if (ToolName.TrimStartAndEnd().IsEmpty())
{
return nullptr;
}
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
{
if (!ToolClass)
{
continue;
}
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
if (ToolDefaultObject && ToolDefaultObject->GetResolvedToolName().Equals(ToolName, ESearchCase::IgnoreCase))
{
return ToolDefaultObject;
}
}
return nullptr;
}
bool UBrainComponentBase::ExecuteBrainToolCalls(const TArray<TSharedPtr<FJsonValue>>& ToolCalls, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools)
{
if (ToolCalls.IsEmpty())
{
return true;
}
bool bAllSucceeded = true;
for (const TSharedPtr<FJsonValue>& ToolCallValue : ToolCalls)
{
const TSharedPtr<FJsonObject>* ToolCallObject = nullptr;
if (!ToolCallValue.IsValid() || !ToolCallValue->TryGetObject(ToolCallObject) || !ToolCallObject || !ToolCallObject->IsValid())
{
bAllSucceeded = false;
continue;
}
const TSharedPtr<FJsonObject>* FunctionObject = nullptr;
if (!(*ToolCallObject)->TryGetObjectField(TEXT("function"), FunctionObject) || !FunctionObject || !FunctionObject->IsValid())
{
BroadcastBrainError(TEXT("Native tool call is missing function object."));
bAllSucceeded = false;
continue;
}
FString ToolName;
if (!(*FunctionObject)->TryGetStringField(TEXT("name"), ToolName))
{
BroadcastBrainError(TEXT("Native tool call is missing function name."));
bAllSucceeded = false;
continue;
}
FString ArgumentsJson;
if (!(*FunctionObject)->TryGetStringField(TEXT("arguments"), ArgumentsJson))
{
ArgumentsJson = TEXT("{}");
}
const UBrainToolBase* ToolDefaultObject = FindAllowedToolDefaultObject(ToolName, AllowedTools);
if (!ToolDefaultObject)
{
BroadcastBrainError(FString::Printf(TEXT("Tried to call disallowed native tool: %s"), *ToolName));
bAllSucceeded = false;
continue;
}
UBrainToolBase* ToolInstance = NewObject<UBrainToolBase>(this, ToolDefaultObject->GetClass());
if (!ToolInstance)
{
BroadcastBrainError(FString::Printf(TEXT("Could not create native tool: %s"), *ToolName));
bAllSucceeded = false;
continue;
}
FString ErrorMessage;
if (!ToolInstance->ExecuteTool(this, ArgumentsJson, ErrorMessage))
{
BroadcastBrainError(ErrorMessage.IsEmpty() ? FString::Printf(TEXT("Native tool failed: %s"), *ToolName) : ErrorMessage);
bAllSucceeded = false;
}
ToolInstance->CleanupTool();
}
return bAllSucceeded;
}
FString UBrainComponentBase::GetChatDirectory() const
{
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / GetChatIdentityId();
IFileManager::Get().MakeDirectory(*Directory, true);
return Directory;
}
FString UBrainComponentBase::GetChatFilePath() const
{
return GetChatSiblingFilePath(TEXT("Chat.json"));
}
FString UBrainComponentBase::GetChatSiblingFilePath(const FString& FileName) const
{
return GetChatDirectory() / FileName;
}
void UBrainComponentBase::SaveRequestBodyToFile(const FString& RequestBody, const FString& FileName) const
{
FFileHelper::SaveStringToFile(RequestBody, *GetChatSiblingFilePath(FileName), FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
}
@@ -0,0 +1,60 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/BrainPromptDataAsset.h"
#include "LLM/BrainToolBase.h"
FString UBrainPromptDataAsset::BuildBrainPrompt() const
{
TArray<FString> Sections;
if (!SystemPrompt.IsEmpty())
{
Sections.Add(SystemPrompt.ToString());
}
const FString ToolsPrompt = BuildToolsPrompt();
if (!ToolsPrompt.IsEmpty())
{
Sections.Add(ToolsPrompt);
}
return FString::Join(Sections, TEXT("\n\n"));
}
FString UBrainPromptDataAsset::BuildToolsPrompt() const
{
TArray<FString> ToolPrompts;
for (const TSubclassOf<UBrainToolBase>& ToolClass : AllowedTools)
{
if (!ToolClass)
{
continue;
}
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
if (ToolDefaultObject)
{
ToolPrompts.Add(ToolDefaultObject->BuildToolPrompt());
}
}
if (ToolPrompts.IsEmpty())
{
return FString();
}
TArray<FString> Sections;
if (!ToolsPromptHeader.IsEmpty())
{
Sections.Add(ToolsPromptHeader.ToString());
}
Sections.Add(FString::Join(ToolPrompts, TEXT("\n")));
if (!ToolsPromptFooter.IsEmpty())
{
Sections.Add(ToolsPromptFooter.ToString());
}
return FString::Join(Sections, TEXT("\n\n"));
}
@@ -0,0 +1,31 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/BrainToolBase.h"
FString UBrainToolBase::GetResolvedToolName() const
{
return ToolName.TrimStartAndEnd();
}
FString UBrainToolBase::BuildToolPrompt() const
{
TArray<FString> Lines;
Lines.Add(FString::Printf(TEXT("- tool: %s"), *GetResolvedToolName()));
if (!Description.IsEmpty())
{
Lines.Add(FString::Printf(TEXT(" description: %s"), *Description.ToString()));
}
return FString::Join(Lines, TEXT("\n"));
}
bool UBrainToolBase::ExecuteTool_Implementation(UBrainComponentBase* Brain, const FString& ArgumentsJson, FString& OutError)
{
OutError = TEXT("Brain tool execution is not implemented.");
return false;
}
void UBrainToolBase::CleanupTool_Implementation()
{
}
@@ -0,0 +1,661 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/DirectorBrainComponent.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "GameFramework/Actor.h"
#include "LLM/BrainToolBase.h"
#include "LLM/DirectorPromptDataAsset.h"
#include "LLM/IdentityDataAsset.h"
#include "LLM/LLMProviderDataAsset.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "World/WorldContextActor.h"
DEFINE_LOG_CATEGORY_STATIC(LogDirectorBrain, Log, All);
UDirectorBrainComponent::UDirectorBrainComponent()
{
ExistingPlayerFactsPromptTemplate = FText::FromString(TEXT(
"EXISTING PLAYER FACTS:\n"
"These facts are already saved in Player.json. Do not save duplicate facts. "
"Only update a key when the new transcript clearly corrects it or adds more specific information.\n{facts}"));
}
void UDirectorBrainComponent::BeginPlay()
{
Super::BeginPlay();
if (!PersonaBrain)
{
if (AActor* Owner = GetOwner())
{
PersonaBrain = Owner->FindComponentByClass<UPersonaBrainComponent>();
}
}
if (PersonaBrain)
{
UE_LOG(LogDirectorBrain, Log, TEXT("BeginPlay binding to PersonaBrain: %s"), *GetNameSafe(PersonaBrain));
BindToPersonaBrain(PersonaBrain);
}
else
{
UE_LOG(LogDirectorBrain, Warning, TEXT("BeginPlay has no PersonaBrain assigned."));
}
}
void UDirectorBrainComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
CancelRequest();
if (PersonaBrain)
{
PersonaBrain->OnLLMResponse.RemoveDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
}
Super::EndPlay(EndPlayReason);
}
void UDirectorBrainComponent::BindToPersonaBrain(UPersonaBrainComponent* Brain)
{
if (PersonaBrain)
{
PersonaBrain->OnLLMResponse.RemoveDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
}
PersonaBrain = Brain;
if (PersonaBrain)
{
PersonaBrain->OnLLMResponse.AddUniqueDynamic(this, &UDirectorBrainComponent::HandlePersonaBrainResponse);
UE_LOG(LogDirectorBrain, Log, TEXT("Bound to PersonaBrain OnLLMResponse: %s"), *GetNameSafe(PersonaBrain));
}
else
{
UE_LOG(LogDirectorBrain, Warning, TEXT("BindToPersonaBrain called with null Persona Brain."));
}
}
void UDirectorBrainComponent::AnalyzeCurrentChat()
{
if (!ValidateSettings())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("AnalyzeCurrentChat aborted: invalid settings."));
return;
}
if (!HasValidAllowedTool())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("AnalyzeCurrentChat skipped: DirectorPrompt has no valid native tools."));
return;
}
UE_LOG(LogDirectorBrain, Log, TEXT("AnalyzeCurrentChat requested. IdentityId=%s ChatFile=%s"), *GetChatIdentityId(), *GetChatFilePath());
CancelRequest();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
if (!ConfigureRequest(Request))
{
SetIsThinking(false);
return;
}
Request->OnProcessRequestComplete().BindUObject(this, &UDirectorBrainComponent::HandleRequestComplete);
ActiveRequest = Request;
SetIsThinking(true);
UE_LOG(LogDirectorBrain, Log, TEXT("Director request started."));
Request->ProcessRequest();
}
void UDirectorBrainComponent::HandlePersonaBrainResponse(const FLLMReplyResponse& Response)
{
if (Response.Reply.TrimStartAndEnd().IsEmpty())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("PersonaBrain response was empty. Director analysis skipped."));
return;
}
UE_LOG(LogDirectorBrain, Log, TEXT("PersonaBrain response received. Starting Director analysis. ReplyLen=%d"), Response.Reply.Len());
AnalyzeCurrentChat();
}
void UDirectorBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const
{
OnDirectorError.Broadcast(ErrorMessage);
}
FString UDirectorBrainComponent::GetBrainDebugName() const
{
return TEXT("Director");
}
void UDirectorBrainComponent::AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
{
check(DirectorPrompt);
UE_LOG(LogDirectorBrain, Log, TEXT("Building Director prompt with %d allowed tool(s)."), DirectorPrompt->AllowedTools.Num());
for (const TSubclassOf<UBrainToolBase>& ToolClass : DirectorPrompt->AllowedTools)
{
if (!ToolClass)
{
UE_LOG(LogDirectorBrain, Warning, TEXT("DirectorPrompt has a null AllowedTools entry."));
continue;
}
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
UE_LOG(
LogDirectorBrain,
Log,
TEXT("Director allowed tool: %s Class=%s"),
ToolDefaultObject ? *ToolDefaultObject->GetResolvedToolName() : TEXT("<no default object>"),
*ToolClass->GetName());
}
static const FString DirectorScopeGuard = TEXT(
"Director scope rules:\n"
"- Director is a hidden router for native tools. It does not roleplay and does not continue the dialogue.\n"
"- Decide whether to call a tool by reading that tool's own description and parameter schema.\n"
"- Tool-specific save/update rules belong to tool descriptions, not to the Director prompt.\n"
"- If a situation does not clearly match any available tool description, do not call a tool.\n"
"\n"
"- Director does NOT control interactive scene objects.\n"
"- Interactive objects and physical actions are handled by Persona interact_action and WorldContext.\n"
"- Do NOT infer, track, or save physical object states from dialogue unless a tool explicitly says to do that.\n"
"\n"
"- The full transcript is context only. Use it to understand continuity, avoid duplicates, and detect corrections.\n"
"- Call tools only for information introduced or clearly confirmed in LATEST TURN TO ANALYZE, unless a tool description explicitly says otherwise.\n"
"- Do NOT mine older transcript messages for new tool calls during later analysis passes.\n"
"- You may call multiple tools if multiple distinct tool-relevant updates are clearly present.\n"
"\n"
"- If the transcript only contains a command affecting a scene object and a character acknowledgement, do NOT call any tool.");
FString DirectorPromptText = DirectorPrompt->BuildDirectorPrompt();
DirectorPromptText = DirectorPromptText.Replace(
TEXT("Use at most one tool call per analysis."),
TEXT("You may call multiple tools if multiple distinct tool-relevant updates are clearly present."));
DirectorPromptText = DirectorPromptText.Replace(
TEXT("Use one tool call per distinct durable fact. Multiple tool calls are allowed."),
TEXT("You may call multiple tools if multiple distinct tool-relevant updates are clearly present."));
AddSystemMessage(Messages, FString::Printf(TEXT("%s\n\n%s"), *DirectorPromptText, *DirectorScopeGuard));
if (WorldContextActor)
{
AddSystemMessage(Messages, WorldContextActor->BuildBaseWorldContextPrompt());
}
}
FString UDirectorBrainComponent::BuildRequestBody(const ULLMProviderDataAsset* EffectiveProvider) const
{
TSharedRef<FJsonObject> RootObject = MakeShared<FJsonObject>();
RootObject->SetStringField(TEXT("model"), EffectiveProvider->Model);
RootObject->SetBoolField(TEXT("stream"), false);
RootObject->SetNumberField(TEXT("temperature"), EffectiveProvider->Temperature);
RootObject->SetNumberField(TEXT("max_tokens"), EffectiveProvider->MaxTokens);
RootObject->SetNumberField(TEXT("top_p"), EffectiveProvider->TopP);
check(DirectorPrompt);
AppendAllowedTools(RootObject, DirectorPrompt->AllowedTools);
TArray<TSharedPtr<FJsonValue>> Messages;
AppendSystemMessages(Messages);
AppendSavedSemanticMemoryMessages(Messages);
AppendSavedPlayerFactsMessage(Messages);
AppendSavedChatHistoryMessages(Messages);
RootObject->SetArrayField(TEXT("messages"), Messages);
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(RootObject, Writer);
return RequestBody;
}
FString UDirectorBrainComponent::GetChatIdentityId() const
{
FString IdentityId = PersonaBrain && PersonaBrain->Identity ? PersonaBrain->Identity->ID : FString();
if (IdentityId.IsEmpty())
{
BroadcastBrainError(TEXT("Director requires PersonaBrain with Identity.ID."));
return FString();
}
return FPaths::MakeValidFileName(IdentityId);
}
FString UDirectorBrainComponent::GetSemanticsFilePath() const
{
return GetChatSiblingFilePath(TEXT("Semantics.json"));
}
void UDirectorBrainComponent::AppendSavedSemanticMemoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
{
const FString FilePath = GetSemanticsFilePath();
FString SemanticsJson;
if (!FFileHelper::LoadFileToString(SemanticsJson, *FilePath))
{
UE_LOG(LogDirectorBrain, Log, TEXT("No saved semantic memories for Director yet: %s"), *FilePath);
return;
}
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(SemanticsJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse semantic memories JSON for Director: %s"), *FilePath);
return;
}
const TArray<TSharedPtr<FJsonValue>>* SavedSemantics = nullptr;
if (!RootObject->TryGetArrayField(TEXT("semantics"), SavedSemantics) || SavedSemantics->IsEmpty())
{
UE_LOG(LogDirectorBrain, Log, TEXT("Semantic memory file has no memories for Director: %s"), *FilePath);
return;
}
TArray<FString> SemanticLines;
const int32 StartIndex = FMath::Max(0, SavedSemantics->Num() - 30);
for (int32 Index = StartIndex; Index < SavedSemantics->Num(); ++Index)
{
const TSharedPtr<FJsonValue>& SemanticValue = (*SavedSemantics)[Index];
const TSharedPtr<FJsonObject>* SemanticObject = nullptr;
if (!SemanticValue.IsValid() || !SemanticValue->TryGetObject(SemanticObject) || !SemanticObject || !SemanticObject->IsValid())
{
continue;
}
FString Text;
if (!(*SemanticObject)->TryGetStringField(TEXT("text"), Text) || Text.TrimStartAndEnd().IsEmpty())
{
continue;
}
FString Category;
(*SemanticObject)->TryGetStringField(TEXT("category"), Category);
if (Category.TrimStartAndEnd().IsEmpty())
{
SemanticLines.Add(FString::Printf(TEXT("- %s"), *Text.TrimStartAndEnd()));
}
else
{
SemanticLines.Add(FString::Printf(TEXT("- [%s] %s"), *Category.TrimStartAndEnd(), *Text.TrimStartAndEnd()));
}
}
if (SemanticLines.IsEmpty())
{
return;
}
TSharedRef<FJsonObject> SemanticMemoryMessage = MakeShared<FJsonObject>();
SemanticMemoryMessage->SetStringField(TEXT("role"), TEXT("user"));
check(DirectorPrompt);
SemanticMemoryMessage->SetStringField(TEXT("content"), DirectorPrompt->BuildSavedSemanticsContext(SemanticLines));
Messages.Add(MakeShared<FJsonValueObject>(SemanticMemoryMessage));
UE_LOG(LogDirectorBrain, Log, TEXT("Director appended %d saved semantic memories."), SemanticLines.Num());
}
void UDirectorBrainComponent::AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const
{
const FString FilePath = GetChatSiblingFilePath(TEXT("Player.json"));
FString PlayerJson;
if (!FFileHelper::LoadFileToString(PlayerJson, *FilePath))
{
UE_LOG(LogDirectorBrain, Log, TEXT("No saved player facts for Director yet: %s"), *FilePath);
return;
}
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(PlayerJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse saved player facts JSON for Director: %s"), *FilePath);
return;
}
const TSharedPtr<FJsonObject>* FactsObject = nullptr;
if (!RootObject->TryGetObjectField(TEXT("facts"), FactsObject) || !FactsObject || !FactsObject->IsValid() || (*FactsObject)->Values.IsEmpty())
{
UE_LOG(LogDirectorBrain, Log, TEXT("Player facts file has no facts for Director: %s"), *FilePath);
return;
}
TArray<FString> FactLines;
for (const TPair<FString, TSharedPtr<FJsonValue>>& FactPair : (*FactsObject)->Values)
{
const TSharedPtr<FJsonValue>& FactValue = FactPair.Value;
const TSharedPtr<FJsonObject>* FactObject = nullptr;
if (!FactValue.IsValid() || !FactValue->TryGetObject(FactObject) || !FactObject || !FactObject->IsValid())
{
continue;
}
FString Value;
if (!(*FactObject)->TryGetStringField(TEXT("value"), Value) || Value.TrimStartAndEnd().IsEmpty())
{
continue;
}
FString Category;
(*FactObject)->TryGetStringField(TEXT("category"), Category);
const FString CleanKey = FactPair.Key.TrimStartAndEnd();
const FString CleanValue = Value.TrimStartAndEnd();
const FString CleanCategory = Category.TrimStartAndEnd();
if (CleanCategory.IsEmpty())
{
FactLines.Add(FString::Printf(TEXT("- %s: %s"), *CleanKey, *CleanValue));
}
else
{
FactLines.Add(FString::Printf(TEXT("- [%s] %s: %s"), *CleanCategory, *CleanKey, *CleanValue));
}
}
if (FactLines.IsEmpty())
{
return;
}
FactLines.Sort();
TSharedRef<FJsonObject> PlayerFactsMessage = MakeShared<FJsonObject>();
PlayerFactsMessage->SetStringField(TEXT("role"), TEXT("user"));
FString ExistingPlayerFactsPrompt = ExistingPlayerFactsPromptTemplate.IsEmptyOrWhitespace()
? FString(TEXT("EXISTING PLAYER FACTS:\n{facts}"))
: ExistingPlayerFactsPromptTemplate.ToString();
ExistingPlayerFactsPrompt.ReplaceInline(TEXT("{facts}"), *FString::Join(FactLines, TEXT("\n")));
PlayerFactsMessage->SetStringField(
TEXT("content"),
ExistingPlayerFactsPrompt);
Messages.Add(MakeShared<FJsonValueObject>(PlayerFactsMessage));
UE_LOG(LogDirectorBrain, Log, TEXT("Director appended %d saved player facts."), FactLines.Num());
}
void UDirectorBrainComponent::AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const
{
const FString FilePath = GetChatFilePath();
FString ChatJson;
if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
{
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not load chat history for Director: %s"), *FilePath);
return;
}
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ChatJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("Could not parse chat history JSON for Director: %s"), *FilePath);
return;
}
const TArray<TSharedPtr<FJsonValue>>* SavedMessages = nullptr;
if (!RootObject->TryGetArrayField(TEXT("messages"), SavedMessages) || SavedMessages->IsEmpty())
{
UE_LOG(LogDirectorBrain, Warning, TEXT("Chat history has no messages for Director: %s"), *FilePath);
return;
}
int32 AddedMessages = 0;
int32 RelevantMessageCount = 0;
TArray<FString> TranscriptLines;
TArray<FString> LatestTurnLines;
const UIdentityDataAsset* EffectiveIdentity = PersonaBrain ? PersonaBrain->Identity.Get() : nullptr;
if (!EffectiveIdentity || EffectiveIdentity->Name.IsEmpty())
{
UE_LOG(LogDirectorBrain, Error, TEXT("Director requires PersonaBrain Identity.Name for transcript speaker labels."));
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain Identity.Name for transcript speaker labels."));
return;
}
FString CharacterName = EffectiveIdentity->Name.ToString();
if (CharacterName.TrimStartAndEnd().IsEmpty())
{
UE_LOG(LogDirectorBrain, Error, TEXT("Director requires non-empty PersonaBrain Identity.Name for transcript speaker labels."));
OnDirectorError.Broadcast(TEXT("Director requires non-empty PersonaBrain Identity.Name for transcript speaker labels."));
return;
}
for (const TSharedPtr<FJsonValue>& MessageValue : *SavedMessages)
{
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
if (!MessageValue.IsValid() || !MessageValue->TryGetObject(MessageObject) || !MessageObject || !MessageObject->IsValid())
{
continue;
}
FString Role;
FString Content;
if ((*MessageObject)->TryGetStringField(TEXT("role"), Role) && (*MessageObject)->TryGetStringField(TEXT("content"), Content))
{
if ((Role != TEXT("user") && Role != TEXT("assistant")) || Content.TrimStartAndEnd().IsEmpty())
{
continue;
}
++RelevantMessageCount;
const FString Speaker = Role == TEXT("assistant") ? CharacterName : TEXT("Player");
const FString TranscriptLine = FString::Printf(TEXT("%s: %s"), *Speaker, *Content.TrimStartAndEnd());
TranscriptLines.Add(TranscriptLine);
++AddedMessages;
}
}
const int32 LatestTurnStartIndex = FMath::Max(0, TranscriptLines.Num() - 2);
for (int32 LineIndex = LatestTurnStartIndex; LineIndex < TranscriptLines.Num(); ++LineIndex)
{
LatestTurnLines.Add(TranscriptLines[LineIndex]);
}
if (!TranscriptLines.IsEmpty())
{
TSharedRef<FJsonObject> TranscriptMessage = MakeShared<FJsonObject>();
TranscriptMessage->SetStringField(TEXT("role"), TEXT("user"));
check(DirectorPrompt);
TArray<FString> Sections;
Sections.Add(TEXT("FULL TRANSCRIPT FOR CONTEXT ONLY.\nUse this section only to understand continuity, avoid duplicates, and detect corrections. Do not save facts that appear only in older messages here."));
Sections.Add(DirectorPrompt->BuildTranscriptContext(TranscriptLines));
if (!LatestTurnLines.IsEmpty())
{
Sections.Add(FString::Printf(
TEXT("LATEST TURN TO ANALYZE.\nOnly tool-relevant information introduced or clearly confirmed in this section may trigger tool calls.\n\n%s"),
*FString::Join(LatestTurnLines, TEXT("\n"))));
}
TranscriptMessage->SetStringField(TEXT("content"), FString::Join(Sections, TEXT("\n\n")));
Messages.Add(MakeShared<FJsonValueObject>(TranscriptMessage));
}
UE_LOG(
LogDirectorBrain,
Log,
TEXT("Director appended %d saved chat messages as transcript. TotalRelevant=%d LatestTurn=%d"),
AddedMessages,
RelevantMessageCount,
LatestTurnLines.Num());
}
bool UDirectorBrainComponent::ValidateSettings() const
{
if (!ValidateProvider(Provider))
{
return false;
}
if (!PersonaBrain)
{
OnDirectorError.Broadcast(TEXT("Director persona brain is not set."));
return false;
}
if (!PersonaBrain->Identity)
{
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity data asset."));
return false;
}
if (PersonaBrain->Identity->ID.TrimStartAndEnd().IsEmpty())
{
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity ID."));
return false;
}
if (PersonaBrain->Identity->Name.IsEmpty() || PersonaBrain->Identity->Name.ToString().TrimStartAndEnd().IsEmpty())
{
OnDirectorError.Broadcast(TEXT("Director requires PersonaBrain identity name."));
return false;
}
if (!DirectorPrompt)
{
OnDirectorError.Broadcast(TEXT("Director prompt data asset is not set."));
return false;
}
return true;
}
bool UDirectorBrainComponent::HasValidAllowedTool() const
{
if (!DirectorPrompt)
{
return false;
}
for (const TSubclassOf<UBrainToolBase>& ToolClass : DirectorPrompt->AllowedTools)
{
if (!ToolClass)
{
continue;
}
const UBrainToolBase* ToolDefaultObject = ToolClass->GetDefaultObject<UBrainToolBase>();
if (!ToolDefaultObject)
{
continue;
}
if (!ToolDefaultObject->GetResolvedToolName().TrimStartAndEnd().IsEmpty() &&
!ToolDefaultObject->ParametersJsonSchema.IsEmpty())
{
return true;
}
}
return false;
}
bool UDirectorBrainComponent::ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request) const
{
if (!ValidateProvider(Provider))
{
return false;
}
const FString RequestBody = BuildRequestBody(Provider);
SaveRequestBodyToFile(RequestBody, TEXT("DirectorLastRequest.json"));
UE_LOG(LogDirectorBrain, Log, TEXT("Director request saved: %s"), *GetChatSiblingFilePath(TEXT("DirectorLastRequest.json")));
return ConfigureRequestWithBody(Request, Provider, RequestBody, TEXT("application/json"));
}
void UDirectorBrainComponent::HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (Request != ActiveRequest)
{
return;
}
ActiveRequest.Reset();
SetIsThinking(false);
if (!bWasSuccessful || !Response.IsValid())
{
OnDirectorError.Broadcast(TEXT("Director LLM request failed."));
UE_LOG(LogDirectorBrain, Error, TEXT("Director LLM request failed: no valid response."));
return;
}
const int32 ResponseCode = Response->GetResponseCode();
const FString ResponseBody = Response->GetContentAsString();
UE_LOG(LogDirectorBrain, Log, TEXT("Director response received. HTTP=%d BodyLen=%d"), ResponseCode, ResponseBody.Len());
SaveRequestBodyToFile(ResponseBody, TEXT("DirectorLastResponse.json"));
if (ResponseCode < 200 || ResponseCode >= 300)
{
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM request failed with HTTP %d: %s"), ResponseCode, *ResponseBody));
UE_LOG(LogDirectorBrain, Error, TEXT("Director LLM request failed with HTTP %d: %s"), ResponseCode, *ResponseBody);
return;
}
FDirectorBrainResponse DirectorResponse;
bool bNativeToolCallsFieldFound = false;
bool bNativeToolCallsSucceeded = true;
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseBody);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
OnDirectorError.Broadcast(FString::Printf(TEXT("Could not parse Director LLM response JSON: %s"), *ResponseBody));
return;
}
const TArray<TSharedPtr<FJsonValue>>* Choices = nullptr;
if (!RootObject->TryGetArrayField(TEXT("choices"), Choices) || Choices->Num() == 0 || !(*Choices)[0].IsValid())
{
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response is missing choices[0]: %s"), *ResponseBody));
return;
}
const TSharedPtr<FJsonObject>* ChoiceObject = nullptr;
if (!(*Choices)[0]->TryGetObject(ChoiceObject) || !ChoiceObject || !ChoiceObject->IsValid())
{
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response choices[0] is not an object: %s"), *ResponseBody));
return;
}
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
if (!(*ChoiceObject)->TryGetObjectField(TEXT("message"), MessageObject) || !MessageObject || !MessageObject->IsValid())
{
OnDirectorError.Broadcast(FString::Printf(TEXT("Director LLM response is missing message object: %s"), *ResponseBody));
return;
}
const TArray<TSharedPtr<FJsonValue>>* NativeToolCalls = nullptr;
if ((*MessageObject)->TryGetArrayField(TEXT("tool_calls"), NativeToolCalls) && NativeToolCalls)
{
bNativeToolCallsFieldFound = true;
check(DirectorPrompt);
bNativeToolCallsSucceeded = ExecuteBrainToolCalls(*NativeToolCalls, DirectorPrompt->AllowedTools);
if (!bNativeToolCallsSucceeded)
{
return;
}
}
FString Content;
if (!(*MessageObject)->TryGetStringField(TEXT("content"), Content) || Content.TrimStartAndEnd().IsEmpty())
{
OnDirectorError.Broadcast(bNativeToolCallsFieldFound
? TEXT("Director LLM returned tool_calls without response content.")
: TEXT("Director LLM response is missing content."));
return;
}
DirectorResponse.Content = Content;
UE_LOG(LogDirectorBrain, Log, TEXT("Director content: %s"), *DirectorResponse.Content);
OnDirectorResponse.Broadcast(DirectorResponse);
}
@@ -0,0 +1,42 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/DirectorPromptDataAsset.h"
FString UDirectorPromptDataAsset::BuildDirectorPrompt() const
{
return BuildBrainPrompt();
}
FString UDirectorPromptDataAsset::BuildSavedSemanticsContext(const TArray<FString>& SemanticLines) const
{
if (SemanticLines.IsEmpty())
{
return FString();
}
TArray<FString> Sections;
if (!SavedSemanticsContextPrompt.IsEmpty())
{
Sections.Add(SavedSemanticsContextPrompt.ToString());
}
Sections.Add(FString::Join(SemanticLines, TEXT("\n")));
return FString::Join(Sections, TEXT("\n\n"));
}
FString UDirectorPromptDataAsset::BuildTranscriptContext(const TArray<FString>& TranscriptLines) const
{
if (TranscriptLines.IsEmpty())
{
return FString();
}
TArray<FString> Sections;
if (!TranscriptContextPrompt.IsEmpty())
{
Sections.Add(TranscriptContextPrompt.ToString());
}
Sections.Add(FString::Join(TranscriptLines, TEXT("\n")));
return FString::Join(Sections, TEXT("\n\n"));
}
@@ -0,0 +1,87 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/IdentityDataAsset.h"
#include "Identity/PersonalityTraitDataAsset.h"
FString UIdentityDataAsset::BuildIdentityPrompt() const
{
TArray<FString> Sections;
const FString BrainPrompt = BuildBrainPrompt();
if (!BrainPrompt.IsEmpty())
{
Sections.Add(BrainPrompt);
}
TArray<FString> Lines;
Lines.Add(TEXT("[IDENTITY]"));
if (!Name.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("- Name: %s"), *Name.ToString()));
}
if (!Age.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("- Age: %s"), *Age.ToString()));
}
if (!Appearance.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("- Appearance: %s"), *Appearance.ToString()));
}
if (!Background.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("- Background: %s"), *Background.ToString()));
}
if (!CoreInstruction.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("\n[CORE INSTRUCTION]\n%s"), *CoreInstruction.ToString()));
}
if (!SpeechStyle.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("\n[SPEECH STYLE]\n%s"), *SpeechStyle.ToString()));
}
if (!DialogueBehavior.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("\n[DIALOGUE BEHAVIOR]\n%s"), *DialogueBehavior.ToString()));
}
TArray<FString> PersonalityPrompts;
for (const TObjectPtr<UPersonalityTraitDataAsset>& Trait : PersonalityTraits)
{
if (Trait && !Trait->Prompt.IsEmpty())
{
PersonalityPrompts.Add(Trait->Prompt.ToString());
}
}
if (!PersonalityPrompts.IsEmpty())
{
TArray<FString> PersonalitySectionLines;
if (!PersonalityTraitsInstruction.IsEmpty())
{
PersonalitySectionLines.Add(PersonalityTraitsInstruction.ToString());
}
PersonalitySectionLines.Add(FString::Join(PersonalityPrompts, TEXT("\n")));
Lines.Add(FString::Printf(TEXT("\n[PERSONALITY TRAITS]\n%s"), *FString::Join(PersonalitySectionLines, TEXT("\n"))));
}
Sections.Add(FString::Join(Lines, TEXT("\n")));
return FString::Join(Sections, TEXT("\n\n"));
}
FString UIdentityDataAsset::BuildFirstMeetingPrompt() const
{
if (FirstMeetingRule.IsEmpty())
{
return FString();
}
return FString::Printf(TEXT("[FIRST MEETING]\n%s"), *FirstMeetingRule.ToString());
}
@@ -0,0 +1,29 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/LLMProviderDataAsset.h"
ULLMProviderDataAsset::ULLMProviderDataAsset()
: ProviderName(TEXT("DeepSeek"))
, ApiBaseUrl(TEXT("https://api.deepseek.com"))
, ChatCompletionsPath(TEXT("/chat/completions"))
, Model(TEXT("deepseek-chat"))
, Temperature(0.7f)
, MaxTokens(1024)
, TopP(1.0f)
, TimeoutSeconds(60.0f)
{
}
FString ULLMProviderDataAsset::GetChatCompletionsUrl() const
{
FString NormalizedBaseUrl = ApiBaseUrl;
NormalizedBaseUrl.RemoveFromEnd(TEXT("/"));
FString NormalizedPath = ChatCompletionsPath;
if (!NormalizedPath.StartsWith(TEXT("/")))
{
NormalizedPath.InsertAt(0, TEXT("/"));
}
return NormalizedBaseUrl + NormalizedPath;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LLM/SystemEventDataAsset.h"
FString USystemEventDataAsset::BuildSystemEventPrompt() const
{
if (Prompt.IsEmpty())
{
return FString();
}
return FString::Printf(
TEXT("System event happened right now.\n")
TEXT("This is not a user message. Do not answer it as a question from the user.\n")
TEXT("Your next assistant reply must be an immediate in-character visible reaction to this event.\n")
TEXT("Do not continue the previous dialogue as if nothing happened.\n")
TEXT("If the event requires an action, acknowledge the interruption and say what you are doing now.\n\n")
TEXT("Event:\n%s"),
*Prompt.ToString());
}
@@ -0,0 +1,650 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/LipSyncComponent.h"
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/MorphControllerComponent.h"
#include "Components/AudioComponent.h"
#include "Components/SceneComponent.h"
#include "GameFramework/Actor.h"
#include "Sound/SoundBase.h"
#include "Sound/SoundWave.h"
ULipSyncComponent::ULipSyncComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.TickInterval = 0.0f;
AudioDrivenVisemeSequence = {
TEXT("A"),
TEXT("I"),
TEXT("U"),
TEXT("E"),
TEXT("O"),
TEXT("aa"),
TEXT("SS"),
TEXT("MBP"),
TEXT("oh")
};
}
void ULipSyncComponent::BeginPlay()
{
Super::BeginPlay();
if (!MorphController && bAutoResolveMorphController)
{
MorphController = ResolveMorphController();
}
}
void ULipSyncComponent::SetVisemeWeight(FName VisemeName, float Weight)
{
if (VisemeName.IsNone())
{
return;
}
const float ClampedWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
if (ClampedWeight <= SilenceThreshold)
{
TargetVisemeWeights.Remove(VisemeName);
}
else
{
TargetVisemeWeights.Add(VisemeName, ClampedWeight);
}
}
void ULipSyncComponent::SetVisemeWeights(const TMap<FName, float>& VisemeWeights)
{
TargetVisemeWeights.Empty();
for (const auto& Pair : VisemeWeights)
{
SetVisemeWeight(Pair.Key, Pair.Value);
}
}
void ULipSyncComponent::ClearVisemes()
{
TargetVisemeWeights.Empty();
CurrentVisemeWeights.Empty();
LastNonSilentMouthWeight = 0.0f;
SilenceHoldRemaining = 0.0f;
if (UMorphControllerComponent* ResolvedController = ResolveMorphController())
{
ResolvedController->ClearLipSyncMorphValues();
}
}
bool ULipSyncComponent::PlayAudioDrivenLipSync(USoundBase* Sound, float StartTime)
{
USoundBase* SoundToPlay = Sound ? Sound : AudioDrivenSound.Get();
if (!SoundToPlay)
{
return false;
}
UAudioComponent* AudioComponent = ResolveAudioDrivenComponent();
if (!AudioComponent)
{
return false;
}
return PlayAudioDrivenLipSyncWithComponent(AudioComponent, SoundToPlay, StartTime);
}
bool ULipSyncComponent::PlayAudioDrivenLipSyncWithComponent(UAudioComponent* AudioComponent, USoundBase* Sound, float StartTime)
{
if (!AudioComponent || !Sound)
{
return false;
}
AudioComponent->Stop();
AudioComponent->SetSound(Sound);
AudioComponent->EnvelopeFollowerAttackTime = 8;
AudioComponent->EnvelopeFollowerReleaseTime = 90;
AudioComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
AudioComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
AudioDrivenComponent = AudioComponent;
AudioDrivenPlaybackTime = FMath::Max(StartTime, 0.0f);
AudioDrivenDuration = FMath::Max(Sound->GetDuration(), 0.0f);
LastEnvelopeValue = 0.0f;
LastNonSilentMouthWeight = 0.0f;
SilenceHoldRemaining = 0.0f;
bReceivedEnvelopeValue = false;
bAudioDrivenControlsPlayback = true;
bWaitingForExternalAudioPlayback = false;
ExternalAudioPlaybackWaitTime = 0.0f;
RebuildGeneratedTranscriptVisemes();
ClearVisemes();
bAudioDrivenLipSyncActive = true;
AudioComponent->Play(StartTime);
return true;
}
bool ULipSyncComponent::FollowExternalAudioComponent(UAudioComponent* AudioComponent, const FText& Transcript)
{
if (!AudioComponent)
{
return false;
}
AudioDrivenComponent = AudioComponent;
AudioDrivenTranscript = Transcript;
AudioDrivenMode = ELipSyncAudioDrivenMode::TranscriptVisemes;
AudioDrivenPlaybackTime = 0.0f;
AudioDrivenDuration = 0.0f;
LastEnvelopeValue = 0.0f;
LastNonSilentMouthWeight = 0.0f;
SilenceHoldRemaining = 0.0f;
bReceivedEnvelopeValue = false;
bAudioDrivenControlsPlayback = false;
bWaitingForExternalAudioPlayback = !AudioComponent->IsPlaying();
ExternalAudioPlaybackWaitTime = 0.0f;
AudioComponent->EnvelopeFollowerAttackTime = 8;
AudioComponent->EnvelopeFollowerReleaseTime = 90;
AudioComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
AudioComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
RebuildGeneratedTranscriptVisemes();
ClearVisemes();
bAudioDrivenLipSyncActive = true;
return true;
}
void ULipSyncComponent::StopAudioDrivenLipSync()
{
bAudioDrivenLipSyncActive = false;
if (AudioDrivenComponent && bAudioDrivenControlsPlayback)
{
AudioDrivenComponent->Stop();
}
bWaitingForExternalAudioPlayback = false;
ExternalAudioPlaybackWaitTime = 0.0f;
ClearVisemes();
}
bool ULipSyncComponent::IsAudioDrivenLipSyncPlaying() const
{
return bAudioDrivenLipSyncActive && AudioDrivenComponent && AudioDrivenComponent->IsPlaying();
}
bool ULipSyncComponent::PlayLipSyncTestAudio(USoundBase* Sound, float StartTime)
{
return PlayAudioDrivenLipSync(Sound, StartTime);
}
void ULipSyncComponent::StopLipSyncTestAudio()
{
StopAudioDrivenLipSync();
}
bool ULipSyncComponent::IsLipSyncTestAudioPlaying() const
{
return IsAudioDrivenLipSyncPlaying();
}
void ULipSyncComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateAudioDrivenLipSync(DeltaTime);
TSet<FName> VisemeNames;
for (const auto& Pair : TargetVisemeWeights)
{
VisemeNames.Add(Pair.Key);
}
for (const auto& Pair : CurrentVisemeWeights)
{
VisemeNames.Add(Pair.Key);
}
for (const FName VisemeName : VisemeNames)
{
const float TargetValue = TargetVisemeWeights.FindRef(VisemeName);
const float CurrentValue = CurrentVisemeWeights.FindRef(VisemeName);
const float SmoothSpeed = TargetValue >= CurrentValue ? VisemeAttackSpeed : VisemeReleaseSpeed;
const float NewValue = FMath::FInterpTo(CurrentValue, TargetValue, DeltaTime, SmoothSpeed);
if (NewValue <= SilenceThreshold && TargetValue <= SilenceThreshold)
{
CurrentVisemeWeights.Remove(VisemeName);
}
else
{
CurrentVisemeWeights.Add(VisemeName, NewValue);
}
}
PushCurrentMorphValues();
}
UMorphControllerComponent* ULipSyncComponent::ResolveMorphController() const
{
if (MorphController)
{
return MorphController;
}
const AActor* Owner = GetOwner();
return Owner ? Owner->FindComponentByClass<UMorphControllerComponent>() : nullptr;
}
UAudioComponent* ULipSyncComponent::ResolveAudioDrivenComponent()
{
if (AudioDrivenComponent)
{
return AudioDrivenComponent;
}
AActor* Owner = GetOwner();
if (!Owner)
{
return nullptr;
}
AudioDrivenComponent = NewObject<UAudioComponent>(Owner, TEXT("LipSyncAudioDrivenComponent"));
if (!AudioDrivenComponent)
{
return nullptr;
}
AudioDrivenComponent->bAutoActivate = false;
AudioDrivenComponent->bAutoDestroy = false;
AudioDrivenComponent->OnAudioFinished.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenFinished);
AudioDrivenComponent->OnAudioPlaybackPercent.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenPlaybackPercent);
AudioDrivenComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &ULipSyncComponent::HandleAudioDrivenEnvelopeValue);
if (USceneComponent* RootComponent = Owner->GetRootComponent())
{
AudioDrivenComponent->SetupAttachment(RootComponent);
}
AudioDrivenComponent->RegisterComponent();
Owner->AddInstanceComponent(AudioDrivenComponent);
return AudioDrivenComponent;
}
void ULipSyncComponent::UpdateAudioDrivenLipSync(float DeltaTime)
{
if (!bAudioDrivenLipSyncActive)
{
return;
}
if (!AudioDrivenComponent || !AudioDrivenComponent->IsPlaying())
{
if (AudioDrivenComponent && !bAudioDrivenControlsPlayback && bWaitingForExternalAudioPlayback)
{
ExternalAudioPlaybackWaitTime += DeltaTime;
if (ExternalAudioPlaybackWaitTime < 10.0f)
{
return;
}
}
HandleAudioDrivenFinished();
return;
}
bWaitingForExternalAudioPlayback = false;
ExternalAudioPlaybackWaitTime = 0.0f;
AudioDrivenPlaybackTime += DeltaTime;
const TArray<FName>& ActiveVisemeSequence = ResolveActiveAudioDrivenVisemeSequence();
if (AudioDrivenMode == ELipSyncAudioDrivenMode::EnvelopeMouthOpen || ActiveVisemeSequence.Num() == 0)
{
TargetVisemeWeights.Empty();
const float MouthWeight = GetAudioDrivenMouthWeight(DeltaTime);
if (MouthWeight > SilenceThreshold && !AudioDrivenEnvelopeViseme.IsNone())
{
TargetVisemeWeights.Add(AudioDrivenEnvelopeViseme, MouthWeight);
}
return;
}
SetAudioDrivenTargetVisemes(GetAudioDrivenMouthWeight(DeltaTime));
}
float ULipSyncComponent::GetAudioDrivenMouthWeight(float DeltaTime)
{
float EnvelopeValue = 0.0f;
bool bHasEnvelope = false;
if (AudioDrivenComponent)
{
bHasEnvelope = AudioDrivenComponent->GetCookedEnvelopeData(EnvelopeValue);
}
if (!bHasEnvelope && bReceivedEnvelopeValue)
{
EnvelopeValue = LastEnvelopeValue;
bHasEnvelope = true;
}
if (!bHasEnvelope && bUseFallbackPulseWithoutEnvelope)
{
const float Pulse = 0.5f + 0.5f * FMath::Sin(AudioDrivenPlaybackTime * AudioDrivenVisemeRate * UE_TWO_PI);
EnvelopeValue = FMath::Lerp(0.15f, 0.75f, Pulse);
bHasEnvelope = true;
}
if (!bHasEnvelope)
{
return 0.0f;
}
const float ScaledEnvelope = FMath::Clamp(EnvelopeValue * AudioDrivenEnvelopeGain, 0.0f, 1.0f);
float MouthWeight = FMath::Pow(ScaledEnvelope, FMath::Max(AudioDrivenEnvelopeGamma, 0.1f));
if (MouthWeight > SilenceThreshold)
{
LastNonSilentMouthWeight = MouthWeight;
SilenceHoldRemaining = SilenceHoldTime;
return MouthWeight;
}
if (SilenceHoldRemaining > 0.0f)
{
SilenceHoldRemaining = FMath::Max(0.0f, SilenceHoldRemaining - DeltaTime);
return LastNonSilentMouthWeight * 0.65f;
}
LastNonSilentMouthWeight = 0.0f;
return 0.0f;
}
void ULipSyncComponent::RebuildGeneratedTranscriptVisemes()
{
GeneratedTranscriptVisemes.Empty();
GeneratedTranscriptVisemeUnits.Empty();
const FString Text = AudioDrivenTranscript.ToString().ToLower();
auto AddCue = [this](FName Viseme, float Units)
{
if (Units <= 0.0f)
{
return;
}
if (GeneratedTranscriptVisemes.Num() > 0 && GeneratedTranscriptVisemes.Last() == Viseme)
{
GeneratedTranscriptVisemeUnits.Last() += Units;
return;
}
GeneratedTranscriptVisemes.Add(Viseme);
GeneratedTranscriptVisemeUnits.Add(Units);
};
for (int32 Index = 0; Index < Text.Len(); ++Index)
{
const TCHAR Ch = Text[Index];
FName Viseme = NAME_None;
float Units = 0.45f;
const TCHAR NextCh = (Index + 1 < Text.Len()) ? Text[Index + 1] : 0;
if ((Ch == TEXT('s') && Ch == NextCh) || (Ch == TEXT('s') && NextCh == TEXT('h')))
{
AddCue(TEXT("SS"), 0.55f);
if (NextCh == TEXT('h')) ++Index;
continue;
}
if (Ch == TEXT('c') && NextCh == TEXT('h'))
{
AddCue(TEXT("CH"), 0.55f);
++Index;
continue;
}
if (Ch == TEXT('t') && NextCh == TEXT('h'))
{
AddCue(TEXT("TH"), 0.5f);
++Index;
continue;
}
if (Ch == TEXT('o') && (NextCh == TEXT('o') || NextCh == TEXT('u')))
{
AddCue(TEXT("U"), 1.15f);
++Index;
continue;
}
if (Ch == TEXT('e') && NextCh == TEXT('e'))
{
AddCue(TEXT("I"), 1.15f);
++Index;
continue;
}
if (FChar::IsWhitespace(Ch))
{
AddCue(NAME_None, 0.35f);
continue;
}
if (Ch == TEXT('.') || Ch == TEXT(',') || Ch == TEXT('!') || Ch == TEXT('?') || Ch == TEXT(';') || Ch == TEXT(':') || Ch == TEXT('-'))
{
AddCue(NAME_None, 0.65f);
continue;
}
switch (Ch)
{
case TEXT('a'): case TEXT('а'): case TEXT('я'):
Viseme = TEXT("A");
Units = 1.2f;
break;
case TEXT('e'): case TEXT('э'): case TEXT('е'): case TEXT('ё'):
Viseme = TEXT("E");
Units = 1.05f;
break;
case TEXT('i'): case TEXT('y'): case TEXT('и'): case TEXT('ы'): case TEXT('й'):
Viseme = TEXT("I");
Units = 1.0f;
break;
case TEXT('o'): case TEXT('о'):
Viseme = TEXT("O");
Units = 1.1f;
break;
case TEXT('u'): case TEXT('w'): case TEXT('у'): case TEXT('ю'):
Viseme = TEXT("U");
Units = 1.05f;
break;
case TEXT('b'): case TEXT('m'): case TEXT('p'): case TEXT('б'): case TEXT('м'): case TEXT('п'):
Viseme = TEXT("MBP");
Units = 0.5f;
break;
case TEXT('f'): case TEXT('v'): case TEXT('ф'): case TEXT('в'):
Viseme = TEXT("FV");
Units = 0.5f;
break;
case TEXT('s'): case TEXT('z'): case TEXT('c'): case TEXT('с'): case TEXT('з'): case TEXT('ц'):
Viseme = TEXT("SS");
Units = 0.5f;
break;
case TEXT('t'): case TEXT('d'): case TEXT('n'): case TEXT('т'): case TEXT('д'): case TEXT('н'):
Viseme = TEXT("DD");
Units = 0.45f;
break;
case TEXT('l'): case TEXT('л'):
Viseme = TEXT("L");
Units = 0.5f;
break;
case TEXT('r'): case TEXT('р'):
Viseme = TEXT("RR");
Units = 0.5f;
break;
case TEXT('k'): case TEXT('g'): case TEXT('q'): case TEXT('x'): case TEXT('к'): case TEXT('г'): case TEXT('х'):
Viseme = TEXT("kk");
Units = 0.5f;
break;
case TEXT('h'): case TEXT('j'): case TEXT('ч'): case TEXT('ш'): case TEXT('щ'): case TEXT('ж'):
Viseme = TEXT("CH");
Units = 0.55f;
break;
default:
break;
}
AddCue(Viseme, Units);
}
}
const TArray<FName>& ULipSyncComponent::ResolveActiveAudioDrivenVisemeSequence() const
{
if (AudioDrivenMode == ELipSyncAudioDrivenMode::TranscriptVisemes)
{
return GeneratedTranscriptVisemes;
}
return AudioDrivenVisemeSequence;
}
float ULipSyncComponent::GetGeneratedTranscriptTotalUnits() const
{
float TotalUnits = 0.0f;
for (float Units : GeneratedTranscriptVisemeUnits)
{
TotalUnits += FMath::Max(Units, 0.0f);
}
return TotalUnits;
}
void ULipSyncComponent::SetAudioDrivenTargetVisemes(float MouthWeight)
{
TargetVisemeWeights.Empty();
const TArray<FName>& ActiveVisemeSequence = ResolveActiveAudioDrivenVisemeSequence();
if (MouthWeight <= SilenceThreshold || ActiveVisemeSequence.Num() == 0)
{
return;
}
float SequencePosition = AudioDrivenPlaybackTime * FMath::Max(AudioDrivenVisemeRate, 0.1f);
if (AudioDrivenMode == ELipSyncAudioDrivenMode::TranscriptVisemes && AudioDrivenDuration > 0.0f)
{
const float Alpha = FMath::Clamp(AudioDrivenPlaybackTime / AudioDrivenDuration, 0.0f, 0.999f);
const float TotalUnits = GetGeneratedTranscriptTotalUnits();
if (TotalUnits > 0.0f && GeneratedTranscriptVisemeUnits.Num() == ActiveVisemeSequence.Num())
{
const float TargetUnit = Alpha * TotalUnits;
float UnitCursor = 0.0f;
int32 CueIndex = 0;
float CueFraction = 0.0f;
for (; CueIndex < GeneratedTranscriptVisemeUnits.Num(); ++CueIndex)
{
const float CueUnits = FMath::Max(GeneratedTranscriptVisemeUnits[CueIndex], 0.001f);
if (TargetUnit <= UnitCursor + CueUnits)
{
CueFraction = FMath::Clamp((TargetUnit - UnitCursor) / CueUnits, 0.0f, 1.0f);
break;
}
UnitCursor += CueUnits;
}
const int32 CurrentIndex = FMath::Clamp(CueIndex, 0, ActiveVisemeSequence.Num() - 1);
const int32 NextIndex = FMath::Min(CurrentIndex + 1, ActiveVisemeSequence.Num() - 1);
const float Crossfade = FMath::Clamp(AudioDrivenVisemeCrossfade, 0.001f, 0.45f);
const float NextBlend = FMath::Clamp((CueFraction - (1.0f - Crossfade)) / Crossfade, 0.0f, 1.0f);
const FName CurrentViseme = ActiveVisemeSequence[CurrentIndex];
const FName NextViseme = ActiveVisemeSequence[NextIndex];
if (!CurrentViseme.IsNone())
{
TargetVisemeWeights.Add(CurrentViseme, MouthWeight * (1.0f - NextBlend));
}
if (!NextViseme.IsNone() && NextBlend > 0.001f)
{
float& ExistingValue = TargetVisemeWeights.FindOrAdd(NextViseme);
ExistingValue = FMath::Max(ExistingValue, MouthWeight * NextBlend);
}
return;
}
SequencePosition = Alpha * ActiveVisemeSequence.Num();
}
const int32 CurrentIndex = FMath::FloorToInt(SequencePosition) % ActiveVisemeSequence.Num();
const int32 NextIndex = (CurrentIndex + 1) % ActiveVisemeSequence.Num();
const float Fraction = FMath::Frac(SequencePosition);
float NextBlend = 0.0f;
if (AudioDrivenVisemeCrossfade > 0.0f)
{
const float Crossfade = FMath::Clamp(AudioDrivenVisemeCrossfade, 0.001f, 0.45f);
NextBlend = FMath::Clamp((Fraction - (1.0f - Crossfade)) / Crossfade, 0.0f, 1.0f);
}
const FName CurrentViseme = ActiveVisemeSequence[CurrentIndex];
const FName NextViseme = ActiveVisemeSequence[NextIndex];
if (!CurrentViseme.IsNone())
{
TargetVisemeWeights.Add(CurrentViseme, MouthWeight * (1.0f - NextBlend));
}
if (!NextViseme.IsNone() && NextBlend > 0.001f)
{
float& ExistingValue = TargetVisemeWeights.FindOrAdd(NextViseme);
ExistingValue = FMath::Max(ExistingValue, MouthWeight * NextBlend);
}
}
void ULipSyncComponent::PushCurrentMorphValues()
{
UMorphControllerComponent* ResolvedController = ResolveMorphController();
if (!ResolvedController)
{
return;
}
if (!Mapping || CurrentVisemeWeights.Num() == 0)
{
ResolvedController->ClearLipSyncMorphValues();
return;
}
TMap<FName, float> MorphValues;
Mapping->AccumulateMorphValues(CurrentVisemeWeights, MorphValues);
ResolvedController->SetLipSyncMorphValues(MorphValues);
}
void ULipSyncComponent::HandleAudioDrivenFinished()
{
if (!bAudioDrivenLipSyncActive)
{
return;
}
bAudioDrivenLipSyncActive = false;
bWaitingForExternalAudioPlayback = false;
ExternalAudioPlaybackWaitTime = 0.0f;
ClearVisemes();
}
void ULipSyncComponent::HandleAudioDrivenPlaybackPercent(const USoundWave* PlayingSoundWave, const float PlaybackPercent)
{
if (!bAudioDrivenControlsPlayback && AudioDrivenDuration <= 0.0f)
{
return;
}
if (PlayingSoundWave)
{
AudioDrivenPlaybackTime = FMath::Max(PlayingSoundWave->Duration * PlaybackPercent, 0.0f);
}
}
void ULipSyncComponent::HandleAudioDrivenEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue)
{
LastEnvelopeValue = FMath::Max(EnvelopeValue, 0.0f);
bReceivedEnvelopeValue = true;
}
@@ -0,0 +1,121 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/LipSyncMappingDataAsset.h"
ULipSyncMappingDataAsset::ULipSyncMappingDataAsset()
{
auto AddMapping = [this](FName VisemeName, TArray<FLipSyncMorphTargetValue>&& MorphTargets)
{
FLipSyncVisemeMapping Mapping;
Mapping.VisemeName = VisemeName;
Mapping.MorphTargets = MoveTemp(MorphTargets);
VisemeMappings.Add(MoveTemp(Mapping));
};
auto Morph = [](const TCHAR* MorphTargetName, float Weight)
{
FLipSyncMorphTargetValue Value;
Value.MorphTargetName = FName(MorphTargetName);
Value.Weight = Weight;
return Value;
};
AddMapping(TEXT("sil"), {});
AddMapping(TEXT("rest"), {});
AddMapping(TEXT("A"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.22f) });
AddMapping(TEXT("I"), { Morph(TEXT("Fcl_MTH_I"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.12f) });
AddMapping(TEXT("U"), { Morph(TEXT("Fcl_MTH_U"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.25f) });
AddMapping(TEXT("E"), { Morph(TEXT("Fcl_MTH_E"), 1.0f), Morph(TEXT("Fcl_MTH_I"), 0.28f) });
AddMapping(TEXT("O"), { Morph(TEXT("Fcl_MTH_O"), 1.0f), Morph(TEXT("Fcl_MTH_U"), 0.18f) });
AddMapping(TEXT("aa"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.25f) });
AddMapping(TEXT("ih"), { Morph(TEXT("Fcl_MTH_I"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.12f) });
AddMapping(TEXT("ee"), { Morph(TEXT("Fcl_MTH_E"), 0.85f), Morph(TEXT("Fcl_MTH_I"), 0.35f) });
AddMapping(TEXT("oh"), { Morph(TEXT("Fcl_MTH_O"), 1.0f) });
AddMapping(TEXT("ou"), { Morph(TEXT("Fcl_MTH_U"), 0.95f), Morph(TEXT("Fcl_MTH_O"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
AddMapping(TEXT("PP"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f), Morph(TEXT("Fcl_MTH_Small"), 0.18f) });
AddMapping(TEXT("MBP"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f) });
AddMapping(TEXT("FF"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
AddMapping(TEXT("FV"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
AddMapping(TEXT("TH"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.18f) });
AddMapping(TEXT("DD"), { Morph(TEXT("Fcl_MTH_I"), 0.45f), Morph(TEXT("Fcl_MTH_A"), 0.18f) });
AddMapping(TEXT("kk"), { Morph(TEXT("Fcl_MTH_A"), 0.5f), Morph(TEXT("Fcl_MTH_Small"), 0.15f) });
AddMapping(TEXT("CH"), { Morph(TEXT("Fcl_MTH_U"), 0.5f), Morph(TEXT("Fcl_MTH_Small"), 0.45f) });
AddMapping(TEXT("SS"), { Morph(TEXT("Fcl_MTH_I"), 0.55f), Morph(TEXT("Fcl_MTH_Small"), 0.45f) });
AddMapping(TEXT("nn"), { Morph(TEXT("Fcl_MTH_I"), 0.45f), Morph(TEXT("Fcl_MTH_Close"), 0.25f) });
AddMapping(TEXT("RR"), { Morph(TEXT("Fcl_MTH_U"), 0.45f), Morph(TEXT("Fcl_MTH_O"), 0.25f) });
AddMapping(TEXT("L"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.22f) });
AddMapping(TEXT("WQ"), { Morph(TEXT("Fcl_MTH_U"), 0.85f), Morph(TEXT("Fcl_MTH_Small"), 0.32f) });
AddMapping(TEXT("Rhubarb_A"), { Morph(TEXT("Fcl_MTH_A"), 0.75f), Morph(TEXT("Fcl_MTH_I"), 0.2f) });
AddMapping(TEXT("Rhubarb_B"), { Morph(TEXT("Fcl_MTH_Close"), 1.0f) });
AddMapping(TEXT("Rhubarb_C"), { Morph(TEXT("Fcl_MTH_E"), 0.85f), Morph(TEXT("Fcl_MTH_I"), 0.28f) });
AddMapping(TEXT("Rhubarb_D"), { Morph(TEXT("Fcl_MTH_A"), 1.0f), Morph(TEXT("Fcl_MTH_Large"), 0.25f) });
AddMapping(TEXT("Rhubarb_E"), { Morph(TEXT("Fcl_MTH_O"), 0.95f), Morph(TEXT("Fcl_MTH_U"), 0.18f) });
AddMapping(TEXT("Rhubarb_F"), { Morph(TEXT("Fcl_MTH_U"), 0.65f), Morph(TEXT("Fcl_MTH_O"), 0.25f) });
AddMapping(TEXT("Rhubarb_G"), { Morph(TEXT("Fcl_MTH_Close"), 0.45f), Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_Small"), 0.2f) });
AddMapping(TEXT("Rhubarb_H"), { Morph(TEXT("Fcl_MTH_I"), 0.35f), Morph(TEXT("Fcl_MTH_A"), 0.22f) });
AddMapping(TEXT("Rhubarb_X"), {});
}
bool ULipSyncMappingDataAsset::GetMorphValuesForViseme(FName VisemeName, float VisemeWeight, TMap<FName, float>& OutMorphValues) const
{
OutMorphValues.Empty();
if (VisemeName.IsNone() || VisemeWeight <= 0.001f)
{
return false;
}
for (const FLipSyncVisemeMapping& Mapping : VisemeMappings)
{
if (Mapping.VisemeName != VisemeName)
{
continue;
}
for (const FLipSyncMorphTargetValue& MorphTarget : Mapping.MorphTargets)
{
if (MorphTarget.IsValid())
{
OutMorphValues.Add(MorphTarget.MorphTargetName, FMath::Clamp(MorphTarget.Weight * VisemeWeight, 0.0f, 1.0f));
}
}
return OutMorphValues.Num() > 0;
}
return false;
}
void ULipSyncMappingDataAsset::AccumulateMorphValues(const TMap<FName, float>& VisemeWeights, TMap<FName, float>& InOutMorphValues) const
{
for (const auto& VisemePair : VisemeWeights)
{
if (VisemePair.Key.IsNone() || VisemePair.Value <= 0.001f)
{
continue;
}
for (const FLipSyncVisemeMapping& Mapping : VisemeMappings)
{
if (Mapping.VisemeName != VisemePair.Key)
{
continue;
}
for (const FLipSyncMorphTargetValue& MorphTarget : Mapping.MorphTargets)
{
if (!MorphTarget.IsValid())
{
continue;
}
const float Contribution = FMath::Clamp(MorphTarget.Weight * VisemePair.Value, 0.0f, 1.0f);
float& ExistingValue = InOutMorphValues.FindOrAdd(MorphTarget.MorphTargetName);
ExistingValue = FMath::Max(ExistingValue, Contribution);
}
break;
}
}
}
@@ -0,0 +1,602 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Character.h"
#include "Engine/World.h"
DEFINE_LOG_CATEGORY_STATIC(LogMorphController, Log, All);
namespace
{
constexpr float MinMorphTargetValue = -1.0f;
constexpr float MaxMorphTargetValue = 1.0f;
float ClampMorphTargetValue(float Value)
{
return FMath::Clamp(Value, MinMorphTargetValue, MaxMorphTargetValue);
}
}
UMorphControllerComponent::UMorphControllerComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.TickInterval = 0.0f; // Tick every frame
EmotionPresetsPromptHeader = FText::FromString(TEXT(
"[EMOTION MORPH PRESETS]\n"
"These presets are available facial/emotional expressions for the character. "
"Use the exact preset name when you need to request or describe an emotion preset. "
"Choose only from this list; if no preset fits, do not invent one."));
}
void UMorphControllerComponent::BeginPlay()
{
Super::BeginPlay();
}
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (bAutoUpdate)
{
UpdateMorphTargets(DeltaTime);
}
}
USkeletalMeshComponent* UMorphControllerComponent::ResolveMeshComponent() const
{
if (TargetMeshComponent)
{
return TargetMeshComponent;
}
if (ACharacter* Character = Cast<ACharacter>(GetOwner()))
{
return Character->GetMesh();
}
if (AActor* Owner = GetOwner())
{
return Owner->FindComponentByClass<USkeletalMeshComponent>();
}
return nullptr;
}
bool UMorphControllerComponent::PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const
{
for (const FMorphTimelineClip& Clip : Preset.MorphTargets)
{
if (!Clip.IsValid() || Clip.Weight <= 0.001f)
{
continue;
}
const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst();
if (!Curve)
{
continue;
}
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle KeyHandle = *It;
if (Curve->IsKeyHandleValid(KeyHandle) && Curve->GetKeyValue(KeyHandle) > 0.001f)
{
return true;
}
}
}
return false;
}
FString UMorphControllerComponent::BuildEmotionPresetsPrompt() const
{
TArray<FString> PromptSections;
const FString Header = EmotionPresetsPromptHeader.ToString().TrimStartAndEnd();
if (!Header.IsEmpty())
{
PromptSections.Add(Header);
}
TArray<FString> PresetLines;
for (const TObjectPtr<UMorphPresetDataAsset>& PresetRef : EmotionPresets)
{
const UMorphPresetDataAsset* Preset = PresetRef.Get();
if (!Preset || !Preset->HasPlayableContent())
{
continue;
}
const FString PresetName = Preset->DisplayName.ToString().TrimStartAndEnd();
const FString Description = Preset->Description.ToString().TrimStartAndEnd();
if (PresetName.IsEmpty())
{
continue;
}
PresetLines.Add(Description.IsEmpty()
? FString::Printf(TEXT("- name: %s"), *PresetName)
: FString::Printf(TEXT("- name: %s | description: %s"), *PresetName, *Description));
}
if (PresetLines.IsEmpty())
{
return FString();
}
PromptSections.Add(TEXT("Available presets:"));
PromptSections.Add(FString::Join(PresetLines, TEXT("\n")));
return FString::Join(PromptSections, TEXT("\n\n"));
}
UMorphPresetDataAsset* UMorphControllerComponent::FindEmotionPresetByName(const FString& PresetName) const
{
const FString CleanPresetName = PresetName.TrimStartAndEnd();
if (CleanPresetName.IsEmpty())
{
return nullptr;
}
for (const TObjectPtr<UMorphPresetDataAsset>& PresetRef : EmotionPresets)
{
UMorphPresetDataAsset* Preset = PresetRef.Get();
if (!Preset || !Preset->HasPlayableContent())
{
continue;
}
const FString DisplayName = Preset->DisplayName.ToString().TrimStartAndEnd();
if (DisplayName.Equals(CleanPresetName, ESearchCase::IgnoreCase))
{
return Preset;
}
}
return nullptr;
}
bool UMorphControllerComponent::PlayEmotionPresetByName(const FString& PresetName, float Weight)
{
UMorphPresetDataAsset* Preset = FindEmotionPresetByName(PresetName);
if (!Preset)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("PlayEmotionPresetByName ignored on %s: no emotion preset named '%s'."), *GetNameSafe(GetOwner()), *PresetName);
}
return false;
}
return PlayPreset(Preset, Weight);
}
bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float Weight)
{
if (!Preset || !Preset->IsValidPreset())
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("AddPreset ignored on %s: preset is null or has no valid clips."), *GetNameSafe(GetOwner()));
}
return false;
}
const float TimelineDuration = Preset->GetTimelineLength();
if (TimelineDuration <= 0.0f)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("AddPreset ignored on %s: preset %s has no timeline duration."), *GetNameSafe(GetOwner()), *GetNameSafe(Preset));
}
return false;
}
if (!ResolveMeshComponent() && bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but no SkeletalMeshComponent was found. Set TargetMeshComponent or place the component on an actor with a skeletal mesh."), *GetNameSafe(GetOwner()));
}
if (!PresetHasAnyPositiveCurveValue(*Preset) && bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but preset %s has no positive curve key values. Default curve keys are 0, so raise keys in the clip curve editor to see morph output."), *GetNameSafe(GetOwner()), *GetNameSafe(Preset));
}
for (FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset == Preset)
{
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
Slot.CurrentWeight = Slot.TargetWeight;
Slot.PlaybackTime = 0.0f;
Slot.LoopDelayRemaining = 0.0f;
Slot.TimelineDuration = TimelineDuration;
UpdateMorphTargets(0.0f);
return true;
}
}
FActiveMorphPresetSlot NewSlot;
NewSlot.Preset = Preset;
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
NewSlot.CurrentWeight = NewSlot.TargetWeight;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
NewSlot.TimelineDuration = TimelineDuration;
ActivePresets.Add(NewSlot);
UpdateMorphTargets(0.0f);
return true;
}
bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float Weight)
{
if (!Preset || !Preset->HasPlayableContent())
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips or animation montage."), *GetNameSafe(GetOwner()));
}
return false;
}
const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false;
const bool bPlayedAnimation = PlayPresetAnimation(Preset);
return bPlayedMorphs || bPlayedAnimation;
}
bool UMorphControllerComponent::PlayPresetAnimation(UMorphPresetDataAsset* Preset)
{
if (!Preset || !Preset->AnimationMontage)
{
return false;
}
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (!Mesh)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
}
return false;
}
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
if (!AnimInstance)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
}
return false;
}
UAnimMontage* Montage = Preset->AnimationMontage;
if (Preset->bRestartAnimationOnReplay && AnimInstance->Montage_IsPlaying(Montage))
{
AnimInstance->Montage_Stop(0.0f, Montage);
}
const float MontageLength = AnimInstance->Montage_Play(Montage, FMath::Max(Preset->AnimationPlayRate, 0.01f));
if (MontageLength <= 0.0f)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation failed on %s: montage %s could not play. Check the Anim Blueprint slot setup."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
}
return false;
}
if (!Preset->AnimationStartSection.IsNone())
{
AnimInstance->Montage_JumpToSection(Preset->AnimationStartSection, Montage);
}
return true;
}
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
{
if (!Preset) return;
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
{
if (ActivePresets[i].Preset == Preset)
{
ActivePresets.RemoveAt(i);
}
}
}
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
{
if (!Preset) return;
for (FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset == Preset)
{
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
return;
}
}
}
void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPresetSlot>& Presets)
{
ActivePresets.Empty();
for (const FActiveMorphPresetSlot& Slot : Presets)
{
if (Slot.Preset && Slot.Preset->IsValidPreset())
{
FActiveMorphPresetSlot NewSlot = Slot;
NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
NewSlot.TimelineDuration = NewSlot.Preset->GetTimelineLength();
if (NewSlot.TimelineDuration > 0.0f)
{
ActivePresets.Add(NewSlot);
}
}
}
}
void UMorphControllerComponent::ClearAllPresets()
{
ActivePresets.Empty();
}
bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const
{
if (!Preset) return false;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset == Preset && Slot.TargetWeight > 0.0f)
{
return true;
}
}
return false;
}
TArray<FActiveMorphPresetSlot> UMorphControllerComponent::GetActivePresets() const
{
TArray<FActiveMorphPresetSlot> Result;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset && Slot.TargetWeight > 0.0f)
{
Result.Add(Slot);
}
}
return Result;
}
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
{
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
}
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
{
DirectMorphOverrides.Remove(MorphTargetName);
}
void UMorphControllerComponent::ClearDirectMorphOverrides()
{
DirectMorphOverrides.Empty();
}
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues)
{
LipSyncMorphValues.Empty();
for (const auto& Pair : MorphValues)
{
if (!Pair.Key.IsNone() && FMath::Abs(Pair.Value) > 0.001f)
{
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
}
}
}
void UMorphControllerComponent::ClearLipSyncMorphValues()
{
LipSyncMorphValues.Empty();
}
void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime)
{
UpdateMorphTargets(FMath::Max(DeltaTime, 0.0f));
}
void UMorphControllerComponent::ClearMorphsImmediately()
{
ActivePresets.Empty();
DirectMorphOverrides.Empty();
LipSyncMorphValues.Empty();
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (Mesh)
{
for (const FName MorphName : LastAppliedMorphNames)
{
Mesh->SetMorphTarget(MorphName, 0.0f, false);
}
}
LastAppliedMorphNames.Empty();
}
void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
{
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (!Mesh)
{
if (bLogWarnings && !bMissingMeshWarningIssued && ActivePresets.Num() > 0)
{
bMissingMeshWarningIssued = true;
UE_LOG(LogMorphController, Warning, TEXT("Morph update skipped on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
}
return;
}
bMissingMeshWarningIssued = false;
// 1. Advance each active preset. Looping presets wrap forever; non-looping presets remove themselves at the end.
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
{
FActiveMorphPresetSlot& Slot = ActivePresets[i];
UMorphPresetDataAsset* Preset = Slot.Preset;
if (!Preset || !Preset->IsValidPreset())
{
ActivePresets.RemoveAt(i);
continue;
}
if (Slot.TimelineDuration <= 0.0f)
{
Slot.TimelineDuration = Preset->GetTimelineLength();
}
if (Slot.TimelineDuration <= 0.0f)
{
ActivePresets.RemoveAt(i);
continue;
}
const float EffectiveBlendSpeed = (Preset->BlendSpeedOverride > 0.0f) ? Preset->BlendSpeedOverride : BlendSpeed;
Slot.CurrentWeight = FMath::FInterpTo(Slot.CurrentWeight, Slot.TargetWeight, DeltaTime, EffectiveBlendSpeed);
if (Preset->bLoop)
{
if (Slot.LoopDelayRemaining > 0.0f)
{
Slot.LoopDelayRemaining = FMath::Max(0.0f, Slot.LoopDelayRemaining - DeltaTime);
Slot.PlaybackTime = Slot.TimelineDuration;
}
else
{
Slot.PlaybackTime += DeltaTime;
if (Slot.PlaybackTime >= Slot.TimelineDuration)
{
Slot.PlaybackTime = 0.0f;
Slot.LoopDelayRemaining = FMath::Max(Preset->LoopDelay, 0.0f);
}
}
}
else
{
Slot.PlaybackTime += DeltaTime;
if (Slot.PlaybackTime >= Slot.TimelineDuration)
{
ActivePresets.RemoveAt(i);
}
}
}
// 2. Gather final morph values. Overlapping clips use the strongest contribution.
TMap<FName, float> FinalValues;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
{
continue;
}
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight, FinalValues);
}
// 3. Apply direct overrides with the same max-based priority rule.
for (const auto& Pair : DirectMorphOverrides)
{
float* Existing = FinalValues.Find(Pair.Key);
if (Existing)
{
*Existing = FMath::Max(*Existing, Pair.Value);
}
else
{
FinalValues.Add(Pair.Key, Pair.Value);
}
}
// 4. Lip sync is a transient top layer, so emotion presets can keep driving smile/frown/eyes while speech drives mouth shapes.
if (bEnableLipSyncLayer)
{
BlendMorphLayer(LipSyncMorphValues, LipSyncLayerWeight, LipSyncBlendMode, FinalValues);
}
// 5. Push the final values to the mesh.
ApplyFinalMorphValues(FinalValues);
}
void UMorphControllerComponent::BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const
{
if (LayerWeight <= 0.001f)
{
return;
}
for (const auto& Pair : LayerValues)
{
if (Pair.Key.IsNone())
{
continue;
}
const float LayerValue = ClampMorphTargetValue(Pair.Value * LayerWeight);
if (FMath::Abs(LayerValue) <= 0.001f)
{
continue;
}
float& ExistingValue = InOutFinalValues.FindOrAdd(Pair.Key);
switch (BlendMode)
{
case EMorphLayerBlendMode::Additive:
ExistingValue = ClampMorphTargetValue(ExistingValue + LayerValue);
break;
case EMorphLayerBlendMode::Override:
ExistingValue = LayerValue;
break;
case EMorphLayerBlendMode::Max:
default:
ExistingValue = FMath::Max(ExistingValue, LayerValue);
break;
}
}
}
void UMorphControllerComponent::ApplyFinalMorphValues(const TMap<FName, float>& FinalValues)
{
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (!Mesh) return;
for (const FName MorphName : LastAppliedMorphNames)
{
if (!FinalValues.Contains(MorphName))
{
Mesh->SetMorphTarget(MorphName, 0.0f, false);
}
}
LastAppliedMorphNames.Empty();
for (const auto& Pair : FinalValues)
{
LastAppliedMorphNames.Add(Pair.Key);
const float ClampedValue = ClampMorphTargetValue(Pair.Value);
if (FMath::Abs(ClampedValue) > 0.001f)
{
Mesh->SetMorphTarget(Pair.Key, ClampedValue, false);
}
else
{
Mesh->SetMorphTarget(Pair.Key, 0.0f, false);
}
}
}
@@ -0,0 +1,102 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/MorphPresetDataAsset.h"
namespace
{
constexpr float MinMorphTargetValue = -1.0f;
constexpr float MaxMorphTargetValue = 1.0f;
float EvaluateTimelineClipCurve(const FMorphTimelineClip& Clip, float Time)
{
const float ClipStart = Clip.StartTime;
const float ClipEnd = Clip.StartTime + Clip.Duration;
if (Time < ClipStart || Time >= ClipEnd)
{
return 0.0f;
}
const float LocalTime = Time - ClipStart;
const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst();
if (!Curve || Curve->GetNumKeys() == 0)
{
return 0.0f;
}
return FMath::Clamp(Curve->Eval(LocalTime, 0.0f), 0.0f, 1.0f);
}
}
void FMorphTimelineClip::EnsureDefaultCurve()
{
FRichCurve* Curve = WeightCurve.GetRichCurve();
if (!Curve || Curve->GetNumKeys() > 0)
{
return;
}
Curve->UpdateOrAddKey(0.0f, 0.0f);
Curve->UpdateOrAddKey(FMath::Max(Duration, 0.001f), 0.0f);
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
{
Curve->SetKeyInterpMode(*It, RCIM_Linear);
}
}
int32 UMorphPresetDataAsset::GetMorphCount() const
{
int32 Count = 0;
for (const FMorphTimelineClip& Clip : MorphTargets)
{
if (Clip.IsValid()) Count++;
}
return Count;
}
float UMorphPresetDataAsset::GetTimelineLength() const
{
if (Duration > 0.0f) return Duration;
float MaxEnd = 0.0f;
for (const FMorphTimelineClip& Clip : MorphTargets)
{
if (Clip.IsValid())
{
MaxEnd = FMath::Max(MaxEnd, Clip.StartTime + Clip.Duration);
}
}
return MaxEnd;
}
float MorphPresetRuntime::EvaluateClipWeight(const FMorphTimelineClip& Clip, float Time)
{
return EvaluateTimelineClipCurve(Clip, Time) * Clip.Weight;
}
void MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(
const UMorphPresetDataAsset& Preset,
float Time,
float PresetWeight,
TMap<FName, float>& InOutValues)
{
if (PresetWeight < 0.001f)
{
return;
}
for (const FMorphTimelineClip& Clip : Preset.MorphTargets)
{
if (!Clip.IsValid())
{
continue;
}
const float Contribution = FMath::Clamp(EvaluateClipWeight(Clip, Time) * PresetWeight, MinMorphTargetValue, MaxMorphTargetValue);
if (FMath::Abs(Contribution) < 0.001f)
{
continue;
}
float& ExistingValue = InOutValues.FindOrAdd(Clip.Name);
ExistingValue = FMath::Max(ExistingValue, Contribution);
}
}
@@ -0,0 +1,175 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/MorphTargetBlueprintLibrary.h"
#include "Morph/MorphPresetDataAsset.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Animation/Skeleton.h"
#include "Components/SkeletalMeshComponent.h"
#include "Animation/MorphTarget.h"
#include "Engine/SkeletalMesh.h"
#include "GameFramework/PlayerController.h"
namespace
{
constexpr float MinMorphTargetValue = -1.0f;
constexpr float MaxMorphTargetValue = 1.0f;
}
TArray<FString> UMorphTargetBlueprintLibrary::GetMorphTargetNames(USkeletalMeshComponent* SkeletalMeshComponent)
{
TArray<FString> Result;
if (!SkeletalMeshComponent)
{
return Result;
}
USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset();
if (!SkeletalMesh)
{
return Result;
}
const TArray<UMorphTarget*>& MorphTargets = SkeletalMesh->GetMorphTargets();
for (UMorphTarget* MorphTarget : MorphTargets)
{
if (MorphTarget)
{
Result.Add(MorphTarget->GetName());
}
}
return Result;
}
TArray<FString> UMorphTargetBlueprintLibrary::GetMorphTargetNamesFromSkeleton(USkeleton* Skeleton)
{
TArray<FString> Result;
if (!Skeleton)
{
return Result;
}
TSet<FString> UniqueMorphNames;
const FString SkeletonPath = Skeleton->GetPathName();
static const FName SkeletonTagName(TEXT("Skeleton"));
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
FARFilter Filter;
Filter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
Filter.bRecursiveClasses = true;
TArray<FAssetData> SkeletalMeshAssets;
AssetRegistry.GetAssets(Filter, SkeletalMeshAssets);
for (const FAssetData& AssetData : SkeletalMeshAssets)
{
FString MeshSkeletonPath;
if (AssetData.GetTagValue(SkeletonTagName, MeshSkeletonPath) && MeshSkeletonPath != SkeletonPath)
{
continue;
}
USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(AssetData.GetAsset());
if (!SkeletalMesh || SkeletalMesh->GetSkeleton() != Skeleton)
{
continue;
}
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (MorphTarget)
{
UniqueMorphNames.Add(MorphTarget->GetName());
}
}
}
for (const FString& MorphName : UniqueMorphNames)
{
Result.Add(MorphName);
}
Result.Sort();
return Result;
}
void UMorphTargetBlueprintLibrary::ApplyPresetToMeshAtTime(USkeletalMeshComponent* SkeletalMeshComponent, UMorphPresetDataAsset* Preset, float Time, float Weight)
{
if (!SkeletalMeshComponent || !Preset || !Preset->IsValidPreset())
{
return;
}
TMap<FName, float> FinalValues;
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Preset, FMath::Max(Time, 0.0f), FMath::Clamp(Weight, 0.0f, 1.0f), FinalValues);
for (const FMorphTimelineClip& Clip : Preset->MorphTargets)
{
if (Clip.IsValid())
{
SkeletalMeshComponent->SetMorphTarget(Clip.Name, 0.0f, false);
}
}
for (const auto& Pair : FinalValues)
{
SkeletalMeshComponent->SetMorphTarget(Pair.Key, FMath::Clamp(Pair.Value, MinMorphTargetValue, MaxMorphTargetValue), false);
}
}
FVector2D UMorphTargetBlueprintLibrary::GetGyroCameraOffset2D(
APlayerController* PlayerController,
float HorizontalAmplitude,
float VerticalAmplitude,
float DeadZone,
float MaxTiltInput,
bool bInvertHorizontal,
bool bInvertVertical)
{
if (!PlayerController)
{
return FVector2D::ZeroVector;
}
FVector Tilt = FVector::ZeroVector;
FVector RotationRate = FVector::ZeroVector;
FVector Gravity = FVector::ZeroVector;
FVector Acceleration = FVector::ZeroVector;
PlayerController->GetInputMotionState(Tilt, RotationRate, Gravity, Acceleration);
const float SafeDeadZone = FMath::Max(0.0f, DeadZone);
const float SafeMaxTiltInput = FMath::Max(SafeDeadZone + KINDA_SMALL_NUMBER, MaxTiltInput);
const auto RemapTilt = [SafeDeadZone, SafeMaxTiltInput](float AxisValue) -> float
{
const float AbsValue = FMath::Abs(AxisValue);
if (AbsValue <= SafeDeadZone)
{
return 0.0f;
}
const float Alpha = (AbsValue - SafeDeadZone) / (SafeMaxTiltInput - SafeDeadZone);
const float Normalized = FMath::Clamp(Alpha, 0.0f, 1.0f);
return FMath::Sign(AxisValue) * Normalized;
};
// Common UE mapping for tilt vectors:
// Tilt.Y -> left/right, Tilt.X -> up/down.
float Horizontal = RemapTilt(Tilt.Y) * HorizontalAmplitude;
float Vertical = RemapTilt(Tilt.X) * VerticalAmplitude;
if (bInvertHorizontal)
{
Horizontal *= -1.0f;
}
if (bInvertVertical)
{
Vertical *= -1.0f;
}
return FVector2D(Horizontal, Vertical);
}
@@ -0,0 +1,326 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Narrative/NarrativeBlueprintLibrary.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "HAL/FileManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
namespace
{
FString NormalizeNarrativeKey(FString Key)
{
Key = Key.TrimStartAndEnd().ToLower();
Key.ReplaceInline(TEXT(" "), TEXT("_"));
return Key;
}
bool SavePlayerFactInternal(
const FString& IdentityId,
const FString& Key,
const FString& Value,
const FString& Category,
const FString& SourceText,
double Confidence,
const TArray<FString>& Tags,
FString& OutError)
{
FString CleanKey = NormalizeNarrativeKey(Key);
if (CleanKey.IsEmpty())
{
OutError = TEXT("Player data key is empty.");
return false;
}
const FString CleanValue = Value.TrimStartAndEnd();
if (CleanValue.IsEmpty())
{
OutError = TEXT("Player data value is empty.");
return false;
}
FString CleanIdentityId = IdentityId.TrimStartAndEnd();
if (CleanIdentityId.IsEmpty())
{
OutError = TEXT("Identity ID is required.");
return false;
}
CleanIdentityId = FPaths::MakeValidFileName(CleanIdentityId);
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / CleanIdentityId;
IFileManager::Get().MakeDirectory(*Directory, true);
const FString FullFilePath = Directory / TEXT("Player.json");
TSharedPtr<FJsonObject> RootObject;
FString ExistingJson;
if (FFileHelper::LoadFileToString(ExistingJson, *FullFilePath))
{
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ExistingJson);
FJsonSerializer::Deserialize(Reader, RootObject);
}
if (!RootObject.IsValid())
{
RootObject = MakeShared<FJsonObject>();
}
const TSharedPtr<FJsonObject>* ExistingFactsObject = nullptr;
TSharedPtr<FJsonObject> FactsObject;
if (RootObject->TryGetObjectField(TEXT("facts"), ExistingFactsObject) && ExistingFactsObject)
{
FactsObject = *ExistingFactsObject;
}
if (!FactsObject.IsValid())
{
FactsObject = MakeShared<FJsonObject>();
}
TSharedRef<FJsonObject> FactObject = MakeShared<FJsonObject>();
FactObject->SetStringField(TEXT("value"), CleanValue);
FactObject->SetStringField(TEXT("updated_at"), FDateTime::Now().ToIso8601());
const FString CleanCategory = Category.TrimStartAndEnd();
if (!CleanCategory.IsEmpty())
{
FactObject->SetStringField(TEXT("category"), CleanCategory);
}
const FString CleanSourceText = SourceText.TrimStartAndEnd();
if (!CleanSourceText.IsEmpty())
{
FactObject->SetStringField(TEXT("source_text"), CleanSourceText);
}
FactObject->SetNumberField(TEXT("confidence"), FMath::Clamp(Confidence, 0.0, 1.0));
TArray<TSharedPtr<FJsonValue>> TagValues;
for (const FString& Tag : Tags)
{
const FString CleanTag = Tag.TrimStartAndEnd();
if (!CleanTag.IsEmpty())
{
TagValues.Add(MakeShared<FJsonValueString>(CleanTag));
}
}
FactObject->SetArrayField(TEXT("tags"), TagValues);
FactsObject->SetObjectField(CleanKey, FactObject);
RootObject->SetStringField(TEXT("updated_at"), FDateTime::Now().ToIso8601());
RootObject->SetObjectField(TEXT("facts"), FactsObject);
FString OutputJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
{
OutError = TEXT("Could not serialize player data JSON.");
return false;
}
if (!FFileHelper::SaveStringToFile(OutputJson, *FullFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutError = FString::Printf(TEXT("Could not save player data file: %s"), *FullFilePath);
return false;
}
return true;
}
}
bool UNarrativeBlueprintLibrary::AppendSemanticMemoryFromArgumentsJson(
const FString& IdentityId,
const FString& ArgumentsJson,
FString& OutError)
{
OutError.Reset();
TSharedPtr<FJsonObject> ArgumentsObject;
TSharedRef<TJsonReader<>> ArgumentsReader = TJsonReaderFactory<>::Create(ArgumentsJson);
if (!FJsonSerializer::Deserialize(ArgumentsReader, ArgumentsObject) || !ArgumentsObject.IsValid())
{
OutError = TEXT("Semantic memory arguments JSON is invalid.");
return false;
}
FString SemanticText;
if (!ArgumentsObject->TryGetStringField(TEXT("text"), SemanticText))
{
OutError = TEXT("Semantic memory arguments must contain a text field.");
return false;
}
const FString CleanSemanticText = SemanticText.TrimStartAndEnd();
if (CleanSemanticText.IsEmpty())
{
OutError = TEXT("Semantic text is empty.");
return false;
}
FString CleanIdentityId = IdentityId.TrimStartAndEnd();
if (CleanIdentityId.IsEmpty())
{
OutError = TEXT("Identity ID is required.");
return false;
}
CleanIdentityId = FPaths::MakeValidFileName(CleanIdentityId);
const FString Directory = FPaths::ProjectSavedDir() / TEXT("Chat") / CleanIdentityId;
IFileManager::Get().MakeDirectory(*Directory, true);
const FString FullFilePath = Directory / TEXT("Semantics.json");
TSharedPtr<FJsonObject> RootObject;
FString ExistingJson;
if (FFileHelper::LoadFileToString(ExistingJson, *FullFilePath))
{
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ExistingJson);
FJsonSerializer::Deserialize(Reader, RootObject);
}
if (!RootObject.IsValid())
{
RootObject = MakeShared<FJsonObject>();
}
TArray<TSharedPtr<FJsonValue>> SemanticValues;
const TArray<TSharedPtr<FJsonValue>>* ExistingSemanticValues = nullptr;
if (RootObject->TryGetArrayField(TEXT("semantics"), ExistingSemanticValues))
{
SemanticValues = *ExistingSemanticValues;
}
TSharedRef<FJsonObject> SemanticObject = MakeShared<FJsonObject>();
SemanticObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601());
SemanticObject->SetStringField(TEXT("text"), CleanSemanticText);
FString Category;
ArgumentsObject->TryGetStringField(TEXT("category"), Category);
if (!Category.TrimStartAndEnd().IsEmpty())
{
SemanticObject->SetStringField(TEXT("category"), Category.TrimStartAndEnd());
}
int32 Importance = 1;
double ImportanceValue = 1.0;
if (ArgumentsObject->TryGetNumberField(TEXT("importance"), ImportanceValue))
{
Importance = FMath::RoundToInt(ImportanceValue);
}
SemanticObject->SetNumberField(TEXT("importance"), FMath::Clamp(Importance, 1, 5));
TArray<TSharedPtr<FJsonValue>> TagValues;
const TArray<TSharedPtr<FJsonValue>>* Tags = nullptr;
if (ArgumentsObject->TryGetArrayField(TEXT("tags"), Tags))
{
for (const TSharedPtr<FJsonValue>& TagValue : *Tags)
{
FString Tag;
if (TagValue.IsValid() && TagValue->TryGetString(Tag))
{
const FString CleanTag = Tag.TrimStartAndEnd();
if (!CleanTag.IsEmpty())
{
TagValues.Add(MakeShared<FJsonValueString>(CleanTag));
}
}
}
}
SemanticObject->SetArrayField(TEXT("tags"), TagValues);
SemanticValues.Add(MakeShared<FJsonValueObject>(SemanticObject));
RootObject->SetArrayField(TEXT("semantics"), SemanticValues);
FString OutputJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
if (!FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer))
{
OutError = TEXT("Could not serialize semantic memory JSON.");
return false;
}
if (!FFileHelper::SaveStringToFile(OutputJson, *FullFilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutError = FString::Printf(TEXT("Could not save semantic memory file: %s"), *FullFilePath);
return false;
}
return true;
}
bool UNarrativeBlueprintLibrary::SavePlayerDataFromArgumentsJson(
const FString& IdentityId,
const FString& ArgumentsJson,
FString& OutError)
{
OutError.Reset();
TSharedPtr<FJsonObject> ArgumentsObject;
TSharedRef<TJsonReader<>> ArgumentsReader = TJsonReaderFactory<>::Create(ArgumentsJson);
if (!FJsonSerializer::Deserialize(ArgumentsReader, ArgumentsObject) || !ArgumentsObject.IsValid())
{
OutError = TEXT("Player data arguments JSON is invalid.");
return false;
}
FString Key;
if (!ArgumentsObject->TryGetStringField(TEXT("key"), Key))
{
OutError = TEXT("Player data arguments must contain a key field.");
return false;
}
FString Value;
if (!ArgumentsObject->TryGetStringField(TEXT("value"), Value))
{
OutError = TEXT("Player data arguments must contain a value field.");
return false;
}
FString Category;
ArgumentsObject->TryGetStringField(TEXT("category"), Category);
FString SourceText;
ArgumentsObject->TryGetStringField(TEXT("source_text"), SourceText);
double Confidence = 1.0;
ArgumentsObject->TryGetNumberField(TEXT("confidence"), Confidence);
TArray<FString> CleanTags;
const TArray<TSharedPtr<FJsonValue>>* Tags = nullptr;
if (ArgumentsObject->TryGetArrayField(TEXT("tags"), Tags))
{
for (const TSharedPtr<FJsonValue>& TagValue : *Tags)
{
FString Tag;
if (TagValue.IsValid() && TagValue->TryGetString(Tag))
{
const FString CleanTag = Tag.TrimStartAndEnd();
if (!CleanTag.IsEmpty())
{
CleanTags.Add(CleanTag);
}
}
}
}
return SavePlayerFactInternal(IdentityId, Key, Value, Category, SourceText, Confidence, CleanTags, OutError);
}
bool UNarrativeBlueprintLibrary::SavePlayerFact(
const FString& IdentityId,
const FString& Key,
const FString& Value,
const FString& Category,
const FString& SourceText,
float Confidence,
const TArray<FString>& Tags,
FString& OutError)
{
OutError.Reset();
return SavePlayerFactInternal(IdentityId, Key, Value, Category, SourceText, Confidence, Tags, OutError);
}
@@ -0,0 +1,516 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/AudioPlaybackThrobber.h"
#include "Animation/CurveSequence.h"
#include "Brushes/SlateRoundedBoxBrush.h"
#include "Components/AudioComponent.h"
#include "Rendering/DrawElements.h"
#include "Sound/SoundWave.h"
#include "Widgets/SLeafWidget.h"
class SAudioPlaybackThrobber : public SLeafWidget
{
public:
SLATE_BEGIN_ARGS(SAudioPlaybackThrobber)
: _BarCount(7)
, _BarWidth(3.0f)
, _BarGap(2.5f)
, _MinHeightFraction(0.18f)
, _AnimationSpeed(4.8f)
, _GlowStrength(0.35f)
, _CenterBoost(0.35f)
, _BarColor(FLinearColor(0.18f, 0.82f, 1.0f, 1.0f))
, _IdleBarColor(FLinearColor(0.18f, 0.82f, 1.0f, 0.18f))
, _IsPlaying(false)
, _AudioComponent(nullptr)
, _UseAudioEnvelope(false)
, _AudioEnvelopeGain(5.0f)
, _AudioEnvelopeGamma(0.65f)
, _AudioEnvelopeSmoothingSpeed(16.0f)
, _ManualAudioLevel(0.0f)
, _AudioSilenceThreshold(0.03f)
, _AudioSilenceOpacity(0.08f)
{
}
SLATE_ATTRIBUTE(int32, BarCount)
SLATE_ATTRIBUTE(float, BarWidth)
SLATE_ATTRIBUTE(float, BarGap)
SLATE_ATTRIBUTE(float, MinHeightFraction)
SLATE_ATTRIBUTE(float, AnimationSpeed)
SLATE_ATTRIBUTE(float, GlowStrength)
SLATE_ATTRIBUTE(float, CenterBoost)
SLATE_ATTRIBUTE(FLinearColor, BarColor)
SLATE_ATTRIBUTE(FLinearColor, IdleBarColor)
SLATE_ATTRIBUTE(bool, IsPlaying)
SLATE_ATTRIBUTE(UAudioComponent*, AudioComponent)
SLATE_ATTRIBUTE(bool, UseAudioEnvelope)
SLATE_ATTRIBUTE(float, AudioEnvelopeGain)
SLATE_ATTRIBUTE(float, AudioEnvelopeGamma)
SLATE_ATTRIBUTE(float, AudioEnvelopeSmoothingSpeed)
SLATE_ATTRIBUTE(float, ManualAudioLevel)
SLATE_ATTRIBUTE(float, AudioSilenceThreshold)
SLATE_ATTRIBUTE(float, AudioSilenceOpacity)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
BarCount = InArgs._BarCount;
BarWidth = InArgs._BarWidth;
BarGap = InArgs._BarGap;
MinHeightFraction = InArgs._MinHeightFraction;
AnimationSpeed = InArgs._AnimationSpeed;
GlowStrength = InArgs._GlowStrength;
CenterBoost = InArgs._CenterBoost;
BarColor = InArgs._BarColor;
IdleBarColor = InArgs._IdleBarColor;
bIsPlaying = InArgs._IsPlaying;
AudioComponent = InArgs._AudioComponent;
bUseAudioEnvelope = InArgs._UseAudioEnvelope;
AudioEnvelopeGain = InArgs._AudioEnvelopeGain;
AudioEnvelopeGamma = InArgs._AudioEnvelopeGamma;
AudioEnvelopeSmoothingSpeed = InArgs._AudioEnvelopeSmoothingSpeed;
ManualAudioLevel = InArgs._ManualAudioLevel;
AudioSilenceThreshold = InArgs._AudioSilenceThreshold;
AudioSilenceOpacity = InArgs._AudioSilenceOpacity;
ConstructSequence();
}
void SetBarCount(int32 InBarCount)
{
BarCount.Set(FMath::Clamp(InBarCount, 1, 16));
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
}
void SetBarWidth(float InBarWidth)
{
BarWidth.Set(FMath::Max(0.5f, InBarWidth));
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
}
void SetBarGap(float InBarGap)
{
BarGap.Set(FMath::Max(0.0f, InBarGap));
Invalidate(EInvalidateWidgetReason::LayoutAndVolatility);
}
void SetMinHeightFraction(float InMinHeightFraction)
{
MinHeightFraction.Set(FMath::Clamp(InMinHeightFraction, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetAnimationSpeed(float InAnimationSpeed)
{
AnimationSpeed.Set(FMath::Max(0.0f, InAnimationSpeed));
ConstructSequence();
}
void SetGlowStrength(float InGlowStrength)
{
GlowStrength.Set(FMath::Clamp(InGlowStrength, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetCenterBoost(float InCenterBoost)
{
CenterBoost.Set(FMath::Clamp(InCenterBoost, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetBarColor(const FLinearColor& InBarColor)
{
BarColor.Set(InBarColor);
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetIdleBarColor(const FLinearColor& InIdleBarColor)
{
IdleBarColor.Set(InIdleBarColor);
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetIsPlaying(bool bInIsPlaying)
{
bIsPlaying.Set(bInIsPlaying);
if (bInIsPlaying && !Sequence.IsPlaying())
{
Sequence.Play(AsShared(), true);
}
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
}
void SetAudioComponent(UAudioComponent* InAudioComponent)
{
AudioComponent.Set(InAudioComponent);
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
}
void SetUseAudioEnvelope(bool bInUseAudioEnvelope)
{
bUseAudioEnvelope.Set(bInUseAudioEnvelope);
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
}
void SetAudioEnvelopeGain(float InAudioEnvelopeGain)
{
AudioEnvelopeGain.Set(FMath::Max(0.0f, InAudioEnvelopeGain));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetAudioEnvelopeGamma(float InAudioEnvelopeGamma)
{
AudioEnvelopeGamma.Set(FMath::Max(0.1f, InAudioEnvelopeGamma));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetAudioEnvelopeSmoothingSpeed(float InSmoothingSpeed)
{
AudioEnvelopeSmoothingSpeed.Set(FMath::Max(0.0f, InSmoothingSpeed));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetManualAudioLevel(float InAudioLevel)
{
ManualAudioLevel.Set(FMath::Clamp(InAudioLevel, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
}
void SetAudioSilenceThreshold(float InSilenceThreshold)
{
AudioSilenceThreshold.Set(FMath::Clamp(InSilenceThreshold, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::Paint);
}
void SetAudioSilenceOpacity(float InSilenceOpacity)
{
AudioSilenceOpacity.Set(FMath::Clamp(InSilenceOpacity, 0.0f, 1.0f));
Invalidate(EInvalidateWidgetReason::Paint);
}
virtual bool ComputeVolatility() const override
{
return (bIsPlaying.Get(false) || bUseAudioEnvelope.Get(false)) && Sequence.IsPlaying();
}
virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override
{
const int32 Count = FMath::Max(1, BarCount.Get(5));
const float Width = FMath::Max(0.5f, BarWidth.Get(4.0f));
const float Gap = FMath::Max(0.0f, BarGap.Get(3.0f));
return FVector2D(Count * Width + (Count - 1) * Gap, 26.0f);
}
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override
{
const float TargetLevel = ResolveAudioLevel();
const float SmoothingSpeed = FMath::Max(0.0f, AudioEnvelopeSmoothingSpeed.Get(16.0f));
if (SmoothingSpeed <= 0.0f)
{
SmoothedAudioLevel = TargetLevel;
}
else
{
SmoothedAudioLevel = FMath::FInterpTo(SmoothedAudioLevel, TargetLevel, InDeltaTime, SmoothingSpeed);
}
if (bIsPlaying.Get(false) || bUseAudioEnvelope.Get(false) || SmoothedAudioLevel > KINDA_SMALL_NUMBER)
{
Invalidate(EInvalidateWidgetReason::Paint);
}
}
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override
{
const int32 Count = FMath::Max(1, BarCount.Get(5));
const float Width = FMath::Max(0.5f, BarWidth.Get(4.0f));
const float Gap = FMath::Max(0.0f, BarGap.Get(3.0f));
const float MinFraction = FMath::Clamp(MinHeightFraction.Get(0.25f), 0.0f, 1.0f);
const bool bPlaying = bIsPlaying.Get(false);
const bool bEnvelopeDriven = bUseAudioEnvelope.Get(false);
const float SilenceThreshold = FMath::Clamp(AudioSilenceThreshold.Get(0.03f), 0.0f, 1.0f);
const float SilenceOpacity = FMath::Clamp(AudioSilenceOpacity.Get(0.08f), 0.0f, 1.0f);
const float AudioPresence = bEnvelopeDriven
? FMath::GetMappedRangeValueClamped(FVector2f(SilenceThreshold, 1.0f), FVector2f(0.0f, 1.0f), SmoothedAudioLevel)
: 1.0f;
const float VisualPresence = bEnvelopeDriven ? FMath::Clamp(SmoothedAudioLevel, 0.0f, 1.0f) : 1.0f;
const bool bActive = bEnvelopeDriven ? AudioPresence > KINDA_SMALL_NUMBER : bPlaying;
const FLinearColor ActiveColor = BarColor.Get(FLinearColor::White);
const FLinearColor IdleColor = IdleBarColor.Get(FLinearColor::White);
const FVector2f LocalSize = AllottedGeometry.GetLocalSize();
const float TotalWidth = Count * Width + (Count - 1) * Gap;
const float StartX = (LocalSize.X - TotalWidth) * 0.5f;
const float Time = Curve.GetLerp() * TWO_PI;
const float CenterIndex = static_cast<float>(Count - 1) * 0.5f;
const float MaxDistance = FMath::Max(1.0f, CenterIndex);
const float Glow = FMath::Clamp(GlowStrength.Get(0.35f), 0.0f, 1.0f);
const float CenterAccent = FMath::Clamp(CenterBoost.Get(0.35f), 0.0f, 1.0f);
for (int32 Index = 0; Index < Count; ++Index)
{
const float DistanceFromCenter = FMath::Abs(static_cast<float>(Index) - CenterIndex) / MaxDistance;
const float CenterWeight = 1.0f - DistanceFromCenter;
float HeightFraction = MinFraction;
float Energy = 0.0f;
if (bActive)
{
const float SymmetricIndex = FMath::Abs(static_cast<float>(Index) - CenterIndex);
const float Phase = Time - SymmetricIndex * 0.72f;
const float PrimaryWave = 0.5f + 0.5f * FMath::Sin(Phase);
const float SecondaryWave = 0.5f + 0.5f * FMath::Sin(Phase * 0.73f + 1.85f);
const float MixedWave = PrimaryWave * 0.72f + SecondaryWave * 0.28f;
const float WaveEnergy = MixedWave * MixedWave * (3.0f - 2.0f * MixedWave);
const float EnvelopeEnergy = bEnvelopeDriven
? FMath::Clamp(AudioPresence * FMath::Lerp(0.50f, 1.20f, WaveEnergy), 0.0f, 1.0f)
: WaveEnergy;
Energy = bEnvelopeDriven ? EnvelopeEnergy : WaveEnergy;
const float BoostScale = bEnvelopeDriven ? AudioPresence : 1.0f;
Energy = FMath::Clamp(Energy + CenterWeight * CenterAccent * BoostScale, 0.0f, 1.0f);
HeightFraction = FMath::Lerp(MinFraction, 1.0f, Energy);
}
else if (bEnvelopeDriven)
{
HeightFraction = MinFraction;
}
else
{
HeightFraction = FMath::Lerp(MinFraction, 0.42f, CenterWeight);
}
const float BarHeight = FMath::Max(1.0f, LocalSize.Y * HeightFraction);
const FVector2f Position(StartX + Index * (Width + Gap), (LocalSize.Y - BarHeight) * 0.5f);
const FVector2f Size(Width, BarHeight);
const float Radius = Width * 0.5f;
const FLinearColor BarTint = bActive
? ActiveColor.CopyWithNewOpacity(VisualPresence * FMath::Lerp(0.55f, ActiveColor.A, FMath::Max(Energy, CenterWeight * 0.45f)))
: (bEnvelopeDriven
? IdleColor.CopyWithNewOpacity(IdleColor.A * FMath::Lerp(SilenceOpacity, 1.0f, VisualPresence))
: IdleColor);
if (bEnvelopeDriven && !bActive && BarTint.A <= KINDA_SMALL_NUMBER)
{
continue;
}
if (bActive && Glow > 0.0f)
{
const float GlowPadX = Width * FMath::Lerp(0.65f, 1.25f, Energy);
const float GlowPadY = FMath::Lerp(2.0f, 5.5f, Energy);
const FVector2f GlowPosition(Position.X - GlowPadX * 0.5f, Position.Y - GlowPadY * 0.5f);
const FVector2f GlowSize(Size.X + GlowPadX, Size.Y + GlowPadY);
const FSlateRoundedBoxBrush GlowBrush(FLinearColor::White, Radius + GlowPadX);
const float GlowScale = bEnvelopeDriven ? VisualPresence : 1.0f;
const FLinearColor GlowTint = ActiveColor.CopyWithNewOpacity(Glow * GlowScale * FMath::Lerp(0.10f, 0.28f, Energy));
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(GlowSize, FSlateLayoutTransform(GlowPosition)),
&GlowBrush,
ESlateDrawEffect::None,
GlowTint * InWidgetStyle.GetColorAndOpacityTint());
}
const FSlateRoundedBoxBrush BarBrush(FLinearColor::White, Radius);
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 1,
AllottedGeometry.ToPaintGeometry(Size, FSlateLayoutTransform(Position)),
&BarBrush,
ESlateDrawEffect::None,
BarTint * InWidgetStyle.GetColorAndOpacityTint());
}
return LayerId + 1;
}
private:
float ResolveAudioLevel() const
{
if (!bUseAudioEnvelope.Get(false))
{
return FMath::Clamp(ManualAudioLevel.Get(0.0f), 0.0f, 1.0f);
}
float EnvelopeValue = 0.0f;
if (UAudioComponent* Component = AudioComponent.Get(nullptr))
{
const bool bHasCookedEnvelope = Component->GetCookedEnvelopeData(EnvelopeValue);
if (!bHasCookedEnvelope)
{
EnvelopeValue = ManualAudioLevel.Get(0.0f);
}
if (!Component->IsPlaying() && EnvelopeValue <= KINDA_SMALL_NUMBER)
{
return 0.0f;
}
}
else
{
EnvelopeValue = ManualAudioLevel.Get(0.0f);
}
const float ScaledEnvelope = FMath::Clamp(EnvelopeValue * AudioEnvelopeGain.Get(5.0f), 0.0f, 1.0f);
return FMath::Pow(ScaledEnvelope, FMath::Max(AudioEnvelopeGamma.Get(0.65f), 0.1f));
}
void ConstructSequence()
{
const float Speed = FMath::Max(0.01f, AnimationSpeed.Get(4.8f));
const float Period = TWO_PI / Speed;
Sequence = FCurveSequence();
Curve = Sequence.AddCurve(0.0f, Period);
Sequence.Play(AsShared(), true);
Invalidate(EInvalidateWidgetReason::PaintAndVolatility);
}
TAttribute<int32> BarCount;
TAttribute<float> BarWidth;
TAttribute<float> BarGap;
TAttribute<float> MinHeightFraction;
TAttribute<float> AnimationSpeed;
TAttribute<float> GlowStrength;
TAttribute<float> CenterBoost;
TAttribute<FLinearColor> BarColor;
TAttribute<FLinearColor> IdleBarColor;
TAttribute<bool> bIsPlaying;
TAttribute<UAudioComponent*> AudioComponent;
TAttribute<bool> bUseAudioEnvelope;
TAttribute<float> AudioEnvelopeGain;
TAttribute<float> AudioEnvelopeGamma;
TAttribute<float> AudioEnvelopeSmoothingSpeed;
TAttribute<float> ManualAudioLevel;
TAttribute<float> AudioSilenceThreshold;
TAttribute<float> AudioSilenceOpacity;
FCurveSequence Sequence;
FCurveHandle Curve;
float SmoothedAudioLevel = 0.0f;
};
UAudioPlaybackThrobber::UAudioPlaybackThrobber()
{
SetVisibility(ESlateVisibility::SelfHitTestInvisible);
}
void UAudioPlaybackThrobber::SetIsPlaying(bool bInIsPlaying)
{
bIsPlaying = bInIsPlaying;
if (MyThrobber.IsValid())
{
MyThrobber->SetIsPlaying(bIsPlaying);
}
}
void UAudioPlaybackThrobber::SetAudioComponent(UAudioComponent* InAudioComponent)
{
if (AudioComponent == InAudioComponent)
{
return;
}
UnbindAudioComponent();
AudioComponent = InAudioComponent;
BindAudioComponent();
if (MyThrobber.IsValid())
{
MyThrobber->SetAudioComponent(AudioComponent);
}
}
void UAudioPlaybackThrobber::SetUseAudioEnvelope(bool bInUseAudioEnvelope)
{
bUseAudioEnvelope = bInUseAudioEnvelope;
if (MyThrobber.IsValid())
{
MyThrobber->SetUseAudioEnvelope(bUseAudioEnvelope);
}
}
void UAudioPlaybackThrobber::SetManualAudioLevel(float InAudioLevel)
{
ManualAudioLevel = FMath::Clamp(InAudioLevel, 0.0f, 1.0f);
if (MyThrobber.IsValid())
{
MyThrobber->SetManualAudioLevel(ManualAudioLevel);
}
}
void UAudioPlaybackThrobber::BeginDestroy()
{
UnbindAudioComponent();
Super::BeginDestroy();
}
TSharedRef<SWidget> UAudioPlaybackThrobber::RebuildWidget()
{
SAssignNew(MyThrobber, SAudioPlaybackThrobber);
SynchronizeProperties();
return MyThrobber.ToSharedRef();
}
void UAudioPlaybackThrobber::SynchronizeProperties()
{
Super::SynchronizeProperties();
if (!MyThrobber.IsValid())
{
return;
}
MyThrobber->SetBarCount(BarCount);
MyThrobber->SetBarWidth(BarWidth);
MyThrobber->SetBarGap(BarGap);
MyThrobber->SetMinHeightFraction(MinHeightFraction);
MyThrobber->SetAnimationSpeed(AnimationSpeed);
MyThrobber->SetGlowStrength(GlowStrength);
MyThrobber->SetCenterBoost(CenterBoost);
MyThrobber->SetBarColor(BarColor);
MyThrobber->SetIdleBarColor(IdleBarColor);
MyThrobber->SetIsPlaying(bIsPlaying);
BindAudioComponent();
MyThrobber->SetAudioComponent(AudioComponent);
MyThrobber->SetUseAudioEnvelope(bUseAudioEnvelope);
MyThrobber->SetAudioEnvelopeGain(AudioEnvelopeGain);
MyThrobber->SetAudioEnvelopeGamma(AudioEnvelopeGamma);
MyThrobber->SetAudioEnvelopeSmoothingSpeed(AudioEnvelopeSmoothingSpeed);
MyThrobber->SetManualAudioLevel(ManualAudioLevel);
MyThrobber->SetAudioSilenceThreshold(AudioSilenceThreshold);
MyThrobber->SetAudioSilenceOpacity(AudioSilenceOpacity);
}
void UAudioPlaybackThrobber::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
MyThrobber.Reset();
}
void UAudioPlaybackThrobber::HandleAudioEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue)
{
SetManualAudioLevel(FMath::Clamp(EnvelopeValue, 0.0f, 1.0f));
}
void UAudioPlaybackThrobber::UnbindAudioComponent()
{
if (AudioComponent && bAudioEnvelopeDelegateBound)
{
AudioComponent->OnAudioSingleEnvelopeValue.RemoveDynamic(this, &UAudioPlaybackThrobber::HandleAudioEnvelopeValue);
}
bAudioEnvelopeDelegateBound = false;
}
void UAudioPlaybackThrobber::BindAudioComponent()
{
if (!AudioComponent || bAudioEnvelopeDelegateBound)
{
return;
}
AudioComponent->EnvelopeFollowerAttackTime = 8;
AudioComponent->EnvelopeFollowerReleaseTime = 90;
AudioComponent->OnAudioSingleEnvelopeValue.AddUniqueDynamic(this, &UAudioPlaybackThrobber::HandleAudioEnvelopeValue);
bAudioEnvelopeDelegateBound = true;
}
@@ -0,0 +1,94 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/FunnyManagedUserWidget.h"
#include "Engine/LocalPlayer.h"
#include "UI/FunnyUILayerManagerSubsystem.h"
namespace
{
UFunnyUILayerManagerSubsystem* GetLayerManager(const UFunnyManagedUserWidget* Widget)
{
if (!Widget)
{
return nullptr;
}
ULocalPlayer* LocalPlayer = const_cast<UFunnyManagedUserWidget*>(Widget)->GetOwningLocalPlayer();
return LocalPlayer ? LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>() : nullptr;
}
}
void UFunnyManagedUserWidget::SetManagedVisibility(ESlateVisibility InVisibility)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
LayerManager->SetWidgetDesiredVisibility(this, InVisibility);
return;
}
SetVisibility(InVisibility);
}
ESlateVisibility UFunnyManagedUserWidget::GetManagedVisibility() const
{
if (const UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
return LayerManager->GetWidgetDesiredVisibility(const_cast<UFunnyManagedUserWidget*>(this));
}
return GetVisibility();
}
void UFunnyManagedUserWidget::SetManagedEnabled(bool bEnabled)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
LayerManager->SetWidgetDesiredEnabled(this, bEnabled);
return;
}
SetIsEnabled(bEnabled);
}
bool UFunnyManagedUserWidget::GetManagedEnabled() const
{
if (const UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
return LayerManager->GetWidgetDesiredEnabled(const_cast<UFunnyManagedUserWidget*>(this));
}
return GetIsEnabled();
}
void UFunnyManagedUserWidget::RegisterWithUILayer()
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
LayerManager->RegisterWidget(UILayerName, this, bAutoShowWithLayer);
}
}
void UFunnyManagedUserWidget::UnregisterFromUILayer()
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetLayerManager(this))
{
LayerManager->UnregisterWidget(this);
}
}
void UFunnyManagedUserWidget::NativeConstruct()
{
Super::NativeConstruct();
if (bAutoRegisterWithUILayer)
{
RegisterWithUILayer();
}
}
void UFunnyManagedUserWidget::NativeDestruct()
{
UnregisterFromUILayer();
Super::NativeDestruct();
}
@@ -0,0 +1,137 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/FunnyUIBlueprintLibrary.h"
#include "Blueprint/UserWidget.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "UI/FunnyUILayerManagerSubsystem.h"
UFunnyUILayerManagerSubsystem* UFunnyUIBlueprintLibrary::GetUILayerManager(const UObject* WorldContextObject, int32 PlayerIndex)
{
if (!WorldContextObject)
{
return nullptr;
}
if (UUserWidget* Widget = const_cast<UUserWidget*>(Cast<UUserWidget>(WorldContextObject)))
{
if (ULocalPlayer* LocalPlayer = Widget->GetOwningLocalPlayer())
{
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
}
}
if (APlayerController* PlayerController = const_cast<APlayerController*>(Cast<APlayerController>(WorldContextObject)))
{
if (ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer())
{
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
}
}
if (ULocalPlayer* LocalPlayer = const_cast<ULocalPlayer*>(Cast<ULocalPlayer>(WorldContextObject)))
{
return LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>();
}
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(WorldContextObject, PlayerIndex);
if (!PlayerController)
{
return nullptr;
}
ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
return LocalPlayer ? LocalPlayer->GetSubsystem<UFunnyUILayerManagerSubsystem>() : nullptr;
}
void UFunnyUIBlueprintLibrary::RegisterWidgetToLayer(const UObject* WorldContextObject, FName LayerName, UUserWidget* Widget, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->RegisterWidget(LayerName, Widget);
}
}
void UFunnyUIBlueprintLibrary::UnregisterWidgetFromLayer(const UObject* WorldContextObject, UUserWidget* Widget, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->UnregisterWidget(Widget);
}
}
void UFunnyUIBlueprintLibrary::SetManagedWidgetVisibility(const UObject* WorldContextObject, UUserWidget* Widget, ESlateVisibility Visibility, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->SetWidgetDesiredVisibility(Widget, Visibility);
}
}
void UFunnyUIBlueprintLibrary::SetManagedWidgetEnabled(const UObject* WorldContextObject, UUserWidget* Widget, bool bEnabled, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->SetWidgetDesiredEnabled(Widget, bEnabled);
}
}
void UFunnyUIBlueprintLibrary::SetUILayerVisibility(const UObject* WorldContextObject, FName LayerName, ESlateVisibility Visibility, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->SetLayerVisibility(LayerName, Visibility);
}
}
void UFunnyUIBlueprintLibrary::ShowUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->ShowLayer(LayerName);
}
}
void UFunnyUIBlueprintLibrary::HideUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->HideLayer(LayerName);
}
}
void UFunnyUIBlueprintLibrary::SetUILayerEnabled(const UObject* WorldContextObject, FName LayerName, bool bEnabled, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->SetLayerEnabled(LayerName, bEnabled);
}
}
void UFunnyUIBlueprintLibrary::ShowOnlyUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->ShowOnlyLayer(LayerName);
}
}
void UFunnyUIBlueprintLibrary::PushUILayerState(const UObject* WorldContextObject, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
LayerManager->PushLayerState();
}
}
bool UFunnyUIBlueprintLibrary::PopUILayerState(const UObject* WorldContextObject, int32 PlayerIndex)
{
if (UFunnyUILayerManagerSubsystem* LayerManager = GetUILayerManager(WorldContextObject, PlayerIndex))
{
return LayerManager->PopLayerState();
}
return false;
}
@@ -0,0 +1,343 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/FunnyUILayerManagerSubsystem.h"
void UFunnyUILayerManagerSubsystem::RegisterWidget(FName LayerName, UUserWidget* Widget)
{
RegisterWidget(LayerName, Widget, false);
}
void UFunnyUILayerManagerSubsystem::RegisterWidget(FName LayerName, UUserWidget* Widget, bool bAutoShowWithLayer)
{
if (LayerName.IsNone() || !Widget)
{
return;
}
UnregisterWidget(Widget);
FFunnyUILayerWidgetEntry Entry;
Entry.Widget = Widget;
Entry.DesiredVisibility = Widget->GetVisibility();
Entry.bDesiredEnabled = Widget->GetIsEnabled();
Entry.bAutoShowWithLayer = bAutoShowWithLayer;
LayerWidgets.FindOrAdd(LayerName).Add(Entry);
ApplyWidgetState(LayerWidgets[LayerName].Last(), FindOrAddLayerState(LayerName));
}
void UFunnyUILayerManagerSubsystem::UnregisterWidget(UUserWidget* Widget)
{
if (!Widget)
{
return;
}
for (TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
LayerPair.Value.RemoveAll([Widget](const FFunnyUILayerWidgetEntry& Entry)
{
return !Entry.Widget.IsValid() || Entry.Widget.Get() == Widget;
});
}
}
void UFunnyUILayerManagerSubsystem::SetWidgetDesiredVisibility(UUserWidget* Widget, ESlateVisibility Visibility)
{
if (FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
{
Entry->DesiredVisibility = Visibility;
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
if (LayerPair.Value.ContainsByPredicate([Widget](const FFunnyUILayerWidgetEntry& Candidate)
{
return Candidate.Widget.Get() == Widget;
}))
{
ApplyWidgetState(*Entry, FindOrAddLayerState(LayerPair.Key));
return;
}
}
}
else if (Widget)
{
Widget->SetVisibility(Visibility);
}
}
ESlateVisibility UFunnyUILayerManagerSubsystem::GetWidgetDesiredVisibility(UUserWidget* Widget) const
{
if (const FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
{
return Entry->DesiredVisibility;
}
return Widget ? Widget->GetVisibility() : ESlateVisibility::Collapsed;
}
void UFunnyUILayerManagerSubsystem::SetWidgetDesiredEnabled(UUserWidget* Widget, bool bEnabled)
{
if (FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
{
Entry->bDesiredEnabled = bEnabled;
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
if (LayerPair.Value.ContainsByPredicate([Widget](const FFunnyUILayerWidgetEntry& Candidate)
{
return Candidate.Widget.Get() == Widget;
}))
{
ApplyWidgetState(*Entry, FindOrAddLayerState(LayerPair.Key));
return;
}
}
}
else if (Widget)
{
Widget->SetIsEnabled(bEnabled);
}
}
bool UFunnyUILayerManagerSubsystem::GetWidgetDesiredEnabled(UUserWidget* Widget) const
{
if (const FFunnyUILayerWidgetEntry* Entry = FindWidgetEntry(Widget))
{
return Entry->bDesiredEnabled;
}
return Widget ? Widget->GetIsEnabled() : false;
}
void UFunnyUILayerManagerSubsystem::SetLayerVisibility(FName LayerName, ESlateVisibility Visibility)
{
if (LayerName.IsNone())
{
return;
}
FindOrAddLayerState(LayerName).Visibility = Visibility;
if (IsVisibleLayerState(Visibility))
{
AutoShowLayerWidgets(LayerName);
}
ApplyLayerState(LayerName);
}
void UFunnyUILayerManagerSubsystem::ShowLayer(FName LayerName)
{
SetLayerVisibility(LayerName, ESlateVisibility::Visible);
}
void UFunnyUILayerManagerSubsystem::HideLayer(FName LayerName)
{
SetLayerVisibility(LayerName, ESlateVisibility::Collapsed);
}
ESlateVisibility UFunnyUILayerManagerSubsystem::GetLayerVisibility(FName LayerName) const
{
if (const FFunnyUILayerState* State = FindLayerState(LayerName))
{
return State->Visibility;
}
return ESlateVisibility::Visible;
}
void UFunnyUILayerManagerSubsystem::SetLayerEnabled(FName LayerName, bool bEnabled)
{
if (LayerName.IsNone())
{
return;
}
FindOrAddLayerState(LayerName).bEnabled = bEnabled;
ApplyLayerState(LayerName);
}
bool UFunnyUILayerManagerSubsystem::IsLayerEnabled(FName LayerName) const
{
if (const FFunnyUILayerState* State = FindLayerState(LayerName))
{
return State->bEnabled;
}
return true;
}
void UFunnyUILayerManagerSubsystem::ShowOnlyLayer(FName LayerName)
{
if (LayerName.IsNone())
{
return;
}
for (TPair<FName, FFunnyUILayerState>& LayerPair : LayerStates)
{
LayerPair.Value.Visibility = LayerPair.Key == LayerName ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
}
FindOrAddLayerState(LayerName).Visibility = ESlateVisibility::Visible;
AutoShowLayerWidgets(LayerName);
ApplyAllLayers();
}
void UFunnyUILayerManagerSubsystem::PushLayerState()
{
LayerStateStack.Push(LayerStates);
}
bool UFunnyUILayerManagerSubsystem::PopLayerState()
{
if (LayerStateStack.IsEmpty())
{
return false;
}
LayerStates = LayerStateStack.Pop(EAllowShrinking::No);
ApplyAllLayers();
return true;
}
void UFunnyUILayerManagerSubsystem::ApplyAllLayers()
{
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
ApplyLayerState(LayerPair.Key);
}
}
FFunnyUILayerState& UFunnyUILayerManagerSubsystem::FindOrAddLayerState(FName LayerName)
{
return LayerStates.FindOrAdd(LayerName);
}
const FFunnyUILayerState* UFunnyUILayerManagerSubsystem::FindLayerState(FName LayerName) const
{
return LayerStates.Find(LayerName);
}
FFunnyUILayerWidgetEntry* UFunnyUILayerManagerSubsystem::FindWidgetEntry(UUserWidget* Widget)
{
if (!Widget)
{
return nullptr;
}
for (TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
for (FFunnyUILayerWidgetEntry& Entry : LayerPair.Value)
{
if (Entry.Widget.Get() == Widget)
{
return &Entry;
}
}
}
return nullptr;
}
const FFunnyUILayerWidgetEntry* UFunnyUILayerManagerSubsystem::FindWidgetEntry(UUserWidget* Widget) const
{
if (!Widget)
{
return nullptr;
}
for (const TPair<FName, TArray<FFunnyUILayerWidgetEntry>>& LayerPair : LayerWidgets)
{
for (const FFunnyUILayerWidgetEntry& Entry : LayerPair.Value)
{
if (Entry.Widget.Get() == Widget)
{
return &Entry;
}
}
}
return nullptr;
}
void UFunnyUILayerManagerSubsystem::PruneInvalidWidgets(FName LayerName)
{
if (TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
{
Widgets->RemoveAll([](const FFunnyUILayerWidgetEntry& Entry)
{
return !Entry.Widget.IsValid();
});
}
}
void UFunnyUILayerManagerSubsystem::AutoShowLayerWidgets(FName LayerName)
{
if (TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
{
for (FFunnyUILayerWidgetEntry& Entry : *Widgets)
{
if (Entry.bAutoShowWithLayer)
{
Entry.DesiredVisibility = ESlateVisibility::Visible;
}
}
}
}
void UFunnyUILayerManagerSubsystem::ApplyLayerState(FName LayerName)
{
PruneInvalidWidgets(LayerName);
const FFunnyUILayerState& LayerState = FindOrAddLayerState(LayerName);
if (const TArray<FFunnyUILayerWidgetEntry>* Widgets = LayerWidgets.Find(LayerName))
{
for (const FFunnyUILayerWidgetEntry& Entry : *Widgets)
{
ApplyWidgetState(Entry, LayerState);
}
}
}
void UFunnyUILayerManagerSubsystem::ApplyWidgetState(const FFunnyUILayerWidgetEntry& Entry, const FFunnyUILayerState& LayerState) const
{
UUserWidget* Widget = Entry.Widget.Get();
if (!Widget)
{
return;
}
Widget->SetVisibility(CombineVisibility(LayerState.Visibility, Entry.DesiredVisibility));
Widget->SetIsEnabled(LayerState.bEnabled && Entry.bDesiredEnabled);
}
bool UFunnyUILayerManagerSubsystem::IsVisibleLayerState(ESlateVisibility Visibility)
{
return Visibility != ESlateVisibility::Collapsed && Visibility != ESlateVisibility::Hidden;
}
ESlateVisibility UFunnyUILayerManagerSubsystem::CombineVisibility(ESlateVisibility LayerVisibility, ESlateVisibility DesiredVisibility)
{
if (LayerVisibility == ESlateVisibility::Collapsed || LayerVisibility == ESlateVisibility::Hidden)
{
return LayerVisibility;
}
if (DesiredVisibility == ESlateVisibility::Collapsed || DesiredVisibility == ESlateVisibility::Hidden)
{
return DesiredVisibility;
}
if (LayerVisibility == ESlateVisibility::HitTestInvisible)
{
return ESlateVisibility::HitTestInvisible;
}
if (LayerVisibility == ESlateVisibility::SelfHitTestInvisible && DesiredVisibility == ESlateVisibility::Visible)
{
return ESlateVisibility::SelfHitTestInvisible;
}
return DesiredVisibility;
}
@@ -0,0 +1,108 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "UI/TypingTextBlock.h"
#include "Engine/World.h"
#include "TimerManager.h"
UTypingTextBlock::UTypingTextBlock()
: CharacterInterval(0.025f)
, bAutoStartTyping(true)
{
SetText(FText::GetEmpty());
}
void UTypingTextBlock::AppendTextChunk(const FString& TextChunk)
{
if (TextChunk.IsEmpty())
{
return;
}
PendingText += TextChunk;
if (bAutoStartTyping)
{
StartTyping();
}
}
void UTypingTextBlock::ClearTypedText()
{
StopTyping();
DisplayedText.Reset();
PendingText.Reset();
SetText(FText::GetEmpty());
}
void UTypingTextBlock::FlushTyping()
{
if (PendingText.IsEmpty())
{
return;
}
DisplayedText += PendingText;
PendingText.Reset();
SetText(FText::FromString(DisplayedText));
StopTyping();
}
bool UTypingTextBlock::IsTyping() const
{
return !PendingText.IsEmpty();
}
void UTypingTextBlock::ReleaseSlateResources(bool bReleaseChildren)
{
StopTyping();
Super::ReleaseSlateResources(bReleaseChildren);
}
void UTypingTextBlock::StartTyping()
{
if (PendingText.IsEmpty())
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
FlushTyping();
return;
}
if (World->GetTimerManager().IsTimerActive(TypingTimerHandle))
{
return;
}
World->GetTimerManager().SetTimer(TypingTimerHandle, this, &UTypingTextBlock::TypeNextCharacter, CharacterInterval, true);
}
void UTypingTextBlock::StopTyping()
{
if (UWorld* World = GetWorld())
{
World->GetTimerManager().ClearTimer(TypingTimerHandle);
}
}
void UTypingTextBlock::TypeNextCharacter()
{
if (PendingText.IsEmpty())
{
StopTyping();
return;
}
DisplayedText.AppendChars(*PendingText, 1);
PendingText.RightChopInline(1, EAllowShrinking::No);
SetText(FText::FromString(DisplayedText));
if (PendingText.IsEmpty())
{
StopTyping();
}
}
@@ -0,0 +1,360 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Utility/FunnyGirlUtilityBlueprintLibrary.h"
#include "Animation/MorphTarget.h"
#include "Components/Button.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/Texture2D.h"
#include "HAL/FileManager.h"
#include "Misc/Base64.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
namespace
{
bool TryParseJsonObject(const FString& Json, TSharedPtr<FJsonObject>& OutObject)
{
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Json);
return FJsonSerializer::Deserialize(Reader, OutObject) && OutObject.IsValid();
}
FString GetLightSaveFilePath()
{
return FPaths::ProjectSavedDir() / TEXT("LightSave") / TEXT("Strings.json");
}
FString EncodeStringBase64(const FString& Value)
{
FTCHARToUTF8 Utf8Value(*Value);
return FBase64::Encode(reinterpret_cast<const uint8*>(Utf8Value.Get()), Utf8Value.Length());
}
bool DecodeStringBase64(const FString& EncodedValue, FString& OutValue)
{
TArray<uint8> DecodedBytes;
if (!FBase64::Decode(EncodedValue, DecodedBytes))
{
OutValue.Reset();
return false;
}
DecodedBytes.Add(0);
OutValue = FString(UTF8_TO_TCHAR(reinterpret_cast<const char*>(DecodedBytes.GetData())));
return true;
}
bool LoadLightSaveObject(TSharedPtr<FJsonObject>& OutObject, FString& OutError)
{
OutObject = MakeShared<FJsonObject>();
OutError.Reset();
const FString SaveFilePath = GetLightSaveFilePath();
if (!FPaths::FileExists(SaveFilePath))
{
return true;
}
FString SaveJson;
if (!FFileHelper::LoadFileToString(SaveJson, *SaveFilePath))
{
OutError = FString::Printf(TEXT("Could not read light save file: %s"), *SaveFilePath);
return false;
}
if (SaveJson.TrimStartAndEnd().IsEmpty())
{
return true;
}
if (!TryParseJsonObject(SaveJson, OutObject))
{
OutError = FString::Printf(TEXT("Could not parse light save file: %s"), *SaveFilePath);
return false;
}
return true;
}
bool SaveLightSaveObject(const TSharedRef<FJsonObject>& SaveObject, FString& OutError)
{
OutError.Reset();
const FString SaveFilePath = GetLightSaveFilePath();
IFileManager::Get().MakeDirectory(*FPaths::GetPath(SaveFilePath), true);
FString SaveJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&SaveJson);
if (!FJsonSerializer::Serialize(SaveObject, Writer))
{
OutError = TEXT("Could not serialize light save JSON.");
return false;
}
if (!FFileHelper::SaveStringToFile(SaveJson, *SaveFilePath))
{
OutError = FString::Printf(TEXT("Could not write light save file: %s"), *SaveFilePath);
return false;
}
return true;
}
}
bool UFunnyGirlUtilityBlueprintLibrary::SaveMorphTargetNamesToFile(
USkeletalMesh* SkeletalMesh,
FString& OutFilePath,
FString& OutError)
{
OutFilePath.Reset();
OutError.Reset();
if (!SkeletalMesh)
{
OutError = TEXT("Skeletal mesh is null.");
return false;
}
FString MeshName = SkeletalMesh->GetName().TrimStartAndEnd();
if (MeshName.IsEmpty())
{
MeshName = TEXT("SkeletalMesh");
}
MeshName = FPaths::MakeValidFileName(MeshName);
const FString Directory = FPaths::ProjectSavedDir() / TEXT("MorphTarget");
IFileManager::Get().MakeDirectory(*Directory, true);
OutFilePath = Directory / FString::Printf(TEXT("%s.json"), *MeshName);
TArray<TSharedPtr<FJsonValue>> MorphTargetValues;
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (!MorphTarget)
{
continue;
}
const FString MorphTargetName = MorphTarget->GetName().TrimStartAndEnd();
if (!MorphTargetName.IsEmpty())
{
MorphTargetValues.Add(MakeShared<FJsonValueString>(MorphTargetName));
}
}
TSharedRef<FJsonObject> RootObject = MakeShared<FJsonObject>();
RootObject->SetStringField(TEXT("mesh_name"), SkeletalMesh->GetName());
RootObject->SetStringField(TEXT("mesh_path"), SkeletalMesh->GetPathName());
RootObject->SetStringField(TEXT("saved_at"), FDateTime::Now().ToIso8601());
RootObject->SetNumberField(TEXT("morph_target_count"), MorphTargetValues.Num());
RootObject->SetArrayField(TEXT("morph_targets"), MorphTargetValues);
FString OutputJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputJson);
if (!FJsonSerializer::Serialize(RootObject, Writer))
{
OutError = TEXT("Could not serialize morph target JSON.");
return false;
}
if (!FFileHelper::SaveStringToFile(OutputJson, *OutFilePath))
{
OutError = FString::Printf(TEXT("Could not save morph target file: %s"), *OutFilePath);
return false;
}
return true;
}
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonStringField(const FString& Json, const FString& FieldName, FString& OutValue)
{
OutValue.Reset();
FString CleanFieldName = FieldName;
CleanFieldName.TrimStartAndEndInline();
if (CleanFieldName.IsEmpty())
{
return false;
}
TSharedPtr<FJsonObject> RootObject;
if (!TryParseJsonObject(Json, RootObject))
{
return false;
}
return RootObject->TryGetStringField(CleanFieldName, OutValue);
}
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonNumberField(const FString& Json, const FString& FieldName, float& OutValue)
{
OutValue = 0.0f;
FString CleanFieldName = FieldName;
CleanFieldName.TrimStartAndEndInline();
if (CleanFieldName.IsEmpty())
{
return false;
}
TSharedPtr<FJsonObject> RootObject;
if (!TryParseJsonObject(Json, RootObject))
{
return false;
}
double NumberValue = 0.0;
if (!RootObject->TryGetNumberField(CleanFieldName, NumberValue))
{
return false;
}
OutValue = static_cast<float>(NumberValue);
return true;
}
bool UFunnyGirlUtilityBlueprintLibrary::GetJsonBoolField(const FString& Json, const FString& FieldName, bool& OutValue)
{
OutValue = false;
FString CleanFieldName = FieldName;
CleanFieldName.TrimStartAndEndInline();
if (CleanFieldName.IsEmpty())
{
return false;
}
TSharedPtr<FJsonObject> RootObject;
if (!TryParseJsonObject(Json, RootObject))
{
return false;
}
return RootObject->TryGetBoolField(CleanFieldName, OutValue);
}
bool UFunnyGirlUtilityBlueprintLibrary::SetButtonStyleTexture(
UButton* Button,
UTexture2D* Texture,
EFunnyGirlButtonBrushState State)
{
if (!Button || !Texture)
{
return false;
}
FButtonStyle ButtonStyle = Button->GetStyle();
FSlateBrush* TargetBrush = nullptr;
switch (State)
{
case EFunnyGirlButtonBrushState::Normal:
TargetBrush = &ButtonStyle.Normal;
break;
case EFunnyGirlButtonBrushState::Hovered:
TargetBrush = &ButtonStyle.Hovered;
break;
case EFunnyGirlButtonBrushState::Pressed:
TargetBrush = &ButtonStyle.Pressed;
break;
default:
return false;
}
TargetBrush->SetResourceObject(Texture);
if (TargetBrush->ImageSize.IsNearlyZero())
{
TargetBrush->ImageSize = FVector2D(Texture->GetSizeX(), Texture->GetSizeY());
}
Button->SetStyle(ButtonStyle);
return true;
}
bool UFunnyGirlUtilityBlueprintLibrary::SaveLightString(const FString& Field, const FString& Value, FString& OutError)
{
OutError.Reset();
const FString CleanField = Field.TrimStartAndEnd();
if (CleanField.IsEmpty())
{
OutError = TEXT("Field is empty.");
return false;
}
TSharedPtr<FJsonObject> SaveObject;
if (!LoadLightSaveObject(SaveObject, OutError))
{
return false;
}
SaveObject->SetStringField(EncodeStringBase64(CleanField), EncodeStringBase64(Value));
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
}
bool UFunnyGirlUtilityBlueprintLibrary::LoadLightString(const FString& Field, FString& OutValue, FString& OutError)
{
OutValue.Reset();
OutError.Reset();
const FString CleanField = Field.TrimStartAndEnd();
if (CleanField.IsEmpty())
{
OutError = TEXT("Field is empty.");
return false;
}
TSharedPtr<FJsonObject> SaveObject;
if (!LoadLightSaveObject(SaveObject, OutError))
{
return false;
}
FString EncodedValue;
if (!SaveObject->TryGetStringField(EncodeStringBase64(CleanField), EncodedValue))
{
OutError = FString::Printf(TEXT("Field not found: %s"), *CleanField);
return false;
}
if (!DecodeStringBase64(EncodedValue, OutValue))
{
OutError = FString::Printf(TEXT("Could not decode value for field: %s"), *CleanField);
return false;
}
return true;
}
bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field, FString& OutError)
{
OutError.Reset();
const FString CleanField = Field.TrimStartAndEnd();
if (CleanField.IsEmpty())
{
OutError = TEXT("Field is empty.");
return false;
}
TSharedPtr<FJsonObject> SaveObject;
if (!LoadLightSaveObject(SaveObject, OutError))
{
return false;
}
const FString EncodedField = EncodeStringBase64(CleanField);
if (!SaveObject->HasField(EncodedField))
{
OutError = FString::Printf(TEXT("Field not found: %s"), *CleanField);
return false;
}
SaveObject->RemoveField(EncodedField);
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
}
@@ -0,0 +1,80 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "World/InteractableObjectComponent.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
UInteractableObjectComponent::UInteractableObjectComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
bool UInteractableObjectComponent::HasAction(FName Action) const
{
FInteractableObjectAction FoundAction;
return FindAction(Action, FoundAction);
}
bool UInteractableObjectComponent::FindAction(FName Action, FInteractableObjectAction& OutAction) const
{
for (const FInteractableObjectAction& Candidate : Actions)
{
if (Candidate.Action == Action)
{
OutAction = Candidate;
return true;
}
}
return false;
}
bool UInteractableObjectComponent::ExecuteAction(const FString& ActionJson)
{
FName Action;
if (!TryExtractActionFromJson(ActionJson, Action))
{
return false;
}
FInteractableObjectAction ActionData;
if (!FindAction(Action, ActionData))
{
return false;
}
OnActionRequested.Broadcast(Action, ActionJson);
return OnExecuteAction(ActionJson);
}
bool UInteractableObjectComponent::OnExecuteAction_Implementation(const FString& ActionJson)
{
return true;
}
bool UInteractableObjectComponent::TryExtractActionFromJson(const FString& ActionJson, FName& OutAction) const
{
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ActionJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
return false;
}
FString ActionString;
if (!RootObject->TryGetStringField(TEXT("action"), ActionString) && !RootObject->TryGetStringField(TEXT("Action"), ActionString))
{
return false;
}
ActionString.TrimStartAndEndInline();
if (ActionString.IsEmpty())
{
return false;
}
OutAction = FName(*ActionString);
return true;
}
@@ -0,0 +1,330 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "World/WorldContextActor.h"
#include "Dom/JsonObject.h"
#include "EngineUtils.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "World/InteractableObjectComponent.h"
#include "World/WorldContextDataAsset.h"
namespace
{
bool TryExtractInteractableObjectID(const FString& ActionJson, FString& OutObjectID)
{
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ActionJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
return false;
}
if (!RootObject->TryGetStringField(TEXT("object_id"), OutObjectID) &&
!RootObject->TryGetStringField(TEXT("objectId"), OutObjectID) &&
!RootObject->TryGetStringField(TEXT("ObjectID"), OutObjectID))
{
return false;
}
OutObjectID.TrimStartAndEndInline();
return !OutObjectID.IsEmpty();
}
TSharedPtr<FJsonObject> BuildInteractableObjectStateJsonObject(const TMap<FString, FString>& State)
{
TSharedRef<FJsonObject> StateObject = MakeShared<FJsonObject>();
for (const TPair<FString, FString>& StatePair : State)
{
FString Key = StatePair.Key;
Key.TrimStartAndEndInline();
if (Key.IsEmpty())
{
continue;
}
FString Value = StatePair.Value;
Value.TrimStartAndEndInline();
if (Value.IsEmpty())
{
StateObject->SetField(Key, MakeShared<FJsonValueNull>());
}
else
{
StateObject->SetStringField(Key, Value);
}
}
if (StateObject->Values.IsEmpty())
{
return nullptr;
}
return StateObject;
}
TSharedPtr<FJsonValue> BuildActionFormatJsonValue(const FText& ActionFormat)
{
const FString FormatText = ActionFormat.ToString().TrimStartAndEnd();
if (FormatText.IsEmpty())
{
return nullptr;
}
TSharedPtr<FJsonObject> FormatObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(FormatText);
if (FJsonSerializer::Deserialize(Reader, FormatObject) && FormatObject.IsValid())
{
return MakeShared<FJsonValueObject>(FormatObject);
}
return MakeShared<FJsonValueString>(FormatText);
}
TSharedPtr<FJsonObject> BuildInteractableObjectJsonObject(const UInteractableObjectComponent* InteractableComponent)
{
if (!InteractableComponent)
{
return nullptr;
}
TSharedRef<FJsonObject> ObjectJson = MakeShared<FJsonObject>();
if (!InteractableComponent->ID.IsEmpty())
{
ObjectJson->SetStringField(TEXT("id"), InteractableComponent->ID);
}
if (!InteractableComponent->Name.IsEmpty())
{
ObjectJson->SetStringField(TEXT("name"), InteractableComponent->Name.ToString());
}
if (!InteractableComponent->Description.IsEmpty())
{
ObjectJson->SetStringField(TEXT("description"), InteractableComponent->Description.ToString());
}
if (!InteractableComponent->State.IsEmpty())
{
const TSharedPtr<FJsonObject> StateJson = BuildInteractableObjectStateJsonObject(InteractableComponent->State);
if (StateJson.IsValid())
{
ObjectJson->SetObjectField(TEXT("state"), StateJson);
}
}
if (!InteractableComponent->CustomData.IsEmpty())
{
ObjectJson->SetStringField(TEXT("custom_data"), InteractableComponent->CustomData.ToString());
}
if (!InteractableComponent->Actions.IsEmpty())
{
TArray<TSharedPtr<FJsonValue>> ActionsJson;
for (const FInteractableObjectAction& Action : InteractableComponent->Actions)
{
if (Action.Action.IsNone() && Action.Description.IsEmpty() && Action.ActionFormat.IsEmpty())
{
continue;
}
TSharedRef<FJsonObject> ActionJson = MakeShared<FJsonObject>();
if (!Action.Action.IsNone())
{
ActionJson->SetStringField(TEXT("action"), Action.Action.ToString());
}
if (!Action.Description.IsEmpty())
{
ActionJson->SetStringField(TEXT("description"), Action.Description.ToString());
}
if (const TSharedPtr<FJsonValue> FormatJson = BuildActionFormatJsonValue(Action.ActionFormat))
{
ActionJson->SetField(TEXT("format"), FormatJson);
}
ActionsJson.Add(MakeShared<FJsonValueObject>(ActionJson));
}
if (!ActionsJson.IsEmpty())
{
ObjectJson->SetArrayField(TEXT("actions"), ActionsJson);
}
}
if (ObjectJson->Values.IsEmpty())
{
return nullptr;
}
return ObjectJson;
}
}
AWorldContextActor::AWorldContextActor()
{
PrimaryActorTick.bCanEverTick = false;
}
FString AWorldContextActor::BuildWorldContextPrompt() const
{
TArray<FString> PromptParts;
const FString BaseWorldContextPrompt = BuildBaseWorldContextPrompt();
if (!BaseWorldContextPrompt.IsEmpty())
{
PromptParts.Add(BaseWorldContextPrompt);
}
const FString InteractableObjectsPrompt = BuildInteractableObjectsPrompt();
if (!InteractableObjectsPrompt.IsEmpty())
{
PromptParts.Add(InteractableObjectsPrompt);
}
return FString::Join(PromptParts, TEXT("\n\n"));
}
FString AWorldContextActor::BuildBaseWorldContextPrompt() const
{
return WorldContext ? WorldContext->BuildWorldContextPrompt() : FString();
}
FString AWorldContextActor::BuildInteractableObjectsPrompt() const
{
EnsureInteractableObjectsCached();
TArray<TSharedPtr<FJsonValue>> ObjectsJson;
for (const TWeakObjectPtr<UInteractableObjectComponent>& InteractableObject : InteractableObjects)
{
const UInteractableObjectComponent* InteractableComponent = InteractableObject.Get();
const TSharedPtr<FJsonObject> ObjectJson = BuildInteractableObjectJsonObject(InteractableComponent);
if (ObjectJson.IsValid())
{
ObjectsJson.Add(MakeShared<FJsonValueObject>(ObjectJson));
}
}
if (ObjectsJson.IsEmpty())
{
return FString();
}
FString ObjectsJsonString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ObjectsJsonString);
FJsonSerializer::Serialize(ObjectsJson, Writer);
return FString::Printf(TEXT("INTERACTABLE OBJECTS JSON:\n%s"), *ObjectsJsonString);
}
void AWorldContextActor::RefreshInteractableObjects() const
{
InteractableObjects.Empty();
InteractableObjectsByID.Empty();
const UWorld* CurrentWorld = GetWorld();
if (!CurrentWorld)
{
bInteractableObjectsCached = false;
return;
}
bInteractableObjectsCached = true;
for (TActorIterator<AActor> ActorIterator(CurrentWorld); ActorIterator; ++ActorIterator)
{
AActor* Actor = *ActorIterator;
if (!Actor || Actor->IsPendingKillPending())
{
continue;
}
TArray<UInteractableObjectComponent*> InteractableComponents;
Actor->GetComponents<UInteractableObjectComponent>(InteractableComponents);
for (UInteractableObjectComponent* InteractableComponent : InteractableComponents)
{
if (!InteractableComponent)
{
continue;
}
FString ObjectID = InteractableComponent->ID;
ObjectID.TrimStartAndEndInline();
if (ObjectID.IsEmpty())
{
continue;
}
if (InteractableObjectsByID.Contains(ObjectID))
{
UE_LOG(LogTemp, Warning, TEXT("Duplicate interactable object ID '%s' on %s. Keeping the first object."), *ObjectID, *GetNameSafe(Actor));
continue;
}
InteractableObjects.Add(InteractableComponent);
InteractableObjectsByID.Add(ObjectID, InteractableComponent);
}
}
}
void AWorldContextActor::EnsureInteractableObjectsCached() const
{
if (!bInteractableObjectsCached)
{
RefreshInteractableObjects();
}
}
UInteractableObjectComponent* AWorldContextActor::FindInteractableObjectByID(const FString& ObjectID) const
{
EnsureInteractableObjectsCached();
FString CleanObjectID = ObjectID;
CleanObjectID.TrimStartAndEndInline();
if (CleanObjectID.IsEmpty())
{
return nullptr;
}
if (const TWeakObjectPtr<UInteractableObjectComponent>* FoundComponent = InteractableObjectsByID.Find(CleanObjectID))
{
return FoundComponent->Get();
}
return nullptr;
}
bool AWorldContextActor::ExecuteInteractableObjectAction(const FString& ActionJson, FString& OutError)
{
FString ObjectID;
if (!TryExtractInteractableObjectID(ActionJson, ObjectID))
{
OutError = TEXT("Interactable action JSON must contain a non-empty object_id.");
return false;
}
UInteractableObjectComponent* InteractableObject = FindInteractableObjectByID(ObjectID);
if (!InteractableObject)
{
OutError = FString::Printf(TEXT("Interactable object not found: %s"), *ObjectID);
UE_LOG(LogTemp, Warning, TEXT("Interactable action failed: %s"), *OutError);
return false;
}
if (!InteractableObject->ExecuteAction(ActionJson))
{
OutError = FString::Printf(TEXT("Interactable object '%s' rejected the requested action."), *ObjectID);
UE_LOG(LogTemp, Warning, TEXT("Interactable action failed: %s ActionJson=%s"), *OutError, *ActionJson);
return false;
}
UE_LOG(LogTemp, Log, TEXT("Interactable action executed on '%s': %s"), *ObjectID, *ActionJson);
OutError.Reset();
return true;
}
@@ -0,0 +1,25 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "World/WorldContextDataAsset.h"
FString UWorldContextDataAsset::BuildWorldContextPrompt() const
{
TArray<FString> Lines;
if (!LocationName.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("- Location: %s"), *LocationName.ToString()));
}
if (!SceneDescription.IsEmpty())
{
Lines.Add(FString::Printf(TEXT("\nScene description:\n%s"), *SceneDescription.ToString()));
}
if (Lines.IsEmpty())
{
return FString();
}
return FString::Printf(TEXT("[WORLD CONTEXT]\n%s"), *FString::Join(Lines, TEXT("\n")));
}
@@ -0,0 +1,26 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "PersonalityTraitDataAsset.generated.h"
UCLASS(BlueprintType)
class FUNNYGIRL_API UPersonalityTraitDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait")
FString ID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait")
FText Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality Trait", meta = (MultiLine = "true"))
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true"))
FText Prompt;
};
@@ -0,0 +1,181 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "TouchInputComponent.generated.h"
UENUM(BlueprintType)
enum class EFunnyGirlTouchSwipeDirection : uint8
{
None UMETA(DisplayName = "None"),
Up UMETA(DisplayName = "Up"),
Down UMETA(DisplayName = "Down"),
Left UMETA(DisplayName = "Left"),
Right UMETA(DisplayName = "Right")
};
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FTouchGestureData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
int32 FingerCount = 0;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
FVector2D StartLocation = FVector2D::ZeroVector;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
FVector2D Location = FVector2D::ZeroVector;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
FVector2D Delta = FVector2D::ZeroVector;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
FVector2D TotalDelta = FVector2D::ZeroVector;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
EFunnyGirlTouchSwipeDirection SwipeDirection = EFunnyGirlTouchSwipeDirection::None;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
float Distance = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "Touch Gesture")
float Duration = 0.0f;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTouchGestureEvent, FTouchGestureData, Gesture);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTouchZoomEvent, float, ZoomDelta);
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
class FUNNYGIRL_API UTouchInputComponent : public UActorComponent
{
GENERATED_BODY()
public:
UTouchInputComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input")
bool bEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
float TapMaxDuration = 0.25f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float TapMaxMovement = 24.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float SwipeMinDistance = 80.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
float SwipeMaxDuration = 0.75f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "1.0"))
float SwipeDirectionBias = 1.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float MoveMinDistancePerFrame = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float MoveStartDelay = 0.12f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.01"))
float HoldDuration = 0.45f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float HoldMaxMovement = 20.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input", meta = (ClampMin = "0.0"))
float PinchMinScaleDelta = 0.01f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Touch Input")
bool bLogWarnings = true;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Tap")
FTouchGestureEvent OnOneFingerTap;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Tap")
FTouchGestureEvent OnTwoFingerTap;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Swipe")
FTouchGestureEvent OnOneFingerSwipe;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Move")
FTouchGestureEvent OnOneFingerMove;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Hold")
FTouchGestureEvent OnOneFingerHold;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Hold")
FTouchGestureEvent OnTwoFingerHold;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Zoom")
FTouchZoomEvent OnZoom;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Touch Input|Pinch")
FTouchGestureEvent OnFourFingerPinch;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Touch Input")
void ResetTouchState();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
virtual void BeginPlay() override;
private:
enum class ETouchGestureState : uint8
{
None,
Move,
Hold,
Swipe,
ZoomOrPinch
};
struct FTrackedTouch
{
bool bPressed = false;
FVector2D StartLocation = FVector2D::ZeroVector;
FVector2D Location = FVector2D::ZeroVector;
double StartTime = 0.0;
};
static constexpr int32 MaxTouchCount = 10;
FTrackedTouch Touches[MaxTouchCount];
bool bGestureActive = false;
ETouchGestureState GestureState = ETouchGestureState::None;
bool bMoveStarted = false;
int32 CurrentFingerCount = 0;
int32 GestureFingerCount = 0;
double GestureStartTime = 0.0;
FVector2D GestureStartCentroid = FVector2D::ZeroVector;
FVector2D PreviousCentroid = FVector2D::ZeroVector;
float InitialSpan = 0.0f;
float PreviousSpan = 0.0f;
float MaxDistanceFromStart = 0.0f;
void PollTouches(float DeltaTime);
void BeginGesture(int32 FingerCount, const FVector2D& Centroid, double CurrentTime);
void EndGesture(double CurrentTime);
void HandleFingerCountChanged(int32 FingerCount, const FVector2D& Centroid, double CurrentTime);
void HandleActiveGesture(int32 FingerCount, const FVector2D& Centroid, float DeltaTime, double CurrentTime);
void ResetGestureTracking();
class APlayerController* ResolvePlayerController() const;
int32 ReadPressedTouches();
FVector2D GetCentroid() const;
float GetAverageSpan(const FVector2D& Centroid) const;
EFunnyGirlTouchSwipeDirection GetSwipeDirection(const FVector2D& TotalDelta) const;
bool IsStableSwipe(const FVector2D& TotalDelta, float Duration) const;
FTouchGestureData BuildGestureData(int32 FingerCount, const FVector2D& Location, const FVector2D& Delta, double CurrentTime) const;
void BroadcastTap(const FTouchGestureData& Gesture);
void BroadcastSwipe(const FTouchGestureData& Gesture);
void BroadcastMove(const FTouchGestureData& Gesture);
void BroadcastHold(const FTouchGestureData& Gesture);
void BroadcastPinchOrZoom(const FTouchGestureData& Gesture, float ZoomDelta);
};
@@ -0,0 +1,74 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Interfaces/IHttpRequest.h"
#include "BrainComponentBase.generated.h"
class AWorldContextActor;
class FJsonObject;
class FJsonValue;
class UBrainToolBase;
class ULLMProviderDataAsset;
UCLASS(Abstract, ClassGroup = (AI))
class FUNNYGIRL_API UBrainComponentBase : public UActorComponent
{
GENERATED_BODY()
public:
UBrainComponentBase();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
TObjectPtr<ULLMProviderDataAsset> Provider;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
TObjectPtr<AWorldContextActor> WorldContextActor;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
bool bAutoFindWorldContextActor;
UPROPERTY(BlueprintReadOnly, Category = "Brain")
bool bIsThinking;
UFUNCTION(BlueprintCallable, Category = "Brain")
virtual void CancelRequest();
UFUNCTION(BlueprintPure, Category = "Brain")
bool IsThinking() const;
UFUNCTION(BlueprintPure, Category = "Brain|History")
virtual FString GetChatIdentityId() const;
protected:
virtual void BeginPlay() override;
TSharedPtr<IHttpRequest, ESPMode::ThreadSafe> ActiveRequest;
virtual void BroadcastBrainError(const FString& ErrorMessage) const;
virtual FString GetBrainDebugName() const;
void SetIsThinking(bool bNewIsThinking);
void AutoFindWorldContextActor();
bool ValidateProvider(const ULLMProviderDataAsset* EffectiveProvider) const;
bool ConfigureRequestWithBody(
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request,
const ULLMProviderDataAsset* EffectiveProvider,
const FString& RequestBody,
const FString& AcceptHeader) const;
void AddSystemMessage(TArray<TSharedPtr<FJsonValue>>& Messages, const FString& Content) const;
TSharedRef<FJsonObject> MakeMessageObject(const FString& Role, const FString& Content) const;
FString SerializeJsonObject(const TSharedRef<FJsonObject>& RootObject) const;
void AppendAllowedTools(TSharedRef<FJsonObject> RootObject, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const;
TSharedPtr<FJsonObject> BuildToolDefinition(const UBrainToolBase* ToolDefaultObject) const;
const UBrainToolBase* FindAllowedToolDefaultObject(const FString& ToolName, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools) const;
bool ExecuteBrainToolCalls(const TArray<TSharedPtr<FJsonValue>>& ToolCalls, const TArray<TSubclassOf<UBrainToolBase>>& AllowedTools);
FString GetChatDirectory() const;
FString GetChatFilePath() const;
FString GetChatSiblingFilePath(const FString& FileName) const;
void SaveRequestBodyToFile(const FString& RequestBody, const FString& FileName) const;
};
@@ -0,0 +1,41 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Templates/SubclassOf.h"
#include "BrainPromptDataAsset.generated.h"
class UBrainToolBase;
UCLASS(BlueprintType)
class FUNNYGIRL_API UBrainPromptDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Brain", meta = (DisplayPriority = "0"))
FString ID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "20"))
FText Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "10"))
FText SystemPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (MultiLine = "true", DisplayPriority = "10"))
FText ToolsPromptHeader;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (MultiLine = "true", DisplayPriority = "30"))
FText ToolsPromptFooter;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Tools", meta = (DisplayPriority = "20"))
TArray<TSubclassOf<UBrainToolBase>> AllowedTools;
UFUNCTION(BlueprintPure, Category = "Brain")
virtual FString BuildBrainPrompt() const;
UFUNCTION(BlueprintPure, Category = "Tools")
FString BuildToolsPrompt() const;
};
@@ -0,0 +1,39 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "BrainToolBase.generated.h"
class UBrainComponentBase;
UCLASS(BlueprintType, Blueprintable, Abstract)
class FUNNYGIRL_API UBrainToolBase : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool")
FString ToolName;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool", meta = (MultiLine = "true"))
FText Description;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Brain|Tool", meta = (MultiLine = "true"))
FText ParametersJsonSchema;
UFUNCTION(BlueprintPure, Category = "Brain|Tool")
FString GetResolvedToolName() const;
UFUNCTION(BlueprintPure, Category = "Brain|Tool")
FString BuildToolPrompt() const;
UFUNCTION(BlueprintNativeEvent, Category = "Brain|Tool")
bool ExecuteTool(UBrainComponentBase* Brain, const FString& ArgumentsJson, UPARAM(ref) FString& OutError);
virtual bool ExecuteTool_Implementation(UBrainComponentBase* Brain, const FString& ArgumentsJson, FString& OutError);
UFUNCTION(BlueprintNativeEvent, Category = "Brain|Tool")
void CleanupTool();
virtual void CleanupTool_Implementation();
};
@@ -0,0 +1,78 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "LLM/BrainComponentBase.h"
#include "LLM/PersonaBrainComponent.h"
#include "DirectorBrainComponent.generated.h"
class UDirectorPromptDataAsset;
class ULLMProviderDataAsset;
class FJsonObject;
class FJsonValue;
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FDirectorBrainResponse
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
FString Content;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDirectorBrainResponseDelegate, const FDirectorBrainResponse&, Response);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDirectorBrainErrorDelegate, const FString&, ErrorMessage);
UCLASS(ClassGroup = (AI), meta = (BlueprintSpawnableComponent, DisplayName = "Director Brain"))
class FUNNYGIRL_API UDirectorBrainComponent : public UBrainComponentBase
{
GENERATED_BODY()
public:
UDirectorBrainComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
TObjectPtr<UPersonaBrainComponent> PersonaBrain;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director")
TObjectPtr<UDirectorPromptDataAsset> DirectorPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Director|Memory", meta = (MultiLine = "true"))
FText ExistingPlayerFactsPromptTemplate;
UPROPERTY(BlueprintAssignable, Category = "Director|Events")
FDirectorBrainResponseDelegate OnDirectorResponse;
UPROPERTY(BlueprintAssignable, Category = "Director|Events")
FDirectorBrainErrorDelegate OnDirectorError;
UFUNCTION(BlueprintCallable, Category = "Director")
void BindToPersonaBrain(UPersonaBrainComponent* Brain);
UFUNCTION(BlueprintCallable, Category = "Director")
void AnalyzeCurrentChat();
virtual FString GetChatIdentityId() const override;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void BroadcastBrainError(const FString& ErrorMessage) const override;
virtual FString GetBrainDebugName() const override;
private:
UFUNCTION()
void HandlePersonaBrainResponse(const FLLMReplyResponse& Response);
void AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
FString BuildRequestBody(const ULLMProviderDataAsset* EffectiveProvider) const;
FString GetSemanticsFilePath() const;
void AppendSavedSemanticMemoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
void AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const;
void AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
bool ValidateSettings() const;
bool HasValidAllowedTool() const;
bool ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request) const;
void HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
};
@@ -0,0 +1,29 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "LLM/BrainPromptDataAsset.h"
#include "DirectorPromptDataAsset.generated.h"
UCLASS(BlueprintType)
class FUNNYGIRL_API UDirectorPromptDataAsset : public UBrainPromptDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Context", meta = (MultiLine = "true"))
FText SavedSemanticsContextPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting|Context", meta = (MultiLine = "true"))
FText TranscriptContextPrompt;
UFUNCTION(BlueprintPure, Category = "Director")
FString BuildDirectorPrompt() const;
UFUNCTION(BlueprintPure, Category = "Director")
FString BuildSavedSemanticsContext(const TArray<FString>& SemanticLines) const;
UFUNCTION(BlueprintPure, Category = "Director")
FString BuildTranscriptContext(const TArray<FString>& TranscriptLines) const;
};
@@ -0,0 +1,49 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "LLM/BrainPromptDataAsset.h"
#include "IdentityDataAsset.generated.h"
class UPersonalityTraitDataAsset;
UCLASS(BlueprintType)
class FUNNYGIRL_API UIdentityDataAsset : public UBrainPromptDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "30"))
FText Age;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "40"))
FText Appearance;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "50"))
FText Background;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "60"))
FText CoreInstruction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "70"))
FText SpeechStyle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "80"))
FText DialogueBehavior;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "90"))
FText PersonalityTraitsInstruction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (DisplayPriority = "100"))
TArray<TObjectPtr<UPersonalityTraitDataAsset>> PersonalityTraits;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true", DisplayPriority = "110"))
FText FirstMeetingRule;
UFUNCTION(BlueprintPure, Category = "Identity")
FString BuildIdentityPrompt() const;
UFUNCTION(BlueprintPure, Category = "Identity")
FString BuildFirstMeetingPrompt() const;
};
@@ -0,0 +1,49 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "LLMProviderDataAsset.generated.h"
UCLASS(BlueprintType)
class FUNNYGIRL_API ULLMProviderDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
ULLMProviderDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
FString ProviderName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
FString ApiBaseUrl;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
FString ChatCompletionsPath;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
FString ApiKey;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
FString Model;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "0.0", ClampMax = "2.0"))
float Temperature;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "1"))
int32 MaxTokens;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Generation", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float TopP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Network", meta = (ClampMin = "1.0"))
float TimeoutSeconds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Provider")
TMap<FString, FString> ExtraHeaders;
UFUNCTION(BlueprintPure, Category = "LLM|Provider")
FString GetChatCompletionsUrl() const;
};
@@ -0,0 +1,129 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "LLM/BrainComponentBase.h"
#include "Interfaces/IHttpRequest.h"
#include "PersonaBrainComponent.generated.h"
class USystemEventDataAsset;
class UIdentityDataAsset;
class UMorphControllerComponent;
class FJsonObject;
class FJsonValue;
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FLLMReplyResponse
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
FString Reply;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Interaction")
FString InteractActionJson;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Emotion")
FString Emotion;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
UCLASS(ClassGroup = (AI), meta = (BlueprintSpawnableComponent, DisplayName = "Persona Brain"))
class FUNNYGIRL_API UPersonaBrainComponent : public UBrainComponentBase
{
GENERATED_BODY()
public:
UPersonaBrainComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
FString SystemPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Interaction", meta = (MultiLine = "true"))
FText InteractionRulesPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|World Context", meta = (MultiLine = "true"))
FText WorldUsageRulesPrompt;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|Memory", meta = (MultiLine = "true"))
FText PlayerFactsPromptTemplate;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Identity")
TObjectPtr<UIdentityDataAsset> Identity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|First Meeting", meta = (ClampMin = "0"))
int32 FirstMeetingPromptAssistantMessages = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM")
bool bUseStreaming;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
TObjectPtr<UMorphControllerComponent> MorphController;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
bool bAutoFindMorphController = true;
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
FLLMResponseDelegate OnLLMResponse;
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
FLLMReplyStreamChunkDelegate OnLLMReplyStreamChunk;
UPROPERTY(BlueprintAssignable, Category = "LLM|Events")
FLLMErrorDelegate OnLLMError;
UFUNCTION(BlueprintCallable, Category = "LLM")
void SendMessage(const FString& Prompt);
UFUNCTION(BlueprintCallable, Category = "LLM|Events")
void SendSystemEvent(USystemEventDataAsset* SystemEvent);
UFUNCTION(BlueprintPure, Category = "LLM|History")
bool GetLastSavedLLMMessage(FString& OutMessage) const;
virtual void CancelRequest() override;
protected:
virtual void BeginPlay() override;
virtual FString GetChatIdentityId() const override;
virtual void BroadcastBrainError(const FString& ErrorMessage) const override;
virtual FString GetBrainDebugName() const override;
private:
int32 StreamBytesProcessed;
FString PendingStreamBuffer;
FString StreamResponseContent;
FString LastStreamReply;
FString CurrentSystemEventPrompt;
void AppendSystemMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
FString BuildWorldUsageRulesPrompt() const;
FString BuildInteractionRulesPrompt() const;
FString BuildResponseContractPrompt() const;
void AppendSavedPlayerFactsMessage(TArray<TSharedPtr<FJsonValue>>& Messages) const;
int32 CountSavedChatMessagesByRole(const FString& RoleToCount) const;
void AppendSavedChatHistoryMessages(TArray<TSharedPtr<FJsonValue>>& Messages) const;
FString BuildRequestBody(const FString& MessageContent, const FString& MessageRole, bool bStream) const;
FString BuildResponseExampleJson() const;
void AppendChatMessageToFile(const FString& Role, const FString& Content, const FString& InteractActionJson = FString(), const FString& Emotion = FString()) const;
FString BuildSavedAssistantMessageContent(const TSharedPtr<FJsonObject>& MessageObject) const;
void BroadcastLLMResponse(const FLLMReplyResponse& Response);
bool TryParseReplyResponse(const FString& Json, FLLMReplyResponse& OutResponse) const;
bool TryExtractPartialReply(const FString& Json, FString& OutReply) const;
void BroadcastReplyStreamContent(const FString& ContentDelta);
void BroadcastFinalStreamResponse();
void SendMessageInternal(const FString& MessageContent, const FString& MessageRole, bool bStream, bool bSaveMessageToHistory, bool bSaveResponseToHistory);
bool ValidateSettings() const;
bool ConfigureRequest(const TSharedRef<IHttpRequest, ESPMode::ThreadSafe>& Request, const FString& MessageContent, const FString& MessageRole, bool bStream) const;
void HandleRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
void BroadcastProtocolError(const FString& ErrorMessage, const FString& ResponseBody = FString());
void HandleStreamProgress(FHttpRequestPtr Request, uint64 BytesSent, uint64 BytesReceived);
void ParseStreamBuffer(const FString& Buffer, bool bFlushPending);
void BroadcastStreamLine(const FString& Line);
bool bSaveCurrentResponseToHistory;
};
@@ -0,0 +1,29 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "SystemEventDataAsset.generated.h"
UCLASS(BlueprintType)
class FUNNYGIRL_API USystemEventDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event")
FString ID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event")
FText Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "System Event", meta = (MultiLine = "true"))
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Prompting", meta = (MultiLine = "true"))
FText Prompt;
UFUNCTION(BlueprintPure, Category = "System Event")
FString BuildSystemEventPrompt() const;
};
@@ -0,0 +1,184 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "LipSyncComponent.generated.h"
class ULipSyncMappingDataAsset;
class UMorphControllerComponent;
class UAudioComponent;
class USoundBase;
class USoundWave;
UENUM(BlueprintType)
enum class ELipSyncAudioDrivenMode : uint8
{
EnvelopeMouthOpen UMETA(DisplayName = "Envelope Mouth Open"),
TranscriptVisemes UMETA(DisplayName = "Transcript Visemes"),
VisemeSequence UMETA(DisplayName = "Viseme Sequence")
};
/**
* Runtime lip sync layer. Feed it viseme weights from TTS/audio analysis; it maps them to morph targets
* and sends the result to MorphControllerComponent without interrupting emotion preset playback.
*/
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
class FUNNYGIRL_API ULipSyncComponent : public UActorComponent
{
GENERATED_BODY()
public:
ULipSyncComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
TObjectPtr<ULipSyncMappingDataAsset> Mapping;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
TObjectPtr<UMorphControllerComponent> MorphController;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
bool bAutoResolveMorphController = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1", DeprecatedProperty, DeprecationMessage = "Use VisemeAttackSpeed and VisemeReleaseSpeed instead."))
float InterpolationSpeed = 18.0f;
/** How quickly viseme weights rise toward the target. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1"))
float VisemeAttackSpeed = 24.0f;
/** How quickly viseme weights fall toward silence. Lower values make speech softer and less choppy. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0.1"))
float VisemeReleaseSpeed = 10.0f;
/** Keeps the previous mouth shape briefly during tiny envelope dips. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0"))
float SilenceHoldTime = 0.045f;
/** Crossfades to the next audio-driven viseme instead of hard switching. 0 disables crossfade. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Smoothing", meta = (ClampMin = "0", ClampMax = "0.45"))
float AudioDrivenVisemeCrossfade = 0.12f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync", meta = (ClampMin = "0"))
float SilenceThreshold = 0.001f;
// --- Audio-driven lip sync ---
/** Default audio asset used by Play Audio Driven Lip Sync when no sound is passed explicitly. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
TObjectPtr<USoundBase> AudioDrivenSound;
/** Audio-only mode cannot know phonemes. Envelope mode opens/closes the mouth naturally; sequence mode cycles fake visemes for debugging. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
ELipSyncAudioDrivenMode AudioDrivenMode = ELipSyncAudioDrivenMode::EnvelopeMouthOpen;
/** Main viseme used by Envelope Mouth Open mode. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
FName AudioDrivenEnvelopeViseme = TEXT("A");
/** Optional transcript for Transcript Visemes mode. The component builds an approximate viseme sequence from this text. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (MultiLine = "true"))
FText AudioDrivenTranscript;
/** Viseme sequence used while audio-driven lip sync is playing. The audio envelope controls intensity. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
TArray<FName> AudioDrivenVisemeSequence;
/** How many viseme steps per second to advance while test audio is playing. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0.1"))
float AudioDrivenVisemeRate = 9.0f;
/** Multiplies the audio envelope before it becomes mouth intensity. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0"))
float AudioDrivenEnvelopeGain = 5.0f;
/** Smooths low audio values into a more visible mouth movement. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven", meta = (ClampMin = "0.1"))
float AudioDrivenEnvelopeGamma = 0.65f;
/** Used when neither cooked envelope data nor realtime envelope callbacks are available. Disable for production if you prefer silence over synthetic movement. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync|Audio Driven")
bool bUseFallbackPulseWithoutEnvelope = false;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
void SetVisemeWeight(FName VisemeName, float Weight);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
void SetVisemeWeights(const TMap<FName, float>& VisemeWeights);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
void ClearVisemes();
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Play Audio Driven Lip Sync"))
bool PlayAudioDrivenLipSync(USoundBase* Sound = nullptr, float StartTime = 0.0f);
bool PlayAudioDrivenLipSyncWithComponent(UAudioComponent* AudioComponent, USoundBase* Sound, float StartTime = 0.0f);
/** Follow an audio component that is controlled elsewhere, such as a streaming TTS component. Does not stop, replace, or start audio playback. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Follow External Audio Component"))
bool FollowExternalAudioComponent(UAudioComponent* AudioComponent, const FText& Transcript);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Stop Audio Driven Lip Sync"))
void StopAudioDrivenLipSync();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync|Audio Driven")
bool IsAudioDrivenLipSyncPlaying() const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Play Lip Sync Test Audio", DeprecatedFunction, DeprecationMessage = "Use Play Audio Driven Lip Sync instead."))
bool PlayLipSyncTestAudio(USoundBase* Sound = nullptr, float StartTime = 0.0f);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DisplayName = "Stop Lip Sync Test Audio", DeprecatedFunction, DeprecationMessage = "Use Stop Audio Driven Lip Sync instead."))
void StopLipSyncTestAudio();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync|Audio Driven", meta = (DeprecatedFunction, DeprecationMessage = "Use Is Audio Driven Lip Sync Playing instead."))
bool IsLipSyncTestAudioPlaying() const;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
TMap<FName, float> TargetVisemeWeights;
UPROPERTY()
TMap<FName, float> CurrentVisemeWeights;
UPROPERTY()
TObjectPtr<UAudioComponent> AudioDrivenComponent;
bool bAudioDrivenLipSyncActive = false;
float AudioDrivenPlaybackTime = 0.0f;
float LastEnvelopeValue = 0.0f;
float LastNonSilentMouthWeight = 0.0f;
float SilenceHoldRemaining = 0.0f;
float AudioDrivenDuration = 0.0f;
bool bReceivedEnvelopeValue = false;
bool bAudioDrivenControlsPlayback = true;
bool bWaitingForExternalAudioPlayback = false;
float ExternalAudioPlaybackWaitTime = 0.0f;
TArray<FName> GeneratedTranscriptVisemes;
TArray<float> GeneratedTranscriptVisemeUnits;
UMorphControllerComponent* ResolveMorphController() const;
UAudioComponent* ResolveAudioDrivenComponent();
void UpdateAudioDrivenLipSync(float DeltaTime);
float GetAudioDrivenMouthWeight(float DeltaTime);
void RebuildGeneratedTranscriptVisemes();
const TArray<FName>& ResolveActiveAudioDrivenVisemeSequence() const;
float GetGeneratedTranscriptTotalUnits() const;
void SetAudioDrivenTargetVisemes(float MouthWeight);
void PushCurrentMorphValues();
UFUNCTION()
void HandleAudioDrivenFinished();
UFUNCTION()
void HandleAudioDrivenPlaybackPercent(const USoundWave* PlayingSoundWave, const float PlaybackPercent);
UFUNCTION()
void HandleAudioDrivenEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue);
};
@@ -0,0 +1,57 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "LipSyncMappingDataAsset.generated.h"
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FLipSyncMorphTargetValue
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
FName MorphTargetName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync", meta = (ClampMin = "0", ClampMax = "2"))
float Weight = 1.0f;
bool IsValid() const { return !MorphTargetName.IsNone() && Weight > 0.0f; }
};
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FLipSyncVisemeMapping
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
FName VisemeName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Lip Sync")
TArray<FLipSyncMorphTargetValue> MorphTargets;
bool IsValid() const { return !VisemeName.IsNone() && MorphTargets.Num() > 0; }
};
/**
* Maps phoneme/viseme names coming from TTS or audio analysis to actual morph targets on the character mesh.
* One viseme can drive several morphs, so speech can combine with smile, frown, and other emotion presets.
*/
UCLASS(BlueprintType)
class FUNNYGIRL_API ULipSyncMappingDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
ULipSyncMappingDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Lip Sync")
TArray<FLipSyncVisemeMapping> VisemeMappings;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Lip Sync")
bool GetMorphValuesForViseme(FName VisemeName, float VisemeWeight, TMap<FName, float>& OutMorphValues) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Lip Sync")
void AccumulateMorphValues(const TMap<FName, float>& VisemeWeights, TMap<FName, float>& InOutMorphValues) const;
};
@@ -0,0 +1,204 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "MorphControllerComponent.generated.h"
class USkeletalMeshComponent;
UENUM(BlueprintType)
enum class EMorphLayerBlendMode : uint8
{
Max UMETA(DisplayName = "Max"),
Additive UMETA(DisplayName = "Additive"),
Override UMETA(DisplayName = "Override")
};
/** Active preset playback state. */
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FActiveMorphPresetSlot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UMorphPresetDataAsset> Preset = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0", ClampMax = "1"))
float TargetWeight = 1.0f;
/** Current blended-in preset weight. */
float CurrentWeight = 0.0f;
/** Current time inside this playback instance. */
float PlaybackTime = 0.0f;
/** Remaining delay before the next loop iteration. */
float LoopDelayRemaining = 0.0f;
/** Resolved timeline length for this playback instance. */
float TimelineDuration = 0.0f;
bool IsValid() const { return Preset != nullptr && Preset->IsValidPreset(); }
};
/**
* Компонент управления морф таргетами персонажа.
* Поддерживает несколько пресетов одновременно, плавные переходы, блендинг.
*/
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent))
class FUNNYGIRL_API UMorphControllerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UMorphControllerComponent();
// --- Конфигурация ---
/** Компонент скелетного меша. Если не задан — берётся GetMesh() у Character. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
TObjectPtr<USkeletalMeshComponent> TargetMeshComponent;
/** Speed used when blending preset weight toward the requested weight. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller", meta = (ClampMin = "0.1"))
float BlendSpeed = 4.0f;
/** Обновлять морфы каждый кадр (выключить для экономии, если управляете вручную) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
bool bAutoUpdate = true;
/** Print warnings when a preset cannot play because of missing mesh, empty curves, or invalid data. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
bool bLogWarnings = true;
/** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets;
/** Prompt text placed before the generated emotion preset list. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions", meta = (MultiLine = "true"))
FText EmotionPresetsPromptHeader;
/** Build a system prompt that explains available emotion morph presets to an LLM. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller|Emotions")
FString BuildEmotionPresetsPrompt() const;
/** Find an emotion preset by its Display Name. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller|Emotions", meta = (DisplayName = "Find Emotion Preset By Name"))
UMorphPresetDataAsset* FindEmotionPresetByName(const FString& PresetName) const;
/** Play an emotion preset from Emotion Presets by its Display Name. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Emotions", meta = (DisplayName = "Play Emotion Preset By Name"))
bool PlayEmotionPresetByName(const FString& PresetName, float Weight = 1.0f);
// --- API: Пресеты ---
/** Play or restart a preset. Looping presets stay active forever until removed. Non-looping presets are removed when playback reaches the end. Returns false if the preset cannot be played. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Add Preset"))
bool AddPreset(UMorphPresetDataAsset* Preset, float Weight = 1.0f);
/** Play or restart a preset. Alias for Add Preset with a clearer Blueprint name. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Play Morph Preset"))
bool PlayPreset(UMorphPresetDataAsset* Preset, float Weight = 1.0f);
/** Remove a preset from active playback immediately. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Remove Preset"))
void RemovePreset(UMorphPresetDataAsset* Preset);
/** Установить вес пресета. Пресет должен быть уже добавлен. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Preset Weight"))
void SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight);
/** Применить несколько пресетов сразу (заменяет текущие активные) */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Active Presets"))
void SetActivePresets(const TArray<FActiveMorphPresetSlot>& Presets);
/** Clear all active preset playback immediately. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear All Presets"))
void ClearAllPresets();
/** Проверить, активен ли пресет */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller")
bool IsPresetActive(UMorphPresetDataAsset* Preset) const;
/** Получить текущие активные пресеты */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller")
TArray<FActiveMorphPresetSlot> GetActivePresets() const;
// --- API: Прямое управление морфами ---
/** Установить морф таргет напрямую (добавляется к пресетам) */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Set Morph Target"))
void SetMorphTarget(FName MorphTargetName, float Value);
/** Remove one direct morph override. Preset playback can still drive this morph afterwards. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Remove Morph Target Override"))
void RemoveMorphTargetOverride(FName MorphTargetName);
/** Clear only direct morph overrides, without stopping active presets. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear Direct Morph Overrides"))
void ClearDirectMorphOverrides();
// --- API: Lip sync layer ---
/** Enable applying transient lip sync morph values on top of active emotion presets. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync")
bool bEnableLipSyncLayer = true;
/** How lip sync morphs are mixed with emotion presets and direct overrides. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync")
EMorphLayerBlendMode LipSyncBlendMode = EMorphLayerBlendMode::Max;
/** Overall multiplier for lip sync values before they are mixed into the final morph set. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Lip Sync", meta = (ClampMin = "0", ClampMax = "2"))
float LipSyncLayerWeight = 1.0f;
/** Set transient lip sync morph values. These values are blended during the next morph update. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Lip Sync", meta = (DisplayName = "Set Lip Sync Morph Values"))
void SetLipSyncMorphValues(const TMap<FName, float>& MorphValues);
/** Clear transient lip sync morph values without stopping emotion presets. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Lip Sync", meta = (DisplayName = "Clear Lip Sync Morph Values"))
void ClearLipSyncMorphValues();
/** Update morph targets immediately. Useful when Auto Update is disabled. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Update Morph Targets Now"))
void UpdateMorphTargetsNow(float DeltaTime = 0.0f);
/** Очистить все морфы мгновенно */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Clear Morphs Immediately"))
void ClearMorphsImmediately();
// --- Внутреннее ---
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
TArray<FActiveMorphPresetSlot> ActivePresets;
/** Прямые значения морфов (дополнительно к пресетам) */
UPROPERTY()
TMap<FName, float> DirectMorphOverrides;
UPROPERTY()
TMap<FName, float> LipSyncMorphValues;
UPROPERTY()
TSet<FName> LastAppliedMorphNames;
bool bMissingMeshWarningIssued = false;
USkeletalMeshComponent* ResolveMeshComponent() const;
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
void UpdateMorphTargets(float DeltaTime);
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues);
};
@@ -0,0 +1,120 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Curves/CurveFloat.h"
#include "Engine/DataAsset.h"
#include "Engine/SkeletalMesh.h"
#include "MorphPresetDataAsset.generated.h"
class UAnimMontage;
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FMorphTimelineClip
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (DisplayName = "Morph Name"))
FName Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0", DisplayName = "Start Time"))
float StartTime = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0.001", DisplayName = "Duration"))
float Duration = 0.2f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (ClampMin = "0", ClampMax = "5"))
float Weight = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Timeline", meta = (DisplayName = "Weight Curve"))
FRuntimeFloatCurve WeightCurve;
bool IsValid() const { return !Name.IsNone() && Duration > 0.0f; }
void EnsureDefaultCurve();
};
UCLASS(BlueprintType)
class FUNNYGIRL_API UMorphPresetDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Preview", meta = (DisplayName = "Preview Mesh"))
TSoftObjectPtr<USkeletalMesh> PreviewMesh;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (DisplayName = "Display Name"))
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset")
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Timeline Clips",
ToolTip = "Morph clips placed on the preset timeline."))
TArray<FMorphTimelineClip> MorphTargets;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Duration",
ToolTip = "Preset duration in seconds. 0 means auto-calculate from clips.",
ClampMin = "0"))
float Duration = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Loop",
ToolTip = "Loop the preset playback."))
bool bLoop = false;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Loop Delay",
ToolTip = "Delay in seconds between loop iterations. Only used when Loop is enabled.",
ClampMin = "0"))
float LoopDelay = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Blend Speed Override",
ToolTip = "0 uses the component default.",
ClampMin = "0"))
float BlendSpeedOverride = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Montage",
ToolTip = "Optional animation montage played when this preset starts. The montage must use a slot that is present in the character Anim Blueprint."))
TObjectPtr<UAnimMontage> AnimationMontage = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Play Rate",
ClampMin = "0.01"))
float AnimationPlayRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Start Section",
ToolTip = "Optional montage section to jump to after playback starts."))
FName AnimationStartSection;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Restart Animation On Replay",
ToolTip = "If enabled, an already playing montage is stopped and restarted when this preset is triggered again."))
bool bRestartAnimationOnReplay = true;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
int32 GetMorphCount() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
float GetTimelineLength() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasAnimation() const { return AnimationMontage != nullptr; }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool IsValidPreset() const { return GetMorphCount() > 0; }
};
namespace MorphPresetRuntime
{
FUNNYGIRL_API float EvaluateClipWeight(const FMorphTimelineClip& Clip, float Time);
FUNNYGIRL_API void AccumulatePresetMorphValuesAtTime(const UMorphPresetDataAsset& Preset, float Time, float PresetWeight, TMap<FName, float>& InOutValues);
}
@@ -0,0 +1,62 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MorphTargetBlueprintLibrary.generated.h"
class USkeletalMeshComponent;
class USkeleton;
class UMorphPresetDataAsset;
class APlayerController;
/**
* Blueprint function library for morph target helpers and camera motion utilities.
*/
UCLASS()
class FUNNYGIRL_API UMorphTargetBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns all morph target names from the skeletal mesh component.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Targets", meta = (
DisplayName = "Get Morph Target Names",
ToolTip = "Get all morph target names from the character mesh."))
static TArray<FString> GetMorphTargetNames(USkeletalMeshComponent* SkeletalMeshComponent);
/**
* Returns unique morph target names from all skeletal meshes that use this skeleton.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Targets", meta = (
DisplayName = "Get Morph Target Names From Skeleton",
ToolTip = "Collects unique morph target names from all skeletal meshes linked to the skeleton."))
static TArray<FString> GetMorphTargetNamesFromSkeleton(USkeleton* Skeleton);
/**
* Applies a morph preset directly to the mesh at a specific timeline time.
*/
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Targets", meta = (
DisplayName = "Apply Preset To Mesh At Time",
ToolTip = "Apply a morph preset to a skeletal mesh using the preset timeline and curves."))
static void ApplyPresetToMeshAtTime(USkeletalMeshComponent* SkeletalMeshComponent, UMorphPresetDataAsset* Preset, float Time, float Weight = 1.0f);
/**
* Converts device tilt to a small 2D camera offset.
* X: left/right, Y: up/down.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Camera|Gyroscope", meta = (
DisplayName = "Get Gyro Camera Offset 2D",
ToolTip = "Converts device tilt to a small camera offset for left/right and up/down movement."))
static FVector2D GetGyroCameraOffset2D(
APlayerController* PlayerController,
float HorizontalAmplitude = 4.0f,
float VerticalAmplitude = 3.0f,
float DeadZone = 0.05f,
float MaxTiltInput = 0.35f,
bool bInvertHorizontal = false,
bool bInvertVertical = false);
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "NarrativeBlueprintLibrary.generated.h"
UCLASS()
class FUNNYGIRL_API UNarrativeBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Narrative|Semantic Memory")
static bool AppendSemanticMemoryFromArgumentsJson(
const FString& IdentityId,
const FString& ArgumentsJson,
FString& OutError);
UFUNCTION(BlueprintCallable, Category = "Narrative|Player Data")
static bool SavePlayerDataFromArgumentsJson(
const FString& IdentityId,
const FString& ArgumentsJson,
FString& OutError);
UFUNCTION(BlueprintCallable, Category = "Narrative|Player Data")
static bool SavePlayerFact(
const FString& IdentityId,
const FString& Key,
const FString& Value,
const FString& Category,
const FString& SourceText,
float Confidence,
const TArray<FString>& Tags,
FString& OutError);
};
@@ -0,0 +1,105 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/Widget.h"
#include "AudioPlaybackThrobber.generated.h"
class SAudioPlaybackThrobber;
class UAudioComponent;
class USoundWave;
UCLASS(BlueprintType, Blueprintable)
class FUNNYGIRL_API UAudioPlaybackThrobber : public UWidget
{
GENERATED_BODY()
public:
UAudioPlaybackThrobber();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "1", ClampMax = "16"))
int32 BarCount = 7;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.5"))
float BarWidth = 3.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0"))
float BarGap = 2.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float MinHeightFraction = 0.18f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0"))
float AnimationSpeed = 4.8f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float GlowStrength = 0.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float CenterBoost = 0.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
FLinearColor BarColor = FLinearColor(0.18f, 0.82f, 1.0f, 1.0f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
FLinearColor IdleBarColor = FLinearColor(0.18f, 0.82f, 1.0f, 0.18f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback")
bool bIsPlaying = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio")
TObjectPtr<UAudioComponent> AudioComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio")
bool bUseAudioEnvelope = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0"))
float AudioEnvelopeGain = 5.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.1"))
float AudioEnvelopeGamma = 0.65f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0"))
float AudioEnvelopeSmoothingSpeed = 16.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ManualAudioLevel = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AudioSilenceThreshold = 0.03f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio Playback|Audio", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AudioSilenceOpacity = 0.08f;
UFUNCTION(BlueprintCallable, Category = "Audio Playback")
void SetIsPlaying(bool bInIsPlaying);
UFUNCTION(BlueprintPure, Category = "Audio Playback")
bool IsPlaying() const { return bIsPlaying; }
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
void SetAudioComponent(UAudioComponent* InAudioComponent);
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
void SetUseAudioEnvelope(bool bInUseAudioEnvelope);
UFUNCTION(BlueprintCallable, Category = "Audio Playback|Audio")
void SetManualAudioLevel(float InAudioLevel);
protected:
virtual void BeginDestroy() override;
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void SynchronizeProperties() override;
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
private:
UFUNCTION()
void HandleAudioEnvelopeValue(const USoundWave* PlayingSoundWave, const float EnvelopeValue);
void UnbindAudioComponent();
void BindAudioComponent();
TSharedPtr<SAudioPlaybackThrobber> MyThrobber;
bool bAudioEnvelopeDelegateBound = false;
};
@@ -0,0 +1,45 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "FunnyManagedUserWidget.generated.h"
UCLASS(BlueprintType, Blueprintable)
class FUNNYGIRL_API UFunnyManagedUserWidget : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
FName UILayerName = TEXT("HUD");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
bool bAutoRegisterWithUILayer = true;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Layer")
bool bAutoShowWithLayer = false;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetManagedVisibility(ESlateVisibility InVisibility);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
ESlateVisibility GetManagedVisibility() const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetManagedEnabled(bool bEnabled);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
bool GetManagedEnabled() const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void RegisterWithUILayer();
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void UnregisterFromUILayer();
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
};
@@ -0,0 +1,53 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "FunnyUIBlueprintLibrary.generated.h"
class UFunnyUILayerManagerSubsystem;
class UUserWidget;
UCLASS()
class FUNNYGIRL_API UFunnyUIBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static UFunnyUILayerManagerSubsystem* GetUILayerManager(const UObject* WorldContextObject, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void RegisterWidgetToLayer(const UObject* WorldContextObject, FName LayerName, UUserWidget* Widget, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void UnregisterWidgetFromLayer(const UObject* WorldContextObject, UUserWidget* Widget, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void SetManagedWidgetVisibility(const UObject* WorldContextObject, UUserWidget* Widget, ESlateVisibility Visibility, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void SetManagedWidgetEnabled(const UObject* WorldContextObject, UUserWidget* Widget, bool bEnabled, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void SetUILayerVisibility(const UObject* WorldContextObject, FName LayerName, ESlateVisibility Visibility, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void ShowUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void HideUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void SetUILayerEnabled(const UObject* WorldContextObject, FName LayerName, bool bEnabled, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void ShowOnlyUILayer(const UObject* WorldContextObject, FName LayerName, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static void PushUILayerState(const UObject* WorldContextObject, int32 PlayerIndex = 0);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers", meta = (WorldContext = "WorldContextObject"))
static bool PopUILayerState(const UObject* WorldContextObject, int32 PlayerIndex = 0);
};
@@ -0,0 +1,113 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Subsystems/LocalPlayerSubsystem.h"
#include "FunnyUILayerManagerSubsystem.generated.h"
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FFunnyUILayerState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Layer")
ESlateVisibility Visibility = ESlateVisibility::Visible;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Layer")
bool bEnabled = true;
};
USTRUCT()
struct FUNNYGIRL_API FFunnyUILayerWidgetEntry
{
GENERATED_BODY()
UPROPERTY()
TWeakObjectPtr<UUserWidget> Widget;
UPROPERTY()
ESlateVisibility DesiredVisibility = ESlateVisibility::Visible;
UPROPERTY()
bool bDesiredEnabled = true;
UPROPERTY()
bool bAutoShowWithLayer = false;
};
UCLASS()
class FUNNYGIRL_API UFunnyUILayerManagerSubsystem : public ULocalPlayerSubsystem
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void RegisterWidget(FName LayerName, UUserWidget* Widget);
void RegisterWidget(FName LayerName, UUserWidget* Widget, bool bAutoShowWithLayer);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void UnregisterWidget(UUserWidget* Widget);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetWidgetDesiredVisibility(UUserWidget* Widget, ESlateVisibility Visibility);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
ESlateVisibility GetWidgetDesiredVisibility(UUserWidget* Widget) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetWidgetDesiredEnabled(UUserWidget* Widget, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
bool GetWidgetDesiredEnabled(UUserWidget* Widget) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetLayerVisibility(FName LayerName, ESlateVisibility Visibility);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void ShowLayer(FName LayerName);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void HideLayer(FName LayerName);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
ESlateVisibility GetLayerVisibility(FName LayerName) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void SetLayerEnabled(FName LayerName, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|UI|Layers")
bool IsLayerEnabled(FName LayerName) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void ShowOnlyLayer(FName LayerName);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void PushLayerState();
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
bool PopLayerState();
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Layers")
void ApplyAllLayers();
private:
TMap<FName, FFunnyUILayerState> LayerStates;
TMap<FName, TArray<FFunnyUILayerWidgetEntry>> LayerWidgets;
TArray<TMap<FName, FFunnyUILayerState>> LayerStateStack;
FFunnyUILayerState& FindOrAddLayerState(FName LayerName);
const FFunnyUILayerState* FindLayerState(FName LayerName) const;
FFunnyUILayerWidgetEntry* FindWidgetEntry(UUserWidget* Widget);
const FFunnyUILayerWidgetEntry* FindWidgetEntry(UUserWidget* Widget) const;
void PruneInvalidWidgets(FName LayerName);
void AutoShowLayerWidgets(FName LayerName);
void ApplyLayerState(FName LayerName);
void ApplyWidgetState(const FFunnyUILayerWidgetEntry& Entry, const FFunnyUILayerState& LayerState) const;
static bool IsVisibleLayerState(ESlateVisibility Visibility);
static ESlateVisibility CombineVisibility(ESlateVisibility LayerVisibility, ESlateVisibility DesiredVisibility);
};
@@ -0,0 +1,46 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/TextBlock.h"
#include "TypingTextBlock.generated.h"
UCLASS(BlueprintType, Blueprintable)
class FUNNYGIRL_API UTypingTextBlock : public UTextBlock
{
GENERATED_BODY()
public:
UTypingTextBlock();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typing", meta = (ClampMin = "0.001"))
float CharacterInterval;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Typing")
bool bAutoStartTyping;
UFUNCTION(BlueprintCallable, Category = "Typing")
void AppendTextChunk(const FString& TextChunk);
UFUNCTION(BlueprintCallable, Category = "Typing")
void ClearTypedText();
UFUNCTION(BlueprintCallable, Category = "Typing")
void FlushTyping();
UFUNCTION(BlueprintPure, Category = "Typing")
bool IsTyping() const;
protected:
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
private:
FTimerHandle TypingTimerHandle;
FString DisplayedText;
FString PendingText;
void StartTyping();
void StopTyping();
void TypeNextCharacter();
};
@@ -0,0 +1,56 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "FunnyGirlUtilityBlueprintLibrary.generated.h"
class USkeletalMesh;
class UButton;
class UTexture2D;
UENUM(BlueprintType)
enum class EFunnyGirlButtonBrushState : uint8
{
Normal UMETA(DisplayName = "Normal"),
Hovered UMETA(DisplayName = "Hovered"),
Pressed UMETA(DisplayName = "Pressed")
};
UCLASS()
class FUNNYGIRL_API UFunnyGirlUtilityBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Targets")
static bool SaveMorphTargetNamesToFile(
USkeletalMesh* SkeletalMesh,
FString& OutFilePath,
FString& OutError);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
static bool GetJsonStringField(const FString& Json, const FString& FieldName, FString& OutValue);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
static bool GetJsonNumberField(const FString& Json, const FString& FieldName, float& OutValue);
UFUNCTION(BlueprintPure, Category = "FunnyGirl|Json")
static bool GetJsonBoolField(const FString& Json, const FString& FieldName, bool& OutValue);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|UI|Button")
static bool SetButtonStyleTexture(
UButton* Button,
UTexture2D* Texture,
EFunnyGirlButtonBrushState State);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool SaveLightString(const FString& Field, const FString& Value, FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool LoadLightString(const FString& Field, FString& OutValue, FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool DeleteLightString(const FString& Field, FString& OutError);
};
@@ -0,0 +1,69 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractableObjectComponent.generated.h"
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FInteractableObjectAction
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action")
FName Action;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action", meta = (MultiLine = "true"))
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Action", meta = (MultiLine = "true"))
FText ActionFormat;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FInteractableObjectActionRequestedDelegate, FName, Action, const FString&, ActionJson);
UCLASS(ClassGroup = (FunnyGirl), meta = (BlueprintSpawnableComponent, DisplayName = "Interactable Object"))
class FUNNYGIRL_API UInteractableObjectComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractableObjectComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
FString ID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
FText Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object", meta = (MultiLine = "true"))
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
TMap<FString, FString> State;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object", meta = (MultiLine = "true"))
FText CustomData;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Object")
TArray<FInteractableObjectAction> Actions;
UPROPERTY(BlueprintAssignable, Category = "FunnyGirl|Interactable Object")
FInteractableObjectActionRequestedDelegate OnActionRequested;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Interactable Object")
bool HasAction(FName Action) const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Interactable Object")
bool FindAction(FName Action, FInteractableObjectAction& OutAction) const;
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Interactable Object")
bool ExecuteAction(const FString& ActionJson);
UFUNCTION(BlueprintNativeEvent, Category = "FunnyGirl|Interactable Object")
bool OnExecuteAction(const FString& ActionJson);
private:
bool TryExtractActionFromJson(const FString& ActionJson, FName& OutAction) const;
};
@@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WorldContextActor.generated.h"
class UWorldContextDataAsset;
class UInteractableObjectComponent;
UCLASS(BlueprintType, Blueprintable)
class FUNNYGIRL_API AWorldContextActor : public AActor
{
GENERATED_BODY()
public:
AWorldContextActor();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
TObjectPtr<UWorldContextDataAsset> WorldContext;
UFUNCTION(BlueprintPure, Category = "World Context")
FString BuildWorldContextPrompt() const;
UFUNCTION(BlueprintPure, Category = "World Context")
FString BuildBaseWorldContextPrompt() const;
UFUNCTION(BlueprintPure, Category = "World Context")
FString BuildInteractableObjectsPrompt() const;
UFUNCTION(BlueprintCallable, Category = "World Context|Interactable Objects")
void RefreshInteractableObjects() const;
UFUNCTION(BlueprintPure, Category = "World Context|Interactable Objects")
UInteractableObjectComponent* FindInteractableObjectByID(const FString& ObjectID) const;
UFUNCTION(BlueprintCallable, Category = "World Context|Interactable Objects")
bool ExecuteInteractableObjectAction(const FString& ActionJson, UPARAM(ref) FString& OutError);
private:
void EnsureInteractableObjectsCached() const;
mutable bool bInteractableObjectsCached = false;
mutable TArray<TWeakObjectPtr<UInteractableObjectComponent>> InteractableObjects;
mutable TMap<FString, TWeakObjectPtr<UInteractableObjectComponent>> InteractableObjectsByID;
};
@@ -0,0 +1,23 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "WorldContextDataAsset.generated.h"
UCLASS(BlueprintType)
class FUNNYGIRL_API UWorldContextDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context")
FText LocationName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Context", meta = (MultiLine = "true"))
FText SceneDescription;
UFUNCTION(BlueprintPure, Category = "World Context")
FString BuildWorldContextPrompt() const;
};
+15
View File
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.Collections.Generic;
public class FunnyGirlEditorTarget : TargetRules
{
public FunnyGirlEditorTarget( TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V6;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
ExtraModuleNames.AddRange(new string[] { "FunnyGirl", "FunnyGirlEditor" });
}
}
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class FunnyGirlEditor : ModuleRules
{
public FunnyGirlEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "FunnyGirl" });
PrivateDependencyModuleNames.AddRange(new string[] { "AdvancedPreviewScene", "AnimationDataController", "AnimGraph", "AssetTools", "AssetRegistry", "ContentBrowser", "EditorWidgets", "InputCore", "Json", "JsonUtilities", "KismetWidgets", "LevelEditor", "Persona", "PropertyEditor", "SequencerWidgets", "Slate", "SlateCore", "ToolMenus", "UnrealEd" });
}
}
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/DirectorPromptDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_DirectorPrompt"
FText FAssetTypeActions_DirectorPrompt::GetName() const
{
return LOCTEXT("Name", "Director Prompt");
}
FColor FAssetTypeActions_DirectorPrompt::GetTypeColor() const
{
return FColor(120, 90, 210);
}
UClass* FAssetTypeActions_DirectorPrompt::GetSupportedClass() const
{
return UDirectorPromptDataAsset::StaticClass();
}
uint32 FAssetTypeActions_DirectorPrompt::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_DirectorPrompt : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_Identity.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/IdentityDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_Identity"
FText FAssetTypeActions_Identity::GetName() const
{
return LOCTEXT("Name", "Identity");
}
FColor FAssetTypeActions_Identity::GetTypeColor() const
{
return FColor(48, 190, 150);
}
UClass* FAssetTypeActions_Identity::GetSupportedClass() const
{
return UIdentityDataAsset::StaticClass();
}
uint32 FAssetTypeActions_Identity::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_Identity : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/LLMProviderDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_LLMProvider"
FText FAssetTypeActions_LLMProvider::GetName() const
{
return LOCTEXT("Name", "LLM Provider");
}
FColor FAssetTypeActions_LLMProvider::GetTypeColor() const
{
return FColor(72, 128, 255);
}
UClass* FAssetTypeActions_LLMProvider::GetSupportedClass() const
{
return ULLMProviderDataAsset::StaticClass();
}
uint32 FAssetTypeActions_LLMProvider::GetCategories()
{
return FFunnyGirlEditorModule::GetLLMAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_LLMProvider : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
#include "FunnyGirlEditorModule.h"
#include "Identity/PersonalityTraitDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_PersonalityTrait"
FText FAssetTypeActions_PersonalityTrait::GetName() const
{
return LOCTEXT("Name", "Personality Trait");
}
FColor FAssetTypeActions_PersonalityTrait::GetTypeColor() const
{
return FColor(225, 150, 65);
}
UClass* FAssetTypeActions_PersonalityTrait::GetSupportedClass() const
{
return UPersonalityTraitDataAsset::StaticClass();
}
uint32 FAssetTypeActions_PersonalityTrait::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_PersonalityTrait : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/SystemEventDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_SystemEvent"
FText FAssetTypeActions_SystemEvent::GetName() const
{
return LOCTEXT("Name", "System Event");
}
FColor FAssetTypeActions_SystemEvent::GetTypeColor() const
{
return FColor(205, 95, 205);
}
UClass* FAssetTypeActions_SystemEvent::GetSupportedClass() const
{
return USystemEventDataAsset::StaticClass();
}
uint32 FAssetTypeActions_SystemEvent::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_SystemEvent : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
#include "FunnyGirlEditorModule.h"
#include "World/WorldContextDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_WorldContext"
FText FAssetTypeActions_WorldContext::GetName() const
{
return LOCTEXT("Name", "World Context");
}
FColor FAssetTypeActions_WorldContext::GetTypeColor() const
{
return FColor(80, 170, 220);
}
UClass* FAssetTypeActions_WorldContext::GetSupportedClass() const
{
return UWorldContextDataAsset::StaticClass();
}
uint32 FAssetTypeActions_WorldContext::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_WorldContext : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/DirectorPromptDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/DirectorPromptDataAsset.h"
#define LOCTEXT_NAMESPACE "DirectorPromptDataAssetFactory"
UDirectorPromptDataAssetFactory::UDirectorPromptDataAssetFactory()
{
SupportedClass = UDirectorPromptDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UDirectorPromptDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UDirectorPromptDataAsset>(InParent, InClass, InName, Flags);
}
FText UDirectorPromptDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Director Prompt");
}
uint32 UDirectorPromptDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UDirectorPromptDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "DirectorPromptDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UDirectorPromptDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UDirectorPromptDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,40 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/DirectorToolBlueprintFactory.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "FunnyGirlEditorModule.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "LLM/BrainToolBase.h"
#define LOCTEXT_NAMESPACE "DirectorToolBlueprintFactory"
UDirectorToolBlueprintFactory::UDirectorToolBlueprintFactory()
{
SupportedClass = UBlueprint::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UDirectorToolBlueprintFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return FKismetEditorUtilities::CreateBlueprint(UBrainToolBase::StaticClass(), InParent, InName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());
}
FText UDirectorToolBlueprintFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Brain Tool");
}
uint32 UDirectorToolBlueprintFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UDirectorToolBlueprintFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "DirectorToolBlueprintFactory.generated.h"
UCLASS(hidecategories = Object)
class UDirectorToolBlueprintFactory : public UFactory
{
GENERATED_BODY()
public:
UDirectorToolBlueprintFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/IdentityDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/IdentityDataAsset.h"
#define LOCTEXT_NAMESPACE "IdentityDataAssetFactory"
UIdentityDataAssetFactory::UIdentityDataAssetFactory()
{
SupportedClass = UIdentityDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UIdentityDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UIdentityDataAsset>(InParent, InClass, InName, Flags);
}
FText UIdentityDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Identity");
}
uint32 UIdentityDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UIdentityDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "IdentityDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UIdentityDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UIdentityDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/LLMProviderDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/LLMProviderDataAsset.h"
#define LOCTEXT_NAMESPACE "LLMProviderDataAssetFactory"
ULLMProviderDataAssetFactory::ULLMProviderDataAssetFactory()
{
SupportedClass = ULLMProviderDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* ULLMProviderDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<ULLMProviderDataAsset>(InParent, InClass, InName, Flags);
}
FText ULLMProviderDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "LLM Provider");
}
uint32 ULLMProviderDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetLLMAssetCategory();
}
bool ULLMProviderDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "LLMProviderDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class ULLMProviderDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
ULLMProviderDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/PersonalityTraitDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "Identity/PersonalityTraitDataAsset.h"
#define LOCTEXT_NAMESPACE "PersonalityTraitDataAssetFactory"
UPersonalityTraitDataAssetFactory::UPersonalityTraitDataAssetFactory()
{
SupportedClass = UPersonalityTraitDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UPersonalityTraitDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UPersonalityTraitDataAsset>(InParent, InClass, InName, Flags);
}
FText UPersonalityTraitDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Personality Trait");
}
uint32 UPersonalityTraitDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UPersonalityTraitDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "PersonalityTraitDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UPersonalityTraitDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UPersonalityTraitDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/SystemEventDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/SystemEventDataAsset.h"
#define LOCTEXT_NAMESPACE "SystemEventDataAssetFactory"
USystemEventDataAssetFactory::USystemEventDataAssetFactory()
{
SupportedClass = USystemEventDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* USystemEventDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<USystemEventDataAsset>(InParent, InClass, InName, Flags);
}
FText USystemEventDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "System Event");
}
uint32 USystemEventDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool USystemEventDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "SystemEventDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class USystemEventDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
USystemEventDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/WorldContextDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "World/WorldContextDataAsset.h"
#define LOCTEXT_NAMESPACE "WorldContextDataAssetFactory"
UWorldContextDataAssetFactory::UWorldContextDataAssetFactory()
{
SupportedClass = UWorldContextDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UWorldContextDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UWorldContextDataAsset>(InParent, InClass, InName, Flags);
}
FText UWorldContextDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "World Context");
}
uint32 UWorldContextDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UWorldContextDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "WorldContextDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UWorldContextDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UWorldContextDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,93 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "FunnyGirlEditorModule.h"
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
#include "AssetTypeActions/AssetTypeActions_Identity.h"
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "IAssetTypeActions.h"
#include "Modules/ModuleManager.h"
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
#include "PropertyEditorModule.h"
#define LOCTEXT_NAMESPACE "FunnyGirlEditor"
EAssetTypeCategories::Type FFunnyGirlEditorModule::LLMAssetCategory = EAssetTypeCategories::Misc;
EAssetTypeCategories::Type FFunnyGirlEditorModule::NarrativeAssetCategory = EAssetTypeCategories::Misc;
void FFunnyGirlEditorModule::StartupModule()
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
LLMAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("LLM")), LOCTEXT("LLMAssetCategory", "LLM"));
NarrativeAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Narrative")), LOCTEXT("NarrativeAssetCategory", "Narrative"));
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_LLMProvider>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_DirectorPrompt>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_Identity>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_PersonalityTrait>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_SystemEvent>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_WorldContext>());
RegisterAssetTypeAction(MakeShared<FMorphPresetAssetTypeActions>());
RegisterAssetTypeAction(MakeShared<FLipSyncMappingAssetTypeActions>());
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
PropertyEditorModule.RegisterCustomClassLayout(
UMorphPresetDataAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FMorphPresetDetailsCustomization::MakeInstance));
PropertyEditorModule.NotifyCustomizationModuleChanged();
bMorphPresetCustomizationRegistered = true;
}
void FFunnyGirlEditorModule::ShutdownModule()
{
if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetTools")))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
for (const TSharedPtr<IAssetTypeActions>& AssetTypeAction : RegisteredAssetTypeActions)
{
if (AssetTypeAction.IsValid())
{
AssetTools.UnregisterAssetTypeActions(AssetTypeAction.ToSharedRef());
}
}
}
RegisteredAssetTypeActions.Empty();
if (bMorphPresetCustomizationRegistered && FModuleManager::Get().IsModuleLoaded(TEXT("PropertyEditor")))
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
PropertyEditorModule.UnregisterCustomClassLayout(UMorphPresetDataAsset::StaticClass()->GetFName());
PropertyEditorModule.NotifyCustomizationModuleChanged();
bMorphPresetCustomizationRegistered = false;
}
}
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetLLMAssetCategory()
{
return LLMAssetCategory;
}
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetNarrativeAssetCategory()
{
return NarrativeAssetCategory;
}
void FFunnyGirlEditorModule::RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction)
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AssetTools.RegisterAssetTypeActions(AssetTypeAction);
RegisteredAssetTypeActions.Add(AssetTypeAction);
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FFunnyGirlEditorModule, FunnyGirlEditor)
@@ -0,0 +1,27 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeCategories.h"
#include "Modules/ModuleInterface.h"
class IAssetTypeActions;
class FFunnyGirlEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static EAssetTypeCategories::Type GetLLMAssetCategory();
static EAssetTypeCategories::Type GetNarrativeAssetCategory();
private:
static EAssetTypeCategories::Type LLMAssetCategory;
static EAssetTypeCategories::Type NarrativeAssetCategory;
TArray<TSharedPtr<IAssetTypeActions>> RegisteredAssetTypeActions;
bool bMorphPresetCustomizationRegistered = false;
void RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction);
};
@@ -0,0 +1,101 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/AnimTimeline/SAnimEditorBase.h"
#include "AnimPreviewInstance.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/BlendSpace.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SOverlay.h"
void SAnimEditorBase::Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene)
{
PreviewScenePtr = InPreviewScene;
OnObjectsSelectedDelegate = InArgs._OnObjectsSelected;
SetInputViewRange(0.0f, GetSequenceLength());
const TSharedRef<SWidget> TimelineWidget = InArgs._DisplayAnimTimeline
? CreateAnimTimelineWidget()
: SNullWidget::NullWidget;
TSharedPtr<SVerticalBox> RootBox;
ChildSlot
[
SAssignNew(RootBox, SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SOverlay)
+ SOverlay::Slot()
[
SAssignNew(NonScrollEditorPanels, SVerticalBox)
]
+ SOverlay::Slot()
[
TimelineWidget
]
]
];
if (InArgs._DisplayAnimScrubBar)
{
RootBox->AddSlot()
.AutoHeight()
[
ConstructAnimScrubPanel(InArgs._DisplayAnimScrubBarEditing)
];
}
}
void SAnimEditorBase::SetInputViewRange(float InViewMinInput, float InViewMaxInput)
{
ViewMaxInput = FMath::Min(InViewMaxInput, GetSequenceLength());
ViewMinInput = FMath::Max(InViewMinInput, 0.0f);
}
float SAnimEditorBase::GetScrubValue() const
{
if (PreviewScenePtr.IsValid())
{
if (UDebugSkelMeshComponent* PreviewMesh = PreviewScenePtr.Pin()->GetPreviewMeshComponent())
{
if (PreviewMesh->PreviewInstance)
{
return PreviewMesh->PreviewInstance->GetCurrentTime();
}
}
}
return 0.0f;
}
void SAnimEditorBase::OnSelectionChanged(const TArray<UObject*>& SelectedItems)
{
OnObjectsSelectedDelegate.ExecuteIfBound(SelectedItems);
}
TSharedRef<SWidget> SAnimEditorBase::CreateDocumentAnchor()
{
return SNullWidget::NullWidget;
}
TSharedRef<SWidget> SAnimEditorBase::ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing)
{
return SNullWidget::NullWidget;
}
float SAnimEditorBase::GetSequenceLength() const
{
if (const UAnimSequenceBase* AnimSequence = Cast<UAnimSequenceBase>(GetEditorObject()))
{
return AnimSequence->GetPlayLength();
}
if (const UBlendSpace* BlendSpace = Cast<UBlendSpace>(GetEditorObject()))
{
return 1.0f;
}
return 0.0f;
}
@@ -0,0 +1,22 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/LipSyncMappingAssetFactory.h"
#include "Morph/LipSyncMappingDataAsset.h"
ULipSyncMappingAssetFactory::ULipSyncMappingAssetFactory()
{
SupportedClass = ULipSyncMappingDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* ULipSyncMappingAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<ULipSyncMappingDataAsset>(InParent, InClass, InName, Flags);
}
FString ULipSyncMappingAssetFactory::GetDefaultNewAssetName() const
{
return TEXT("LipSyncMapping");
}
@@ -0,0 +1,34 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
#include "Morph/LipSyncMappingDataAsset.h"
#define LOCTEXT_NAMESPACE "LipSyncMapping"
FText FLipSyncMappingAssetTypeActions::GetName() const
{
return LOCTEXT("AssetTypeActions_LipSyncMapping", "Lip Sync Mapping");
}
FColor FLipSyncMappingAssetTypeActions::GetTypeColor() const
{
return FColor(120, 210, 255);
}
UClass* FLipSyncMappingAssetTypeActions::GetSupportedClass() const
{
return ULipSyncMappingDataAsset::StaticClass();
}
uint32 FLipSyncMappingAssetTypeActions::GetCategories()
{
return EAssetTypeCategories::Animation;
}
FText FLipSyncMappingAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
{
return LOCTEXT("LipSyncMappingDescription", "Maps lip sync viseme names to character morph targets.");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,169 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetEditor.h"
#include "IDetailsView.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "Morph/Editor/MorphPresetTimelinePanel.h"
#include "Morph/Editor/MorphPresetViewportPanel.h"
#include "PropertyEditorModule.h"
#include "Widgets/Docking/SDockTab.h"
#define LOCTEXT_NAMESPACE "MorphPresetAssetEditor"
const FName FMorphPresetAssetEditor::EditorName(TEXT("MorphPresetAssetEditor"));
const FName FMorphPresetAssetEditor::ViewportTabId(TEXT("MorphPresetEditor_Viewport"));
const FName FMorphPresetAssetEditor::TimelineTabId(TEXT("MorphPresetEditor_Timeline"));
const FName FMorphPresetAssetEditor::DetailsTabId(TEXT("MorphPresetEditor_Details"));
void FMorphPresetAssetEditor::InitMorphPresetAssetEditor(
const EToolkitMode::Type Mode,
const TSharedPtr<IToolkitHost>& InitToolkitHost,
UMorphPresetDataAsset* InAsset)
{
Preset = InAsset;
EditorController = MakeShared<FMorphPresetEditorController>(InAsset);
EditorController->OnPresetChanged().AddSP(this, &FMorphPresetAssetEditor::HandlePresetChanged);
EditorController->Initialize();
TSharedRef<FTabManager::FLayout> Layout = FTabManager::NewLayout("MorphPresetEditorLayout_V2")
->AddArea(
FTabManager::NewPrimaryArea()
->SetOrientation(Orient_Horizontal)
->Split(
FTabManager::NewSplitter()
->SetOrientation(Orient_Vertical)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.68f)
->AddTab(ViewportTabId, ETabState::OpenedTab)
)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.32f)
->AddTab(TimelineTabId, ETabState::OpenedTab)
)
)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.25f)
->AddTab(DetailsTabId, ETabState::OpenedTab)
)
);
FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, EditorName, Layout, true, true, InAsset);
RegenerateMenusAndToolbars();
}
void FMorphPresetAssetEditor::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
InTabManager->RegisterTabSpawner(ViewportTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Viewport))
.SetDisplayName(LOCTEXT("ViewportTab", "Viewport"));
InTabManager->RegisterTabSpawner(TimelineTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Timeline))
.SetDisplayName(LOCTEXT("TimelineTab", "Timeline"));
InTabManager->RegisterTabSpawner(DetailsTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Details))
.SetDisplayName(LOCTEXT("DetailsTab", "Details"));
}
void FMorphPresetAssetEditor::UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
InTabManager->UnregisterTabSpawner(ViewportTabId);
InTabManager->UnregisterTabSpawner(TimelineTabId);
InTabManager->UnregisterTabSpawner(DetailsTabId);
}
FName FMorphPresetAssetEditor::GetToolkitFName() const
{
return EditorName;
}
FText FMorphPresetAssetEditor::GetBaseToolkitName() const
{
return LOCTEXT("ToolkitName", "Morph Target Preset Editor");
}
FString FMorphPresetAssetEditor::GetWorldCentricTabPrefix() const
{
return TEXT("Morph Preset ");
}
FLinearColor FMorphPresetAssetEditor::GetWorldCentricTabColorScale() const
{
return FLinearColor(0.18f, 0.47f, 0.83f);
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args)
{
ViewportWidget = SNew(SMorphPresetViewportPanel)
.Controller(EditorController)
.AssetEditorToolkit(SharedThis(this));
return SNew(SDockTab)
.Label(LOCTEXT("ViewportTabLabel", "Viewport"))
[
ViewportWidget.ToSharedRef()
];
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Timeline(const FSpawnTabArgs& Args)
{
TimelineWidget = SNew(SMorphPresetTimelinePanel)
.Controller(EditorController);
return SNew(SDockTab)
.Label(LOCTEXT("TimelineTabLabel", "Timeline"))
[
TimelineWidget.ToSharedRef()
];
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args)
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsArgs;
DetailsArgs.bAllowSearch = true;
DetailsArgs.bHideSelectionTip = true;
DetailsView = PropertyModule.CreateDetailView(DetailsArgs);
DetailsView->OnFinishedChangingProperties().AddSP(this, &FMorphPresetAssetEditor::HandleDetailsChanged);
DetailsView->SetObjects(TArray<UObject*>{ Preset.Get() });
return SNew(SDockTab)
.Label(LOCTEXT("DetailsTabLabel", "Details"))
[
DetailsView.ToSharedRef()
];
}
void FMorphPresetAssetEditor::HandleDetailsChanged(const FPropertyChangedEvent&)
{
if (EditorController.IsValid())
{
EditorController->NotifyPresetEdited();
}
if (TimelineWidget.IsValid())
{
TimelineWidget->Refresh();
}
}
void FMorphPresetAssetEditor::HandlePresetChanged()
{
if (DetailsView.IsValid())
{
DetailsView->ForceRefresh();
}
if (TimelineWidget.IsValid())
{
TimelineWidget->Refresh();
}
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,21 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetFactory.h"
#include "Morph/MorphPresetDataAsset.h"
UMorphPresetAssetFactory::UMorphPresetAssetFactory()
{
SupportedClass = UMorphPresetDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UMorphPresetAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UMorphPresetDataAsset>(InParent, InClass, InName, Flags);
}
FString UMorphPresetAssetFactory::GetDefaultNewAssetName() const
{
return TEXT("MorphPreset");
}
@@ -0,0 +1,46 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
#include "Morph/Editor/MorphPresetAssetEditor.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetAssetFactory.h"
#include "Toolkits/IToolkitHost.h"
#define LOCTEXT_NAMESPACE "MorphPreset"
FText FMorphPresetAssetTypeActions::GetName() const
{
return LOCTEXT("AssetTypeActions_MorphPreset", "Morph Preset");
}
FColor FMorphPresetAssetTypeActions::GetTypeColor() const
{
return FColor(200, 150, 255); // Светло-фиолетовый
}
UClass* FMorphPresetAssetTypeActions::GetSupportedClass() const
{
return UMorphPresetDataAsset::StaticClass();
}
uint32 FMorphPresetAssetTypeActions::GetCategories()
{
return EAssetTypeCategories::Animation;
}
FText FMorphPresetAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
{
return LOCTEXT("MorphPresetDescription", "Пресет морф таргетов для персонажа. Улыбка, грусть, удивление и т.д.");
}
void FMorphPresetAssetTypeActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
if (InObjects.Num() == 0) return;
UMorphPresetDataAsset* Preset = Cast<UMorphPresetDataAsset>(InObjects[0]);
if (!Preset) return;
TSharedRef<FMorphPresetAssetEditor> Editor = MakeShared<FMorphPresetAssetEditor>();
Editor->InitMorphPresetAssetEditor(EToolkitMode::Standalone, EditWithinLevelEditor, Preset);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,245 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "IPropertyUtilities.h"
#include "JsonObjectConverter.h"
#include "Dom/JsonObject.h"
#include "Policies/PrettyJsonPrintPolicy.h"
#include "Serialization/JsonSerializer.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "MorphPresetDetailsCustomization"
namespace
{
FString SerializePresetToJson(const UMorphPresetDataAsset* Preset)
{
if (!Preset)
{
return TEXT("{}");
}
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
if (!FJsonObjectConverter::UStructToJsonObject(Preset->GetClass(), Preset, JsonObject, 0, 0))
{
return TEXT("{}");
}
JsonObject->RemoveField(TEXT("PreviewMesh"));
FString JsonString;
const TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonString);
FJsonSerializer::Serialize(JsonObject, Writer);
return JsonString;
}
bool DeserializePresetFromJson(const FString& JsonString, UMorphPresetDataAsset& OutPreset)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
return false;
}
JsonObject->RemoveField(TEXT("PreviewMesh"));
return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutPreset, 0, 0);
}
void CopyPresetValues(const UMorphPresetDataAsset& SourcePreset, UMorphPresetDataAsset& TargetPreset)
{
TargetPreset.DisplayName = SourcePreset.DisplayName;
TargetPreset.Description = SourcePreset.Description;
TargetPreset.MorphTargets = SourcePreset.MorphTargets;
TargetPreset.Duration = SourcePreset.Duration;
TargetPreset.bLoop = SourcePreset.bLoop;
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
}
}
TSharedRef<IDetailCustomization> FMorphPresetDetailsCustomization::MakeInstance()
{
return MakeShared<FMorphPresetDetailsCustomization>();
}
void FMorphPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
const TArray<TWeakObjectPtr<UObject>>& CustomizedObjects = DetailBuilder.GetSelectedObjects();
for (const TWeakObjectPtr<UObject>& Object : CustomizedObjects)
{
if (UMorphPresetDataAsset* MorphPreset = Cast<UMorphPresetDataAsset>(Object.Get()))
{
Preset = MorphPreset;
break;
}
}
PropertyUtilities = DetailBuilder.GetPropertyUtilities();
RefreshJsonFromPreset();
IDetailCategoryBuilder& JsonCategory = DetailBuilder.EditCategory("JSON", LOCTEXT("JsonCategory", "JSON"), ECategoryPriority::Uncommon);
JsonCategory.AddCustomRow(LOCTEXT("PresetJsonFilter", "Preset JSON"))
.WholeRowContent()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 6.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("JsonHelpText", "JSON representation of the current preset. Pasted valid JSON is applied immediately."))
.AutoWrapText(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SBox)
.MinDesiredHeight(260.0f)
.MaxDesiredHeight(420.0f)
[
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
.Text(FText::FromString(CachedJsonText))
.OnTextChanged(this, &FMorphPresetDetailsCustomization::HandleJsonTextChanged)
.AutoWrapText(false)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 6.0f, 0.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("ApplyJsonButton", "Apply JSON"))
.OnClicked(this, &FMorphPresetDetailsCustomization::OnApplyJsonClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("ResetJsonButton", "Reset From Preset"))
.OnClicked(this, &FMorphPresetDetailsCustomization::OnResetJsonClicked)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(this, &FMorphPresetDetailsCustomization::GetStatusText)
.ColorAndOpacity(this, &FMorphPresetDetailsCustomization::GetStatusColor)
.AutoWrapText(true)
]
]
];
}
void FMorphPresetDetailsCustomization::RefreshJsonFromPreset()
{
CachedJsonText = SerializePresetToJson(Preset.Get());
bIsUpdatingJsonText = true;
if (JsonTextBox.IsValid())
{
JsonTextBox->SetText(FText::FromString(CachedJsonText));
}
bIsUpdatingJsonText = false;
}
void FMorphPresetDetailsCustomization::ApplyJsonToPreset(const FString& JsonString)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
bLastApplySucceeded = false;
StatusText = LOCTEXT("NoPresetStatus", "Preset is no longer available.");
return;
}
UMorphPresetDataAsset* TempPreset = NewObject<UMorphPresetDataAsset>(GetTransientPackage(), UMorphPresetDataAsset::StaticClass(), NAME_None, RF_Transient);
if (!DeserializePresetFromJson(JsonString, *TempPreset))
{
bLastApplySucceeded = false;
StatusText = LOCTEXT("InvalidJsonStatus", "JSON is invalid or does not match the preset structure.");
return;
}
PresetAsset->Modify();
CopyPresetValues(*TempPreset, *PresetAsset);
PresetAsset->MarkPackageDirty();
PresetAsset->PostEditChange();
bLastApplySucceeded = true;
StatusText = LOCTEXT("AppliedJsonStatus", "JSON applied to the preset.");
RefreshJsonFromPreset();
if (PropertyUtilities.IsValid())
{
const FPropertyChangedEvent ChangeEvent(nullptr, EPropertyChangeType::ValueSet);
PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent);
PropertyUtilities->ForceRefresh();
}
}
FText FMorphPresetDetailsCustomization::GetStatusText() const
{
return StatusText;
}
FSlateColor FMorphPresetDetailsCustomization::GetStatusColor() const
{
return bLastApplySucceeded ? FSlateColor(FLinearColor(0.35f, 0.8f, 0.45f, 1.0f)) : FSlateColor(FLinearColor(0.95f, 0.4f, 0.35f, 1.0f));
}
FReply FMorphPresetDetailsCustomization::OnApplyJsonClicked()
{
if (JsonTextBox.IsValid())
{
ApplyJsonToPreset(JsonTextBox->GetText().ToString());
}
return FReply::Handled();
}
FReply FMorphPresetDetailsCustomization::OnResetJsonClicked()
{
RefreshJsonFromPreset();
bLastApplySucceeded = true;
StatusText = LOCTEXT("ResetJsonStatus", "JSON reset to the current preset state.");
return FReply::Handled();
}
void FMorphPresetDetailsCustomization::HandleJsonTextChanged(const FText& NewText)
{
if (bIsUpdatingJsonText)
{
return;
}
const FString NewJsonText = NewText.ToString();
if (NewJsonText == CachedJsonText)
{
return;
}
CachedJsonText = NewJsonText;
ApplyJsonToPreset(NewJsonText);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,733 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetEditorController.h"
#include "Animation/AnimData/IAnimationDataController.h"
#include "AnimPreviewInstance.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/AnimationAsset.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Animation/Skeleton.h"
#include "Components/SkeletalMeshComponent.h"
#include "Editor.h"
#include "Framework/Commands/UICommandList.h"
#include "GameFramework/Actor.h"
#include "IPersonaPreviewScene.h"
#include "IPersonaToolkit.h"
#include "Modules/ModuleManager.h"
#include "Morph/LipSyncComponent.h"
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "PersonaModule.h"
#include "ReferenceSkeleton.h"
#include "Sound/SoundBase.h"
DEFINE_LOG_CATEGORY_STATIC(LogMorphPresetEditorPreview, Log, All);
namespace
{
void SetAnimSequencePlayLength(UAnimSequence& AnimSequence, float PlayLength, bool bShouldTransact)
{
IAnimationDataController& Controller = AnimSequence.GetController();
Controller.SetNumberOfFrames(FMath::Max(Controller.ConvertSecondsToFrameNumber(PlayLength).Value, 1), bShouldTransact);
}
}
FMorphPresetEditorController::FMorphPresetEditorController(UMorphPresetDataAsset* InPreset)
: Preset(InPreset)
{
CommandList = MakeShared<FUICommandList>();
DefaultLipSyncPreviewMapping = TStrongObjectPtr<ULipSyncMappingDataAsset>(
NewObject<ULipSyncMappingDataAsset>(GetTransientPackage(), NAME_None, RF_Transient));
}
FMorphPresetEditorController::~FMorphPresetEditorController()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
if (PersonaToolkit.IsValid())
{
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = PersonaToolkit->GetPreviewScene())
{
PreviewScene->ClearSelectedBone();
}
}
}
void FMorphPresetEditorController::Initialize()
{
RebuildPreviewScene();
SetCurrentTime(0.0f);
}
UMorphPresetDataAsset* FMorphPresetEditorController::GetPreset() const
{
return Preset.Get();
}
USkeletalMesh* FMorphPresetEditorController::GetPreviewMesh() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
return PresetAsset->PreviewMesh.LoadSynchronous();
}
return nullptr;
}
bool FMorphPresetEditorController::SetPreviewMesh(USkeletalMesh* InPreviewMesh)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
return false;
}
if (PresetAsset->PreviewMesh.Get() == InPreviewMesh)
{
return false;
}
PresetAsset->Modify();
PresetAsset->PreviewMesh = InPreviewMesh;
PresetAsset->MarkPackageDirty();
RebuildPreviewScene();
return true;
}
TSharedPtr<IPersonaPreviewScene> FMorphPresetEditorController::GetPreviewScene() const
{
TSharedPtr<IPersonaPreviewScene> PreviewScene;
if (PersonaToolkit.IsValid())
{
PreviewScene = PersonaToolkit->GetPreviewScene();
}
return PreviewScene;
}
float FMorphPresetEditorController::GetTimelineLength() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
float TimelineLength = PresetAsset->GetTimelineLength();
if (PresetAsset->AnimationMontage)
{
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
const UAnimSequenceBase* PreviewSequence = PresetAsset->AnimationMontage->GetFirstAnimReference();
const float AnimationLength = PreviewSequence ? PreviewSequence->GetPlayLength() : PresetAsset->AnimationMontage->GetPlayLength();
TimelineLength = FMath::Max(TimelineLength, AnimationLength / PlayRate);
}
return FMath::Max(TimelineLength, 0.1f);
}
return 1.0f;
}
float FMorphPresetEditorController::GetCurrentTime() const
{
return CurrentTime;
}
void FMorphPresetEditorController::SetCurrentTime(float InTime)
{
CurrentTime = FMath::Clamp(InTime, 0.0f, GetTimelineLength());
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
RefreshPreviewAnimationPose();
}
}
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
}
bool FMorphPresetEditorController::TryGetHeadLocation(FVector& OutHeadLocation) const
{
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (!PreviewMesh)
{
return false;
}
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
int32 HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("head"));
if (HeadBoneIndex == INDEX_NONE)
{
HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("Head"));
}
if (HeadBoneIndex == INDEX_NONE)
{
return false;
}
const TArray<FTransform>& RefPose = RefSkeleton.GetRefBonePose();
FTransform HeadTransform = FTransform::Identity;
int32 BoneIndex = HeadBoneIndex;
while (BoneIndex != INDEX_NONE)
{
HeadTransform = RefPose[BoneIndex] * HeadTransform;
BoneIndex = RefSkeleton.GetParentIndex(BoneIndex);
}
OutHeadLocation = HeadTransform.GetLocation();
return true;
}
bool FMorphPresetEditorController::ResolvePreviewBoneName(FName InBoneName, FName& OutBoneName) const
{
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (!PreviewMesh || InBoneName.IsNone())
{
return false;
}
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
if (RefSkeleton.FindBoneIndex(InBoneName) != INDEX_NONE)
{
OutBoneName = InBoneName;
return true;
}
const FString RequestedBoneName = InBoneName.ToString();
for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex)
{
const FName CandidateBoneName = RefSkeleton.GetBoneName(BoneIndex);
if (CandidateBoneName.ToString().Equals(RequestedBoneName, ESearchCase::IgnoreCase))
{
OutBoneName = CandidateBoneName;
return true;
}
}
return false;
}
void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChanged)
{
if (UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
PresetAsset->MarkPackageDirty();
}
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
UAnimationAsset* CurrentPreviewAsset = nullptr;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
}
if (DesiredPreviewAsset != CurrentPreviewAsset ||
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
{
RebuildPreviewAnimation();
}
else if (PreviewAnimation.IsValid())
{
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
}
CurrentTime = FMath::Clamp(CurrentTime, 0.0f, GetTimelineLength());
// Re-apply morph weights immediately so viewport reflects drag/resize edits without stalling.
ApplyPresetToPreview();
if (bBroadcastPresetChanged)
{
PresetChanged.Broadcast();
}
}
void FMorphPresetEditorController::SetPlaying(bool bInPlaying)
{
if (bIsPlaying == bInPlaying)
{
return;
}
bIsPlaying = bInPlaying;
PlaybackStateChanged.Broadcast();
}
void FMorphPresetEditorController::TogglePlaying()
{
SetPlaying(!bIsPlaying);
}
void FMorphPresetEditorController::Tick(float DeltaTime)
{
if (bIsPlaying)
{
float NewTime = CurrentTime + DeltaTime;
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
const float Length = GetTimelineLength();
if (PresetAsset && PresetAsset->bLoop && Length > KINDA_SMALL_NUMBER)
{
NewTime = FMath::Fmod(NewTime, Length);
if (NewTime < 0.0f)
{
NewTime += Length;
}
}
else if (NewTime >= Length)
{
NewTime = Length;
bIsPlaying = false;
}
SetCurrentTime(NewTime);
}
else
{
ApplyPresetToPreview();
}
TickLipSyncPreviewComponents(DeltaTime);
if (bLipSyncPreviewAudioActive && !IsLipSyncPreviewAudioPlaying())
{
bLipSyncPreviewAudioActive = false;
PlaybackStateChanged.Broadcast();
}
}
USoundBase* FMorphPresetEditorController::GetLipSyncPreviewSound() const
{
return LipSyncPreviewSound.Get();
}
bool FMorphPresetEditorController::SetLipSyncPreviewSound(USoundBase* InSound)
{
if (LipSyncPreviewSound.Get() == InSound)
{
return false;
}
StopLipSyncPreviewAudio();
LipSyncPreviewSound = InSound;
PlaybackStateChanged.Broadcast();
return true;
}
bool FMorphPresetEditorController::PlayLipSyncPreviewAudio(float StartTime)
{
USoundBase* Sound = LipSyncPreviewSound.Get();
if (!Sound)
{
return false;
}
RebuildLipSyncPreviewComponents();
ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get();
if (!PreviewLipSync || !GEditor)
{
return false;
}
UAudioComponent* PreviewAudioComponent = GEditor->ResetPreviewAudioComponent(Sound);
if (!PreviewAudioComponent)
{
return false;
}
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
PreviewLipSync->AudioDrivenSound = Sound;
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
PreviewLipSync->bUseFallbackPulseWithoutEnvelope = false;
bLipSyncPreviewAudioActive = PreviewLipSync->PlayAudioDrivenLipSyncWithComponent(PreviewAudioComponent, Sound, StartTime);
PlaybackStateChanged.Broadcast();
return bLipSyncPreviewAudioActive;
}
void FMorphPresetEditorController::StopLipSyncPreviewAudio()
{
bLipSyncPreviewAudioActive = false;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->StopAudioDrivenLipSync();
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
}
bool FMorphPresetEditorController::IsLipSyncPreviewAudioPlaying() const
{
return bLipSyncPreviewAudioActive && LipSyncPreviewComponent.IsValid() && LipSyncPreviewComponent->IsAudioDrivenLipSyncPlaying();
}
void FMorphPresetEditorController::SetLipSyncPreviewSmoothness(float InSmoothness)
{
LipSyncPreviewSmoothness = FMath::Clamp(InSmoothness, 0.0f, 1.0f);
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
}
PlaybackStateChanged.Broadcast();
}
void FMorphPresetEditorController::SetLipSyncPreviewTranscript(const FText& InTranscript)
{
LipSyncPreviewTranscript = InTranscript;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
}
PlaybackStateChanged.Broadcast();
}
ULipSyncMappingDataAsset* FMorphPresetEditorController::GetLipSyncPreviewMapping() const
{
return LipSyncPreviewMapping.Get();
}
bool FMorphPresetEditorController::SetLipSyncPreviewMapping(ULipSyncMappingDataAsset* InMapping)
{
if (LipSyncPreviewMapping.Get() == InMapping)
{
return false;
}
LipSyncPreviewMapping = InMapping;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
return true;
}
void FMorphPresetEditorController::RebuildPreviewScene()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
PersonaToolkit.Reset();
PreviewAnimation.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (PreviewMesh == nullptr)
{
PreviewSceneChanged.Broadcast();
return;
}
FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
FPersonaToolkitArgs ToolkitArgs;
PersonaToolkit = PersonaModule.CreatePersonaToolkit(PreviewMesh, ToolkitArgs);
RebuildPreviewAnimation();
RebuildLipSyncPreviewComponents();
SetCurrentTime(CurrentTime);
PreviewSceneChanged.Broadcast();
}
void FMorphPresetEditorController::RebuildPreviewAnimation()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UAnimSequence* AnimSequence = NewObject<UAnimSequence>(GetTransientPackage(), NAME_None, RF_Transient);
PreviewAnimation.Reset();
PreviewStatusText = FText::GetEmpty();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
{
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
const FString PreviewMeshSkeletonName = PreviewMesh && PreviewMesh->GetSkeleton()
? PreviewMesh->GetSkeleton()->GetPathName()
: TEXT("None");
const FString AnimationSkeletonName = PreviewAsset->GetSkeleton()
? PreviewAsset->GetSkeleton()->GetPathName()
: TEXT("None");
PreviewStatusText = FText::FromString(FString::Printf(
TEXT("Animation preview is disabled: montage sequence '%s' uses skeleton '%s', but Preview Mesh uses '%s'. Retarget the animation to the Preview Mesh skeleton."),
*GetNameSafe(PreviewAsset),
*AnimationSkeletonName,
*PreviewMeshSkeletonName));
UE_LOG(LogMorphPresetEditorPreview, Warning, TEXT("%s"), *PreviewStatusText.ToString());
PreviewAsset = nullptr;
}
if (!PreviewAsset)
{
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
PreviewAsset = AnimSequence;
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
{
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
{
AnimSequence->SetSkeleton(Skeleton);
}
}
}
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true);
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
if (PreviewMeshComponent->PreviewInstance)
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetLooping(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
RefreshPreviewAnimationPose();
}
}
}
void FMorphPresetEditorController::RefreshPreviewAnimationPose()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewMeshComponent)
{
return;
}
PreviewMeshComponent->TickAnimation(0.0f, false);
PreviewMeshComponent->RefreshBoneTransforms();
PreviewMeshComponent->MarkRenderStateDirty();
PreviewScene->InvalidateViews();
}
UAnimationAsset* FMorphPresetEditorController::ResolvePreviewAnimationAsset() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (PresetAsset && PresetAsset->AnimationMontage)
{
if (UAnimSequenceBase* FirstAnimReference = PresetAsset->AnimationMontage->GetFirstAnimReference())
{
return FirstAnimReference;
}
}
return nullptr;
}
float FMorphPresetEditorController::GetPreviewAnimationPosition() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (!PresetAsset || !PreviewAsset)
{
return CurrentTime;
}
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
float PreviewPosition = CurrentTime * PlayRate;
if (const UAnimSequenceBase* PreviewSequence = Cast<UAnimSequenceBase>(PreviewAsset))
{
return FMath::Clamp(PreviewPosition, 0.0f, PreviewSequence->GetPlayLength());
}
return PreviewPosition;
}
bool FMorphPresetEditorController::IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const
{
if (!PreviewAsset)
{
return true;
}
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
const USkeleton* PreviewSkeleton = PreviewMesh ? PreviewMesh->GetSkeleton() : nullptr;
const USkeleton* AnimationSkeleton = PreviewAsset->GetSkeleton();
return PreviewSkeleton && AnimationSkeleton && PreviewSkeleton->IsCompatibleForEditor(AnimationSkeleton);
}
void FMorphPresetEditorController::ApplyPresetToPreview()
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PresetAsset || !PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewMeshComponent)
{
return;
}
PreviewMeshComponent->ClearMorphTargets();
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
}
TMap<FName, float> FinalValues;
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*PresetAsset, CurrentTime, 1.0f, FinalValues);
for (const TPair<FName, float>& Pair : FinalValues)
{
if (Pair.Value <= 0.001f)
{
continue;
}
PreviewMeshComponent->SetMorphTarget(Pair.Key, Pair.Value, false);
}
RefreshPreviewAnimationPose();
}
void FMorphPresetEditorController::RebuildLipSyncPreviewComponents()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
UWorld* PreviewWorld = PreviewScene->GetWorld();
if (!PreviewMeshComponent || !PreviewWorld)
{
return;
}
if (LipSyncPreviewActor.IsValid() && LipSyncPreviewMorphController.IsValid() && LipSyncPreviewComponent.IsValid())
{
LipSyncPreviewMorphController->TargetMeshComponent = PreviewMeshComponent;
LipSyncPreviewComponent->Mapping = ResolveLipSyncPreviewMapping();
return;
}
AActor* PreviewActor = PreviewWorld->SpawnActor<AActor>();
if (!PreviewActor)
{
return;
}
PreviewActor->SetFlags(RF_Transient);
UMorphControllerComponent* MorphController = NewObject<UMorphControllerComponent>(PreviewActor, TEXT("MorphEditorPreviewMorphController"), RF_Transient);
ULipSyncComponent* LipSyncComponent = NewObject<ULipSyncComponent>(PreviewActor, TEXT("MorphEditorPreviewLipSyncComponent"), RF_Transient);
if (!MorphController || !LipSyncComponent)
{
PreviewActor->Destroy();
return;
}
PreviewActor->AddInstanceComponent(MorphController);
PreviewActor->AddInstanceComponent(LipSyncComponent);
MorphController->TargetMeshComponent = PreviewMeshComponent;
MorphController->bAutoUpdate = false;
MorphController->bLogWarnings = false;
MorphController->RegisterComponent();
LipSyncComponent->MorphController = MorphController;
LipSyncComponent->Mapping = ResolveLipSyncPreviewMapping();
LipSyncComponent->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
LipSyncComponent->AudioDrivenTranscript = LipSyncPreviewTranscript;
LipSyncComponent->bAutoResolveMorphController = false;
LipSyncComponent->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
LipSyncComponent->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
LipSyncComponent->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
LipSyncComponent->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
LipSyncComponent->bUseFallbackPulseWithoutEnvelope = false;
LipSyncComponent->RegisterComponent();
LipSyncPreviewActor = PreviewActor;
LipSyncPreviewMorphController = MorphController;
LipSyncPreviewComponent = LipSyncComponent;
}
void FMorphPresetEditorController::DestroyLipSyncPreviewComponents()
{
if (ULipSyncComponent* LipSyncComponent = LipSyncPreviewComponent.Get())
{
LipSyncComponent->StopAudioDrivenLipSync();
LipSyncComponent->DestroyComponent();
}
if (UMorphControllerComponent* MorphController = LipSyncPreviewMorphController.Get())
{
MorphController->DestroyComponent();
}
if (AActor* PreviewActor = LipSyncPreviewActor.Get())
{
PreviewActor->Destroy();
}
LipSyncPreviewComponent.Reset();
LipSyncPreviewMorphController.Reset();
LipSyncPreviewActor.Reset();
}
void FMorphPresetEditorController::TickLipSyncPreviewComponents(float DeltaTime)
{
if (!LipSyncPreviewComponent.IsValid() || !LipSyncPreviewMorphController.IsValid())
{
return;
}
LipSyncPreviewComponent->TickComponent(DeltaTime, LEVELTICK_All, nullptr);
LipSyncPreviewMorphController->UpdateMorphTargetsNow(DeltaTime);
}
ULipSyncMappingDataAsset* FMorphPresetEditorController::ResolveLipSyncPreviewMapping() const
{
if (ULipSyncMappingDataAsset* SelectedMapping = LipSyncPreviewMapping.Get())
{
return SelectedMapping;
}
return DefaultLipSyncPreviewMapping.Get();
}
@@ -0,0 +1,617 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetEditorWindow.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SWindow.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Text/STextBlock.h"
#include "PropertyCustomizationHelpers.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Curves/RichCurve.h"
#define LOCTEXT_NAMESPACE "MorphPresetEditor"
namespace
{
FString CurveInterpToString(ERichCurveInterpMode InterpMode)
{
switch (InterpMode)
{
case RCIM_Constant:
return TEXT("constant");
case RCIM_Cubic:
return TEXT("ease");
case RCIM_Linear:
default:
return TEXT("linear");
}
}
FString CurveInterpToUnrealString(ERichCurveInterpMode InterpMode)
{
switch (InterpMode)
{
case RCIM_Constant:
return TEXT("RCIM_Constant");
case RCIM_Cubic:
return TEXT("RCIM_Cubic");
case RCIM_Linear:
default:
return TEXT("RCIM_Linear");
}
}
ERichCurveInterpMode ParseCurveInterp(const TSharedPtr<FJsonValue>& Value)
{
FString InterpName;
if (!Value.IsValid() || !Value->TryGetString(InterpName))
{
return RCIM_Linear;
}
InterpName = InterpName.ToLower();
if (InterpName == TEXT("constant") || InterpName == TEXT("step"))
{
return RCIM_Constant;
}
if (InterpName == TEXT("ease") || InterpName == TEXT("cubic") || InterpName == TEXT("auto") || InterpName == TEXT("rcim_cubic"))
{
return RCIM_Cubic;
}
return RCIM_Linear;
}
FKeyHandle FindAdjacentKeyHandle(const FRichCurve& Curve, FKeyHandle KeyHandle, bool bFindNext)
{
const float KeyTime = Curve.GetKeyTime(KeyHandle);
FKeyHandle BestHandle;
float BestDistance = TNumericLimits<float>::Max();
for (auto It = Curve.GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle CandidateHandle = *It;
if (CandidateHandle == KeyHandle)
{
continue;
}
const float CandidateTime = Curve.GetKeyTime(CandidateHandle);
const float Distance = bFindNext ? CandidateTime - KeyTime : KeyTime - CandidateTime;
if (Distance > 0.0f && Distance < BestDistance)
{
BestDistance = Distance;
BestHandle = CandidateHandle;
}
}
return BestHandle;
}
void ApplyVisibleEaseTangents(FRichCurve& Curve, FKeyHandle KeyHandle)
{
FKeyHandle SegmentStartHandle = KeyHandle;
FKeyHandle SegmentEndHandle = FindAdjacentKeyHandle(Curve, SegmentStartHandle, true);
if (!SegmentEndHandle.IsValid())
{
SegmentEndHandle = SegmentStartHandle;
SegmentStartHandle = FindAdjacentKeyHandle(Curve, SegmentEndHandle, false);
}
if (!SegmentStartHandle.IsValid() || !SegmentEndHandle.IsValid())
{
return;
}
Curve.SetKeyTangentMode(SegmentStartHandle, RCTM_Break, false);
Curve.SetKeyTangentMode(SegmentEndHandle, RCTM_Break, false);
Curve.GetKey(SegmentStartHandle).LeaveTangent = 0.0f;
Curve.GetKey(SegmentEndHandle).ArriveTangent = 0.0f;
}
void WriteCurveKeys(const FMorphTimelineClip& Clip, const TSharedPtr<FJsonObject>& Obj)
{
TArray<TSharedPtr<FJsonValue>> KeyArray;
if (const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst())
{
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle KeyHandle = *It;
TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
KeyObj->SetNumberField(TEXT("time"), Curve->GetKeyTime(KeyHandle));
KeyObj->SetNumberField(TEXT("value"), Curve->GetKeyValue(KeyHandle));
KeyObj->SetStringField(TEXT("interp"), CurveInterpToString(Curve->GetKeyInterpMode(KeyHandle)));
KeyObj->SetStringField(TEXT("interpMode"), CurveInterpToUnrealString(Curve->GetKeyInterpMode(KeyHandle)));
KeyObj->SetStringField(TEXT("tangentMode"), TEXT("RCTM_Auto"));
KeyObj->SetStringField(TEXT("tangentWeightMode"), TEXT("RCTWM_WeightedNone"));
KeyObj->SetNumberField(TEXT("arriveTangent"), 0.0);
KeyObj->SetNumberField(TEXT("arriveTangentWeight"), 0.0);
KeyObj->SetNumberField(TEXT("leaveTangent"), 0.0);
KeyObj->SetNumberField(TEXT("leaveTangentWeight"), 0.0);
KeyArray.Add(MakeShared<FJsonValueObject>(KeyObj));
}
}
Obj->SetArrayField(TEXT("curve_keys"), KeyArray);
Obj->SetArrayField(TEXT("curveKeys"), KeyArray);
TSharedPtr<FJsonObject> EditorCurveDataObj = MakeShared<FJsonObject>();
EditorCurveDataObj->SetArrayField(TEXT("keys"), KeyArray);
EditorCurveDataObj->SetNumberField(TEXT("defaultValue"), TNumericLimits<float>::Max());
EditorCurveDataObj->SetStringField(TEXT("preInfinityExtrap"), TEXT("RCCE_Constant"));
EditorCurveDataObj->SetStringField(TEXT("postInfinityExtrap"), TEXT("RCCE_Constant"));
TSharedPtr<FJsonObject> WeightCurveObj = MakeShared<FJsonObject>();
WeightCurveObj->SetObjectField(TEXT("editorCurveData"), EditorCurveDataObj);
WeightCurveObj->SetStringField(TEXT("externalCurve"), TEXT("None"));
Obj->SetObjectField(TEXT("weightCurve"), WeightCurveObj);
}
float ParseCurveNumber(const TSharedPtr<FJsonValue>& Value, float DefaultValue)
{
double Number = 0.0;
return Value.IsValid() && Value->TryGetNumber(Number) ? static_cast<float>(Number) : DefaultValue;
}
void ReadCurveKeys(const TSharedPtr<FJsonObject>& Obj, FMorphTimelineClip& Clip)
{
if (!Obj.IsValid())
{
Clip.EnsureDefaultCurve();
return;
}
const TArray<TSharedPtr<FJsonValue>>* KeyArray = nullptr;
if ((!Obj->TryGetArrayField(TEXT("curve_keys"), KeyArray) || !KeyArray) &&
(!Obj->TryGetArrayField(TEXT("curveKeys"), KeyArray) || !KeyArray))
{
if (const TSharedPtr<FJsonObject>* WeightCurveObj = nullptr; Obj->TryGetObjectField(TEXT("weightCurve"), WeightCurveObj) && WeightCurveObj->IsValid())
{
const TSharedPtr<FJsonObject>* EditorCurveDataObj = nullptr;
if (!(*WeightCurveObj)->TryGetObjectField(TEXT("editorCurveData"), EditorCurveDataObj) || !EditorCurveDataObj || !EditorCurveDataObj->IsValid() ||
!(*EditorCurveDataObj)->TryGetArrayField(TEXT("keys"), KeyArray) || !KeyArray)
{
Clip.EnsureDefaultCurve();
return;
}
}
else
{
Clip.EnsureDefaultCurve();
return;
}
}
FRichCurve* Curve = Clip.WeightCurve.GetRichCurve();
if (!Curve)
{
return;
}
Curve->Reset();
TArray<FKeyHandle> EaseKeyHandles;
for (const TSharedPtr<FJsonValue>& KeyValue : *KeyArray)
{
const TSharedPtr<FJsonObject>* KeyObj = nullptr;
if (!KeyValue.IsValid() || !KeyValue->TryGetObject(KeyObj) || !KeyObj || !KeyObj->IsValid())
{
continue;
}
const float Time = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("time")), 0.0f), 0.0f, FMath::Max(Clip.Duration, 0.001f));
const float Value = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("value")), 0.0f), 0.0f, 1.0f);
const ERichCurveInterpMode InterpMode = ParseCurveInterp((*KeyObj)->TryGetField(TEXT("interp")).IsValid()
? (*KeyObj)->TryGetField(TEXT("interp"))
: (*KeyObj)->TryGetField(TEXT("interpMode")));
const FKeyHandle KeyHandle = Curve->UpdateOrAddKey(Time, Value);
Curve->SetKeyInterpMode(KeyHandle, InterpMode, false);
if (InterpMode == RCIM_Cubic)
{
EaseKeyHandles.Add(KeyHandle);
}
}
for (const FKeyHandle KeyHandle : EaseKeyHandles)
{
ApplyVisibleEaseTangents(*Curve, KeyHandle);
}
Clip.EnsureDefaultCurve();
}
}
void SMorphPresetEditorWindow::Construct(const FArguments& InArgs)
{
if (UMorphPresetDataAsset* PresetArg = InArgs._Preset)
{
SelectedPreset = PresetArg;
bEmbeddedMode = true;
}
ChildSlot
[
BuildContent()
];
if (bEmbeddedMode && SelectedPreset.IsValid())
{
RefreshFromPreset();
}
}
TSharedRef<SWidget> SMorphPresetEditorWindow::BuildContent()
{
TSharedRef<SVerticalBox> Root = SNew(SVerticalBox);
if (!bEmbeddedMode)
{
Root->AddSlot()
.AutoHeight()
.Padding(4.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0, 0, 8, 0)
[
SNew(STextBlock)
.Text(LOCTEXT("PresetLabel", "Morph Preset:"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SAssignNew(PresetPicker, SObjectPropertyEntryBox)
.AllowedClass(UMorphPresetDataAsset::StaticClass())
.ObjectPath(this, &SMorphPresetEditorWindow::GetSelectedPresetPath)
.OnObjectChanged(this, &SMorphPresetEditorWindow::OnPresetChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
.AllowCreate(false)
]
];
}
Root->AddSlot()
.FillHeight(1.0f)
.Padding(4.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.Text(LOCTEXT("JsonLabel", "JSON (структурированный формат):"))
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0, 4)
[
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
.HintText(LOCTEXT("JsonHint",
"{\n"
" \"metadata\": { \"display_name\": \"Моргание\", \"description\": \"Полное моргание\" },\n"
" \"settings\": { \"duration\": 0.15, \"loop\": true, \"loop_delay\": 2.5, \"blend_speed\": 18 },\n"
" \"morph_targets\": [\n"
" { \"name\": \"eyeBlinkLeft\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] },\n"
" { \"name\": \"eyeBlinkRight\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] }\n"
" ]\n"
"}"))
.Font(FCoreStyle::GetDefaultFontStyle("Mono", 10))
]
];
Root->AddSlot()
.AutoHeight()
.Padding(4.0f)
.HAlign(HAlign_Right)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0, 0, 8, 0)
[
SNew(SButton)
.Text(LOCTEXT("ExportButton", "Export"))
.OnClicked(this, &SMorphPresetEditorWindow::OnExportClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("ApplyButton", "Apply"))
.OnClicked(this, &SMorphPresetEditorWindow::OnApplyClicked)
]
];
return Root;
}
void SMorphPresetEditorWindow::RefreshFromPreset()
{
if (!SelectedPreset.IsValid() || !JsonTextBox.IsValid()) return;
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
RootObj->SetStringField(TEXT("displayName"), SelectedPreset->DisplayName.ToString());
RootObj->SetStringField(TEXT("description"), SelectedPreset->Description.ToString());
TArray<TSharedPtr<FJsonValue>> MorphArr;
for (const FMorphTimelineClip& Clip : SelectedPreset->MorphTargets)
{
if (!Clip.IsValid()) continue;
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
WriteCurveKeys(Clip, Obj);
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
}
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
RootObj->SetNumberField(TEXT("duration"), SelectedPreset->Duration);
RootObj->SetBoolField(TEXT("bLoop"), SelectedPreset->bLoop);
RootObj->SetNumberField(TEXT("loopDelay"), SelectedPreset->LoopDelay);
RootObj->SetNumberField(TEXT("blendSpeedOverride"), SelectedPreset->BlendSpeedOverride);
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
JsonTextBox->SetText(FText::FromString(OutputString));
}
FString SMorphPresetEditorWindow::GetSelectedPresetPath() const
{
if (UMorphPresetDataAsset* Preset = SelectedPreset.Get())
{
return Preset->GetPathName();
}
return FString();
}
void SMorphPresetEditorWindow::OnPresetChanged(const FAssetData& AssetData)
{
SelectedPreset = Cast<UMorphPresetDataAsset>(AssetData.GetAsset());
}
FReply SMorphPresetEditorWindow::OnApplyClicked()
{
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
if (!Preset)
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
return FReply::Handled();
}
FString JsonString = JsonTextBox->GetText().ToString();
if (JsonString.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: JSON is empty."));
return FReply::Handled();
}
// Parse JSON
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("Morph Preset Editor: Invalid JSON: %s"), *Reader->GetErrorMessage());
return FReply::Handled();
}
// Структурированный парсинг: metadata, settings, morph_targets
// Поддержка обратной совместимости: плоский формат (все поля в корне)
auto ParseFloat = [](const TSharedPtr<FJsonValue>& Val, float Default) -> float
{
double Num = 0.0;
return Val.IsValid() && Val->TryGetNumber(Num) ? static_cast<float>(FMath::Max(0.0, Num)) : Default;
};
auto ParseBool = [](const TSharedPtr<FJsonValue>& Val, bool Default) -> bool
{
bool b = false;
if (Val.IsValid() && Val->TryGetBool(b)) return b;
double Num = 0.0;
if (Val.IsValid() && Val->TryGetNumber(Num)) return Num != 0.0;
return Default;
};
auto ParseInt = [](const TSharedPtr<FJsonValue>& Val, int32 Default) -> int32
{
double Num = 0.0;
return Val.IsValid() && Val->TryGetNumber(Num) ? FMath::Max(0, static_cast<int32>(Num)) : Default;
};
auto ParseString = [](const TSharedPtr<FJsonValue>& Val) -> FString
{
FString S;
return Val.IsValid() && Val->TryGetString(S) ? S : FString();
};
TArray<FMorphTimelineClip> NewMorphTargets;
// metadata
if (const TSharedPtr<FJsonObject>* MetaObj = nullptr; JsonObject->TryGetObjectField(TEXT("metadata"), MetaObj) && MetaObj->IsValid())
{
Preset->DisplayName = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("display_name"))));
Preset->Description = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("description"))));
}
else
{
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("displayName")));
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
}
// settings
if (const TSharedPtr<FJsonObject>* SettingsObj = nullptr; JsonObject->TryGetObjectField(TEXT("settings"), SettingsObj) && SettingsObj->IsValid())
{
Preset->Duration = ParseFloat((*SettingsObj)->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool((*SettingsObj)->TryGetField(TEXT("loop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat((*SettingsObj)->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat((*SettingsObj)->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
}
else
{
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("bLoop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loopDelay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blendSpeedOverride")), Preset->BlendSpeedOverride);
}
// morph_targets: массив объектов { name, start_time, duration, weight, curve_keys? }
const TArray<TSharedPtr<FJsonValue>>* MorphArr = nullptr;
if ((JsonObject->TryGetArrayField(TEXT("morph_targets"), MorphArr) || JsonObject->TryGetArrayField(TEXT("morphTargets"), MorphArr)) && MorphArr)
{
for (const TSharedPtr<FJsonValue>& Val : *MorphArr)
{
const TSharedPtr<FJsonObject>* Obj = nullptr;
if (Val->TryGetObject(Obj) && Obj->IsValid())
{
FString NameStr = ParseString((*Obj)->TryGetField(TEXT("name")));
if (NameStr.IsEmpty()) continue;
FMorphTimelineClip Clip;
Clip.Name = FName(*NameStr);
Clip.StartTime = ParseFloat((*Obj)->TryGetField(TEXT("start_time")),
ParseFloat((*Obj)->TryGetField(TEXT("startTime")), ParseFloat((*Obj)->TryGetField(TEXT("delay")), 0.0f)));
Clip.Duration = ParseFloat((*Obj)->TryGetField(TEXT("duration")), 0.2f);
if (Clip.Duration < 0.001f) Clip.Duration = 0.2f;
Clip.Weight = ParseFloat((*Obj)->TryGetField(TEXT("weight")), 1.0f);
Clip.Weight = FMath::Clamp(Clip.Weight, 0.0f, 5.0f);
ReadCurveKeys(*Obj, Clip);
NewMorphTargets.Add(Clip);
}
}
}
// Обратная совместимость: старый формат morph_targets как объект { "name": weight }
if (NewMorphTargets.Num() == 0)
{
if (const TSharedPtr<FJsonObject>* MorphObj = nullptr; JsonObject->TryGetObjectField(TEXT("morph_targets"), MorphObj) && MorphObj->IsValid())
{
for (const auto& Pair : (*MorphObj)->Values)
{
double NumValue = 0.0;
if (Pair.Value->TryGetNumber(NumValue))
{
FMorphTimelineClip Clip;
Clip.Name = FName(*Pair.Key);
Clip.StartTime = 0.0f;
Clip.Duration = 0.2f;
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
Clip.EnsureDefaultCurve();
NewMorphTargets.Add(Clip);
}
}
}
// Старый morph_target_settings (если morph_targets был объектом)
if (const TSharedPtr<FJsonObject>* SettingsMap = nullptr; JsonObject->TryGetObjectField(TEXT("morph_target_settings"), SettingsMap) && SettingsMap->IsValid())
{
for (const auto& Pair : (*SettingsMap)->Values)
{
const TSharedPtr<FJsonObject>* TimingObj = nullptr;
if (Pair.Value->TryGetObject(TimingObj) && TimingObj->IsValid())
{
FName MorphName(*Pair.Key);
for (FMorphTimelineClip& Clip : NewMorphTargets)
{
if (Clip.Name == MorphName)
{
Clip.StartTime = ParseFloat((*TimingObj)->TryGetField(TEXT("delay")), 0.0f);
break;
}
}
}
}
}
}
// Плоский формат (поля в корне) — если morph_targets не найден
if (NewMorphTargets.Num() == 0)
{
for (const auto& Pair : JsonObject->Values)
{
const FString& Key = Pair.Key;
const TSharedPtr<FJsonValue>& Value = Pair.Value;
if (Key.Equals(TEXT("metadata"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("settings"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("morph_targets"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morph_target_settings"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("duration"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loop"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("loop_delay"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("blend_speed"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("display_name"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("description"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morphTargets"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("bLoop"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loopDelay"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("blendSpeedOverride"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("displayName"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("nativeClass"), ESearchCase::IgnoreCase))
{
continue;
}
double NumValue = 0.0;
if (Value->TryGetNumber(NumValue))
{
FMorphTimelineClip Clip;
Clip.Name = FName(*Key);
Clip.StartTime = 0.0f;
Clip.Duration = 0.2f;
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
Clip.EnsureDefaultCurve();
NewMorphTargets.Add(Clip);
}
}
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("loop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("display_name")));
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
}
Preset->MorphTargets = MoveTemp(NewMorphTargets);
Preset->MarkPackageDirty();
UE_LOG(LogTemp, Log, TEXT("Morph Preset Editor: Applied %d morph targets to %s"), Preset->GetMorphCount(), *Preset->GetName());
return FReply::Handled();
}
FReply SMorphPresetEditorWindow::OnExportClicked()
{
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
if (!Preset)
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
return FReply::Handled();
}
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
RootObj->SetStringField(TEXT("displayName"), Preset->DisplayName.ToString());
RootObj->SetStringField(TEXT("description"), Preset->Description.ToString());
// morph_targets — массив объектов (timeline clips)
TArray<TSharedPtr<FJsonValue>> MorphArr;
for (const FMorphTimelineClip& Clip : Preset->MorphTargets)
{
if (!Clip.IsValid()) continue;
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
WriteCurveKeys(Clip, Obj);
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
}
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
RootObj->SetNumberField(TEXT("duration"), Preset->Duration);
RootObj->SetBoolField(TEXT("bLoop"), Preset->bLoop);
RootObj->SetNumberField(TEXT("loopDelay"), Preset->LoopDelay);
RootObj->SetNumberField(TEXT("blendSpeedOverride"), Preset->BlendSpeedOverride);
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
JsonTextBox->SetText(FText::FromString(OutputString));
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,479 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetViewportPanel.h"
#include "AssetRegistry/AssetData.h"
#include "AdvancedPreviewScene.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "EditorViewportClient.h"
#include "Engine/SkeletalMesh.h"
#include "IPersonaPreviewScene.h"
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "PropertyCustomizationHelpers.h"
#include "SEditorViewport.h"
#include "Sound/SoundBase.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SSlider.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "MorphPresetViewportPanel"
namespace
{
static constexpr float MinFaceCameraDistance = 90.0f;
static constexpr float MaxFaceCameraDistance = 220.0f;
FName FindHeadBoneName(const UDebugSkelMeshComponent* PreviewMeshComponent)
{
if (!PreviewMeshComponent)
{
return NAME_None;
}
static const FName CandidateBoneNames[] =
{
TEXT("head"),
TEXT("Head"),
TEXT("head_jnt"),
TEXT("Head_jnt")
};
for (const FName CandidateBoneName : CandidateBoneNames)
{
if (PreviewMeshComponent->GetBoneIndex(CandidateBoneName) != INDEX_NONE)
{
return CandidateBoneName;
}
}
return NAME_None;
}
FVector ResolveFaceForwardDirection(const FTransform& HeadTransform)
{
const FVector PreferredDirection = FVector::YAxisVector;
const FVector CandidateDirections[] =
{
HeadTransform.GetUnitAxis(EAxis::X),
-HeadTransform.GetUnitAxis(EAxis::X),
HeadTransform.GetUnitAxis(EAxis::Y),
-HeadTransform.GetUnitAxis(EAxis::Y)
};
float BestScore = -FLT_MAX;
FVector BestDirection = PreferredDirection;
for (const FVector& CandidateDirection : CandidateDirections)
{
FVector FlattenedDirection = FVector(CandidateDirection.X, CandidateDirection.Y, 0.0f);
if (!FlattenedDirection.Normalize())
{
continue;
}
const float Score = FVector::DotProduct(FlattenedDirection, PreferredDirection);
if (Score > BestScore)
{
BestScore = Score;
BestDirection = FlattenedDirection;
}
}
return BestDirection;
}
void ResolveDefaultCamera(const TSharedPtr<IPersonaPreviewScene>& PreviewScene, FVector& OutCameraLocation, FVector& OutCameraTarget)
{
OutCameraTarget = FVector(0.0f, 0.0f, 80.0f);
FVector FaceForwardDirection = FVector::YAxisVector;
float CameraDistance = 165.0f;
if (PreviewScene.IsValid())
{
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
const FBoxSphereBounds Bounds = PreviewMeshComponent->Bounds;
CameraDistance = FMath::Clamp(FMath::Max(Bounds.BoxExtent.X, Bounds.BoxExtent.Y) * 1.35f, MinFaceCameraDistance, MaxFaceCameraDistance);
OutCameraTarget = Bounds.Origin + FVector(0.0f, 0.0f, Bounds.BoxExtent.Z * 0.62f);
const FName HeadBoneName = FindHeadBoneName(PreviewMeshComponent);
if (HeadBoneName != NAME_None)
{
const FTransform HeadTransform = PreviewMeshComponent->GetSocketTransform(HeadBoneName, RTS_World);
OutCameraTarget = HeadTransform.GetLocation() + FVector(0.0f, 0.0f, 4.0f);
FaceForwardDirection = ResolveFaceForwardDirection(HeadTransform);
}
}
}
OutCameraLocation = OutCameraTarget + FaceForwardDirection * CameraDistance + FVector(0.0f, 0.0f, 8.0f);
}
class SMorphPresetPreviewViewport : public SEditorViewport
{
public:
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
SEditorViewport::Construct(SEditorViewport::FArguments());
}
protected:
virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr;
check(PreviewScene.IsValid());
TSharedRef<FEditorViewportClient> ViewportClient = MakeShared<FEditorViewportClient>(nullptr, static_cast<FPreviewScene*>(PreviewScene.Get()), SharedThis(this));
FVector CameraLocation = FVector::ZeroVector;
FVector CameraTarget = FVector::ZeroVector;
ResolveDefaultCamera(PreviewScene, CameraLocation, CameraTarget);
ViewportClient->SetViewLocation(CameraLocation);
ViewportClient->SetViewRotation((CameraTarget - CameraLocation).Rotation());
ViewportClient->SetLookAtLocation(CameraTarget);
ViewportClient->SetViewModes(VMI_Lit, VMI_Lit);
ViewportClient->SetRealtime(true);
return ViewportClient;
}
virtual TSharedPtr<SWidget> BuildViewportToolbar() override
{
return SNullWidget::NullWidget;
}
private:
TSharedPtr<FMorphPresetEditorController> Controller;
};
}
void SMorphPresetViewportPanel::Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
AssetEditorToolkit = InArgs._AssetEditorToolkit;
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
Controller->OnPresetChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
}
ChildSlot
[
SAssignNew(RootBorder, SBorder)
.Padding(8.0f)
];
RebuildContent();
RegisterActiveTimer(0.0f, FWidgetActiveTimerDelegate::CreateSP(this, &SMorphPresetViewportPanel::HandleActiveTimer));
}
SMorphPresetViewportPanel::~SMorphPresetViewportPanel()
{
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().RemoveAll(this);
Controller->OnPresetChanged().RemoveAll(this);
}
}
void SMorphPresetViewportPanel::RebuildContent()
{
if (!RootBorder.IsValid())
{
return;
}
if (!Controller.IsValid() || !Controller->GetPreviewScene().IsValid())
{
RootBorder->SetContent(
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("PreviewMeshLabel", "Preview Mesh"))
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(USkeletalMesh::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedPreviewMeshPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnPreviewMeshChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
.Padding(0.0f, 16.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("NoPreviewMesh", "Assign a skeletal mesh to see the Morph Target preset in the viewport."))
]);
return;
}
RootBorder->SetContent(
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("LipSyncMappingLabel", "Lip Sync Mapping"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(ULipSyncMappingDataAsset::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncMappingChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("LipSyncAudioLabel", "Lip Sync Audio"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(USoundBase::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncSoundChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
[
SNew(SButton)
.Text(this, &SMorphPresetViewportPanel::GetLipSyncPlaybackText)
.OnClicked(this, &SMorphPresetViewportPanel::OnPlayLipSyncClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("StopLipSync", "Stop"))
.OnClicked(this, &SMorphPresetViewportPanel::OnStopLipSyncClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(12.0f, 0.0f, 6.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("LipSyncSmoothLabel", "Smooth"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SSlider)
.MinValue(0.0f)
.MaxValue(1.0f)
.Value(this, &SMorphPresetViewportPanel::GetLipSyncSmoothness)
.OnValueChanged(this, &SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(SEditableTextBox)
.HintText(LOCTEXT("LipSyncTranscriptHint", "Paste audio transcript here for text-assisted visemes"))
.Text(this, &SMorphPresetViewportPanel::GetLipSyncTranscript)
.OnTextChanged(this, &SMorphPresetViewportPanel::OnLipSyncTranscriptChanged)
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(STextBlock)
.Text(this, &SMorphPresetViewportPanel::GetPreviewStatusText)
.Visibility(this, &SMorphPresetViewportPanel::GetPreviewStatusVisibility)
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.56f, 0.12f)))
.AutoWrapText(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SMorphPresetPreviewViewport)
.Controller(Controller)
]);
}
void SMorphPresetViewportPanel::OnPreviewMeshChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetPreviewMesh(Cast<USkeletalMesh>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnLipSyncSoundChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewSound(Cast<USoundBase>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnLipSyncMappingChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewMapping(Cast<ULipSyncMappingDataAsset>(AssetData.GetAsset()));
}
}
FString SMorphPresetViewportPanel::GetSelectedPreviewMeshPath() const
{
if (Controller.IsValid())
{
if (USkeletalMesh* Mesh = Controller->GetPreviewMesh())
{
return Mesh->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath() const
{
if (Controller.IsValid())
{
if (USoundBase* Sound = Controller->GetLipSyncPreviewSound())
{
return Sound->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath() const
{
if (Controller.IsValid())
{
if (ULipSyncMappingDataAsset* Mapping = Controller->GetLipSyncPreviewMapping())
{
return Mapping->GetPathName();
}
}
return FString();
}
FReply SMorphPresetViewportPanel::OnPlayLipSyncClicked()
{
if (Controller.IsValid())
{
if (Controller->IsLipSyncPreviewAudioPlaying())
{
Controller->StopLipSyncPreviewAudio();
}
else
{
Controller->PlayLipSyncPreviewAudio();
}
}
return FReply::Handled();
}
FReply SMorphPresetViewportPanel::OnStopLipSyncClicked()
{
if (Controller.IsValid())
{
Controller->StopLipSyncPreviewAudio();
}
return FReply::Handled();
}
FText SMorphPresetViewportPanel::GetLipSyncPlaybackText() const
{
return Controller.IsValid() && Controller->IsLipSyncPreviewAudioPlaying()
? LOCTEXT("PauseLipSync", "Pause Lip Sync")
: LOCTEXT("PlayLipSync", "Play Lip Sync");
}
float SMorphPresetViewportPanel::GetLipSyncSmoothness() const
{
return Controller.IsValid() ? Controller->GetLipSyncPreviewSmoothness() : 0.55f;
}
void SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged(float NewValue)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewSmoothness(NewValue);
}
}
FText SMorphPresetViewportPanel::GetLipSyncTranscript() const
{
return Controller.IsValid() ? Controller->GetLipSyncPreviewTranscript() : FText::GetEmpty();
}
void SMorphPresetViewportPanel::OnLipSyncTranscriptChanged(const FText& NewText)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewTranscript(NewText);
}
}
FText SMorphPresetViewportPanel::GetPreviewStatusText() const
{
return Controller.IsValid() ? Controller->GetPreviewStatusText() : FText::GetEmpty();
}
EVisibility SMorphPresetViewportPanel::GetPreviewStatusVisibility() const
{
return !GetPreviewStatusText().IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed;
}
EActiveTimerReturnType SMorphPresetViewportPanel::HandleActiveTimer(double, float InDeltaTime)
{
if (Controller.IsValid())
{
Controller->Tick(InDeltaTime);
}
return EActiveTimerReturnType::Continue;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,62 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Vendored for FunnyGirlEditor (adapted from UE Persona private SAnimEditorBase).
#pragma once
#include "CoreMinimal.h"
#include "UObject/GCObject.h"
#include "IPersonaPreviewScene.h"
#include "Widgets/SCompoundWidget.h"
#include "PersonaDelegates.h"
class UAnimationAsset;
class UAnimPreviewInstance;
class FUNNYGIRLEDITOR_API SAnimEditorBase : public SCompoundWidget, public FGCObject
{
public:
SLATE_BEGIN_ARGS(SAnimEditorBase)
: _DisplayAnimTimeline(true)
, _DisplayAnimScrubBar(false)
, _DisplayAnimScrubBarEditing(false)
{}
SLATE_EVENT(FOnObjectsSelected, OnObjectsSelected)
SLATE_ARGUMENT(bool, DisplayAnimTimeline)
SLATE_ARGUMENT(bool, DisplayAnimScrubBar)
SLATE_ARGUMENT(bool, DisplayAnimScrubBarEditing)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene);
float GetViewMinInput() const { return ViewMinInput; }
float GetViewMaxInput() const { return ViewMaxInput; }
float GetMinInput() const { return 0.0f; }
float GetMaxInput() const { return GetSequenceLength(); }
void SetInputViewRange(float InViewMinInput, float InViewMaxInput);
float GetScrubValue() const;
void OnSelectionChanged(const TArray<UObject*>& SelectedItems);
// FGCObject
virtual void AddReferencedObjects(FReferenceCollector& Collector) override {}
virtual FString GetReferencerName() const override { return TEXT("FunnyGirlVendoredSAnimEditorBase"); }
virtual TSharedRef<SWidget> CreateDocumentAnchor();
protected:
virtual TSharedRef<SWidget> CreateAnimTimelineWidget() = 0;
virtual TSharedRef<SWidget> ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing);
virtual UAnimationAsset* GetEditorObject() const = 0;
float GetSequenceLength() const;
TSharedRef<IPersonaPreviewScene> GetPreviewScene() const { return PreviewScenePtr.Pin().ToSharedRef(); }
protected:
TWeakPtr<IPersonaPreviewScene> PreviewScenePtr;
TSharedPtr<SVerticalBox> EditorPanels;
TSharedPtr<SVerticalBox> NonScrollEditorPanels;
FOnObjectsSelected OnObjectsSelectedDelegate;
float ViewMinInput = 0.0f;
float ViewMaxInput = 0.0f;
};
@@ -0,0 +1,19 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "LipSyncMappingAssetFactory.generated.h"
UCLASS()
class FUNNYGIRLEDITOR_API ULipSyncMappingAssetFactory : public UFactory
{
GENERATED_BODY()
public:
ULipSyncMappingAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FString GetDefaultNewAssetName() const override;
};
@@ -0,0 +1,16 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class FLipSyncMappingAssetTypeActions : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
virtual FText GetAssetDescription(const FAssetData& AssetData) const override;
};

Some files were not shown because too many files have changed in this diff Show More