Add new plugins, refactor exp, introduce stat forge to replace GAS
This commit is contained in:
BIN
Plugins/InteractionSystem/Content/Materials/M_UiBackgroundBlur.uasset
LFS
Normal file
BIN
Plugins/InteractionSystem/Content/Materials/M_UiBackgroundBlur.uasset
LFS
Normal file
Binary file not shown.
BIN
Plugins/InteractionSystem/Content/WBP_Interaction.uasset
LFS
Normal file
BIN
Plugins/InteractionSystem/Content/WBP_Interaction.uasset
LFS
Normal file
Binary file not shown.
24
Plugins/InteractionSystem/InteractionSystem.uplugin
Normal file
24
Plugins/InteractionSystem/InteractionSystem.uplugin
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "InteractionSystem",
|
||||
"Description": "",
|
||||
"Category": "Gameplay",
|
||||
"CreatedBy": "",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "InteractionSystem",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/InteractionSystem/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/InteractionSystem/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,53 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class InteractionSystem : ModuleRules
|
||||
{
|
||||
public InteractionSystem(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core", "UMG",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"DeveloperSettings"
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Components/InteractionWidgetComponent.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "Interface/InteractableActorInterface.h"
|
||||
#include "Widgets/BaseInteractionWidget.h"
|
||||
|
||||
|
||||
UInteractionWidgetComponent::UInteractionWidgetComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
|
||||
Space = EWidgetSpace::Screen;
|
||||
bManuallyRedraw = true;
|
||||
RedrawTime = 1.f;
|
||||
bDrawAtDesiredSize = true;
|
||||
Pivot = FVector2D(0.5f, 0.5f);
|
||||
TickMode = ETickMode::Disabled;
|
||||
if (const UInteractionSettings* InteractionSettings = GetDefault<UInteractionSettings>())
|
||||
{
|
||||
WidgetClass = InteractionSettings->DefaultInteractionWidgetClass;
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionWidgetComponent::ToggleWidget(bool bActive)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
if (UBaseInteractionWidget* InteractionWidget = Cast<UBaseInteractionWidget>(GetWidget()))
|
||||
{
|
||||
InteractionWidget->UpdateInfo(IInteractableActorInterface::Execute_GetInteractionText(GetOwner()));
|
||||
}
|
||||
}
|
||||
|
||||
SetVisibility(bActive);
|
||||
RequestRenderUpdate();
|
||||
}
|
||||
|
||||
void UInteractionWidgetComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (UBaseInteractionWidget* InteractionWidget = Cast<UBaseInteractionWidget>(GetWidget()))
|
||||
{
|
||||
InteractionWidget->UpdateInfo(IInteractableActorInterface::Execute_GetInteractionText(GetOwner()));
|
||||
}
|
||||
|
||||
SetPivot(GetPivot() + WidgetOffset);
|
||||
ToggleWidget(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InteractionSystem.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FInteractionSystemModule"
|
||||
|
||||
void FInteractionSystemModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FInteractionSystemModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FInteractionSystemModule, InteractionSystem)
|
||||
@@ -0,0 +1,180 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Subsystem/InteractionSubsystem.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Interface/InteractableActorInterface.h"
|
||||
|
||||
void UInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
if (GetWorld())
|
||||
{
|
||||
GetWorld()->AddOnActorSpawnedHandler(FOnActorSpawned::FDelegate::CreateUObject(this, &UInteractionSubsystem::RegisterInteractableActor));
|
||||
GetWorld()->AddOnActorDestroyedHandler(FOnActorDestroyed::FDelegate::CreateUObject(this, &UInteractionSubsystem::UnregisterInteractableActor));
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::Deinitialize()
|
||||
{
|
||||
InteractableActorList.Empty();
|
||||
|
||||
if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem<UInteractionSubsystem>())
|
||||
{
|
||||
if (IS->CurrentlyInteractableObject.IsValid())
|
||||
{
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
||||
}
|
||||
IS->CurrentlyInteractableObject = nullptr;
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
bool UInteractionSubsystem::ShouldCreateSubsystem(UObject* Outer) const
|
||||
{
|
||||
if (!Super::ShouldCreateSubsystem(Outer)) return false;
|
||||
|
||||
if (!Outer || !Outer->GetWorld()) return false;
|
||||
|
||||
UWorld* World = Outer->GetWorld();
|
||||
return World->WorldType == EWorldType::Type::Game || World->WorldType == EWorldType::Type::PIE;
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
|
||||
RescanWorld();
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::Tick(float DeltaTime)
|
||||
{
|
||||
GetClosestInteractable(this);
|
||||
}
|
||||
|
||||
AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContext)
|
||||
{
|
||||
AActor* Closest = nullptr;
|
||||
|
||||
if (!IsValid(WorldContext) || !IsValid(WorldContext->GetWorld()))
|
||||
{
|
||||
return Closest;
|
||||
}
|
||||
|
||||
UInteractionSubsystem* IS = WorldContext->GetWorld()->GetSubsystem<UInteractionSubsystem>();
|
||||
ensure(IS);
|
||||
|
||||
const APlayerController* PC = WorldContext->GetWorld()->GetFirstPlayerController();
|
||||
if (!PC) return Closest;
|
||||
|
||||
const ACharacter* PlayerCharacter = Cast<ACharacter>(PC->GetPawn());
|
||||
if (!PlayerCharacter) return Closest;
|
||||
|
||||
const UInteractionSettings* InteractionSettings = GetDefault<UInteractionSettings>();
|
||||
ensure(InteractionSettings);
|
||||
|
||||
float ClosestDist = 9999999.f;
|
||||
for (TWeakObjectPtr<AActor> Actor : IS->InteractableActorList)
|
||||
{
|
||||
if (!Actor.IsValid())
|
||||
continue;
|
||||
|
||||
const float Dist = FVector::DistSquared(Actor->GetActorLocation(), PlayerCharacter->GetActorLocation());
|
||||
const float MaxInteractionDistance = IInteractableActorInterface::Execute_GetInteractionDistance(Actor.Get());
|
||||
|
||||
if (FMath::Pow(MaxInteractionDistance, 2) < Dist || Dist > ClosestDist)
|
||||
continue;
|
||||
|
||||
FVector2D ScreenLoc;
|
||||
if (!PC->ProjectWorldLocationToScreen(Actor->GetActorLocation(), ScreenLoc))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IInteractableActorInterface::Execute_IsInteractable(Actor.Get()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ClosestDist = Dist;
|
||||
Closest = Actor.Get();
|
||||
}
|
||||
|
||||
if (IS->CurrentlyInteractableObject != Closest)
|
||||
{
|
||||
if (IS->CurrentlyInteractableObject.IsValid())
|
||||
{
|
||||
// UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel())
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false);
|
||||
}
|
||||
if (Closest)
|
||||
{
|
||||
// UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *Closest->GetActorNameOrLabel())
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(Closest, true);
|
||||
}
|
||||
IS->CurrentlyInteractableObject = Closest;
|
||||
}
|
||||
|
||||
return Closest;
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::UpdateInteractableText(AActor* Interactable)
|
||||
{
|
||||
if (!Interactable) return;
|
||||
|
||||
UInteractionSubsystem* InteractionSubsystem = Interactable->GetWorld()->GetSubsystem<UInteractionSubsystem>();
|
||||
if (!InteractionSubsystem) return;
|
||||
|
||||
if (InteractionSubsystem->CurrentlyInteractableObject != Interactable) return;
|
||||
|
||||
IInteractableActorInterface::Execute_ToggleInteractableUI(InteractionSubsystem->CurrentlyInteractableObject.Get(), true);
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::RescanWorld()
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
int32 Count = 0;
|
||||
|
||||
if (!World)
|
||||
return;
|
||||
|
||||
// Prune stuff
|
||||
InteractableActorList.RemoveAllSwap(
|
||||
[](TWeakObjectPtr<AActor> Actor)
|
||||
{
|
||||
return !Actor.IsValid();
|
||||
}
|
||||
);
|
||||
|
||||
for (TActorIterator<AActor> It(World); It; ++It)
|
||||
{
|
||||
Count++;
|
||||
AActor* Actor = *It;
|
||||
if (IsValid(Actor) && Actor->Implements<UInteractableActorInterface>())
|
||||
{
|
||||
RegisterInteractableActor(Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::RegisterInteractableActor(AActor* InteractableActor)
|
||||
{
|
||||
if (IsValid(InteractableActor) && InteractableActor->Implements<UInteractableActorInterface>())
|
||||
{
|
||||
InteractableActorList.Add(InteractableActor);
|
||||
}
|
||||
}
|
||||
|
||||
void UInteractionSubsystem::UnregisterInteractableActor(AActor* InteractableActor)
|
||||
{
|
||||
InteractableActorList.RemoveAllSwap(
|
||||
[InteractableActor](TWeakObjectPtr<AActor> Actor)
|
||||
{
|
||||
return !Actor.IsValid() || Actor.Get() == InteractableActor;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "Widgets/BaseInteractionWidget.h"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "InteractionWidgetComponent.generated.h"
|
||||
|
||||
|
||||
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
|
||||
class INTERACTIONSYSTEM_API UInteractionWidgetComponent : public UWidgetComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UInteractionWidgetComponent();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Interaction Widget Component")
|
||||
void ToggleWidget(bool bActive);
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Interaction Widget Component")
|
||||
FVector2D WidgetOffset;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "InteractionSettings.generated.h"
|
||||
|
||||
class UBaseInteractionWidget;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="Interaction System"))
|
||||
class INTERACTIONSYSTEM_API UInteractionSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
// Max distance at which interactable will be considered active
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
float InteractableMaxDistance = 1000.f;
|
||||
|
||||
// Angle from the camera view towards object to consider "in view"
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
float InteractableCameraViewAngle = 60.f;
|
||||
|
||||
UPROPERTY(Config, EditAnywhere)
|
||||
TSubclassOf<UBaseInteractionWidget> DefaultInteractionWidgetClass;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FInteractionSystemModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "InteractionSettings.h"
|
||||
#include "InteractableActorInterface.generated.h"
|
||||
|
||||
UINTERFACE(MinimalAPI)
|
||||
class UInteractableActorInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class INTERACTIONSYSTEM_API IInteractableActorInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
FText GetInteractionText() const;
|
||||
virtual FText GetInteractionText_Implementation() const { return FText::GetEmpty(); }
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
bool IsInteractable() const;
|
||||
virtual bool IsInteractable_Implementation() const { return false; }
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
void Interact(AActor* InteractionInstigator);
|
||||
virtual void Interact_Implementation(AActor* InteractionInstigator) {}
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
void ToggleInteractableUI(bool bActive);
|
||||
virtual void ToggleInteractableUI_Implementation(bool bActive) {}
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interactable")
|
||||
float GetInteractionDistance() const;
|
||||
virtual float GetInteractionDistance_Implementation() const
|
||||
{
|
||||
return GetDefault<UInteractionSettings>()->InteractableMaxDistance;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/WorldSubsystem.h"
|
||||
#include "Tickable.h"
|
||||
#include "Stats/Stats.h"
|
||||
|
||||
#include "InteractionSubsystem.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class INTERACTIONSYSTEM_API UInteractionSubsystem : public UWorldSubsystem, public FTickableGameObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
|
||||
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||
virtual bool IsTickable() const override { return !IsTemplate(); }
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(UInteractionSubsystem, STATGROUP_Tickables); }
|
||||
|
||||
static AActor* GetClosestInteractable(const UObject* WorldContext);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Interactable")
|
||||
static void UpdateInteractableText(AActor* Interactable);
|
||||
|
||||
protected:
|
||||
|
||||
UFUNCTION()
|
||||
void RescanWorld();
|
||||
|
||||
void RegisterInteractableActor(AActor* InteractableActor);
|
||||
void UnregisterInteractableActor(AActor* InteractableActor);
|
||||
|
||||
UPROPERTY()
|
||||
TArray<TWeakObjectPtr<AActor>> InteractableActorList;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<AActor> CurrentlyInteractableObject;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "BaseInteractionWidget.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class INTERACTIONSYSTEM_API UBaseInteractionWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Interaction System")
|
||||
void UpdateInfo(const FText& InteractionText);
|
||||
|
||||
};
|
||||
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal file
27
Plugins/NoiseMapGenerator/NoiseMapGenerator.uplugin
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0.0",
|
||||
"FriendlyName": "Noise Map Generator",
|
||||
"Description": "Bake tiling noise textures with live preview.",
|
||||
"Category": "Editor",
|
||||
"CreatedBy": "UNmisterIZE(Sebastien Durocher)",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "https://drive.google.com/file/d/134xAHp1LABTME2vNDhH0gcCkiqj1sL-o/view?usp=sharing",
|
||||
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/2fd402da-2ee0-4532-b219-a0703790151d",
|
||||
"SupportURL": "suroduro7@gmail.com",
|
||||
"CanContainContent": true,
|
||||
"Installed": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "NoiseMapGenerator",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/NoiseMapGenerator/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
Project: NoiseMapGenerator Plugin */
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class NoiseMapGenerator : ModuleRules
|
||||
{
|
||||
public NoiseMapGenerator(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core", "CoreUObject", "Engine"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Slate", "SlateCore", "EditorStyle", "UnrealEd",
|
||||
"Projects", "InputCore", "ToolMenus", "AssetTools",
|
||||
"AssetRegistry", "RenderCore", "RHI", "DeveloperSettings"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGenerator.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGenerator.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
|
||||
////// ---------------------------------------------------------
|
||||
namespace TileableNoise
|
||||
{
|
||||
static FORCEINLINE float Fade(float t){ return t*t*t*(t*(t*6 - 15) + 10); }
|
||||
static FORCEINLINE float Lerp(float a,float b,float t){ return a + t*(b - a); }
|
||||
|
||||
// Permutation table (512 entries = 256 duplicated)
|
||||
struct Perm
|
||||
{
|
||||
TArray<int32> P;
|
||||
explicit Perm(int32 Seed)
|
||||
{
|
||||
P.SetNumUninitialized(512);
|
||||
TArray<int32> base; base.SetNumUninitialized(256);
|
||||
for (int32 i=0;i<256;++i) base[i]=i;
|
||||
FRandomStream RNG(Seed);
|
||||
for (int32 i=255;i>0;--i){ const int32 j=RNG.RandRange(0,i); Swap(base[i],base[j]); }
|
||||
for (int32 i=0;i<512;++i) P[i]=base[i & 255];
|
||||
}
|
||||
FORCEINLINE int32 H4(int32 x,int32 y,int32 z,int32 w) const
|
||||
{ return P[P[P[P[x & 255] + (y & 255)] + (z & 255)] + (w & 255)]; }
|
||||
};
|
||||
|
||||
// 4D gradient (32 dirs)
|
||||
static FORCEINLINE float Grad4(int32 h, float x,float y,float z,float w)
|
||||
{
|
||||
const int32 b = h & 31;
|
||||
switch (b)
|
||||
{
|
||||
case 0: return x + y + z + w; case 1: return x + y + z - w;
|
||||
case 2: return x + y - z + w; case 3: return x + y - z - w;
|
||||
case 4: return x - y + z + w; case 5: return x - y + z - w;
|
||||
case 6: return x - y - z + w; case 7: return x - y - z - w;
|
||||
case 8: return -x + y + z + w; case 9: return -x + y + z - w;
|
||||
case 10: return -x + y - z + w; case 11: return -x + y - z - w;
|
||||
case 12: return -x - y + z + w; case 13: return -x - y + z - w;
|
||||
case 14: return -x - y - z + w; case 15: return -x - y - z - w;
|
||||
case 16: return x + y + z; case 17: return x + y - z;
|
||||
case 18: return x - y + z; case 19: return x - y - z;
|
||||
case 20: return -x + y + z; case 21: return -x + y - z;
|
||||
case 22: return -x - y + z; case 23: return -x - y - z;
|
||||
case 24: return x + z + w; case 25: return x + z - w;
|
||||
case 26: return x - z + w; case 27: return x - z - w;
|
||||
case 28: return -x + z + w; case 29: return -x + z - w;
|
||||
case 30: return -x - z + w; default: return -x - z - w;
|
||||
}
|
||||
}
|
||||
|
||||
static float Perlin4D(float x,float y,float z,float w,const Perm& perm)
|
||||
{
|
||||
const int32 X0 = FMath::FloorToInt(x), Y0 = FMath::FloorToInt(y), Z0 = FMath::FloorToInt(z), W0 = FMath::FloorToInt(w);
|
||||
const float xf = x - X0, yf = y - Y0, zf = z - Z0, wf = w - W0;
|
||||
const float u = Fade(xf), v = Fade(yf), s = Fade(zf), t = Fade(wf);
|
||||
const int32 X1=X0+1,Y1=Y0+1,Z1=Z0+1,W1=W0+1;
|
||||
|
||||
const float n0000 = Grad4(perm.H4(X0,Y0,Z0,W0), xf, yf, zf, wf);
|
||||
const float n1000 = Grad4(perm.H4(X1,Y0,Z0,W0), xf-1.f, yf, zf, wf);
|
||||
const float n0100 = Grad4(perm.H4(X0,Y1,Z0,W0), xf, yf-1.f, zf, wf);
|
||||
const float n1100 = Grad4(perm.H4(X1,Y1,Z0,W0), xf-1.f, yf-1.f, zf, wf);
|
||||
const float n0010 = Grad4(perm.H4(X0,Y0,Z1,W0), xf, yf, zf-1.f, wf);
|
||||
const float n1010 = Grad4(perm.H4(X1,Y0,Z1,W0), xf-1.f, yf, zf-1.f, wf);
|
||||
const float n0110 = Grad4(perm.H4(X0,Y1,Z1,W0), xf, yf-1.f, zf-1.f, wf);
|
||||
const float n1110 = Grad4(perm.H4(X1,Y1,Z1,W0), xf-1.f, yf-1.f, zf-1.f, wf);
|
||||
|
||||
const float n0001 = Grad4(perm.H4(X0,Y0,Z0,W1), xf, yf, zf, wf-1.f);
|
||||
const float n1001 = Grad4(perm.H4(X1,Y0,Z0,W1), xf-1.f, yf, zf, wf-1.f);
|
||||
const float n0101 = Grad4(perm.H4(X0,Y1,Z0,W1), xf, yf-1.f, zf, wf-1.f);
|
||||
const float n1101 = Grad4(perm.H4(X1,Y1,Z0,W1), xf-1.f, yf-1.f, zf, wf-1.f);
|
||||
const float n0011 = Grad4(perm.H4(X0,Y0,Z1,W1), xf, yf, zf-1.f, wf-1.f);
|
||||
const float n1011 = Grad4(perm.H4(X1,Y0,Z1,W1), xf-1.f, yf, zf-1.f, wf-1.f);
|
||||
const float n0111 = Grad4(perm.H4(X0,Y1,Z1,W1), xf, yf-1.f, zf-1.f, wf-1.f);
|
||||
const float n1111 = Grad4(perm.H4(X1,Y1,Z1,W1), xf-1.f, yf-1.f, zf-1.f, wf-1.f);
|
||||
|
||||
const float nx00 = Lerp(n0000, n1000, u);
|
||||
const float nx10 = Lerp(n0100, n1100, u);
|
||||
const float nx01 = Lerp(n0010, n1010, u);
|
||||
const float nx11 = Lerp(n0110, n1110, u);
|
||||
const float nxy0 = Lerp(nx00, nx10, v);
|
||||
const float nxy1 = Lerp(nx01, nx11, v);
|
||||
const float nxyz0 = Lerp(nxy0, nxy1, s);
|
||||
|
||||
const float nx00b = Lerp(n0001, n1001, u);
|
||||
const float nx10b = Lerp(n0101, n1101, u);
|
||||
const float nx01b = Lerp(n0011, n1011, u);
|
||||
const float nx11b = Lerp(n0111, n1111, u);
|
||||
const float nxy0b = Lerp(nx00b, nx10b, v);
|
||||
const float nxy1b = Lerp(nx01b, nx11b, v);
|
||||
const float nxyz1 = Lerp(nxy0b, nxy1b, s);
|
||||
|
||||
return Lerp(nxyz0, nxyz1, t); // [-1,1]
|
||||
}
|
||||
|
||||
static float FBM4D(float x,float y,float z,float w,const Perm& perm,int32 Octaves,float Lac,float Gain)
|
||||
{
|
||||
float amp=1.f,sum=0.f,range=0.f;
|
||||
float X=x,Y=y,Z=z,W=w;
|
||||
for (int32 o=0;o<Octaves;++o)
|
||||
{
|
||||
sum += amp * Perlin4D(X,Y,Z,W,perm);
|
||||
range += amp;
|
||||
X*=Lac; Y*=Lac; Z*=Lac; W*=Lac;
|
||||
amp*=Gain;
|
||||
}
|
||||
return (range>0.f)? sum/range : 0.f;
|
||||
}
|
||||
|
||||
// Hash utilities for Worley
|
||||
static FORCEINLINE uint32 Hash2D(int32 x,int32 y,uint32 seed)
|
||||
{
|
||||
uint32 h = (uint32)x * 0x27d4eb2d ^ (uint32)y * 0x165667b1u ^ seed*0x9e3779b9u;
|
||||
h ^= (h >> 16); h *= 0x7feb352d; h ^= (h >> 15); h *= 0x846ca68b; h ^= (h >> 16);
|
||||
return h;
|
||||
}
|
||||
static FORCEINLINE float Frand01(uint32& state)
|
||||
{
|
||||
state ^= state << 13; state ^= state >> 17; state ^= state << 5;
|
||||
return (state & 0x00FFFFFF) / 16777216.0f;
|
||||
}
|
||||
|
||||
// Torus-safe distance between (u,v) and feature in neighbor cell (iu,iv) with wrap
|
||||
static float WorleyF1_Torus(float u,float v,int32 CellsU,int32 CellsV,uint32 seed)
|
||||
{
|
||||
// Base integer cell
|
||||
const float fu = u * CellsU;
|
||||
const float fv = v * CellsV;
|
||||
const int32 iu0 = FMath::FloorToInt(fu);
|
||||
const int32 iv0 = FMath::FloorToInt(fv);
|
||||
|
||||
float best = 1e9f;
|
||||
|
||||
for (int du=-1; du<=1; ++du)
|
||||
for (int dv=-1; dv<=1; ++dv)
|
||||
{
|
||||
const int32 iu = (iu0 + du + CellsU) % CellsU;
|
||||
const int32 iv = (iv0 + dv + CellsV) % CellsV;
|
||||
|
||||
uint32 st = Hash2D(iu, iv, seed);
|
||||
const float jitterX = Frand01(st);
|
||||
const float jitterY = Frand01(st);
|
||||
|
||||
const float fx = (float(iu) + jitterX) / float(CellsU);
|
||||
const float fy = (float(iv) + jitterY) / float(CellsV);
|
||||
|
||||
// torus wrap distance in [0,1]
|
||||
float dx = FMath::Abs(u - fx); dx = FMath::Min(dx, 1.0f - dx);
|
||||
float dy = FMath::Abs(v - fy); dy = FMath::Min(dy, 1.0f - dy);
|
||||
|
||||
best = FMath::Min(best, FMath::Sqrt(dx*dx + dy*dy));
|
||||
}
|
||||
return best; // [0..~0.5]
|
||||
}
|
||||
|
||||
}
|
||||
/////// ---------------------------------------------------------
|
||||
|
||||
static void MakeUniqueAssetPath(const FString& Folder, const FString& BaseName, bool bOverwrite, FString& OutPkg, FString& OutAsset)
|
||||
{
|
||||
FString CleanFolder = Folder;
|
||||
if (!CleanFolder.StartsWith(TEXT("/"))) CleanFolder = TEXT("/") + CleanFolder;
|
||||
if (!CleanFolder.StartsWith(TEXT("/Game"))) CleanFolder = TEXT("/Game") + CleanFolder;
|
||||
|
||||
FString CandidateName = BaseName;
|
||||
FString CandidatePkg = CleanFolder / CandidateName;
|
||||
|
||||
if (bOverwrite)
|
||||
{
|
||||
OutPkg = CandidatePkg;
|
||||
OutAsset = CandidateName;
|
||||
return;
|
||||
}
|
||||
|
||||
int32 Suffix = 1;
|
||||
while (FPackageName::DoesPackageExist(CandidatePkg))
|
||||
{
|
||||
CandidateName = FString::Printf(TEXT("%s_%03d"), *BaseName, Suffix++);
|
||||
CandidatePkg = CleanFolder / CandidateName;
|
||||
}
|
||||
OutPkg = CandidatePkg;
|
||||
OutAsset = CandidateName;
|
||||
}
|
||||
|
||||
|
||||
bool FNoiseMapGenerator::GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH)
|
||||
{
|
||||
// --- size ---
|
||||
const int32 W = S.Width;
|
||||
const int32 H = S.Height;
|
||||
if (W <= 0 || H <= 0)
|
||||
{
|
||||
OutW = OutH = 0;
|
||||
OutPixels.Reset();
|
||||
return false;
|
||||
}
|
||||
OutW = W;
|
||||
OutH = H;
|
||||
OutPixels.SetNumUninitialized(W * H);
|
||||
|
||||
// --- common setup ---
|
||||
const float invW = 1.f / float(W);
|
||||
const float invH = 1.f / float(H);
|
||||
const float TwoPi = 6.28318530717958647692f;
|
||||
const float Scale = 1.0f / FMath::Max(S.Frequency, 1e-6f);
|
||||
|
||||
TileableNoise::Perm perm(S.Seed);
|
||||
|
||||
// 4D torus mapping (period-1 in u,v)
|
||||
auto AnglesFromUV = [&](float u, float v)
|
||||
{
|
||||
const float th = TwoPi * u;
|
||||
const float ph = TwoPi * v;
|
||||
struct { float X, Y, Z, W; } R
|
||||
{
|
||||
FMath::Cos(th) * Scale,
|
||||
FMath::Sin(th) * Scale,
|
||||
FMath::Cos(ph) * Scale,
|
||||
FMath::Sin(ph) * Scale
|
||||
};
|
||||
return R;
|
||||
};
|
||||
|
||||
// Base sampler per algorithm, returns [-1,1]
|
||||
auto SampleBase = [&](float u, float v) -> float
|
||||
{
|
||||
switch (S.Algorithm)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin:
|
||||
{
|
||||
const auto C = AnglesFromUV(u, v);
|
||||
return (S.Mode == ENoiseMode::FBM)
|
||||
? TileableNoise::FBM4D(C.X, C.Y, C.Z, C.W, perm, S.Octaves, S.Lacunarity, S.Gain)
|
||||
: TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm);
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::RidgedFBM:
|
||||
{
|
||||
float amp = 0.5f;
|
||||
float sum = 0.f;
|
||||
float range = 0.f;
|
||||
float freqMul = 1.f;
|
||||
|
||||
for (int32 o = 0; o < S.Octaves; ++o)
|
||||
{
|
||||
const float ou = FMath::Frac(u * freqMul);
|
||||
const float ov = FMath::Frac(v * freqMul);
|
||||
const auto C = AnglesFromUV(ou, ov);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
const float r = 1.f - FMath::Abs(p); // [0,1]
|
||||
|
||||
sum += amp * r;
|
||||
range += amp;
|
||||
|
||||
freqMul *= S.Lacunarity;
|
||||
amp *= S.Gain;
|
||||
}
|
||||
|
||||
return (range > 0.f) ? (sum / range) * 2.f - 1.f : 0.f;
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::Worley:
|
||||
{
|
||||
const float cellsF = FMath::Clamp(
|
||||
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
|
||||
1.f, 512.f
|
||||
);
|
||||
|
||||
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
|
||||
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsF)) * RepeatsUV);
|
||||
|
||||
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed); // [0..~0.5]
|
||||
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f);
|
||||
return v01 * 2.f - 1.f;
|
||||
}
|
||||
default:
|
||||
return 0.f;
|
||||
}
|
||||
};
|
||||
|
||||
auto SampleNthOctave01 = [&](float u, float v, int32 OctIdx) -> float
|
||||
{
|
||||
switch (S.Algorithm)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin:
|
||||
{
|
||||
// Higher octave = higher frequency
|
||||
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const auto C = AnglesFromUV(u * freqMul, v * freqMul);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
return 0.5f * (p + 1.f); // → [0,1]
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::RidgedFBM:
|
||||
{
|
||||
const float freqMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const float ou = FMath::Frac(u * freqMul);
|
||||
const float ov = FMath::Frac(v * freqMul);
|
||||
const auto C = AnglesFromUV(ou, ov);
|
||||
const float p = TileableNoise::Perlin4D(C.X, C.Y, C.Z, C.W, perm); // [-1,1]
|
||||
const float r = 1.f - FMath::Abs(p); // [0,1] – "ridge" shape
|
||||
return r;
|
||||
}
|
||||
|
||||
case ENoiseAlgorithm::Worley:
|
||||
{
|
||||
// More cells per octave for finer detail
|
||||
const float cellsFBase = FMath::Clamp(
|
||||
(1.0f / FMath::Max(S.Frequency, 1e-6f)) * 0.75f,
|
||||
1.f, 512.f
|
||||
);
|
||||
const float octaveMul = FMath::Pow(S.Lacunarity, OctIdx);
|
||||
const int32 CellsU = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
|
||||
const int32 CellsV = FMath::Max(1, int32(FMath::RoundToInt(cellsFBase * octaveMul)) * RepeatsUV);
|
||||
|
||||
const float d = TileableNoise::WorleyF1_Torus(u, v, CellsU, CellsV, (uint32)S.Seed);
|
||||
const float v01 = 1.0f - FMath::Clamp(d * 2.0f, 0.f, 1.f); // [0,1], "cells" as bright
|
||||
return v01;
|
||||
}
|
||||
|
||||
default:
|
||||
// Fallback mid-gray
|
||||
return 0.5f;
|
||||
}
|
||||
};
|
||||
|
||||
// Write pixels
|
||||
if (S.OutputMode == ENoiseOutputMode::Grayscale)
|
||||
{
|
||||
float minV = 1e9f;
|
||||
float maxV = -1e9f;
|
||||
TArray<float> tmp;
|
||||
tmp.SetNumUninitialized(W * H);
|
||||
|
||||
int32 I = 0;
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
const float u = float(RepeatsUV) * float(x) * invW;
|
||||
const float v = float(RepeatsUV) * float(y) * invH;
|
||||
|
||||
const float vraw = SampleBase(u, v); // [-1,1]
|
||||
tmp[I++] = vraw;
|
||||
minV = FMath::Min(minV, vraw);
|
||||
maxV = FMath::Max(maxV, vraw);
|
||||
}
|
||||
}
|
||||
|
||||
const float invRange =
|
||||
(S.bNormalize && maxV > minV) ? 1.f / (maxV - minV) : 0.5f;
|
||||
|
||||
I = 0;
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
float v = tmp[I++];
|
||||
v = S.bNormalize
|
||||
? (v - minV) * invRange // [0,1]
|
||||
: 0.5f * (v + 1.f); // [-1,1] → [0,1]
|
||||
|
||||
const uint8 g = (uint8)FMath::Clamp(
|
||||
FMath::RoundToInt(v * 255.f), 0, 255);
|
||||
|
||||
OutPixels[y * W + x] = FColor(g, g, g, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
else // Split First 3 Octaves to RGB channles
|
||||
{
|
||||
for (int32 y = 0; y < H; ++y)
|
||||
{
|
||||
for (int32 x = 0; x < W; ++x)
|
||||
{
|
||||
const float u = float(RepeatsUV) * float(x) * invW;
|
||||
const float v = float(RepeatsUV) * float(y) * invH;
|
||||
|
||||
const float r = SampleNthOctave01(u, v, 0);
|
||||
const float g = SampleNthOctave01(u, v, 1);
|
||||
const float b = SampleNthOctave01(u, v, 2);
|
||||
|
||||
OutPixels[y * W + x] = FColor(
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(r * 255.f), 0, 255),
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(g * 255.f), 0, 255),
|
||||
(uint8)FMath::Clamp(FMath::RoundToInt(b * 255.f), 0, 255),
|
||||
255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FNoiseMapGenerator::BakeFromPixels(
|
||||
const FNoiseBakeSettings& S,
|
||||
const TArray<FColor>& Pixels,
|
||||
int32 W,
|
||||
int32 H)
|
||||
{
|
||||
if (W <= 0 || H <= 0 || Pixels.Num() != W * H)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Resolve package path/name ---
|
||||
FString OutPath = S.OutputPath.IsEmpty()
|
||||
? TEXT("/Game/Noise")
|
||||
: S.OutputPath;
|
||||
|
||||
if (!OutPath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
OutPath = TEXT("/Game/Noise");
|
||||
}
|
||||
|
||||
FString PackageName, AssetName;
|
||||
MakeUniqueAssetPath(OutPath, S.BaseName, S.bOverwrite, PackageName, AssetName);
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!Package)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UTexture2D* Tex = NewObject<UTexture2D>(
|
||||
Package,
|
||||
*AssetName,
|
||||
RF_Public | RF_Standalone
|
||||
);
|
||||
if (!Tex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tex->Modify();
|
||||
|
||||
// Match preview flags
|
||||
Tex->SRGB = false;
|
||||
|
||||
if (S.OutputMode == ENoiseOutputMode::Grayscale)
|
||||
{
|
||||
Tex->CompressionSettings = TC_Grayscale;
|
||||
}
|
||||
else
|
||||
{
|
||||
Tex->CompressionSettings = TC_VectorDisplacementmap; // linear RGB
|
||||
}
|
||||
|
||||
Tex->CompressionNone = true;
|
||||
Tex->DeferCompression = false;
|
||||
Tex->MipGenSettings = TMGS_NoMipmaps;
|
||||
Tex->AddressX = TA_Wrap;
|
||||
Tex->AddressY = TA_Wrap;
|
||||
Tex->LODGroup = TEXTUREGROUP_World;
|
||||
|
||||
// Copy pixels into source mip
|
||||
Tex->Source.Init(W, H, 1, 1, TSF_BGRA8);
|
||||
{
|
||||
uint8* Src = Tex->Source.LockMip(0);
|
||||
const int64 Bytes = int64(W) * H * sizeof(FColor);
|
||||
FMemory::Memcpy(Src, Pixels.GetData(), Bytes);
|
||||
Tex->Source.UnlockMip(0);
|
||||
}
|
||||
|
||||
Tex->PostEditChange();
|
||||
Tex->UpdateResource();
|
||||
|
||||
FAssetRegistryModule::AssetCreated(Tex);
|
||||
Package->MarkPackageDirty();
|
||||
|
||||
const FString PkgFile =
|
||||
FPackageName::LongPackageNameToFilename(
|
||||
PackageName,
|
||||
FPackageName::GetAssetPackageExtension()
|
||||
);
|
||||
|
||||
FSavePackageArgs Args;
|
||||
Args.TopLevelFlags = RF_Public | RF_Standalone;
|
||||
Args.SaveFlags = SAVE_None;
|
||||
Args.Error = GError;
|
||||
Args.bWarnOfLongFilename = true;
|
||||
|
||||
UPackage::SavePackage(Package, Tex, *PkgFile, Args);
|
||||
}
|
||||
|
||||
void FNoiseMapGenerator::BakeNoiseTexture(const FNoiseBakeSettings& S)
|
||||
{
|
||||
TArray<FColor> Pixels;
|
||||
int32 W = 0, H = 0;
|
||||
|
||||
// IMPORTANT: RepeatsUV = 1 for a single seamless period
|
||||
if (!GeneratePixels(S, /*RepeatsUV=*/1, Pixels, W, H))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BakeFromPixels(S, Pixels, W, H);
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorModule.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGeneratorModule.h"
|
||||
#include "NoiseMapGenerator.h"
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Input/SSpinBox.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "ToolMenus.h"
|
||||
#include "LevelEditor.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "ContentBrowserModule.h"
|
||||
#include "IContentBrowserSingleton.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
static const FName NoiseMapGenTabName("NoiseMapGeneratorTab");
|
||||
|
||||
// Preview helper
|
||||
static UTexture2D* CreateTransientTexture(int32 W, int32 H, const TArray<FColor>& Pixels)
|
||||
{
|
||||
if (Pixels.Num() != W * H) return nullptr;
|
||||
UTexture2D* T = UTexture2D::CreateTransient(W, H, PF_B8G8R8A8);
|
||||
if (!T) return nullptr;
|
||||
T->SRGB = false;
|
||||
T->CompressionSettings = TC_Default;
|
||||
T->MipGenSettings = TMGS_NoMipmaps;
|
||||
|
||||
FTexture2DMipMap& Mip = T->GetPlatformData()->Mips[0];
|
||||
void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
|
||||
const int64 Bytes = (int64)W * H * sizeof(FColor);
|
||||
FMemory::Memcpy(Data, Pixels.GetData(), Bytes);
|
||||
Mip.BulkData.Unlock();
|
||||
T->UpdateResource();
|
||||
return T;
|
||||
}
|
||||
|
||||
// Plugin UI setup
|
||||
class SNoiseMapGeneratorWidget : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SNoiseMapGeneratorWidget) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments&)
|
||||
{
|
||||
Settings = GetMutableDefault<UNoiseMapGeneratorSettings>();
|
||||
PreviewBrush = MakeShared<FSlateBrush>();
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SBorder)
|
||||
[
|
||||
SNew(SSplitter)
|
||||
|
||||
+ SSplitter::Slot().Value(0.33f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
[
|
||||
SNew(SBox)
|
||||
.MinDesiredWidth(580.f)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ SNew(STextBlock).Text(FText::FromString("Noise Settings")) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Width", Settings->Width, 8, 8192, 64) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Height", Settings->Height, 8, 8192, 64) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Preview Tiling", Settings->TileRepeats, 1, 64, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Seed", Settings->Seed, 0, INT_MAX, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowInt("Octaves", Settings->Octaves, 1, 12, 1) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Frequency", Settings->Frequency, 0.001f, 2.0f, 0.005f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Lacunarity", Settings->Lacunarity, 1.f, 8.f, 0.1f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowFloat("Gain", Settings->Gain, 0.f, 1.f, 0.05f) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Normalize 0..1", Settings->bNormalize) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowMode() ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowOutputMode() ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowAlgorithm() ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Save Path (/Game/...)", Settings->OutputPath) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowText("Base Name", Settings->BaseName) ]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Overwrite Existing", Settings->bOverwrite) ]
|
||||
// + SVerticalBox::Slot().AutoHeight().Padding(6)[ MakeRowBool("Generate Mips", Settings->bGenerateMips) ]
|
||||
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(6)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnGenerate)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Generate Preview")) ] ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0,0,6,0)
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnBake)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Bake & Save")) ] ]
|
||||
+ SHorizontalBox::Slot().AutoWidth()
|
||||
[ SNew(SButton).OnClicked(this, &SNoiseMapGeneratorWidget::OnQuit)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Quit")) ] ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
+ SSplitter::Slot().Value(0.67f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
[ SAssignNew(PreviewImage, SImage).Image(nullptr) ]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
private:
|
||||
// UI helpers
|
||||
TSharedRef<SWidget> MakeRowInt(const char* Label, int32& V, int32 Min, int32 Max, int32 Step)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SSpinBox<int32>).MinValue(Min).MaxValue(Max).Delta(Step)
|
||||
.Value_Lambda([&V]{ return V; })
|
||||
.OnValueChanged_Lambda([&V](int32 NV){ V = NV; }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowFloat(const char* Label, float& V, float Min, float Max, float Step)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SSpinBox<float>).MinValue(Min).MaxValue(Max).Delta(Step)
|
||||
.Value_Lambda([&V]{ return V; })
|
||||
.OnValueChanged_Lambda([&V](float NV){ V = NV; }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowBool(const char* Label, bool& B)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.IsChecked_Lambda([&B]{ return B ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.OnCheckStateChanged_Lambda([&B](ECheckBoxState S){ B = (S == ECheckBoxState::Checked); }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowText(const char* Label, FString& T)
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString(Label)) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[ SNew(SEditableTextBox)
|
||||
.Text_Lambda([&T]{ return FText::FromString(T); })
|
||||
.OnTextCommitted_Lambda([&T](const FText& New, ETextCommit::Type){ T = New.ToString(); }) ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowMode()
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Mode")) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->Mode = (S == ECheckBoxState::Checked) ? ENoiseMode::FBM : ENoiseMode::Perlin; Regenerate(); })
|
||||
.IsChecked_Lambda([this]{ return Settings->Mode == ENoiseMode::FBM ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.Content()[ SNew(STextBlock).Text(FText::FromString("FBM (unchecked = Perlin)")) ] ];
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowOutputMode()
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)[ SNew(STextBlock).Text(FText::FromString("Output")) ]
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||
[ SNew(SCheckBox)
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState S){ Settings->OutputMode = (S == ECheckBoxState::Checked) ? ENoiseOutputMode::SplitFirst3Octaves : ENoiseOutputMode::Grayscale; Regenerate(); })
|
||||
.IsChecked_Lambda([this]{ return Settings->OutputMode == ENoiseOutputMode::SplitFirst3Octaves ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
|
||||
.Content()[ SNew(STextBlock).Text(FText::FromString("Split 3 Octaves to RGB")) ] ];
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
static FString AlgoToString(ENoiseAlgorithm A)
|
||||
{
|
||||
switch (A)
|
||||
{
|
||||
case ENoiseAlgorithm::Perlin: return TEXT("Perlin");
|
||||
case ENoiseAlgorithm::Worley: return TEXT("Worley (Cellular)");
|
||||
case ENoiseAlgorithm::RidgedFBM: return TEXT("Ridged FBM");
|
||||
default: return TEXT("Perlin");
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> OnGenerateAlgoWidget(TSharedPtr<ENoiseAlgorithm> InItem)
|
||||
{
|
||||
return SNew(STextBlock).Text(FText::FromString(AlgoToString(*InItem)));
|
||||
}
|
||||
|
||||
void OnAlgoChanged(TSharedPtr<ENoiseAlgorithm> NewSel, ESelectInfo::Type)
|
||||
{
|
||||
if (!NewSel.IsValid()) return;
|
||||
Settings->Algorithm = *NewSel;
|
||||
SelectedAlgo = NewSel;
|
||||
Regenerate();
|
||||
}
|
||||
|
||||
FText GetAlgoCurrentText() const
|
||||
{
|
||||
return FText::FromString(AlgoToString(Settings->Algorithm));
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> MakeRowAlgorithm()
|
||||
{
|
||||
// Populate items once if empty
|
||||
if (AlgoItems.Num() == 0)
|
||||
{
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Perlin));
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::Worley));
|
||||
AlgoItems.Add(MakeShared<ENoiseAlgorithm>(ENoiseAlgorithm::RidgedFBM));
|
||||
|
||||
// pick current
|
||||
for (const auto& It : AlgoItems)
|
||||
{
|
||||
if (*It == Settings->Algorithm)
|
||||
{
|
||||
SelectedAlgo = It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!SelectedAlgo.IsValid())
|
||||
{
|
||||
SelectedAlgo = AlgoItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
return SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0,0,8,0)
|
||||
[ SNew(STextBlock).Text(FText::FromString("Algorithm")) ]
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f)
|
||||
[
|
||||
SNew(SComboBox<TSharedPtr<ENoiseAlgorithm>>)
|
||||
.OptionsSource(&AlgoItems)
|
||||
.InitiallySelectedItem(SelectedAlgo)
|
||||
.OnGenerateWidget(this, &SNoiseMapGeneratorWidget::OnGenerateAlgoWidget)
|
||||
.OnSelectionChanged(this, &SNoiseMapGeneratorWidget::OnAlgoChanged)
|
||||
[
|
||||
SNew(STextBlock).Text_Lambda([this]{ return GetAlgoCurrentText(); })
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<ENoiseAlgorithm>> AlgoItems;
|
||||
TSharedPtr<ENoiseAlgorithm> SelectedAlgo;
|
||||
|
||||
FReply OnGenerate()
|
||||
{
|
||||
Regenerate();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply OnBake()
|
||||
{
|
||||
FNoiseBakeSettings B;
|
||||
B.Width = Settings->Width;
|
||||
B.Height = Settings->Height;
|
||||
B.TileRepeats = Settings->TileRepeats;
|
||||
B.Seed = Settings->Seed;
|
||||
B.Octaves = Settings->Octaves;
|
||||
B.Frequency = Settings->Frequency;
|
||||
B.Lacunarity = Settings->Lacunarity;
|
||||
B.Gain = Settings->Gain;
|
||||
B.bNormalize = Settings->bNormalize;
|
||||
B.Mode = Settings->Mode;
|
||||
B.OutputMode = Settings->OutputMode;
|
||||
B.OutputPath = Settings->OutputPath;
|
||||
B.BaseName = Settings->BaseName;
|
||||
B.bOverwrite = Settings->bOverwrite;
|
||||
//B.bGenerateMips = Settings->bGenerateMips;
|
||||
B.Algorithm = Settings->Algorithm;
|
||||
|
||||
// Calls GeneratePixels then bakes with the same flags as preview helper.
|
||||
FNoiseMapGenerator::BakeNoiseTexture(B);
|
||||
|
||||
// Open content browser at location
|
||||
{
|
||||
FString FolderPathStr = Settings->OutputPath;
|
||||
if (!FolderPathStr.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
FolderPathStr = TEXT("/Game");
|
||||
}
|
||||
|
||||
TArray<FString> Folders;
|
||||
Folders.Add(FolderPathStr);
|
||||
|
||||
IContentBrowserSingleton& CB =
|
||||
FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser").Get();
|
||||
CB.SyncBrowserToFolders(Folders);
|
||||
}
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply OnQuit()
|
||||
{
|
||||
TSharedPtr<SDockTab> Tab = FGlobalTabmanager::Get()->FindExistingLiveTab(NoiseMapGenTabName);
|
||||
if (Tab.IsValid())
|
||||
{
|
||||
Tab->RequestCloseTab();
|
||||
}
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
void Regenerate()
|
||||
{
|
||||
FNoiseBakeSettings B;
|
||||
B.Width=Settings->Width; B.Height=Settings->Height; B.TileRepeats=Settings->TileRepeats; B.Seed=Settings->Seed;
|
||||
B.Octaves=Settings->Octaves; B.Frequency=Settings->Frequency; B.Lacunarity=Settings->Lacunarity; B.Gain=Settings->Gain;
|
||||
B.bNormalize=Settings->bNormalize; B.Mode=Settings->Mode; B.OutputMode=Settings->OutputMode;
|
||||
B.OutputPath=Settings->OutputPath; B.BaseName=Settings->BaseName; B.bOverwrite=Settings->bOverwrite; //B.bGenerateMips=Settings->bGenerateMips;
|
||||
B.Algorithm = Settings->Algorithm;
|
||||
|
||||
TArray<FColor> Pixels; int32 W=0, H=0;
|
||||
if (FNoiseMapGenerator::GeneratePixels(B,Settings->TileRepeats, Pixels, W, H))
|
||||
{
|
||||
if (UTexture2D* T = CreateTransientTexture(W, H, Pixels))
|
||||
{
|
||||
if (!PreviewBrush.IsValid()) PreviewBrush = MakeShareable(new FSlateBrush());
|
||||
PreviewBrush->ImageSize = FVector2D(W, H);
|
||||
PreviewBrush->SetResourceObject(T);
|
||||
PreviewImage->SetImage(PreviewBrush.Get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
UNoiseMapGeneratorSettings* Settings = nullptr;
|
||||
TSharedPtr<SImage> PreviewImage;
|
||||
TSharedPtr<FSlateBrush> PreviewBrush;
|
||||
};
|
||||
|
||||
void FNoiseMapGeneratorModule::StartupModule()
|
||||
{
|
||||
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
|
||||
}
|
||||
|
||||
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
|
||||
NoiseMapGenTabName,
|
||||
FOnSpawnTab::CreateLambda([](const FSpawnTabArgs&)
|
||||
{
|
||||
return SNew(SDockTab)
|
||||
.TabRole(ETabRole::NomadTab)
|
||||
[
|
||||
SNew(SNoiseMapGeneratorWidget)
|
||||
];
|
||||
})
|
||||
)
|
||||
.SetDisplayName(FText::FromString("Noise Map Generator"))
|
||||
.SetMenuType(ETabSpawnerMenuType::Hidden);
|
||||
|
||||
if (UToolMenus::IsToolMenuUIEnabled())
|
||||
{
|
||||
RegisterMenus();
|
||||
}
|
||||
else
|
||||
{
|
||||
UToolMenus::RegisterStartupCallback(
|
||||
FSimpleMulticastDelegate::FDelegate::CreateRaw(
|
||||
this, &FNoiseMapGeneratorModule::RegisterMenus));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FNoiseMapGeneratorModule::ShutdownModule()
|
||||
{
|
||||
if (FGlobalTabmanager::Get()->HasTabSpawner(NoiseMapGenTabName))
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(NoiseMapGenTabName);
|
||||
}
|
||||
UToolMenus::UnregisterOwner(this);
|
||||
}
|
||||
|
||||
|
||||
void FNoiseMapGeneratorModule::OpenWindow()
|
||||
{
|
||||
FGlobalTabmanager::Get()->TryInvokeTab(NoiseMapGenTabName);
|
||||
}
|
||||
|
||||
void FNoiseMapGeneratorModule::RegisterMenus()
|
||||
{
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
|
||||
if (UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window"))
|
||||
{
|
||||
// Insert a custom section at the top.
|
||||
FToolMenuSection& TopSection = Menu->AddSection(
|
||||
"NoiseMapGeneratorSection",
|
||||
FText::GetEmpty(),
|
||||
FToolMenuInsert(NAME_None, EToolMenuInsertType::First)
|
||||
);
|
||||
|
||||
TopSection.AddMenuEntry(
|
||||
"OpenNoiseMapGenerator",
|
||||
FText::FromString("Noise Map Generator"),
|
||||
FText::FromString("Open the Noise Map Generator window"),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateRaw(this, &FNoiseMapGeneratorModule::OpenWindow))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FNoiseMapGeneratorModule, NoiseMapGenerator)
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorSettings.cpp
|
||||
*/
|
||||
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGenerator.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "NoiseMapGeneratorSettings.h"
|
||||
|
||||
class UTexture2D;
|
||||
|
||||
struct FNoiseBakeSettings
|
||||
{
|
||||
int32 Width = 1024;
|
||||
int32 Height = 1024;
|
||||
int32 TileRepeats = 1;
|
||||
int32 Seed = 1337;
|
||||
int32 Octaves = 5;
|
||||
float Frequency = 0.25f;
|
||||
float Lacunarity = 2.0f;
|
||||
float Gain = 0.5f;
|
||||
bool bNormalize = true;
|
||||
ENoiseMode Mode = ENoiseMode::FBM;
|
||||
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
|
||||
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
|
||||
FString OutputPath = TEXT("/Game/Noise");
|
||||
FString BaseName = TEXT("T_Noise");
|
||||
bool bOverwrite = false;
|
||||
bool bGenerateMips = false;
|
||||
};
|
||||
|
||||
class NOISEMAPGENERATOR_API FNoiseMapGenerator
|
||||
|
||||
{
|
||||
public:
|
||||
static bool GeneratePixels(const FNoiseBakeSettings& S, int32 RepeatsUV, TArray<FColor>& OutPixels, int32& OutW, int32& OutH);
|
||||
static void BakeNoiseTexture(const FNoiseBakeSettings& Settings);
|
||||
static void BakeFromPixels(const FNoiseBakeSettings& S, const TArray<FColor>& Pixels, int32 W, int32 H);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorModule.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ToolMenus.h"
|
||||
|
||||
class FNoiseMapGeneratorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
private:
|
||||
void OpenWindow();
|
||||
void RegisterMenus();
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
|
||||
|
||||
Project: NoiseMapGenerator Plugin
|
||||
File: NoiseMapGeneratorSettings.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "NoiseMapGeneratorSettings.generated.h"
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseMode : uint8
|
||||
{
|
||||
Perlin,
|
||||
FBM
|
||||
};
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseOutputMode : uint8
|
||||
{
|
||||
Grayscale,
|
||||
SplitFirst3Octaves
|
||||
};
|
||||
|
||||
UENUM()
|
||||
enum class ENoiseAlgorithm : uint8
|
||||
{
|
||||
Perlin UMETA(DisplayName="Perlin"),
|
||||
Worley UMETA(DisplayName="Worley (Cellular)"),
|
||||
RidgedFBM UMETA(DisplayName="Ridged FBM")
|
||||
};
|
||||
|
||||
|
||||
UCLASS(config=EditorPerProjectUserSettings, defaultconfig)
|
||||
class UNoiseMapGeneratorSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
|
||||
virtual FText GetSectionText() const override { return FText::FromString(TEXT("Noise Map Generator")); }
|
||||
|
||||
|
||||
// Resolution
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
|
||||
int32 Width = 1024;
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="8", ClampMax="16384"))
|
||||
int32 Height = 1024;
|
||||
|
||||
// Tiling
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="64"))
|
||||
int32 TileRepeats = 1;
|
||||
|
||||
// Noise parameters
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
int32 Seed = 1337;
|
||||
|
||||
// Used by FBM (ignored for Perlin-only grayscale)
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1", ClampMax="12"))
|
||||
int32 Octaves = 5;
|
||||
|
||||
// features size
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.001", ClampMax="2.0"))
|
||||
float Frequency = 0.5f;
|
||||
|
||||
// Frequency multiplier per octave
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="1.0", ClampMax="8.0"))
|
||||
float Lacunarity = 2.0f;
|
||||
|
||||
// Amplitude multiplier per octave
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise", meta=(ClampMin="0.0", ClampMax="1.0"))
|
||||
float Gain = 0.5f;
|
||||
|
||||
// Normalize result to [0..1] (if off, uses 0.5*(v+1))
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
bool bNormalize = true;
|
||||
|
||||
// Perlin or FBM for grayscale output
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseMode Mode = ENoiseMode::FBM;
|
||||
|
||||
// Which base algorithm to use
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseAlgorithm Algorithm = ENoiseAlgorithm::Perlin;
|
||||
|
||||
// Grayscale or pack first 3 octaves into RGB
|
||||
UPROPERTY(EditAnywhere, config, Category = "Noise")
|
||||
ENoiseOutputMode OutputMode = ENoiseOutputMode::Grayscale;
|
||||
|
||||
// Save opt
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
FString OutputPath = TEXT("/Game/Noise");
|
||||
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
FString BaseName = TEXT("T_Noise");
|
||||
|
||||
UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
bool bOverwrite = false;
|
||||
|
||||
//UPROPERTY(EditAnywhere, config, Category = "Save")
|
||||
// bool bGenerateMips = false;
|
||||
};
|
||||
25
Plugins/PSOFunctionLibrary/PSOFunctions.uplugin
Normal file
25
Plugins/PSOFunctionLibrary/PSOFunctions.uplugin
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "PSOFunctions",
|
||||
"Description": "Esposes ShaderPipelineCache functions to Blueprint project",
|
||||
"Category": "Other",
|
||||
"CreatedBy": "Danielel",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "https://www.dropbox.com/scl/fi/bjhz5wmklikkojlukz92g/PSOFunctions-Plugin-Documentation.pdf?rlkey=hfowk5im2gnnc58xwrsbfxvjr&st=82lih7p5&dl=0",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "",
|
||||
"CanContainContent": true,
|
||||
"Installed": true,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "PSOFunctions",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Plugins/PSOFunctionLibrary/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/PSOFunctionLibrary/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class PSOFunctions : ModuleRules
|
||||
{
|
||||
public PSOFunctions(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"RenderCore"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#include "PSOFunctions.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FPSOFunctionsModule"
|
||||
|
||||
void FPSOFunctionsModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FPSOFunctionsModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FPSOFunctionsModule, PSOFunctions)
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#include "ShaderPipeline.h"
|
||||
#include "ShaderPipelineCache.h"
|
||||
#include "RenderResource.h"
|
||||
|
||||
|
||||
void UShaderPipeline::SetBatchModeFast()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Fast);
|
||||
}
|
||||
|
||||
void UShaderPipeline::SetBatchModeBackground()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Background);
|
||||
}
|
||||
|
||||
void UShaderPipeline::SetBatchModePrecompile()
|
||||
{
|
||||
FShaderPipelineCache::SetBatchMode(FShaderPipelineCache::BatchMode::Precompile);
|
||||
}
|
||||
|
||||
void UShaderPipeline::PauseBatching()
|
||||
{
|
||||
FShaderPipelineCache::PauseBatching();
|
||||
}
|
||||
|
||||
void UShaderPipeline::ResumeBatching()
|
||||
{
|
||||
FShaderPipelineCache::ResumeBatching();
|
||||
}
|
||||
|
||||
bool UShaderPipeline::IsPrecompiling()
|
||||
{
|
||||
return FShaderPipelineCache::IsPrecompiling();
|
||||
}
|
||||
|
||||
int UShaderPipeline::NumPrecompilesRemaining()
|
||||
{
|
||||
return FShaderPipelineCache::NumPrecompilesRemaining();
|
||||
}
|
||||
|
||||
bool UShaderPipeline::IsBatchingPaused()
|
||||
{
|
||||
return FShaderPipelineCache::IsBatchingPaused();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
class FPSOFunctionsModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2025 Danielel. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "ShaderPipelineCache.h"
|
||||
|
||||
#include "ShaderPipeline.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class PSOFUNCTIONS_API UShaderPipeline : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Fast"))
|
||||
static void SetBatchModeFast();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Background"))
|
||||
static void SetBatchModeBackground();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Sets the precompilation batching mode to Precompile"))
|
||||
static void SetBatchModePrecompile();
|
||||
|
||||
/** Pauses precompilation batching. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Pauses precompilation batching"))
|
||||
static void PauseBatching();
|
||||
|
||||
/** Resumes precompilation batching. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", meta = (ToolTip = "Resumes precompilation batching"))
|
||||
static void ResumeBatching();
|
||||
|
||||
/** true if pipeline cache(s) are precompiling. */
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "True if pipeline cache(s) are precompiling"))
|
||||
static bool IsPrecompiling();
|
||||
|
||||
/** Returns the number of pipelines waiting for precompilation of the current PSOFC task.
|
||||
if there are multiple PSOFCs pending this will return a minimum value of 1. It may increase as subsequent caches are processed.*/
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "Returns the number of pipelines waiting for precompilation of the current PSOFC task. If there are multiple PSOFCs pending this will return a minimum value of 1. It may increase as subsequent caches are processed."))
|
||||
static int NumPrecompilesRemaining();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PSO precompile", BlueprintPure, meta = (ToolTip = "Returns if the precompilation batching is paused"))
|
||||
static bool IsBatchingPaused();
|
||||
};
|
||||
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png
LFS
Normal file
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_16x.png
LFS
Normal file
Binary file not shown.
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png
LFS
Normal file
BIN
Plugins/SUDS/Content/Editor/Slate/Icons/SUDSScript_64x.png
LFS
Normal file
Binary file not shown.
6
Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json
vendored
Normal file
6
Plugins/SUDS/Extras/PygmentsLexer/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"speakerline",
|
||||
"squarebrackets"
|
||||
]
|
||||
}
|
||||
12
Plugins/SUDS/Extras/PygmentsLexer/setup.py
Normal file
12
Plugins/SUDS/Extras/PygmentsLexer/setup.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup (
|
||||
name='sudslexer',
|
||||
packages=find_packages(),
|
||||
version="0.4.0",
|
||||
entry_points =
|
||||
"""
|
||||
[pygments.lexers]
|
||||
sudslexer = sudslexer.lexer:SudsLexer
|
||||
""",
|
||||
)
|
||||
71
Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py
Normal file
71
Plugins/SUDS/Extras/PygmentsLexer/sudslexer/lexer.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from pygments.lexer import RegexLexer, bygroups
|
||||
from pygments.token import *
|
||||
|
||||
class SudsLexer(RegexLexer):
|
||||
name = 'SUDS'
|
||||
aliases = ['suds']
|
||||
filenames = ['*.sud']
|
||||
|
||||
tokens = {
|
||||
'root': [
|
||||
(r'===\s*\n', Generic.Heading),
|
||||
# Choices
|
||||
(r'\s*\*\-?\s+[^\@\n]+[\@\n]', Generic.Subheading),
|
||||
(r'\s*#.*\n', Comment),
|
||||
# Speaker lines
|
||||
(r'\s*\S+\:', Name.Class, 'speakerline'),
|
||||
# Open brackets for special functions
|
||||
(r'\[', Operator, 'squarebrackets'),
|
||||
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)),
|
||||
(r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant),
|
||||
# Line IDs
|
||||
(r'\@[0-9a-fA-F]+\@', Comment.Special),
|
||||
# Embedded markup
|
||||
(r'\<\w+\>', Name.Decorator),
|
||||
(r'\<\/\>', Name.Decorator),
|
||||
# Goto labels
|
||||
(r'\s*:\S+\n', Name.Label),
|
||||
# Comments
|
||||
(r'\s*\#[\=\+\%]?.*\n', Comment.Special),
|
||||
# Fallback for all other text
|
||||
# Needs an optional \n on the end to finish lines correctly
|
||||
(r'\s+[\n]?', Text),
|
||||
(r'\S+?[\n]?', Text), # non-greedy so we don't consume all non-whitespace
|
||||
|
||||
|
||||
],
|
||||
# While in a speaker line, we ignore everything else except variables & markup
|
||||
'speakerline' : [
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'(\|)(plural|gender)(\()(.*?)(\))', bygroups(Operator, Keyword, Operator, Keyword, Operator)),
|
||||
(r'[\@\n]', Text, '#pop'),
|
||||
# Embedded markup
|
||||
(r'\<\w+\>', Name.Decorator),
|
||||
(r'\<\/\>', Name.Decorator),
|
||||
(r'[^\@\n\<\{]+', Text),
|
||||
|
||||
],
|
||||
# While in a [] block, highlight operators etc (don't do it elsewhere)
|
||||
'squarebrackets' : [
|
||||
# Close bracket finishes
|
||||
(r'\]\s*[\n]?', Operator, '#pop'),
|
||||
# Variables
|
||||
(r'(\{)([\w\.]+)(\})', bygroups(Operator, Name.Variable, Operator)),
|
||||
(r'\+\/\-\*\!\%', Operator),
|
||||
(r'\"[^\"]*\"', String.Double),
|
||||
(r'\`[^\`]*\`', String.Escape),
|
||||
(r'\d+(\.\d+)?', Number),
|
||||
(r'\b([tT]rue|[fF]alse|[mM]asculine|[fF]eminine|[nN]euter)\b', Name.Constant),
|
||||
# Set, event commands so we can highlight variable/event differently
|
||||
(r'\s*(set|event)(\s+)([^\]\s]+)', bygroups(Keyword, Text, Name.Variable)),
|
||||
(r'\s*(if|else|elseif|endif|event|return|goto|gosub|go to|go sub|random|endrandom)\b', Keyword),
|
||||
(r'\b(and|or|&&|\|\||not)\b', Operator),
|
||||
(r'[,]', Punctuation),
|
||||
(r'\s+', Text), # whitespace OK
|
||||
|
||||
]
|
||||
}
|
||||
18
Plugins/SUDS/License.txt
Normal file
18
Plugins/SUDS/License.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
The MIT License (MIT) Copyright © 2022 Steve Streeting
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the “Software”), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
BIN
Plugins/SUDS/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/SUDS/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
BIN
Plugins/SUDS/Resources/logo.afdesign
Normal file
BIN
Plugins/SUDS/Resources/logo.afdesign
Normal file
Binary file not shown.
33
Plugins/SUDS/SUDS.uplugin
Normal file
33
Plugins/SUDS/SUDS.uplugin
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "SUDS",
|
||||
"Description": "Steve's Unreal Dialogue System",
|
||||
"Category": "Other",
|
||||
"CreatedBy": "Steve Streeting",
|
||||
"CreatedByURL": "",
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "SUDS",
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "SUDSEditor",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "SUDSTest",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal file
21
Plugins/SUDS/Source/SUDS/Private/SUDS.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDS.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
}
|
||||
|
||||
void FSUDSModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSModule, SUDS)
|
||||
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
4
Plugins/SUDS/Source/SUDS/Private/SUDSCommon.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "SUDSCommon.h"
|
||||
|
||||
const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR);
|
||||
|
||||
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
1379
Plugins/SUDS/Source/SUDS/Private/SUDSDialogue.cpp
Normal file
File diff suppressed because it is too large
Load Diff
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal file
465
Plugins/SUDS/Source/SUDS/Private/SUDSExpression.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
#include "SUDSLibrary.h"
|
||||
#include "Misc/DefaultValueHelper.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
|
||||
bool FSUDSExpression::ParseFromString(const FString& Expression, FString* OutParseError)
|
||||
{
|
||||
// Assume invalid until we've parsed something
|
||||
bIsValid = false;
|
||||
Queue.Empty();
|
||||
VariableNames.Empty();
|
||||
SourceString = Expression;
|
||||
|
||||
// Shunting-yard algorithm
|
||||
// Thanks to Nathan Reed https://www.reedbeta.com/blog/the-shunting-yard-algorithm/
|
||||
// We take a natural string, parse it and then turn it into a queue of tokens (operators and operands)
|
||||
// expressed in Reverse Polish Notation, which can be easily executed later
|
||||
// Variables are not resolved at this point, only at execution time.
|
||||
|
||||
// Split into individual tokens; sections of the regex are:
|
||||
// - {Variable}
|
||||
// - Literal numbers (with or without decimal point, with or without preceding negation)
|
||||
// - Arithmetic operators & parentheses
|
||||
// - Boolean operators & comparisons
|
||||
// - Predefined constants (Masculine, feminine, true, false etc)
|
||||
// - Quoted strings "string"
|
||||
// - Including ignoring escaped double quotes
|
||||
// - Quoted names `name`
|
||||
const FRegexPattern Pattern(TEXT("(\\{[\\w\\.]+\\}|-?\\d+(?:\\.\\d*)?|[-+*\\/%\\(\\)]|and|&&|\\|\\||or|not|\\<\\>|!=|!|\\<=?|\\>=?|==?|[mM]asculine|[fF]eminine|[nN]euter|[tT]rue|[fF]alse|\"(?:[^\"\\\\]|\\\\.)*\"|`([^`]*)`)"));
|
||||
FRegexMatcher Regex(Pattern, Expression);
|
||||
// Stacks that we use to construct
|
||||
TArray<ESUDSExpressionItemType> OperatorStack;
|
||||
bool bParsedSomething = false;
|
||||
bool bErrors = false;
|
||||
while (Regex.FindNext())
|
||||
{
|
||||
FString Str = Regex.GetCaptureGroup(1);
|
||||
ESUDSExpressionItemType OpType = ParseOperator(Str);
|
||||
if (OpType != ESUDSExpressionItemType::Null)
|
||||
{
|
||||
bParsedSomething = true;
|
||||
|
||||
if (OpType == ESUDSExpressionItemType::LParens)
|
||||
{
|
||||
OperatorStack.Push(OpType);
|
||||
}
|
||||
else if (OpType == ESUDSExpressionItemType::RParens)
|
||||
{
|
||||
if (OperatorStack.IsEmpty())
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
bErrors = true;
|
||||
break;
|
||||
}
|
||||
|
||||
while (OperatorStack.Num() > 0 && OperatorStack.Top() != ESUDSExpressionItemType::LParens)
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
if (OperatorStack.IsEmpty())
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
bErrors = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Discard left parens
|
||||
OperatorStack.Pop();
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// All operators are left-associative except not
|
||||
const bool bLeftAssociative = OpType != ESUDSExpressionItemType::Not;
|
||||
// Valid operator
|
||||
// Apply anything on the operator stack which is higher / equal precedence
|
||||
while (OperatorStack.Num() > 0 &&
|
||||
// higher precedence applied now, and equal precedence if left-associative
|
||||
(static_cast<int>(OperatorStack.Top()) < static_cast<int>(OpType) ||
|
||||
(static_cast<int>(OperatorStack.Top()) <= static_cast<int>(OpType) && bLeftAssociative)))
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
|
||||
OperatorStack.Push(OpType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to parse operand
|
||||
FSUDSValue Operand;
|
||||
if (ParseOperand(Str, Operand))
|
||||
{
|
||||
bParsedSomething = true;
|
||||
Queue.Add(FSUDSExpressionItem(Operand));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutParseError)
|
||||
*OutParseError = FString::Printf(TEXT("Unrecognised token %s"), *Str);
|
||||
bErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// finish up
|
||||
while (OperatorStack.Num() > 0)
|
||||
{
|
||||
if (OperatorStack.Top() == ESUDSExpressionItemType::LParens ||
|
||||
OperatorStack.Top() == ESUDSExpressionItemType::RParens)
|
||||
{
|
||||
bErrors = true;
|
||||
if (OutParseError)
|
||||
*OutParseError = TEXT("Mismatched parentheses");
|
||||
break;
|
||||
}
|
||||
|
||||
Queue.Add(FSUDSExpressionItem(OperatorStack.Pop()));
|
||||
}
|
||||
|
||||
if (!Validate() ||
|
||||
(Expression.Len() > 0 && Queue.IsEmpty())) // Empty expressions validate correctly, but if there was text incoming that resolved to nothing, this is an error
|
||||
{
|
||||
bErrors = true;
|
||||
if (OutParseError)
|
||||
*OutParseError = FString::Printf(TEXT("Bad expression '%s'"), *Expression);
|
||||
}
|
||||
|
||||
bIsValid = bParsedSomething && !bErrors;
|
||||
|
||||
// Build list of variables
|
||||
if (bIsValid)
|
||||
{
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperand() && Item.GetOperandValue().IsVariable())
|
||||
{
|
||||
VariableNames.AddUnique(Item.GetOperandValue().GetVariableNameValue());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return bIsValid;
|
||||
}
|
||||
|
||||
void FSUDSExpression::Reset()
|
||||
{
|
||||
bIsValid = true;
|
||||
Queue.Empty();
|
||||
VariableNames.Empty();
|
||||
SourceString = "";
|
||||
}
|
||||
|
||||
bool FSUDSExpression::IsRandomCondition() const
|
||||
{
|
||||
if (Queue.Num() > 0 && Queue[0].GetType() == ESUDSExpressionItemType::Operand)
|
||||
{
|
||||
const auto& Operand = Queue[0].GetOperandValue();
|
||||
return Operand.IsVariable() && Operand.GetVariableNameValue() == FSUDSConstants::RandomItemSelectIndexVarName;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ESUDSExpressionItemType FSUDSExpression::ParseOperator(const FString& OpStr)
|
||||
{
|
||||
if (OpStr == "+")
|
||||
return ESUDSExpressionItemType::Add;
|
||||
if (OpStr == "-")
|
||||
return ESUDSExpressionItemType::Subtract;
|
||||
if (OpStr == "*")
|
||||
return ESUDSExpressionItemType::Multiply;
|
||||
if (OpStr == "/")
|
||||
return ESUDSExpressionItemType::Divide;
|
||||
if (OpStr == "%")
|
||||
return ESUDSExpressionItemType::Modulo;
|
||||
if (OpStr == "and" || OpStr == "&&")
|
||||
return ESUDSExpressionItemType::And;
|
||||
if (OpStr == "or" || OpStr == "||")
|
||||
return ESUDSExpressionItemType::Or;
|
||||
if (OpStr == "not" || OpStr == "!")
|
||||
return ESUDSExpressionItemType::Not;
|
||||
if (OpStr == "==" || OpStr == "=")
|
||||
return ESUDSExpressionItemType::Equal;
|
||||
if (OpStr == ">=")
|
||||
return ESUDSExpressionItemType::GreaterEqual;
|
||||
if (OpStr == ">")
|
||||
return ESUDSExpressionItemType::Greater;
|
||||
if (OpStr == "<=")
|
||||
return ESUDSExpressionItemType::LessEqual;
|
||||
if (OpStr == "<")
|
||||
return ESUDSExpressionItemType::Less;
|
||||
if (OpStr == "<>" || OpStr == "!=")
|
||||
return ESUDSExpressionItemType::NotEqual;
|
||||
if (OpStr == "(")
|
||||
return ESUDSExpressionItemType::LParens;
|
||||
if (OpStr == ")")
|
||||
return ESUDSExpressionItemType::RParens;
|
||||
|
||||
return ESUDSExpressionItemType::Null;
|
||||
}
|
||||
|
||||
bool FSUDSExpression::ParseOperand(const FString& ValueStr, FSUDSValue& OutVal)
|
||||
{
|
||||
// Try Boolean first since only 2 options
|
||||
{
|
||||
if (ValueStr.Compare("true", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(true);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("false", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try gender
|
||||
{
|
||||
if (ValueStr.Compare("masculine", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Masculine);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("feminine", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Feminine);
|
||||
return true;
|
||||
}
|
||||
if (ValueStr.Compare("neuter", ESearchCase::IgnoreCase) == 0)
|
||||
{
|
||||
OutVal = FSUDSValue(ETextGender::Neuter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try quoted text (will be localised later in asset conversion)
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^\"((?:[^\"\\\\]|\\\\.)*)\"$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
FString Val = Regex.GetCaptureGroup(1);
|
||||
// Consolidate any escaped double quotes into just quotes
|
||||
Val.ReplaceInline(TEXT("\\\""), TEXT("\""));
|
||||
OutVal = FSUDSValue(FText::FromString(Val));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try FName
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^`([^`]*)`$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
const FString Name = Regex.GetCaptureGroup(1);
|
||||
OutVal = FSUDSValue(FName(Name), false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try variable name
|
||||
{
|
||||
const FRegexPattern Pattern(TEXT("^\\{([^\\}]*)\\}$"));
|
||||
FRegexMatcher Regex(Pattern, ValueStr);
|
||||
if (Regex.FindNext())
|
||||
{
|
||||
const FName VariableName(Regex.GetCaptureGroup(1));
|
||||
OutVal = FSUDSValue(VariableName, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Try Numbers
|
||||
{
|
||||
float FloatVal;
|
||||
int IntVal;
|
||||
// look for int first; anything with a decimal point will fail
|
||||
if (FDefaultValueHelper::ParseInt(ValueStr, IntVal))
|
||||
{
|
||||
OutVal = FSUDSValue(IntVal);
|
||||
return true;
|
||||
}
|
||||
if (FDefaultValueHelper::ParseFloat(ValueStr, FloatVal))
|
||||
{
|
||||
OutVal = FSUDSValue(FloatVal);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSExpression::Validate()
|
||||
{
|
||||
// Empty expressions are always valid, mean "true"
|
||||
if (Queue.IsEmpty())
|
||||
return true;
|
||||
|
||||
// Same algorithm as Execute, we just don't execute
|
||||
TArray<FSUDSExpressionItem> EvalStack;
|
||||
const TMap<FName, FSUDSValue> TempVariables;
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperator())
|
||||
{
|
||||
FSUDSExpressionItem Arg1, Arg2;
|
||||
if (Item.IsBinaryOperator())
|
||||
{
|
||||
if (EvalStack.IsEmpty())
|
||||
return false;
|
||||
Arg2 = EvalStack.Pop();
|
||||
}
|
||||
if (EvalStack.IsEmpty())
|
||||
return false;
|
||||
Arg1 = EvalStack.Pop();
|
||||
|
||||
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, TempVariables, TempVariables));
|
||||
}
|
||||
else
|
||||
{
|
||||
EvalStack.Push(Item);
|
||||
}
|
||||
}
|
||||
|
||||
// Must be one item left and must be an operand
|
||||
return EvalStack.Num() == 1 && EvalStack[0].IsOperand();
|
||||
|
||||
}
|
||||
|
||||
FSUDSValue FSUDSExpression::Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
checkf(bIsValid, TEXT("Cannot execute an invalid expression tree"));
|
||||
|
||||
// Blanks are mostly used for conditionals, for simplicity always return true
|
||||
if (Queue.IsEmpty())
|
||||
return FSUDSValue(true);
|
||||
|
||||
TArray<FSUDSExpressionItem> EvalStack;
|
||||
// We could pre-optimise all literal expressions, but let's not for now
|
||||
for (auto& Item : Queue)
|
||||
{
|
||||
if (Item.IsOperator())
|
||||
{
|
||||
FSUDSExpressionItem Arg1, Arg2;
|
||||
// Arg2 (RHS) has to be popped first
|
||||
if (Item.IsBinaryOperator())
|
||||
{
|
||||
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
|
||||
Arg2 = EvalStack.Pop();
|
||||
}
|
||||
checkf(!EvalStack.IsEmpty(), TEXT("Args missing before operator, bad expression"));
|
||||
Arg1 = EvalStack.Pop();
|
||||
EvalStack.Push(EvaluateOperator(Item.GetType(), Arg1, Arg2, Variables, GlobalVariables));
|
||||
}
|
||||
else
|
||||
{
|
||||
EvalStack.Push(Item);
|
||||
}
|
||||
}
|
||||
|
||||
checkf(EvalStack.Num() == 1, TEXT("We should end with a single item in the eval stack and it should be an operand"));
|
||||
|
||||
return EvaluateOperand(EvalStack.Top().GetOperandValue(), Variables, GlobalVariables);
|
||||
}
|
||||
|
||||
bool FSUDSExpression::EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const
|
||||
{
|
||||
const auto Result = Evaluate(Variables, GlobalVariables);
|
||||
|
||||
if (Result.GetType() != ESUDSValueType::Boolean &&
|
||||
Result.GetType() != ESUDSValueType::Variable) // Allow unresolved variable, will assume false
|
||||
{
|
||||
UE_LOG(LogSUDS, Error, TEXT("%s: Condition '%s' did not return a boolean result"), *ErrorContext, *SourceString)
|
||||
}
|
||||
|
||||
return Result.GetBooleanValue();
|
||||
}
|
||||
|
||||
FSUDSExpressionItem FSUDSExpression::EvaluateOperator(ESUDSExpressionItemType Op,
|
||||
const FSUDSExpressionItem& Arg1,
|
||||
const FSUDSExpressionItem& Arg2,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
const FSUDSValue Val1 = EvaluateOperand(Arg1.GetOperandValue(), Variables, GlobalVariables);
|
||||
FSUDSValue Val2;
|
||||
if (Arg1.IsBinaryOperator())
|
||||
{
|
||||
Val2 = EvaluateOperand(Arg2.GetOperandValue(), Variables, GlobalVariables);
|
||||
}
|
||||
|
||||
switch (Op)
|
||||
{
|
||||
case ESUDSExpressionItemType::Not:
|
||||
return FSUDSExpressionItem(!Val1);
|
||||
case ESUDSExpressionItemType::Multiply:
|
||||
return FSUDSExpressionItem(Val1 * Val2);
|
||||
case ESUDSExpressionItemType::Divide:
|
||||
return FSUDSExpressionItem(Val1 / Val2);
|
||||
case ESUDSExpressionItemType::Modulo:
|
||||
return FSUDSExpressionItem(Val1 % Val2);
|
||||
case ESUDSExpressionItemType::Add:
|
||||
return FSUDSExpressionItem(Val1 + Val2);
|
||||
case ESUDSExpressionItemType::Subtract:
|
||||
return FSUDSExpressionItem(Val1 - Val2);
|
||||
case ESUDSExpressionItemType::Less:
|
||||
return FSUDSExpressionItem(Val1 < Val2);
|
||||
case ESUDSExpressionItemType::LessEqual:
|
||||
return FSUDSExpressionItem(Val1 <= Val2);
|
||||
case ESUDSExpressionItemType::Greater:
|
||||
return FSUDSExpressionItem(Val1 > Val2);
|
||||
case ESUDSExpressionItemType::GreaterEqual:
|
||||
return FSUDSExpressionItem(Val1 >= Val2);
|
||||
case ESUDSExpressionItemType::Equal:
|
||||
return FSUDSExpressionItem(Val1 == Val2);
|
||||
case ESUDSExpressionItemType::NotEqual:
|
||||
return FSUDSExpressionItem(Val1 != Val2);
|
||||
case ESUDSExpressionItemType::And:
|
||||
return FSUDSExpressionItem(Val1 && Val2);
|
||||
case ESUDSExpressionItemType::Or:
|
||||
return FSUDSExpressionItem(Val1 || Val2);
|
||||
|
||||
|
||||
default: // these won't occur
|
||||
case ESUDSExpressionItemType::Null:
|
||||
case ESUDSExpressionItemType::Operand:
|
||||
case ESUDSExpressionItemType::LParens:
|
||||
case ESUDSExpressionItemType::RParens:
|
||||
return FSUDSExpressionItem();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
FSUDSValue FSUDSExpression::EvaluateOperand(const FSUDSValue& Operand,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const
|
||||
{
|
||||
// Simplify conversion to variable values
|
||||
if (Operand.IsVariable())
|
||||
{
|
||||
FName Name = Operand.GetVariableNameValue();
|
||||
FName GlobalName;
|
||||
if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName))
|
||||
{
|
||||
// This will have stripped the prefix so direct find is OK
|
||||
if (const auto Var = GlobalVariables.Find(GlobalName))
|
||||
{
|
||||
return *Var;
|
||||
}
|
||||
}
|
||||
if (const auto Var = Variables.Find(Operand.GetVariableNameValue()))
|
||||
{
|
||||
return *Var;
|
||||
}
|
||||
// Note: we're NOT warning about unset variables here, and just defaulting to initial values (false, 0 etc)
|
||||
// This is more usable in practice than complaining about it
|
||||
}
|
||||
|
||||
return Operand;
|
||||
}
|
||||
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal file
41
Plugins/SUDS/Source/SUDS/Private/SUDSInternal.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "SUDSSubsystem.h"
|
||||
#include "SUDSValue.h"
|
||||
|
||||
inline const TMap<FName, FSUDSValue>& InternalGetGlobalVariables(UWorld* WorldContext)
|
||||
{
|
||||
if (auto Sub = GetSUDSSubsystem(WorldContext))
|
||||
{
|
||||
return Sub->GetGlobalVariables();
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// In editor mode, return static global vars since we may be unit testing or in editor tester
|
||||
return USUDSSubsystem::Test_DummyGlobalVariables;
|
||||
#else
|
||||
static TMap<FName, FSUDSValue> Blank;
|
||||
return Blank;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// For our code only
|
||||
inline void InternalSetGlobalVariable(UWorld* WorldContext, FName Name, const FSUDSValue& Value, bool bFromScript, const FString& ScriptName, int LineNo)
|
||||
{
|
||||
if (auto Sub = GetSUDSSubsystem(WorldContext))
|
||||
{
|
||||
Sub->InternalSetGlobalVariable(Name, Value, bFromScript, LineNo);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// In editor mode, update static global vars since we may be unit testing or in editor tester
|
||||
USUDSSubsystem::Test_DummyGlobalVariables.Add(Name, Value);
|
||||
#else
|
||||
checkf(false, TEXT("%s: Attempted to set global variable %s with no world context, did you forget to set the dialogue owner?"), *ScriptName, *Name.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal file
154
Plugins/SUDS/Source/SUDS/Private/SUDSLibrary.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSLibrary.h"
|
||||
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogue(UObject* Owner, USUDSScript* Script, bool bStartImmediately, FName StartLabel)
|
||||
{
|
||||
if (IsValid(Script))
|
||||
{
|
||||
if (!IsValid(Owner))
|
||||
{
|
||||
Owner = GetTransientPackage();
|
||||
}
|
||||
const FName Name = MakeUniqueObjectName(Owner, USUDSDialogue::StaticClass(), Script->GetFName());
|
||||
USUDSDialogue* Ret = NewObject<USUDSDialogue>(Owner, Name);
|
||||
Ret->Initialise(Script);
|
||||
if (bStartImmediately)
|
||||
{
|
||||
Ret->Start(StartLabel);
|
||||
}
|
||||
return Ret;
|
||||
}
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipants(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
const TArray<UObject*>& Participants, bool bStartImmediately, FName StartLabel)
|
||||
{
|
||||
if (IsValid(Script))
|
||||
{
|
||||
if (!IsValid(Owner))
|
||||
{
|
||||
Owner = GetTransientPackage();
|
||||
}
|
||||
// Don't use the base CreateDialogue to start, we want to set participants first before init/start
|
||||
USUDSDialogue* Dlg = NewObject<USUDSDialogue>(Owner, Script->GetFName());
|
||||
Dlg->SetParticipants(Participants);
|
||||
Dlg->Initialise(Script);
|
||||
if (bStartImmediately)
|
||||
{
|
||||
Dlg->Start(StartLabel);
|
||||
}
|
||||
return Dlg;
|
||||
}
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called CreateDialogue with an invalid script"))
|
||||
return nullptr;
|
||||
|
||||
|
||||
}
|
||||
|
||||
USUDSDialogue* USUDSLibrary::CreateDialogueWithParticipant(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
UObject* Participant,
|
||||
bool bStartImmediately,
|
||||
FName StartLabel)
|
||||
{
|
||||
TArray<UObject*> Participants;
|
||||
Participants.Add(Participant);
|
||||
return CreateDialogueWithParticipants(Owner, Script, Participants, bStartImmediately, StartLabel);
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Text)
|
||||
{
|
||||
TextValue = Value.GetTextValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Boolean)
|
||||
{
|
||||
BoolValue = Value.GetBooleanValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Int)
|
||||
{
|
||||
IntValue = Value.GetIntValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Float)
|
||||
{
|
||||
FloatValue = Value.GetFloatValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Gender)
|
||||
{
|
||||
GenderValue = Value.GetGenderValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue)
|
||||
{
|
||||
if (Value.GetType() == ESUDSValueType::Name)
|
||||
{
|
||||
NameValue = Value.GetNameValue();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
ESUDSValueType USUDSLibrary::GetDialogueValueType(const FSUDSValue& Value)
|
||||
{
|
||||
return Value.GetType();
|
||||
}
|
||||
|
||||
bool USUDSLibrary::GetDialogueValueIsEmpty(const FSUDSValue& Value)
|
||||
{
|
||||
return Value.IsEmpty();
|
||||
}
|
||||
|
||||
bool USUDSLibrary::IsDialogueVariableGlobal(const FName& Name, FName& OutName)
|
||||
{
|
||||
static const FString Prefix(TEXT("global."));
|
||||
FString TempStr;
|
||||
Name.ToString(TempStr);
|
||||
if (TempStr.StartsWith(Prefix, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TempStr.RightChopInline(Prefix.Len());
|
||||
OutName = FName(TempStr);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutName = Name;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
5
Plugins/SUDS/Source/SUDS/Private/SUDSParticipant.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSParticipant.h"
|
||||
|
||||
|
||||
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal file
276
Plugins/SUDS/Source/SUDS/Private/SUDSScript.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScript.h"
|
||||
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
|
||||
void USUDSScript::StartImport(TArray<TObjectPtr<USUDSScriptNode>>** ppNodes,
|
||||
TArray<TObjectPtr<USUDSScriptNode>>** ppHeaderNodes,
|
||||
TMap<FName, int>** ppLabelList,
|
||||
TMap<FName, int>** ppHeaderLabelList,
|
||||
TArray<FString>** ppSpeakerList)
|
||||
{
|
||||
*ppNodes = &Nodes;
|
||||
*ppHeaderNodes = &HeaderNodes;
|
||||
*ppLabelList = &LabelList;
|
||||
*ppHeaderLabelList = &HeaderLabelList;
|
||||
*ppSpeakerList = &Speakers;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetNextNode(const USUDSScriptNode* Node) const
|
||||
{
|
||||
switch (Node->GetEdgeCount())
|
||||
{
|
||||
case 0:
|
||||
return nullptr;
|
||||
case 1:
|
||||
return Node->GetEdge(0)->GetTargetNode().Get();
|
||||
default:
|
||||
UE_LOG(LogSUDS, Error, TEXT("Called GetNextNode on a node with more than one edge"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#define kChoiceFound 1
|
||||
#define kChoiceNotFoundBeforeText -1
|
||||
#define kChoiceNotFoundBeforeEnd 0
|
||||
|
||||
|
||||
bool USUDSScript::DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode)
|
||||
{
|
||||
// Look for any possible choice following a node (text or gosub)
|
||||
// If it's possible to find a choice in one of the paths ahead, before another text node, the return true
|
||||
// Given that there might be conditionals, not all paths might lead to a choice, but we only care if one of them does
|
||||
// We recurse into conditional paths where they exist until we know.
|
||||
// For a gosub this is looking for the next after a return, not inside the sub
|
||||
USUDSScriptNode* CurrNode = GetNextNode(FromNode);
|
||||
|
||||
return RecurseLookForChoice(CurrNode) == kChoiceFound;
|
||||
}
|
||||
|
||||
|
||||
int USUDSScript::RecurseLookForChoice(USUDSScriptNode* CurrNode)
|
||||
{
|
||||
// Return int so that we can differentiate:
|
||||
// 1 = we found a choice
|
||||
// 0 = we didn't find a choice, but also didn't hit another text node (reached end, or gosub return)
|
||||
// -1 = we hit a text node
|
||||
while (CurrNode)
|
||||
{
|
||||
switch (CurrNode->GetNodeType())
|
||||
{
|
||||
case ESUDSScriptNodeType::Text:
|
||||
// if we hit a text node, there was no choice
|
||||
return kChoiceNotFoundBeforeText;
|
||||
case ESUDSScriptNodeType::Choice:
|
||||
// we found a choice
|
||||
return kChoiceFound;
|
||||
case ESUDSScriptNodeType::Select:
|
||||
{
|
||||
// Explore all possible routes
|
||||
int WorstResult = kChoiceNotFoundBeforeEnd;
|
||||
for (auto& Edge : CurrNode->GetEdges())
|
||||
{
|
||||
auto TargetNode = Edge.GetTargetNode();
|
||||
if (TargetNode.IsValid())
|
||||
{
|
||||
const int ConditionalPath = RecurseLookForChoice(TargetNode.Get());
|
||||
if (ConditionalPath == kChoiceFound)
|
||||
return kChoiceFound;
|
||||
WorstResult = FMath::Min(ConditionalPath, WorstResult);
|
||||
}
|
||||
}
|
||||
return WorstResult;
|
||||
}
|
||||
case ESUDSScriptNodeType::Event:
|
||||
case ESUDSScriptNodeType::SetVariable:
|
||||
CurrNode = GetNextNode(CurrNode);
|
||||
break;
|
||||
case ESUDSScriptNodeType::Gosub:
|
||||
// When we hit a gosub here we go into it, not after it
|
||||
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(CurrNode))
|
||||
{
|
||||
int SubResult = RecurseLookForChoice(GetNodeByLabel(GosubNode->GetLabelName()));
|
||||
if (SubResult != 0)
|
||||
{
|
||||
// Found definitive result (choice or text) inside sub
|
||||
return SubResult;
|
||||
}
|
||||
}
|
||||
// Otherwise, we didn't conclude within the sub, continue following it
|
||||
CurrNode = GetNextNode(CurrNode);
|
||||
break;
|
||||
default: ;
|
||||
case ESUDSScriptNodeType::Return:
|
||||
// this is when we're exploring a sub for the choice
|
||||
return kChoiceNotFoundBeforeEnd;
|
||||
};
|
||||
}
|
||||
|
||||
return kChoiceNotFoundBeforeEnd;
|
||||
}
|
||||
|
||||
void USUDSScript::FinishImport()
|
||||
{
|
||||
// As an optimisation, make all text/gosub nodes pre-scan their follow-on nodes for choice nodes
|
||||
// We can actually have intermediate nodes, for example set nodes which run for all choices that are placed
|
||||
// between the text and the first choice. Resolve whether they exist now
|
||||
for (auto Node : Nodes)
|
||||
{
|
||||
if (Node->GetNodeType() == ESUDSScriptNodeType::Text ||
|
||||
Node->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (DoesAnyPathAfterLeadToChoice(Node))
|
||||
{
|
||||
switch (Node->GetNodeType())
|
||||
{
|
||||
case ESUDSScriptNodeType::Text:
|
||||
{
|
||||
if (auto TextNode = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
TextNode->NotifyMayHaveChoices();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESUDSScriptNodeType::Gosub:
|
||||
{
|
||||
if (auto GosubNode = Cast<USUDSScriptNodeGosub>(Node))
|
||||
{
|
||||
GosubNode->NotifyMayHaveChoices();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetHeaderNode() const
|
||||
{
|
||||
if (HeaderNodes.Num() > 0)
|
||||
return HeaderNodes[0];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetFirstNode() const
|
||||
{
|
||||
if (Nodes.Num() > 0)
|
||||
return Nodes[0];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNode* USUDSScript::GetNodeByLabel(const FName& Label) const
|
||||
{
|
||||
if (const int* pIdx = LabelList.Find(Label))
|
||||
{
|
||||
return Nodes[*pIdx];
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
USUDSScriptNodeText* USUDSScript::GetNodeByTextID(const FString& TextID) const
|
||||
{
|
||||
for (auto N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
if (TextID.Equals(TN->GetTextID()))
|
||||
{
|
||||
return TN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
USUDSScriptNodeGosub* USUDSScript::GetNodeByGosubID(const FString& ID) const
|
||||
{
|
||||
for (auto N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (auto GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
{
|
||||
if (ID.Equals(GN->GetGosubID()))
|
||||
{
|
||||
return GN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UDialogueVoice* USUDSScript::GetSpeakerVoice(const FString& SpeakerID) const
|
||||
{
|
||||
if (auto pVoice = SpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
return *pVoice;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void USUDSScript::SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice)
|
||||
{
|
||||
SpeakerVoices.Add(SpeakerID, Voice);
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
|
||||
void USUDSScript::PostInitProperties()
|
||||
{
|
||||
if (!HasAnyFlags(RF_ClassDefaultObject))
|
||||
{
|
||||
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
|
||||
}
|
||||
Super::PostInitProperties();
|
||||
}
|
||||
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
void USUDSScript::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
|
||||
{
|
||||
if (AssetImportData)
|
||||
{
|
||||
Context.AddTag( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
|
||||
}
|
||||
|
||||
Super::GetAssetRegistryTags(Context);
|
||||
}
|
||||
#else
|
||||
void USUDSScript::GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const
|
||||
{
|
||||
if (AssetImportData)
|
||||
{
|
||||
OutTags.Add( FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden) );
|
||||
}
|
||||
|
||||
Super::GetAssetRegistryTags(OutTags);
|
||||
}
|
||||
#endif
|
||||
|
||||
void USUDSScript::Serialize(FArchive& Ar)
|
||||
{
|
||||
Super::Serialize(Ar);
|
||||
|
||||
if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !AssetImportData)
|
||||
{
|
||||
// AssetImportData should always be valid
|
||||
AssetImportData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal file
79
Plugins/SUDS/Source/SUDS/Private/SUDSScriptEdge.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptEdge.h"
|
||||
|
||||
#include "SUDSScriptNode.h"
|
||||
|
||||
|
||||
FSUDSScriptEdge::FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo): Type(InType),
|
||||
TargetNode(ToNode),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSScriptEdge::FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo): Text(InText),
|
||||
Type(ESUDSEdgeType::Decision),
|
||||
TargetNode(ToNode),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::ExtractFormat() const
|
||||
{
|
||||
// Only do this on demand, and only once
|
||||
TextFormat = Text;
|
||||
ParameterNames.Empty();
|
||||
TArray<FString> TextParams;
|
||||
TextFormat.GetFormatArgumentNames(TextParams);
|
||||
for (auto Param : TextParams)
|
||||
{
|
||||
ParameterNames.Add(FName(Param));
|
||||
}
|
||||
bFormatExtracted = true;
|
||||
}
|
||||
|
||||
FString FSUDSScriptEdge::GetTextID() const
|
||||
{
|
||||
return SUDS_GET_TEXT_KEY(Text);
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::SetText(const FText& InText)
|
||||
{
|
||||
Text = InText;
|
||||
bFormatExtracted = false;
|
||||
}
|
||||
|
||||
void FSUDSScriptEdge::SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode)
|
||||
{
|
||||
TargetNode = InTargetNode;
|
||||
}
|
||||
|
||||
const FTextFormat& FSUDSScriptEdge::GetTextFormat() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return TextFormat;
|
||||
|
||||
}
|
||||
|
||||
const TArray<FName>& FSUDSScriptEdge::GetParameterNames() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return ParameterNames;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSScriptEdge::HasParameters() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return !ParameterNames.IsEmpty();
|
||||
|
||||
}
|
||||
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal file
38
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNode.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNode.h"
|
||||
|
||||
USUDSScriptNode::USUDSScriptNode()
|
||||
{
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitChoice(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Choice;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitSelect(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Select;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
void USUDSScriptNode::InitReturn(int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Return;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
|
||||
bool USUDSScriptNode::IsRandomSelect() const
|
||||
{
|
||||
return NodeType == ESUDSScriptNodeType::Select &&
|
||||
GetEdgeCount() > 0 &&
|
||||
GetEdge(0)->GetCondition().IsRandomCondition();
|
||||
}
|
||||
|
||||
void USUDSScriptNode::AddEdge(const FSUDSScriptEdge& NewEdge)
|
||||
{
|
||||
Edges.Add(NewEdge);
|
||||
}
|
||||
|
||||
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal file
12
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeEvent.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeEvent.h"
|
||||
|
||||
void USUDSScriptNodeEvent::Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Event;
|
||||
EventName = FName(EvtName);
|
||||
Args = InArgs;
|
||||
SourceLineNo = LineNo;
|
||||
|
||||
}
|
||||
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal file
11
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeSet.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
|
||||
void USUDSScriptNodeSet::Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::SetVariable;
|
||||
Identifier = FName(VarName);
|
||||
Expression = InExpression;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal file
62
Plugins/SUDS/Source/SUDS/Private/SUDSScriptNodeText.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
void USUDSScriptNodeText::Init(const FString& InSpeakerID, const FText& InText, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Text;
|
||||
SpeakerID = InSpeakerID;
|
||||
Text = InText;
|
||||
TextFormat = Text;
|
||||
SourceLineNo = LineNo;
|
||||
bFormatExtracted = false;
|
||||
|
||||
}
|
||||
|
||||
FString USUDSScriptNodeText::GetTextID() const
|
||||
{
|
||||
return SUDS_GET_TEXT_KEY(Text);
|
||||
}
|
||||
|
||||
const FTextFormat& USUDSScriptNodeText::GetTextFormat() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return TextFormat;
|
||||
}
|
||||
|
||||
const TArray<FName>& USUDSScriptNodeText::GetParameterNames() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return ParameterNames;
|
||||
}
|
||||
|
||||
bool USUDSScriptNodeText::HasParameters() const
|
||||
{
|
||||
if (!bFormatExtracted)
|
||||
{
|
||||
ExtractFormat();
|
||||
}
|
||||
return !ParameterNames.IsEmpty();
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptNodeText::ExtractFormat() const
|
||||
{
|
||||
// Only do this on demand, and only once
|
||||
TextFormat = Text;
|
||||
ParameterNames.Empty();
|
||||
|
||||
TArray<FString> TextParams;
|
||||
TextFormat.GetFormatArgumentNames(TextParams);
|
||||
for (auto Param : TextParams)
|
||||
{
|
||||
ParameterNames.Add(FName(Param));
|
||||
}
|
||||
bFormatExtracted = true;
|
||||
}
|
||||
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal file
199
Plugins/SUDS/Source/SUDS/Private/SUDSSubsystem.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSSubsystem.h"
|
||||
#include "Sound/SoundConcurrency.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSUDSSubsystem)
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
TMap<FName, FSUDSValue> USUDSSubsystem::Test_DummyGlobalVariables;
|
||||
#endif
|
||||
|
||||
void USUDSSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Default to a single voice line being played at once
|
||||
VoiceConcurrency = NewObject<USoundConcurrency>(this);
|
||||
VoiceConcurrency->Concurrency.MaxCount = 1;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::Deinitialize()
|
||||
{
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetMaxConcurrentVoicedLines(int ConcurrentLines)
|
||||
{
|
||||
if (IsValid(VoiceConcurrency))
|
||||
{
|
||||
VoiceConcurrency->Concurrency.MaxCount = ConcurrentLines;
|
||||
}
|
||||
}
|
||||
|
||||
int USUDSSubsystem::GetMaxConcurrentVoicedLines() const
|
||||
{
|
||||
if (IsValid(VoiceConcurrency))
|
||||
{
|
||||
return VoiceConcurrency->Concurrency.MaxCount;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::ResetGlobalState(bool bResetVariables)
|
||||
{
|
||||
if (bResetVariables)
|
||||
GlobalVariableState.Empty();
|
||||
}
|
||||
|
||||
FSUDSGlobalState USUDSSubsystem::GetSavedGlobalState() const
|
||||
{
|
||||
return FSUDSGlobalState(GlobalVariableState);
|
||||
}
|
||||
|
||||
void USUDSSubsystem::RestoreSavedGlobalState(const FSUDSGlobalState& State)
|
||||
{
|
||||
ResetGlobalState();
|
||||
GlobalVariableState.Append(State.GetGlobalVariables());
|
||||
}
|
||||
|
||||
|
||||
FText USUDSSubsystem::GetGlobalVariableText(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
if (Arg->GetType() == ESUDSValueType::Text)
|
||||
{
|
||||
return Arg->GetTextValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return FText();
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableInt(FName Name, int32 Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
int USUDSSubsystem::GetGlobalVariableInt(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue();
|
||||
case ESUDSValueType::Float:
|
||||
UE_LOG(LogSUDSSubsystem, Warning, TEXT("Casting variable %s to int, data loss may occur"), *Name.ToString());
|
||||
return Arg->GetFloatValue();
|
||||
default:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible integer type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableFloat(FName Name, float Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
float USUDSSubsystem::GetGlobalVariableFloat(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue();
|
||||
case ESUDSValueType::Float:
|
||||
return Arg->GetFloatValue();
|
||||
default:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible float type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableGender(FName Name, ETextGender Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
ETextGender USUDSSubsystem::GetGlobalVariableGender(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Gender:
|
||||
return Arg->GetGenderValue();
|
||||
default:
|
||||
case ESUDSValueType::Int:
|
||||
case ESUDSValueType::Float:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible gender type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return ETextGender::Neuter;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableBoolean(FName Name, bool Value)
|
||||
{
|
||||
// Use explicit FSUDSValue constructor to avoid default int conversion
|
||||
SetGlobalVariable(Name, FSUDSValue(Value));
|
||||
}
|
||||
|
||||
bool USUDSSubsystem::GetGlobalVariableBoolean(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
switch (Arg->GetType())
|
||||
{
|
||||
case ESUDSValueType::Boolean:
|
||||
return Arg->GetBooleanValue();
|
||||
case ESUDSValueType::Int:
|
||||
return Arg->GetIntValue() != 0;
|
||||
default:
|
||||
case ESUDSValueType::Float:
|
||||
case ESUDSValueType::Gender:
|
||||
case ESUDSValueType::Text:
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Variable %s is not a compatible boolean type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::SetGlobalVariableName(FName Name, FName Value)
|
||||
{
|
||||
SetGlobalVariable(Name, FSUDSValue(Value, false));
|
||||
}
|
||||
|
||||
FName USUDSSubsystem::GetGlobalVariableName(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
if (Arg->GetType() == ESUDSValueType::Name)
|
||||
{
|
||||
return Arg->GetNameValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSSubsystem, Error, TEXT("Requested variable %s of type text but was not a compatible type"), *Name.ToString());
|
||||
}
|
||||
}
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
void USUDSSubsystem::UnSetGlobalVariable(FName Name)
|
||||
{
|
||||
GlobalVariableState.Remove(Name);
|
||||
}
|
||||
100
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal file
100
Plugins/SUDS/Source/SUDS/Private/SUDSValue.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSValue.h"
|
||||
|
||||
#include "UObject/Class.h"
|
||||
#include "Internationalization/Text.h"
|
||||
|
||||
FArchive& operator<<(FArchive& Ar, FSUDSValue& Value)
|
||||
{
|
||||
// Custom serialisation since we can't auto-serialise union, TOptional
|
||||
uint8 TypeAsInt = (uint8)Value.Type;
|
||||
Ar << TypeAsInt;
|
||||
if (Ar.IsLoading())
|
||||
Value.Type = static_cast<ESUDSValueType>(TypeAsInt);
|
||||
|
||||
// This gets/sets float value too
|
||||
Ar << Value.IntValue;
|
||||
|
||||
if (Value.Type == ESUDSValueType::Text)
|
||||
{
|
||||
FText Text = Value.TextValue.Get(FText::GetEmpty());
|
||||
Ar << Text;
|
||||
if (Ar.IsLoading())
|
||||
Value.TextValue = Text;
|
||||
}
|
||||
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
|
||||
{
|
||||
FString VarNameStr = Value.Name.Get(NAME_None).ToString();
|
||||
Ar << VarNameStr;
|
||||
if (Ar.IsLoading())
|
||||
Value.Name = FName(VarNameStr);
|
||||
}
|
||||
|
||||
return Ar;
|
||||
}
|
||||
|
||||
void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value)
|
||||
{
|
||||
FStructuredArchive::FRecord Record = Slot.EnterRecord();
|
||||
Record
|
||||
<< SA_VALUE(TEXT("Type"), Value.Type)
|
||||
<< SA_VALUE(TEXT("IntValue"), Value.IntValue); // gets/sets float/boolean/gender too
|
||||
|
||||
if (Value.Type == ESUDSValueType::Text)
|
||||
{
|
||||
Record << SA_VALUE(TEXT("TextValue"), Value.TextValue);
|
||||
}
|
||||
else if (Value.Type == ESUDSValueType::Variable || Value.Type == ESUDSValueType::Name)
|
||||
{
|
||||
Record << SA_VALUE(TEXT("Name"), Value.Name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FString FSUDSValue::ToString() const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ESUDSValueType::Text:
|
||||
return GetTextValue().ToString();
|
||||
case ESUDSValueType::Int:
|
||||
return FString::FromInt(GetIntValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FString::SanitizeFloat(GetFloatValue());
|
||||
case ESUDSValueType::Boolean:
|
||||
return GetBooleanValue() ? "True" : "False";
|
||||
case ESUDSValueType::Gender:
|
||||
#if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8
|
||||
return LexToString(GetGenderValue());
|
||||
#else
|
||||
return *StaticEnum<ETextGender>()->GetNameStringByValue((int64)GetGenderValue());
|
||||
#endif
|
||||
case ESUDSValueType::Name:
|
||||
return GetNameValue().ToString();
|
||||
case ESUDSValueType::Variable:
|
||||
return GetVariableNameValue().ToString();
|
||||
default:
|
||||
case ESUDSValueType::Empty:
|
||||
return "Empty";
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSValue::ExportTextItem(FString& ValueStr,
|
||||
FSUDSValue const& DefaultValue,
|
||||
UObject* Parent,
|
||||
int32 PortFlags,
|
||||
UObject* ExportRootScope) const
|
||||
{
|
||||
// This is used to generate the blueprint debugger, but also used in serialisation
|
||||
// We need to only implement it for debugging to avoid breaking anything else
|
||||
if (0 != (PortFlags & EPropertyPortFlags::PPF_BlueprintDebugView))
|
||||
{
|
||||
ValueStr.Appendf(TEXT("Type=%s Value=%s"), *StaticEnum<ESUDSValueType>()->GetDisplayValueAsText(Type).ToString(), *ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use the default for everything else
|
||||
return false;
|
||||
|
||||
}
|
||||
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
15
Plugins/SUDS/Source/SUDS/Public/SUDS.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
23
Plugins/SUDS/Source/SUDS/Public/SUDSCommon.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
|
||||
// Use DECLARE_LOG_CATEGORY_CLASS not DECLARE_LOG_CATEGORY_EXTERN because we use UE_LOG in headers
|
||||
DECLARE_LOG_CATEGORY_CLASS(LogSUDS, Warning, All)
|
||||
|
||||
#define SUDS_RANDOMITEM_VAR "SUDS.RandomItem"
|
||||
|
||||
struct FSUDSConstants
|
||||
{
|
||||
/// Reserved variable named use to create random results from select nodes
|
||||
static const FName RandomItemSelectIndexVarName;
|
||||
|
||||
};
|
||||
|
||||
#if ENGINE_MINOR_VERSION >= 5
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().ToString()
|
||||
#else
|
||||
#define SUDS_GET_TEXT_KEY(Text) FTextInspector::GetTextId(Text).GetKey().GetChars()
|
||||
#endif
|
||||
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
694
Plugins/SUDS/Source/SUDS/Public/SUDSDialogue.h
Normal file
@@ -0,0 +1,694 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSDialogue.generated.h"
|
||||
|
||||
class USUDSScriptNodeGosub;
|
||||
class USUDSScriptNodeText;
|
||||
struct FSUDSScriptEdge;
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
class UDialogueWave;
|
||||
class UDialogueVoice;
|
||||
class USoundBase;
|
||||
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueSpeakerLine, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueChoice, class USUDSDialogue*, Dialogue, int, ChoiceIndex);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueProceeding, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDialogueStarting, class USUDSDialogue*, Dialogue, FName, AtLabel);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDialogueFinished, class USUDSDialogue*, Dialogue);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDialogueEvent, class USUDSDialogue*, Dialogue, FName, EventName, const TArray<FSUDSValue>&, Arguments);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnVariableChangedEvent, class USUDSDialogue*, Dialogue, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnVariableRequestedEvent, class USUDSDialogue*, Dialogue, FName, VariableName);
|
||||
|
||||
#if WITH_EDITOR
|
||||
// Non-dynamic events for editor use
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueSpeakerLineInternal, class USUDSDialogue* /* Dialogue */, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueChoiceInternal, class USUDSDialogue* /* Dialogue*/, int /*ChoiceIndex*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueProceedingInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_TwoParams(FOnDialogueStartingInternal, class USUDSDialogue* /*Dialogue*/, FName /*AtLabel*/);
|
||||
DECLARE_DELEGATE_OneParam(FOnDialogueFinishedInternal, class USUDSDialogue* /*Dialogue*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueEventInternal, class USUDSDialogue* /*Dialogue*/, FName /*EventName*/, const TArray<FSUDSValue>& /*Arguments*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_FiveParams(FOnDialogueVarChangedByScriptInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/, const FString& /*ExprString*/, int /*SourceLineNo*/);
|
||||
DECLARE_DELEGATE_ThreeParams(FOnDialogueVarChangedByCodeInternal, class USUDSDialogue* /* Dialogue*/, FName /*VariableName*/, const FSUDSValue& /*Value*/);
|
||||
DECLARE_DELEGATE_FourParams(FOnDialogueSelectEval, class USUDSDialogue* /*Dialogue*/, const FString& /*ConditionString*/, bool /*bResult*/, int /*SourceLineNo*/);
|
||||
#endif
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSDialogue, Verbose, All);
|
||||
|
||||
/// Copy of the internal state of a dialogue
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSDialogueState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
FString TextNodeID;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> Variables;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ChoicesTaken;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS|Dialogue")
|
||||
TArray<FString> ReturnStack;
|
||||
|
||||
public:
|
||||
FSUDSDialogueState() {}
|
||||
|
||||
FSUDSDialogueState(const FString& TxtID,
|
||||
const TMap<FName, FSUDSValue>& InVars,
|
||||
const TSet<FString>& InChoices,
|
||||
const TArray<FString>& InReturnStack) : TextNodeID(TxtID),
|
||||
Variables(InVars),
|
||||
ChoicesTaken(InChoices.Array()),
|
||||
ReturnStack(InReturnStack)
|
||||
{
|
||||
}
|
||||
|
||||
const FString& GetTextNodeID() const { return TextNodeID; }
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return Variables; }
|
||||
const TArray<FString>& GetChoicesTaken() const { return ChoicesTaken; }
|
||||
const TArray<FString>& GetReturnStack() const { return ReturnStack; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSDialogueState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSDialogueState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* A Dialogue is a runtime instance of a Script (the asset on which the dialogue is based)
|
||||
* An Dialogue always stops on a speaker line, which may have player choices. It progresses when you call Continue()
|
||||
* or Choose() and will run that continuation until it hits the next speaker line. In between, other things may occur
|
||||
* such as setting variables, raising events etc, depending on the script.
|
||||
* Each dialogue instance has its own state, so you can invoke the same Script multiple times as different dialogues if you want.
|
||||
* Each dialogue maintains its own internal state, which includes a set of variables.
|
||||
* Dialogues can have Participants, which are objects closely involved in the dialogue and which have the best access to
|
||||
* supply and retrieve variables and get events first. Other objects can simply listen to the exposed events; while they
|
||||
* can manipulate dialogue state too, they have less controllable access in terms of *when* this happens. It's best to
|
||||
* have at least one Participant driving state on the dialogue (relaying it to external objects), and to have read-only
|
||||
* users like UIs use the event delegates instead.
|
||||
* Dialogues need to be owned by an object, mainly for garbage collection. It's recommended that you set the owner to
|
||||
* one of the NPCs in the dialogue.
|
||||
* You can save/restore the state of a dialogue via GetSavedState/RestoreSavedState.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSDialogue : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
/// Event raised when dialogue progresses and a new speaker line, potentially with new choices, is ready to be displayed
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueSpeakerLine OnSpeakerLine;
|
||||
/// Event raised when a choice is made in the dialogue by the player. At this point, the dialogue has not progressed
|
||||
/// as a result of that choice so the index passed can be used to reference the choice
|
||||
/// This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueChoice OnChoice;
|
||||
/// Event raised when the dialog is about to proceed away from the current speaker line (because of a choice or continue)
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueProceeding OnProceeding;
|
||||
/// Event raised when an event is sent from the dialogue script. Any listeners or participants can process the event.
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueEvent OnEvent;
|
||||
/// Event raised when a variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableChangedEvent OnVariableChanged;
|
||||
/// Event raised when a variable is requested by the dialogue script. You can use this hook to set variables in the
|
||||
/// dialogue on-demand rather than up-front; anything set during this hook will be immediately used by the dialogue
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnVariableRequestedEvent OnVariableRequested;
|
||||
/// Event raised when the dialogue is starting, before the first speaker line
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueStarting OnStarting;
|
||||
/// Event raised when the dialogue finishes
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnDialogueFinished OnFinished;
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScript> BaseScript;
|
||||
UPROPERTY()
|
||||
TObjectPtr<USUDSScriptNodeText> CurrentSpeakerNode;
|
||||
UPROPERTY()
|
||||
TObjectPtr<const USUDSScriptNode> CurrentRootChoiceNode;
|
||||
|
||||
/// External objects which want to closely participate in the dialogue (not just listen to events)
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UObject>> Participants;
|
||||
|
||||
|
||||
/// All of the dialogue variables
|
||||
/// Dialogue variable state is all held locally. Dialogue participants can retrieve or set values in state.
|
||||
/// All state is saved with the dialogue. Variables can be used as text substitution parameters, conditionals,
|
||||
/// or communication with external state.
|
||||
typedef TMap<FName, FSUDSValue> FSUDSValueMap;
|
||||
FSUDSValueMap VariableState;
|
||||
|
||||
/// Stack of Gosub nodes to return to
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<USUDSScriptNodeGosub>> GosubReturnStack;
|
||||
|
||||
/// Set of all the TextIDs of choices taken already in this dialogue
|
||||
TSet<FString> ChoicesTaken;
|
||||
|
||||
TSet<FName> CurrentRequestedParamNames;
|
||||
bool bParamNamesExtracted;
|
||||
|
||||
/// Cached derived info
|
||||
mutable FText CurrentSpeakerDisplayName;
|
||||
/// All valid choices
|
||||
TArray<FSUDSScriptEdge> CurrentChoices;
|
||||
int CurrentSourceLineNo;
|
||||
static const FText DummyText;
|
||||
static const FString DummyString;
|
||||
|
||||
void InitVariables();
|
||||
void RunUntilNextSpeakerNodeOrEnd(USUDSScriptNode* FromNode, bool bRaiseAtEnd);
|
||||
const USUDSScriptNode* WalkToNextChoiceNode(USUDSScriptNode* FromNode, bool bExecute);
|
||||
USUDSScriptNode* RecurseWalkToNextChoiceOrTextNode(USUDSScriptNode* Node, bool bExecute, TArray<TObjectPtr<USUDSScriptNodeGosub>>& LocalGosubStack);
|
||||
const USUDSScriptNode* RunUntilNextChoiceNode(USUDSScriptNode* FromTextNode);
|
||||
const USUDSScriptNode* FindNextChoiceNode(USUDSScriptNode* FromNode);
|
||||
void SetCurrentSpeakerNode(USUDSScriptNodeText* Node, bool bQuietly);
|
||||
void SortParticipants();
|
||||
void RaiseStarting(FName StartLabel);
|
||||
void RaiseFinished();
|
||||
void RaiseNewSpeakerLine();
|
||||
void RaiseChoiceMade(int Index, int LineNo);
|
||||
void RaiseProceeding();
|
||||
void RaiseVariableChange(const FName& VarName, const FSUDSValue& Value, bool bFromScript, int LineNo);
|
||||
void RaiseVariableRequested(const FName& VarName, int LineNo);
|
||||
void RaiseExpressionVariablesRequested(const FSUDSExpression& Expression, int LineNo);
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const;
|
||||
|
||||
USUDSScriptNode* GetNextNode(USUDSScriptNode* Node);
|
||||
bool IsChoiceOrTextNode(ESUDSScriptNodeType Type);
|
||||
USUDSScriptNode* RunNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSelectNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunSetVariableNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunEventNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunGosubNode(USUDSScriptNode* Node);
|
||||
USUDSScriptNode* RunReturnNode(USUDSScriptNode* Node);
|
||||
void UpdateChoices();
|
||||
void RecurseAppendChoices(const USUDSScriptNode* Node, TArray<FSUDSScriptEdge>& OutChoices);
|
||||
USoundBase* GetSoundForCurrentLine(bool bAllowAnyTarget) const;
|
||||
UDialogueVoice* GetTargetVoice() const;
|
||||
class USoundConcurrency* GetVoiceSoundConcurrency() const;
|
||||
|
||||
FText ResolveParameterisedText(const TArray<FName> Params, const FTextFormat& TextFormat, int LineNo);
|
||||
void GetTextFormatArgs(const TArray<FName>& ArgNames, FFormatNamedArguments& OutArgs) const;
|
||||
bool CurrentNodeHasChoices() const;
|
||||
void SetVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetVariable(Name);
|
||||
if (!IsVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
VariableState.Add(Name, Value);
|
||||
RaiseVariableChange(Name, Value, bFromScript, LineNo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
USUDSDialogue();
|
||||
// virtual ~USUDSDialogue() override
|
||||
// {
|
||||
// UE_LOG(LogTemp, Warning, TEXT("*********** Destroyed Dialogue!"));
|
||||
// }
|
||||
void Initialise(const USUDSScript* Script);
|
||||
|
||||
/// Get the script asset this dialogue is based on
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const USUDSScript* GetScript() const { return BaseScript; }
|
||||
|
||||
/**
|
||||
* Begin the dialogue. Make sure you've added all participants before calling this.
|
||||
* This may not be the first time you've started this dialogue. All previous state is maintained to enable you
|
||||
* for example to take branching paths based on whether you've spoken to this character before.
|
||||
* If you want to reset *all* state, call Restart(true). However this is an extreme case; if you want to just
|
||||
* reset some variables then use the header section of the script to set variables to a default starting point.
|
||||
* @param Label The start point for this dialogue. If None, starts from the beginning.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Start(FName Label = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Add a participant to this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param Participant The participant object, which must implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void AddParticipant(UObject* Participant);
|
||||
|
||||
/// Retrieve participants from this dialogue
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<UObject*>& GetParticipants() const { return ObjectPtrDecay(Participants); }
|
||||
|
||||
/**
|
||||
* Set the complete list of participants for this dialogue instance.
|
||||
* Participants are objects which want to be more closely involved in the dialogue. As opposed to event listeners,
|
||||
* participants get advance notice of events in the dialogue, and are also called in a known order, determined by
|
||||
* their priority. If you're providing variables to the dialogue, it is best to do it as a participant since it
|
||||
* gives you much more control.
|
||||
* @param NewParticipants List of new participants. Each should implement ISUDSParticipant
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetParticipants(const TArray<UObject*>& NewParticipants);
|
||||
|
||||
|
||||
/// Get the speech text for the current dialogue node
|
||||
/// Any parameters required will be requested from participants in the dialogue and replaced
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetText();
|
||||
|
||||
/// Get the DialogueWave associated with the current dialogue node
|
||||
/// Returns null if there is no wave for this line.
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueWave* GetWave() const;
|
||||
|
||||
/// Return whether the current dialogue node has a Dialogue Wave associated with it
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsCurrentLineVoiced() const;
|
||||
|
||||
/// Get the ID of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
const FString& GetSpeakerID() const;
|
||||
|
||||
/// Get the display name of the current speaker
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
FText GetSpeakerDisplayName() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the current speaker, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetSpeakerVoice() const;
|
||||
|
||||
/// Get the Dialogue Voice belonging to the named participant, if voiced (Null otherwise)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
UDialogueVoice* GetVoice(FString Name) const;
|
||||
|
||||
/** If the current line is voiced, plays it in 2D.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, plays it at the given location.
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
void PlayVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it in 2D. Use this if you want to control the sound while it's playing.
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "2", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLine2D(float VolumeMultiplier = 1.f, float PitchMultiplier = 1.f, bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, spawn a sound for it at the given location. Unlike PlayVoicedLineAtLocation you can
|
||||
* attach this sound to a moving object if you want
|
||||
* @param Location World position to play dialogue at
|
||||
* @param Rotation World rotation to play dialogue at
|
||||
* @param VolumeMultiplier A linear scalar multiplied with the volume, in order to make the sound louder or softer.
|
||||
* @param PitchMultiplier A linear scalar multiplied with the pitch.
|
||||
* @param AttenuationSettings Override attenuation settings package to play sound with
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
UAudioComponent* SpawnVoicedLineAtLocation(FVector Location,
|
||||
FRotator Rotation,
|
||||
float VolumeMultiplier = 1.f,
|
||||
float PitchMultiplier = 1.f,
|
||||
USoundAttenuation* AttenuationSettings = nullptr,
|
||||
bool bLooselyMatchTarget = true);
|
||||
|
||||
/** If the current line is voiced, get the sound which would be played for it.
|
||||
* @param bLooselyMatchTarget When finding the sound, don't require the target DialogueVoice to match precisely (recommended)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue", meta=(AdvancedDisplay = "4", UnsafeDuringActorConstruction = "true", Keywords = "play"))
|
||||
USoundBase* GetVoicedLineSound(bool bLooselyMatchTarget = true);
|
||||
|
||||
/**
|
||||
* Get the number of choices available from this node.
|
||||
* Note, this will return 1 in the case of just linear text progression. The difference between just linked text
|
||||
* lines and a choice with only 1 option is whether the choice text is blank or not.
|
||||
* See also IsSimpleContinue()
|
||||
* @return The number of choices available
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
int GetNumberOfChoices() const;
|
||||
|
||||
/**
|
||||
* Return whether to progress from here is a simple continue (no choices, no text), meaning you probably want
|
||||
* to display a simpler prompt to the player.
|
||||
* This will return false even if there's only one choice, if that choice has text associated with it.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsSimpleContinue() const;
|
||||
|
||||
/**
|
||||
* Get the text associated with a choice.
|
||||
* @param Index The index of the choice
|
||||
* @return The text. This may be blank if this represents just a link between 2 nodes and not a choice at all.
|
||||
* Note that if you want to have only 1 choice but with associated text, this is fine and should be a choice
|
||||
* line just like any other.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetChoiceText(int Index);
|
||||
|
||||
/// Get all the current choices available, if you prefer this format
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TArray<FSUDSScriptEdge>& GetChoices() const;
|
||||
|
||||
/** Returns whether the choice at the given index has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceIndexBeenTakenPreviously(int Index);
|
||||
|
||||
/** Returns whether a choice has been taken previously.
|
||||
* This is saved in dialogue state so will be remembered across save/restore.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool HasChoiceBeenTakenPreviously(const FSUDSScriptEdge& Choice);
|
||||
|
||||
|
||||
/**
|
||||
* Continues the dialogue if (and ONLY if) there is only one valid path/choice out of the current node.
|
||||
* @return True if the dialogue continues after this, false if the dialogue is now at an end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Continue();
|
||||
|
||||
/**
|
||||
* Picks one of the available choices
|
||||
* If there's only 1 you can still call this with Index = 0, but also see Continue
|
||||
* @param Index The index of the choice to make
|
||||
* @return True if the dialogue continues, false if it has now reached the end.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool Choose(int Index);
|
||||
|
||||
/// Returns true if the dialogue has reached the end
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsEnded() const;
|
||||
|
||||
/// Returns whether the current speaker line is the last line of dialogue, i.e. there are no
|
||||
/// further choices and the next continue will end the dialogue. This allows you to anticipate
|
||||
/// the end of dialogue (IsEnded() will still return false until the last continue is taken)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS|Dialogue")
|
||||
bool IsFinalLine() const;
|
||||
|
||||
|
||||
/// End the dialogue early
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void End(bool bQuietly);
|
||||
|
||||
/// Get the source line number of the current position of the dialogue (returns 0 if not applicable)
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetCurrentSourceLine() const;
|
||||
|
||||
|
||||
/**
|
||||
* Restart the dialogue, either from the start or from a named label.
|
||||
* @param bResetState Whether to reset ALL dialogue state, as if the dialogue had been created anew. You mostly don't want
|
||||
* to do this; if you have certain things you want to reset every time, then use [set] commands in the header section
|
||||
* which runs every time the dialogue starts.
|
||||
* @param StartLabel Label to start running from; if None start from the beginning.
|
||||
* @param bReRunHeader If true (default), re-runs the header nodes before starting. Header nodes let you initialise
|
||||
* state that should always be reset when the dialogue is restarted
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void Restart(bool bResetState = false, FName StartLabel = NAME_None, bool bReRunHeader = true);
|
||||
|
||||
/**
|
||||
* Reset the state of this dialogue.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
* @param bResetPosition If true, resets the current position in the dialogue (which speaker line is next)
|
||||
* @param bResetVisited If true, resets the memory of which choices have been made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void ResetState(bool bResetVariables = true, bool bResetPosition = true, bool bResetVisited = true);
|
||||
|
||||
/** Retrieve a copy of the state of this dialogue.
|
||||
* This is useful for saving the state of this dialogue.
|
||||
* @return A static copy of the current state of this dialogue. This struct can be serialised with your save data,
|
||||
* and contains both the state of variables and the current speaking node ID.
|
||||
* @note If you save/load mid-dialogue then you're need to have written Text ID's into the source text to ensure they
|
||||
* stay the same between edits, as you do for localisation. If you only save/load after dialogue has ended then
|
||||
* you don't need to worry about this since the dialogue will always start from the beginning
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSDialogueState GetSavedState() const;
|
||||
|
||||
/** Restore the saved state of this dialogue.
|
||||
* This is useful for restoring the state of this dialogue. It will attempt to restore both the value of variables,
|
||||
* and the current speaking node in the dialogue. If you expect to be able to restore to a point mid-dialogue,
|
||||
* it's important that Text IDs are defined in your source file (as for localisation) since that's used as the
|
||||
* identifier of the current speaking node. If you only save/load after dialogue has ended then you don't need
|
||||
* to worry about this as dialogue will restart each time.
|
||||
* @param State Dialogue state that you previously retrieved from GetSavedState().
|
||||
* @note After restoring, you'll want to either call Start() or Continue(), depending on whether you restored
|
||||
* mid-dialogue or not (see IsEnded() to tell whether you did)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedState(const FSUDSDialogueState& State);
|
||||
|
||||
/// Get the set of text parameters that are actually being asked for in the current state of the dialogue.
|
||||
/// This will include parameters in the text, and parameters in any current choices being displayed.
|
||||
/// Use this if you want to be more specific about what parameters you supply when ISUDSParticipant::UpdateDialogueParameters
|
||||
/// is called.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TSet<FName> GetParametersInUse();
|
||||
|
||||
|
||||
/// Set a variable in dialogue state
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetVariableText, SetVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = VariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool IsVariableSet(FName Name) const
|
||||
{
|
||||
return VariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
const TMap<FName, FSUDSValue>& GetVariables() const { return VariableState; }
|
||||
|
||||
/**
|
||||
* Set a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FText GetVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a dialogue variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
int GetVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
float GetVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
ETextGender GetVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
bool GetVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void SetVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name dialogue variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FName GetVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void UnSetVariable(FName Name);
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetSpeakerLineUserMetadata(FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for the current speaker line.
|
||||
* User metadata is assigned by adding a special comment before a speaker line, and can be used
|
||||
* for anything where you need additional data associated with a particular line (as opposed to
|
||||
* setting a variable or sending an event).
|
||||
* @return All key/value user metadata for the current speaker line
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllSpeakerLineUserMetadata() const;
|
||||
|
||||
/**
|
||||
* Get a piece of user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @param Key The metadata key
|
||||
* @return The current metadata value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
FSUDSValue GetChoiceUserMetadata(int Index, FName Key) const;
|
||||
|
||||
/**
|
||||
* Get all user-specified metadata for a choice in the current list of choices.
|
||||
* User metadata is assigned by adding a special comment before a choice line, and can be used
|
||||
* for anything where you need additional data associated with a particular choice, such as RPG
|
||||
* stat requirements.
|
||||
* @param Index The choice index
|
||||
* @return All key/value user metadata for the choice
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
TMap<FName, FSUDSValue> GetAllChoiceUserMetadata(int Index) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
FOnDialogueSpeakerLineInternal InternalOnSpeakerLine;
|
||||
FOnDialogueChoiceInternal InternalOnChoice;
|
||||
FOnDialogueProceedingInternal InternalOnProceeding;
|
||||
FOnDialogueEventInternal InternalOnEvent;
|
||||
FOnDialogueVarChangedByScriptInternal InternalOnSetVar;
|
||||
FOnDialogueVarChangedByCodeInternal InternalOnSetVarByCode;
|
||||
FOnDialogueSelectEval InternalOnSelectEval;
|
||||
FOnDialogueStartingInternal InternalOnStarting;
|
||||
FOnDialogueFinishedInternal InternalOnFinished;
|
||||
#endif
|
||||
};
|
||||
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
234
Plugins/SUDS/Source/SUDS/Public/SUDSExpression.h
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSExpression.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSExpressionItemType : uint8
|
||||
{
|
||||
Null = 0 UMETA(Hidden),
|
||||
// Operators (must be 0-127, in order of precedence, highest first - gaps left in case we need them)
|
||||
Not = 4,
|
||||
Multiply = 10,
|
||||
Divide = 11,
|
||||
Modulo = 12,
|
||||
Add = 20,
|
||||
Subtract = 21,
|
||||
Less = 30,
|
||||
LessEqual = 31,
|
||||
Greater = 32,
|
||||
GreaterEqual = 33,
|
||||
Equal = 34,
|
||||
NotEqual = 35,
|
||||
And = 40,
|
||||
Or = 41,
|
||||
|
||||
LParens = 100,
|
||||
RParens = 101,
|
||||
|
||||
// Operands (must be 128+)
|
||||
Operand = 128
|
||||
|
||||
};
|
||||
|
||||
/// An item in an expression queue, can be operator or operand
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpressionItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
ESUDSExpressionItemType Type;
|
||||
|
||||
// Value if an operand node
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FSUDSValue OperandValue;
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpressionItem() : Type(ESUDSExpressionItemType::Operand) {}
|
||||
FSUDSExpressionItem(ESUDSExpressionItemType Operator) : Type(Operator) {}
|
||||
|
||||
FSUDSExpressionItem(const FSUDSValue& LiteralOrVariable)
|
||||
: Type(ESUDSExpressionItemType::Operand),
|
||||
OperandValue(LiteralOrVariable)
|
||||
{
|
||||
}
|
||||
|
||||
ESUDSExpressionItemType GetType() const { return Type; }
|
||||
// Only valid if optype is operand
|
||||
const FSUDSValue& GetOperandValue() const { return OperandValue; }
|
||||
void SetOperandValue(const FSUDSValue& NewVal) { OperandValue = NewVal; }
|
||||
|
||||
bool IsOperator() const { return static_cast<uint8>(Type) < 128; }
|
||||
bool IsOperand() const { return !IsOperator(); }
|
||||
bool IsBinaryOperator() const
|
||||
{
|
||||
// Only not is unary right now
|
||||
return Type != ESUDSExpressionItemType::Not;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/// An expression holds an executable expression, whether it's a simple single literal
|
||||
/// or a compound expression with variables
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSExpression
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// The output queue in Reverse Polish Notation order
|
||||
UPROPERTY()
|
||||
TArray<FSUDSExpressionItem> Queue;
|
||||
|
||||
/// Whether the tree is valid to execute
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
bool bIsValid;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FName> VariableNames;
|
||||
|
||||
|
||||
/// The original string version of the expression, for reference
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS|Expression")
|
||||
FString SourceString;
|
||||
|
||||
FSUDSExpressionItem EvaluateOperator(ESUDSExpressionItemType Op,
|
||||
const FSUDSExpressionItem& Arg1,
|
||||
const FSUDSExpressionItem& Arg2,
|
||||
const TMap<FName, FSUDSValue>& Variables,
|
||||
const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
FSUDSValue EvaluateOperand(const FSUDSValue& Operand, const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
bool Validate();
|
||||
|
||||
public:
|
||||
|
||||
FSUDSExpression() : bIsValid(true) {}
|
||||
|
||||
/// Initialise an expression tree just with a single literal or variable
|
||||
FSUDSExpression(const FSUDSValue& LiteralOrVariable)
|
||||
{
|
||||
Queue.Add(FSUDSExpressionItem(LiteralOrVariable));
|
||||
if (LiteralOrVariable.IsVariable())
|
||||
VariableNames.Add(LiteralOrVariable.GetVariableNameValue());
|
||||
bIsValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse an expression from a string
|
||||
* @param Expression The string to parse
|
||||
* @param OutParseError If there are any errors, pointer to a string to complete with the details
|
||||
* @return Whether the parsing was successful
|
||||
*/
|
||||
bool ParseFromString(const FString& Expression, FString* OutParseError);
|
||||
|
||||
/// Reset the expression to return true
|
||||
void Reset();
|
||||
|
||||
|
||||
/// Evaluate the expression and return the result, using a given variable state
|
||||
FSUDSValue Evaluate(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables) const;
|
||||
|
||||
/// Evaluate the expression and return the result as a boolean, using a given variable state
|
||||
bool EvaluateBoolean(const TMap<FName, FSUDSValue>& Variables, const TMap<FName, FSUDSValue>& GlobalVariables, const FString& ErrorContext) const;
|
||||
|
||||
/// Get the original source of the expression as a string
|
||||
const FString& GetSourceString() const { return SourceString; }
|
||||
|
||||
/// Whether this expression can be run (or is empty)
|
||||
bool IsValid() const { return bIsValid; }
|
||||
|
||||
/// Whether this expression is blank
|
||||
bool IsEmpty() const { return Queue.IsEmpty(); }
|
||||
|
||||
/// Get the list of variables this expression needs
|
||||
const TArray<FName>& GetVariableNames() const { return VariableNames; }
|
||||
|
||||
/// Return whether this expression is a generated random condition
|
||||
bool IsRandomCondition() const;
|
||||
|
||||
/**
|
||||
* Attempt to parse an operand from a string. Returns true if this string is a valid operand, which means a literal
|
||||
* (int, float, quoted string, boolean, gender), or a variable reference ({VariableName})
|
||||
* @param ValueStr The string to parse
|
||||
* @param OutVal The operand value which will be populated if successful
|
||||
* @return True if successful, false if not
|
||||
*/
|
||||
static bool ParseOperand(const FString& ValueStr, FSUDSValue& OutVal);
|
||||
|
||||
// Attempt to parse an operator from an incoming string
|
||||
static ESUDSExpressionItemType ParseOperator(const FString& OpStr);
|
||||
|
||||
/// Access the internal RPN execution queue
|
||||
const TArray<FSUDSExpressionItem>& GetQueue() { return Queue; }
|
||||
|
||||
/// Return whether this is a single literal
|
||||
bool IsLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() != ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
/// Helper method to get literal values
|
||||
FSUDSValue GetLiteralValue() const
|
||||
{
|
||||
check(IsLiteral());
|
||||
return Queue[0].GetOperandValue();
|
||||
}
|
||||
|
||||
/// Return whenter this is a text literal
|
||||
bool IsTextLiteral() const
|
||||
{
|
||||
return bIsValid && Queue.Num() == 1 && Queue[0].IsOperand() && Queue[0].GetOperandValue().GetType() == ESUDSValueType::Text;
|
||||
}
|
||||
|
||||
/// Helper method to get a text literal value, for easier localisation
|
||||
FText GetTextLiteralValue() const
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
return GetLiteralValue().GetTextValue();
|
||||
}
|
||||
/// Helper method to override a text literal
|
||||
void SetTextLiteralValue(const FText& NewLiteral)
|
||||
{
|
||||
check(IsTextLiteral());
|
||||
Queue[0].SetOperandValue(NewLiteral);
|
||||
}
|
||||
|
||||
/// Helper method to get boolean literal value
|
||||
bool GetBooleanLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Boolean);
|
||||
return GetLiteralValue().GetBooleanValue();
|
||||
}
|
||||
/// Helper method to get int literal value
|
||||
int GetIntLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Int);
|
||||
return GetLiteralValue().GetIntValue();
|
||||
}
|
||||
/// Helper method to get float literal value
|
||||
float GetFloatLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Float);
|
||||
return GetLiteralValue().GetFloatValue();
|
||||
}
|
||||
/// Helper method to get gender literal value
|
||||
ETextGender GetGenderLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Gender);
|
||||
return GetLiteralValue().GetGenderValue();
|
||||
}
|
||||
/// Helper method to get name literal value
|
||||
FName GetNameLiteralValue() const
|
||||
{
|
||||
check(IsLiteral() && GetLiteralValue().GetType() == ESUDSValueType::Name);
|
||||
return GetLiteralValue().GetNameValue();
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
145
Plugins/SUDS/Source/SUDS/Public/SUDSLibrary.h
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "SUDSLibrary.generated.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSDialogue;
|
||||
UCLASS()
|
||||
class SUDS_API USUDSLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with no participants.
|
||||
* You should subsequently call "SetParticipants" or "AddParticipant" on the returned dialogue if you expect any
|
||||
* parameters or speaker names to work.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogue(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with an initial set of participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participants List of participants, each of which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipants(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
const TArray<UObject*>& Participants,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
|
||||
/**
|
||||
* Create a dialogue instance based on a script, with a single participants.
|
||||
* @param Owner The owner of this instance. Can be any object but determines the lifespan of this dialogue,
|
||||
* could make sense to make the owner the NPC you're talking to for example.
|
||||
* @param Script The script to base this dialogue on
|
||||
* @param Participant The participant, which must implement the ISUDSParticipant interface to be used.
|
||||
* Participants are objects that want to be closely involved in the dialogue to provide variables and receive all events.
|
||||
* Other objects can subscribe to events separately but do not have as much control.
|
||||
* @param bStartImmediately Whether to call Start() on the dialogue automatically before returning
|
||||
* @param StartLabel If set to start immediately, which label to start from (None means start from the beginning)
|
||||
* @return The dialogue instance.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static USUDSDialogue* CreateDialogueWithParticipant(UObject* Owner,
|
||||
USUDSScript* Script,
|
||||
UObject* Participant,
|
||||
bool bStartImmediately = false,
|
||||
FName StartLabel = NAME_None);
|
||||
|
||||
/**
|
||||
* Try to extract a text value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param TextValue The text value
|
||||
* @return True if the value was of type text and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsText(const FSUDSValue& Value, FText& TextValue);
|
||||
/**
|
||||
* Try to extract a boolean value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param BoolValue The boolean value
|
||||
* @return True if the value was of type boolean and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsBoolean(const FSUDSValue& Value, bool& BoolValue);
|
||||
/**
|
||||
* Try to extract an integer value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param IntValue The integer value
|
||||
* @return True if the value was of type integer and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsInt(const FSUDSValue& Value, int& IntValue);
|
||||
/**
|
||||
* Try to extract a float value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param FloatValue The float value
|
||||
* @return True if the value was of type float and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsFloat(const FSUDSValue& Value, float& FloatValue);
|
||||
/**
|
||||
* Try to extract a gender value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param GenderValue The gender value
|
||||
* @return True if the value was of type gender and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsGender(const FSUDSValue& Value, ETextGender& GenderValue);
|
||||
/**
|
||||
* Try to extract a Name value from a general SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @param NameValue The Name value
|
||||
* @return True if the value was of type Name and extracted correctly. False if not.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static UPARAM(DisplayName="Success") bool GetDialogueValueAsName(const FSUDSValue& Value, FName& NameValue);
|
||||
|
||||
/** Retrieve the type of a SUDS value.
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return The value type
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static ESUDSValueType GetDialogueValueType(const FSUDSValue& Value);
|
||||
|
||||
/** Determine whether a SUDS value is empty (uninitialised).
|
||||
* @param Value The SUDS value, which may contain many types of value
|
||||
* @return Whether the value is empty
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool GetDialogueValueIsEmpty(const FSUDSValue& Value);
|
||||
|
||||
/**
|
||||
* Determine whether a SUDS variable name refers to a global variable
|
||||
* @param Name The full name of the variable
|
||||
* @param OutName The name of the variable, trimmed if necessary to remove a global prefix
|
||||
* @return Whether the variable was global
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
static bool IsDialogueVariableGlobal(const FName& Name, UPARAM(ref) FName& OutName);
|
||||
};
|
||||
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
129
Plugins/SUDS/Source/SUDS/Public/SUDSParticipant.h
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "SUDSParticipant.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
UINTERFACE(MinimalAPI)
|
||||
class USUDSParticipant : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to be implemented by participant objects in a given dialogue.
|
||||
* A participant is simply any object which wants to be closely involved in supplying data to, or retrieving data from,
|
||||
* the dialogue. Although you could do this simply by subscribing to the delegate events on a dialogue, the advantage
|
||||
* of making a participant is that you have better control over the ordering of multiple participants, in case for example
|
||||
* there's some common variable that they both want to set.
|
||||
* It's also a clearer interface to look for vs ad-hoc delegate hooks.
|
||||
* Generally we recommend that:
|
||||
* - Objects providing data to the dialogue should implement ISUDSParticipant
|
||||
* - Anything that just wants to observe the dialogue (like a UI) should just listen to events
|
||||
*
|
||||
* Participants are *guaranteed* to be called earlier than delegates, which means you can set variables from the
|
||||
* Participant callback and when the UI delegate reads text back, all substitution variables will be up to date.
|
||||
*
|
||||
*/
|
||||
class SUDS_API ISUDSParticipant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Called when a dialogue involving this participant is starting.
|
||||
* The implementation should probably set any starting variables referenced by the dialogue here (or you can do that
|
||||
* later during other functions). At this point there is no active speaker line, we're bootstrapping.
|
||||
* @param Dialogue The dialogue
|
||||
* @param AtLabel The label that the dialogue has started at (None if starting at the beginning)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName AtLabel);
|
||||
|
||||
/**
|
||||
* Called when a dialogue finishes.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a new speaker line, potentially with attached choices, has become active in the dialogue.
|
||||
* This participant can provide any variable updates if it needs to at this point.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue);
|
||||
|
||||
/**
|
||||
* Called when a choice is made by the player.
|
||||
* At this point, the dialogue has not progressed as a result of that choice, so the index passed can be used to
|
||||
* reference the choice.
|
||||
* This event is ONLY raised if there's a choice of paths, not for just continuing a linear path.
|
||||
* See OnDialogueProceeding for a more general callback.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
* @param ChoiceIndex The index of the choice that was made
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueChoiceMade(USUDSDialogue* Dialogue, int ChoiceIndex);
|
||||
|
||||
/**
|
||||
* Called just before proceeding with the dialogue from the current speaker line; just after either a choice is made by the player
|
||||
* or the dialogue is just prompted to proceed with its single path.
|
||||
* Participants will be called before any dialogue event listeners.
|
||||
* @param Dialogue The dialogue
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
|
||||
|
||||
/**
|
||||
* Called when an event is raised from dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param EventName The name of the event that has been raised
|
||||
* @param Arguments
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Arguments);
|
||||
|
||||
/**
|
||||
* Called when a variable changes value in the dialogue
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
* @param Value The new value
|
||||
* @param bFromScript True if the value changed because of a script line, false if it changed because of code calling SetVariable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableChanged(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& Value, bool bFromScript);
|
||||
|
||||
/**
|
||||
* Called when a variable value is requested by the dialogue script.
|
||||
* While you can set variables on the dialogue at any time and they're persistent, you can implement this method to
|
||||
* provide on-demand variable values (call SetVariable on the dialogue) if you want. This hook is called just before
|
||||
* the variables are used.
|
||||
* @param Dialogue The dialogue instance
|
||||
* @param VariableName The name of the variable which has changed value
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
void OnDialogueVariableRequested(USUDSDialogue* Dialogue, FName VariableName);
|
||||
|
||||
/**
|
||||
* Return the priority of this participant (default 0).
|
||||
* If for some reason you need to control the order multiple participants in a dialogue are called,
|
||||
* override this method; higher priority participants will be called *later* so that their variables etc override
|
||||
* previously set values.
|
||||
* @return Relative priority, default 0, higher numbers override lower ones.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category="SUDS")
|
||||
int GetDialogueParticipantPriority() const;
|
||||
|
||||
};
|
||||
|
||||
|
||||
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
114
Plugins/SUDS/Source/SUDS/Public/SUDSScript.h
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScript.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScriptNodeText;
|
||||
class USUDSScriptNodeGosub;
|
||||
/**
|
||||
* A single SUDS script asset.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScript : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Array of nodes (static after import)
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> Nodes;
|
||||
|
||||
/// Map of labels to nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TMap<FName, int> LabelList;
|
||||
|
||||
// Header equivalents for startup
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<TObjectPtr<USUDSScriptNode>> HeaderNodes;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TMap<FName, int> HeaderLabelList;
|
||||
|
||||
/// Array of all speaker IDs found in this script
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
TArray<FString> Speakers;
|
||||
|
||||
/// When using VO, Dialogue Voice assets are associated with speaker IDs
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TMap<FString, TObjectPtr<UDialogueVoice>> SpeakerVoices;
|
||||
|
||||
bool DoesAnyPathAfterLeadToChoice(USUDSScriptNode* FromNode);
|
||||
int RecurseLookForChoice(USUDSScriptNode* CurrNode);
|
||||
|
||||
public:
|
||||
void StartImport(TArray<TObjectPtr<USUDSScriptNode>>** Nodes,
|
||||
TArray<TObjectPtr<USUDSScriptNode>>** HeaderNodes,
|
||||
TMap<FName, int>** LabelList,
|
||||
TMap<FName, int>** ppHeaderLabelList,
|
||||
TArray<FString>** SpeakerList);
|
||||
void FinishImport();
|
||||
|
||||
const TArray<USUDSScriptNode*>& GetNodes() const { return ObjectPtrDecay(Nodes); }
|
||||
const TArray<USUDSScriptNode*>& GetHeaderNodes() const { return ObjectPtrDecay(HeaderNodes); }
|
||||
const TMap<FName, int>& GetLabelList() const { return LabelList; }
|
||||
const TMap<FName, int>& GetHeaderLabelList() const { return HeaderLabelList; }
|
||||
|
||||
|
||||
/// Get the first header node, if any (header nodes are run every time the script starts)
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetHeaderNode() const;
|
||||
|
||||
/// Get the first node of the script, if starting from the beginning
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="SUDS")
|
||||
USUDSScriptNode* GetFirstNode() const;
|
||||
|
||||
/// Get the next node after a given node, ONLY if there's only one way to go
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNextNode(const USUDSScriptNode* Node) const;
|
||||
|
||||
/// Get the first node of the script following a label, or null if the label wasn't found
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNode* GetNodeByLabel(const FName& Label) const;
|
||||
|
||||
/// Try to find a speaker node by its text ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeText* GetNodeByTextID(const FString& TextID) const;
|
||||
/// Try to find a gosub node by its gosub ID
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
USUDSScriptNodeGosub* GetNodeByGosubID(const FString& ID) const;
|
||||
|
||||
|
||||
/// Get the list of speakers
|
||||
const TArray<FString>& GetSpeakers() const { return Speakers; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS")
|
||||
UDialogueVoice* GetSpeakerVoice(const FString& SpeakerID) const;
|
||||
|
||||
/// Set up the speaker voice association
|
||||
void SetSpeakerVoice(const FString& SpeakerID, UDialogueVoice* Voice);
|
||||
const TMap<FString, UDialogueVoice*>& GetSpeakerVoices() const { return ObjectPtrDecay(SpeakerVoices); }
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
// Import data for this
|
||||
UPROPERTY(VisibleAnywhere, Instanced, Category=ImportSettings)
|
||||
TObjectPtr<class UAssetImportData> AssetImportData;
|
||||
|
||||
// UObject interface
|
||||
virtual void PostInitProperties() override;
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 4
|
||||
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
|
||||
#else
|
||||
virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag>& OutTags) const override;
|
||||
#endif
|
||||
virtual void Serialize(FArchive& Ar) override;
|
||||
// End of UObject interface
|
||||
#endif
|
||||
|
||||
};
|
||||
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
88
Plugins/SUDS/Source/SUDS/Public/SUDSScriptEdge.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptEdge.generated.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSEdgeType : uint8
|
||||
{
|
||||
/// A simple continuation; usually for sequences with no choices
|
||||
Continue,
|
||||
/// A decision made by the player from a list of choices
|
||||
Decision,
|
||||
/// A conditional path, taken automatically based on the situation
|
||||
Condition,
|
||||
/// An edge which forms a chain of nodes which are supposed to be considered together
|
||||
/// This links a text node to its choices, and also potentially compound choices underneath if there are selects
|
||||
Chained
|
||||
|
||||
};
|
||||
/**
|
||||
* Edge in the script graph. An edge leads to another node (unidirectional)
|
||||
* Edges can have conditions which mean whether they're valid or not, either as automatic
|
||||
* choices or player choices.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSScriptEdge
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Text, if a user choice. Always references a string table
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FText Text;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSEdgeType Type;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TWeakObjectPtr<USUDSScriptNode> TargetNode;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Condition;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
/// User metadata associated with this edge. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
FSUDSScriptEdge(): Type(ESUDSEdgeType::Continue), SourceLineNo(0)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSScriptEdge(USUDSScriptNode* ToNode, ESUDSEdgeType InType, int LineNo);
|
||||
|
||||
FSUDSScriptEdge(const FText& InText, USUDSScriptNode* ToNode, int LineNo);
|
||||
|
||||
FText GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
ESUDSEdgeType GetType() const { return Type; }
|
||||
TWeakObjectPtr<USUDSScriptNode> GetTargetNode() const { return TargetNode; }
|
||||
const FSUDSExpression& GetCondition() const { return Condition; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
|
||||
void SetText(const FText& Text);
|
||||
void SetType(ESUDSEdgeType InType) { Type = InType; }
|
||||
void SetTargetNode(const TWeakObjectPtr<USUDSScriptNode>& InTargetNode);
|
||||
void SetCondition(const FSUDSExpression& InCondition) { Condition = InCondition; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
};
|
||||
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
81
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNode.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptEdge.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSScriptNode.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSScriptNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state
|
||||
Select,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event,
|
||||
/// Gosub node
|
||||
Gosub,
|
||||
/// Return node
|
||||
Return,
|
||||
};
|
||||
/**
|
||||
* A node in the script graph.
|
||||
* Nodes are either text, or branch points (user choice or automatic branching logic)
|
||||
* Text nodes always lead to a single next step, be that another text node or a branch.
|
||||
* Branch nodes are separate from the text so that jumping can return to a choice point without emitting more text.
|
||||
* At runtime if a text node's next step is a user choice, it will be available immediately. Otherwise it's not
|
||||
* evaluated until the dialogue progresses.
|
||||
* Edges connect everything. Edges may have text if they're user choices (even if that's a single choice), and
|
||||
* may have conditions.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNode : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/// Type of node
|
||||
/// To make it easier to check rather than having to cast to subtypes blindly. And also not all types need a subtype
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
ESUDSScriptNodeType NodeType = ESUDSScriptNodeType::Text;
|
||||
/// Links to other nodes
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSScriptEdge> Edges;
|
||||
/// The line number in the script that this node came from
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
int SourceLineNo;
|
||||
|
||||
|
||||
public:
|
||||
USUDSScriptNode();
|
||||
|
||||
ESUDSScriptNodeType GetNodeType() const { return NodeType; }
|
||||
const TArray<FSUDSScriptEdge>& GetEdges() const { return Edges; }
|
||||
int GetSourceLineNo() const { return SourceLineNo; }
|
||||
|
||||
void AddEdge(const FSUDSScriptEdge& NewEdge);
|
||||
void InitChoice(int LineNo);
|
||||
void InitSelect(int LineNo);
|
||||
void InitReturn(int LineNo);
|
||||
|
||||
int GetEdgeCount() const { return Edges.Num(); }
|
||||
const FSUDSScriptEdge* GetEdge(int Index) const
|
||||
{
|
||||
if (Edges.IsValidIndex(Index))
|
||||
{
|
||||
return &Edges[Index];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// Determine if this node is a Select node that's representing a [random]
|
||||
bool IsRandomSelect() const;
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeEvent.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeEvent.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeEvent : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName EventName;
|
||||
|
||||
/// Literal arguments
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
TArray<FSUDSExpression> Args;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& EvtName, const TArray<FSUDSExpression>& InArgs, int LineNo);
|
||||
FName GetEventName() const { return EventName; }
|
||||
const TArray<FSUDSExpression>& GetArgs() const { return Args; }
|
||||
|
||||
|
||||
};
|
||||
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
49
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeGosub.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeGosub : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
/// Name of the label which we'll jump to before returning
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName LabelName;
|
||||
|
||||
/// Generated ID for use when saving state
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FString GosubID;
|
||||
|
||||
/// Convenience flag to let you know whether this node MAY HAVE any choices directly after it
|
||||
/// Internally this also lets us know to look for the next choice node after returning
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& Label, const FString ID, int LineNo)
|
||||
{
|
||||
NodeType = ESUDSScriptNodeType::Gosub;
|
||||
LabelName = FName(Label);
|
||||
GosubID = ID;
|
||||
SourceLineNo = LineNo;
|
||||
}
|
||||
FName GetLabelName() const { return LabelName; }
|
||||
const FString& GetGosubID() const { return GosubID; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
};
|
||||
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
33
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeSet.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeSet.generated.h"
|
||||
|
||||
/**
|
||||
* Set variable node
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSScriptNodeSet : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
// Variable identifier
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FName Identifier;
|
||||
|
||||
/// Expression to provide value to set
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
FSUDSExpression Expression;
|
||||
|
||||
public:
|
||||
|
||||
void Init(const FString& VarName, const FSUDSExpression& InExpression, int LineNo);
|
||||
const FName& GetIdentifier() const { return Identifier; }
|
||||
const FSUDSExpression& GetExpression() const { return Expression; }
|
||||
|
||||
};
|
||||
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
72
Plugins/SUDS/Source/SUDS/Public/SUDSScriptNodeText.h
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeText.generated.h"
|
||||
|
||||
class UDialogueWave;
|
||||
|
||||
/**
|
||||
* A node which contains speaker text
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SUDS_API USUDSScriptNodeText : public USUDSScriptNode
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
/// Identifier of the speaker for text nodes
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FString SpeakerID;
|
||||
/// Text, always references a string table. Parameters will not have been completed.
|
||||
/// Note: if you're using voiced dialogue, see the Wave property and its subtitle functionality
|
||||
UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly, Category="SUDS")
|
||||
FText Text;
|
||||
/// DialogueWave asset link for voiced dialogue
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category="SUDS")
|
||||
TObjectPtr<UDialogueWave> Wave;
|
||||
|
||||
/// Convenience flag to let you know whether this text node MAY HAVE choices attached
|
||||
/// If false, there's only one way to proceed from here and no text associated with that
|
||||
/// If true, either there can be > 1 choice options, or a single choice with associated text (this can be when
|
||||
/// you have no choice but want text rather than just a continue button)
|
||||
/// Internally this also lets us know to look for the next choice node
|
||||
/// It's possible that where there are conditionals ahead, there are only choices on some of the paths.
|
||||
/// This flag is to let us know to look for choices, but if conditionals apply we may not find any using actual dialogue state.
|
||||
UPROPERTY(BlueprintReadOnly, Category="SUDS")
|
||||
bool bHasChoices = false;
|
||||
|
||||
/// User metadata associated with this speaker line. May include derived expressions
|
||||
UPROPERTY()
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
mutable bool bFormatExtracted = false;
|
||||
mutable TArray<FName> ParameterNames;
|
||||
mutable FTextFormat TextFormat;
|
||||
|
||||
void ExtractFormat() const;
|
||||
|
||||
public:
|
||||
const FString& GetSpeakerID() const { return SpeakerID; }
|
||||
const FText& GetText() const { return Text; }
|
||||
FString GetTextID() const;
|
||||
UDialogueWave* GetWave() const { return Wave; }
|
||||
/// Whether on one select path or another a choice was found
|
||||
/// Doesn't help if within a Gosub as call site may be anywhere
|
||||
bool MayHaveChoices() const { return bHasChoices; }
|
||||
|
||||
void Init(const FString& SpeakerID, const FText& Text, int LineNo);
|
||||
void SetWave(UDialogueWave* InWave) { Wave = InWave; }
|
||||
const FTextFormat& GetTextFormat() const;
|
||||
const TArray<FName>& GetParameterNames() const;
|
||||
bool HasParameters() const;
|
||||
|
||||
void NotifyMayHaveChoices() { bHasChoices = true; }
|
||||
|
||||
const TMap<FName, FSUDSExpression>& GetUserMetadata() const { return UserMetadata; }
|
||||
void SetUserMetadata(const TMap<FName, FSUDSExpression>& Meta) { UserMetadata = Meta; }
|
||||
|
||||
|
||||
};
|
||||
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
292
Plugins/SUDS/Source/SUDS/Public/SUDSSubsystem.h
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "SUDSSubsystem.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
class USoundConcurrency;
|
||||
struct FSoundConcurrencySettings;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSSubsystem, Log, All);
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnGlobalVariableChangedEvent, FName, VariableName, const FSUDSValue&, Value, bool, bFromScript);
|
||||
|
||||
/// Copy of the global state of the system
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSUDSGlobalState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
UPROPERTY(BlueprintReadOnly, SaveGame, Category="SUDS")
|
||||
TMap<FName, FSUDSValue> GlobalVariables;
|
||||
|
||||
public:
|
||||
FSUDSGlobalState() {}
|
||||
|
||||
FSUDSGlobalState(const TMap<FName, FSUDSValue>& InGlobalVars) : GlobalVariables(InGlobalVars)
|
||||
{
|
||||
}
|
||||
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariables; }
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSGlobalState& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSGlobalState& Value);
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDS_API USUDSSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/// Event raised when a global variable is changed. "FromScript" is true if the variable was set by the script, false if set from code
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FOnGlobalVariableChangedEvent OnGlobalVariableChanged;
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
TObjectPtr<USoundConcurrency> VoiceConcurrency;
|
||||
|
||||
/// Global variable state
|
||||
TMap<FName, FSUDSValue> GlobalVariableState;
|
||||
|
||||
void SetGlobalVariableImpl(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo)
|
||||
{
|
||||
const FSUDSValue OldValue = GetGlobalVariable(Name);
|
||||
if (!IsGlobalVariableSet(Name) ||
|
||||
(OldValue != Value).GetBooleanValue())
|
||||
{
|
||||
GlobalVariableState.Add(Name, Value);
|
||||
OnGlobalVariableChanged.Broadcast(Name, Value, bFromScript);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Sets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped. If you would prefer that they overlap, set
|
||||
* this to >1
|
||||
* @param ConcurrentLines The number of voice lines that can be played at once.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
void SetMaxConcurrentVoicedLines(int ConcurrentLines);
|
||||
|
||||
/**
|
||||
* Gets the number of voiced lines that can be played at once. Defaults to 1, so that when a new voiced line is
|
||||
* played while another is still playing, the previous one is stopped.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Settings")
|
||||
int GetMaxConcurrentVoicedLines() const;
|
||||
|
||||
USoundConcurrency* GetVoicedLineConcurrency() const { return VoiceConcurrency; }
|
||||
|
||||
|
||||
/**
|
||||
* Reset the global state of the system.
|
||||
* @param bResetVariables If true, resets all variable state
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
void ResetGlobalState(bool bResetVariables = true);
|
||||
|
||||
/** Retrieve a copy of the global state of the system.
|
||||
* This is useful for saving the state of e.g. global variables.
|
||||
* @return A static copy of the current global state. This struct can be serialised with your save data,
|
||||
* and contains the state of global variables.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global State")
|
||||
FSUDSGlobalState GetSavedGlobalState() const;
|
||||
|
||||
/** Restore the global saved state.
|
||||
* This is useful for restoring global state eg global variables when you're loading a game.
|
||||
* @param State Global state that you previously retrieved from GetSavedGlobalState().
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Dialogue")
|
||||
void RestoreSavedGlobalState(const FSUDSGlobalState& State);
|
||||
|
||||
/// Set a global variable
|
||||
/// This is mostly only useful if you happen to already have a general purpose FSUDSValue.
|
||||
/// See SetGlobalVariableText, SetGlobalVariableInt etc for literal-friendly versions
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariable(FName Name, FSUDSValue Value)
|
||||
{
|
||||
SetGlobalVariableImpl(Name, Value, false, 0);
|
||||
}
|
||||
|
||||
/// Internal use only
|
||||
void InternalSetGlobalVariable(FName Name, const FSUDSValue& Value, bool bFromScript, int LineNo) { SetGlobalVariableImpl(Name, Value, bFromScript, LineNo); }
|
||||
|
||||
/// Get a variable in dialogue state as a general value type
|
||||
/// See GetDialogueText, GetDialogueInt etc for more type friendly versions, but if you want to access the state
|
||||
/// as a type-flexible value then you can do so with this function.
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FSUDSValue GetGlobalVariable(FName Name) const
|
||||
{
|
||||
if (const auto Arg = GlobalVariableState.Find(Name))
|
||||
{
|
||||
return *Arg;
|
||||
}
|
||||
return FSUDSValue();
|
||||
}
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool IsGlobalVariableSet(FName Name) const
|
||||
{
|
||||
return GlobalVariableState.Contains(Name);
|
||||
}
|
||||
|
||||
/// Get all variables
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
const TMap<FName, FSUDSValue>& GetGlobalVariables() const { return GlobalVariableState; }
|
||||
|
||||
/**
|
||||
* Set a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableText(FName Name, FText Value)
|
||||
{
|
||||
SetGlobalVariable(Name, Value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a text global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FText GetGlobalVariableText(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a global variable on the passed in parameters collection.
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableInt(FName Name, int32 Value);
|
||||
|
||||
/**
|
||||
* Get an int global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
int GetGlobalVariableInt(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableFloat(FName Name, float Value);
|
||||
|
||||
/**
|
||||
* Get a float global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
float GetGlobalVariableFloat(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableGender(FName Name, ETextGender Value);
|
||||
|
||||
/**
|
||||
* Get a gender global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
ETextGender GetGlobalVariableGender(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableBoolean(FName Name, bool Value);
|
||||
|
||||
/**
|
||||
* Get a boolean global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
bool GetGlobalVariableBoolean(FName Name) const;
|
||||
|
||||
/**
|
||||
* Set a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @param Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void SetGlobalVariableName(FName Name, FName Value);
|
||||
|
||||
/**
|
||||
* Get a name global variable
|
||||
* @param Name The name of the variable
|
||||
* @returns Value The value of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
FName GetGlobalVariableName(FName Name) const;
|
||||
|
||||
|
||||
/**
|
||||
* Remove the definition of a global variable.
|
||||
* This has much same effect as setting the variable back to the default value for this type, since attempting to
|
||||
* retrieve a missing variable result in a default value.
|
||||
* @param Name The name of the variable
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category="SUDS|Global Variables")
|
||||
void UnSetGlobalVariable(FName Name);
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
/// Only for use by tests / editor tools when real subsystem isn't running
|
||||
static TMap<FName, FSUDSValue> Test_DummyGlobalVariables;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
inline USUDSSubsystem* GetSUDSSubsystem(UWorld* WorldContext)
|
||||
{
|
||||
if (IsValid(WorldContext) && WorldContext->IsGameWorld())
|
||||
{
|
||||
auto GI = WorldContext->GetGameInstance();
|
||||
if (IsValid(GI))
|
||||
return GI->GetSubsystem<USUDSSubsystem>();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
416
Plugins/SUDS/Source/SUDS/Public/SUDSValue.h
Normal file
@@ -0,0 +1,416 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "SUDSCommon.h"
|
||||
#include "SUDSValue.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSValueType : uint8
|
||||
{
|
||||
Text = 0,
|
||||
Int = 1,
|
||||
Float = 2,
|
||||
Boolean = 3,
|
||||
Gender = 4,
|
||||
Name = 5,
|
||||
/// Access the value of another variable
|
||||
Variable = 10,
|
||||
|
||||
Empty = 99
|
||||
};
|
||||
/// Struct which can hold any of the value types that SUDS needs to use, in a Blueprint friendly manner
|
||||
/// For getting / setting these values from blueprints, see blueprint library functions SetSUDSValue<Type>() / GetSUDSValue<Type>()
|
||||
/// For convenience these are wrapped in USUDSDialogue but in e.g. event callbacks they're not
|
||||
USTRUCT(BlueprintType)
|
||||
struct SUDS_API FSUDSValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
protected:
|
||||
ESUDSValueType Type;
|
||||
union
|
||||
{
|
||||
int32 IntValue;
|
||||
float FloatValue;
|
||||
};
|
||||
TOptional<FText> TextValue;
|
||||
// Used for variables and name values
|
||||
TOptional<FName> Name;
|
||||
public:
|
||||
|
||||
FSUDSValue() : Type(ESUDSValueType::Empty), IntValue(0), TextValue(FText::GetEmpty()) {}
|
||||
|
||||
FSUDSValue(const int32 Value)
|
||||
: Type(ESUDSValueType::Int) { IntValue = Value; }
|
||||
|
||||
FSUDSValue(const float Value)
|
||||
: Type(ESUDSValueType::Float) { FloatValue = Value; }
|
||||
|
||||
FSUDSValue(const FText& Value)
|
||||
: Type(ESUDSValueType::Text),
|
||||
IntValue(0),
|
||||
TextValue(Value)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(FText&& Value)
|
||||
: Type(ESUDSValueType::Text), IntValue(0), TextValue(MoveTemp(Value))
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSValue(ETextGender Value)
|
||||
: Type(ESUDSValueType::Gender), IntValue(0)
|
||||
{
|
||||
IntValue = static_cast<int32>(Value);
|
||||
}
|
||||
|
||||
FSUDSValue(bool Value)
|
||||
: Type(ESUDSValueType::Boolean), IntValue(0)
|
||||
{
|
||||
IntValue = Value ? 1 : 0;
|
||||
}
|
||||
|
||||
FSUDSValue(const FName& ReferencedName, bool bIsVariable)
|
||||
: Type(bIsVariable ? ESUDSValueType::Variable : ESUDSValueType::Name),
|
||||
IntValue(0),
|
||||
Name(ReferencedName)
|
||||
{
|
||||
}
|
||||
|
||||
// Construct a default value of a given type
|
||||
explicit FSUDSValue(ESUDSValueType ValType)
|
||||
: Type(ValType), IntValue(0)
|
||||
{
|
||||
}
|
||||
|
||||
/// Whether this value is empty, i.e. hasn't been set to anything
|
||||
FORCEINLINE bool IsEmpty() const
|
||||
{
|
||||
return Type == ESUDSValueType::Empty;
|
||||
}
|
||||
|
||||
FORCEINLINE ESUDSValueType GetType() const
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 GetIntValue() const
|
||||
{
|
||||
// We don't warn for unset variables / uninitialised values, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Int && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as int but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue;
|
||||
}
|
||||
|
||||
FORCEINLINE float GetFloatValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && !IsNumeric() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Type == ESUDSValueType::Int)
|
||||
{
|
||||
// Allow int widening to float
|
||||
return GetIntValue();
|
||||
}
|
||||
return FloatValue;
|
||||
}
|
||||
|
||||
FORCEINLINE const FText& GetTextValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Text && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as text but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (TextValue.IsSet())
|
||||
return TextValue.GetValue();
|
||||
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
FORCEINLINE ETextGender GetGenderValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Gender && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as float but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return static_cast<ETextGender>(IntValue);
|
||||
}
|
||||
|
||||
FORCEINLINE bool GetBooleanValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Boolean && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as boolean but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
return IntValue != 0;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetNameValue() const
|
||||
{
|
||||
// We don't warn for unset variables, use the defaults
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Name && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as Name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE FName GetVariableNameValue() const
|
||||
{
|
||||
if (!IsEmpty() && Type != ESUDSValueType::Variable)
|
||||
UE_LOG(LogSUDS, Warning, TEXT("Getting value as variable name but was type %s"), *StaticEnum<ESUDSValueType>()->GetValueAsString(Type))
|
||||
|
||||
if (Name.IsSet())
|
||||
return Name.GetValue();
|
||||
|
||||
return NAME_None;
|
||||
}
|
||||
|
||||
FORCEINLINE bool IsVariable() const
|
||||
{
|
||||
return Type == ESUDSValueType::Variable;
|
||||
}
|
||||
|
||||
bool IsNumeric() const
|
||||
{
|
||||
return Type == ESUDSValueType::Float || Type == ESUDSValueType::Int;
|
||||
}
|
||||
|
||||
FFormatArgumentValue ToFormatArg() const
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
default:
|
||||
case ESUDSValueType::Text:
|
||||
return FFormatArgumentValue(TextValue.GetValue());
|
||||
case ESUDSValueType::Int:
|
||||
return FFormatArgumentValue(GetIntValue());
|
||||
case ESUDSValueType::Boolean:
|
||||
return FFormatArgumentValue(GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FFormatArgumentValue(GetGenderValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FFormatArgumentValue(GetFloatValue());
|
||||
case ESUDSValueType::Name:
|
||||
// return something useful from FName even though technically shouldn't use in visible text
|
||||
return FFormatArgumentValue(FText::FromName(GetNameValue()));
|
||||
case ESUDSValueType::Empty:
|
||||
case ESUDSValueType::Variable:
|
||||
return FFormatArgumentValue();
|
||||
}
|
||||
}
|
||||
|
||||
void SetValue(const FSUDSValue& RValue)
|
||||
{
|
||||
*this = RValue;
|
||||
}
|
||||
|
||||
/// Not operation, technically only valid on booleans, but not enforced so as to allow unset vars & conversions
|
||||
FSUDSValue operator!() const
|
||||
{
|
||||
return FSUDSValue(!GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator*(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() * Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
*
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator/(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() / Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
/
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator%(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() % Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
float lval = (Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue());
|
||||
float rval = (Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue());
|
||||
// Prevent NaN and errors reported when executing FMath::Fmod with invalid values.
|
||||
return FSUDSValue(rval != 0 ? FMath::Fmod(lval, rval) : 0.0f);
|
||||
}
|
||||
FSUDSValue operator+(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() + Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
+
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator-(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We don't force types to be numeric here so that we can safely use unset variables
|
||||
// If both int, keep int
|
||||
if (Type == ESUDSValueType::Int && Rhs.Type == Type)
|
||||
{
|
||||
return FSUDSValue(GetIntValue() - Rhs.GetIntValue());
|
||||
}
|
||||
// Otherwise we'll let the type widening handle mixed types for us (ternary will always resolve to float)
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
-
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
}
|
||||
FSUDSValue operator<(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// Don't check types here. We'll fall back on int comparisons which is important for cases where
|
||||
// a variable hasn't been set
|
||||
// result is boolean so no need to protect types
|
||||
return FSUDSValue(
|
||||
(Type == ESUDSValueType::Float ? GetFloatValue() : GetIntValue())
|
||||
<
|
||||
(Rhs.GetType() == ESUDSValueType::Float ? Rhs.GetFloatValue() : Rhs.GetIntValue()));
|
||||
|
||||
}
|
||||
FSUDSValue operator==(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if (IsNumeric() || Rhs.IsNumeric())
|
||||
{
|
||||
if (GetType() == ESUDSValueType::Float || Rhs.GetType() == ESUDSValueType::Float)
|
||||
{
|
||||
// For floats, use tolerance
|
||||
return FSUDSValue(FMath::IsNearlyEqual(
|
||||
GetType() == ESUDSValueType::Int ? (float)GetIntValue() : GetFloatValue(),
|
||||
Rhs.GetType() == ESUDSValueType::Int ? (float)Rhs.GetIntValue() : Rhs.GetFloatValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// no auto conversion here
|
||||
// however, tolerate unresolved variables, they will return initial values
|
||||
if (Type != Rhs.Type && Type != ESUDSValueType::Variable && Rhs.Type != ESUDSValueType::Variable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare using type from whichever one isn't a variable (get values on unset variables will return defaults)
|
||||
ESUDSValueType UseType = IsVariable() ? Rhs.GetType() : GetType();
|
||||
switch (UseType)
|
||||
{
|
||||
case ESUDSValueType::Text:
|
||||
return FSUDSValue(GetTextValue().EqualTo(Rhs.GetTextValue()));
|
||||
case ESUDSValueType::Boolean:
|
||||
return FSUDSValue(GetBooleanValue() == Rhs.GetBooleanValue());
|
||||
case ESUDSValueType::Gender:
|
||||
return FSUDSValue(GetGenderValue() == Rhs.GetGenderValue());
|
||||
case ESUDSValueType::Variable:
|
||||
return FSUDSValue(GetVariableNameValue() == Rhs.GetVariableNameValue());
|
||||
case ESUDSValueType::Name:
|
||||
return FSUDSValue(GetNameValue() == Rhs.GetNameValue());
|
||||
// deal with int/float again here, this mops up cases where one side is an unset variable
|
||||
case ESUDSValueType::Int:
|
||||
return FSUDSValue(GetIntValue() == Rhs.GetIntValue());
|
||||
case ESUDSValueType::Float:
|
||||
return FSUDSValue(GetFloatValue() == Rhs.GetFloatValue());
|
||||
default:
|
||||
break;
|
||||
};
|
||||
}
|
||||
return FSUDSValue(false);
|
||||
}
|
||||
FSUDSValue operator<=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
if ((*this < Rhs).GetBooleanValue())
|
||||
return FSUDSValue(true);
|
||||
return (*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator>(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs < *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator>=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return Rhs <= *this;
|
||||
}
|
||||
|
||||
FSUDSValue operator!=(const FSUDSValue& Rhs) const
|
||||
{
|
||||
return !(*this == Rhs);
|
||||
}
|
||||
|
||||
FSUDSValue operator&&(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() && Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
FSUDSValue operator||(const FSUDSValue& Rhs) const
|
||||
{
|
||||
// We always let unset variables degrade to false
|
||||
check(Type == ESUDSValueType::Boolean || Type == ESUDSValueType::Variable);
|
||||
check(Rhs.Type == ESUDSValueType::Boolean || Rhs.Type == ESUDSValueType::Variable);
|
||||
return FSUDSValue(GetBooleanValue() || Rhs.GetBooleanValue());
|
||||
}
|
||||
|
||||
SUDS_API friend FArchive& operator<<(FArchive& Ar, FSUDSValue& Value);
|
||||
SUDS_API friend void operator<<(FStructuredArchive::FSlot Slot, FSUDSValue& Value);
|
||||
bool Serialize(FArchive& Ar)
|
||||
{
|
||||
Ar << *this;
|
||||
return true;
|
||||
}
|
||||
bool Serialize(FStructuredArchive::FSlot Slot)
|
||||
{
|
||||
Slot << *this;
|
||||
return true;
|
||||
}
|
||||
|
||||
FString ToString() const;
|
||||
|
||||
bool ExportTextItem(FString& ValueStr, FSUDSValue const& DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope) const;
|
||||
};
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FSUDSValue> : public TStructOpsTypeTraitsBase2<FSUDSValue>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithSerializer = true,
|
||||
WithExportTextItem = true
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
53
Plugins/SUDS/Source/SUDS/SUDS.Build.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDS : ModuleRules
|
||||
{
|
||||
public SUDS(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
65
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditor.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditor.h"
|
||||
|
||||
#include "ISettingsModule.h"
|
||||
#include "ISettingsSection.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSScriptActions.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Styling/SlateStyle.h"
|
||||
#include "Styling/SlateStyleRegistry.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FSUDSModule"
|
||||
|
||||
void FSUDSEditorModule::StartupModule()
|
||||
{
|
||||
ScriptActions = MakeShared<FSUDSScriptActions>();
|
||||
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
auto SudsPlugin = IPluginManager::Get().FindPlugin(TEXT("SUDS"));
|
||||
if (SudsPlugin.IsValid())
|
||||
{
|
||||
const FString IconDir = SudsPlugin->GetBaseDir() + "/Content/Editor/Slate/Icons";
|
||||
const FVector2D Sz16 = FVector2D(16.0f, 16.0f);
|
||||
const FVector2D Sz64 = FVector2D(64.0f, 64.0f);
|
||||
|
||||
StyleSet = MakeShared<FSlateStyleSet>("SUDSStyleSet");
|
||||
StyleSet->Set(TEXT("ClassIcon.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_16x.png"), Sz16));
|
||||
StyleSet->Set(TEXT("ClassThumbnail.SUDSScript"), new FSlateImageBrush(IconDir / TEXT("SUDSScript_64x.png"), Sz64));
|
||||
|
||||
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
// register settings
|
||||
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
|
||||
if (SettingsModule)
|
||||
{
|
||||
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "SUDS Editor",
|
||||
LOCTEXT("SUDSEditorSettingsName", "SUDS Editor"),
|
||||
LOCTEXT("SUDSEditorSettingsDescription", "Configure the editor parts of SUDS."),
|
||||
GetMutableDefault<USUDSEditorSettings>()
|
||||
);
|
||||
}
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Started"))
|
||||
|
||||
}
|
||||
|
||||
void FSUDSEditorModule::ShutdownModule()
|
||||
{
|
||||
if (StyleSet.IsValid())
|
||||
{
|
||||
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
|
||||
}
|
||||
|
||||
if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
|
||||
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(ScriptActions.ToSharedRef());
|
||||
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("SUDS Editor Module Shut Down"))
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
IMPLEMENT_MODULE(FSUDSEditorModule, SUDSEditor)
|
||||
DEFINE_LOG_CATEGORY(LogSUDSEditor);
|
||||
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
280
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorScriptTools.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "SUDSScriptNode.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeSet.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/PackageName.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
void FSUDSEditorScriptTools::WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
const auto& SrcData = Script->AssetImportData->SourceData;
|
||||
if (SrcData.SourceFiles.Num() == 1)
|
||||
{
|
||||
TArray<FString> Lines;
|
||||
FString SourceFile = SrcData.SourceFiles[0].RelativeFilename;
|
||||
auto Package = Script->GetPackage();
|
||||
if (FPaths::IsRelative(SourceFile) && Package)
|
||||
{
|
||||
FString PackagePath = FPackageName::LongPackageNameToFilename(FPackageName::GetLongPackagePath(Package->GetPathName()));
|
||||
SourceFile = FPaths::ConvertRelativePathToFull(PackagePath, SourceFile);
|
||||
}
|
||||
if (FFileHelper::LoadFileToStringArray(Lines, *SourceFile))
|
||||
{
|
||||
const int PrevErrs = Logger.NumErrors();
|
||||
const bool bHeaderChanges = WriteBackTextIDsFromNodes(Script->GetHeaderNodes(), Lines, Script->GetName(), Logger);
|
||||
const bool bBodyChanges = WriteBackTextIDsFromNodes(Script->GetNodes(), Lines, Script->GetName(), Logger);
|
||||
|
||||
if (Logger.NumErrors() > PrevErrs)
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Errors prevented saving updates to %s."), *Script->GetName())));
|
||||
}
|
||||
else if (bHeaderChanges || bBodyChanges)
|
||||
{
|
||||
// We need to re-hash file in memory before saving to prevent re-import
|
||||
int32 Length = 10;
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
Length += Line.Len() + UE_ARRAY_COUNT(LINE_TERMINATOR);
|
||||
}
|
||||
FString CombinedString;
|
||||
CombinedString.Reserve(Length);
|
||||
|
||||
for(const FString& Line : Lines)
|
||||
{
|
||||
CombinedString += Line;
|
||||
CombinedString += LINE_TERMINATOR;
|
||||
}
|
||||
|
||||
// Write source file back; always encode as UTF8 not default ANSI/UTF16
|
||||
// Do this before updating asset import data so it picks up new file timestamp
|
||||
FFileHelper::SaveStringToFile(FStringView(CombinedString), *SourceFile, FFileHelper::EEncodingOptions::ForceUTF8);
|
||||
|
||||
/* BEGIN try to prevent re-import prompt
|
||||
* This unfortunately didn't work, so removed & let it reimport
|
||||
|
||||
const FMD5Hash FileHash = FSUDSScriptImporter::CalculateHash(*CombinedString, CombinedString.Len());
|
||||
Script->AssetImportData->Update(SourceFile, FileHash);
|
||||
|
||||
// Need to mark asset dirty since hash has changed
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
|
||||
// Even this doesn't work:
|
||||
GUnrealEd->AutoReimportManager->IgnoreFileModification(SourceFile);
|
||||
|
||||
* END try to prevent re-import
|
||||
*/
|
||||
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("Successfully updated %s"), *SourceFile)));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Info,
|
||||
FText::FromString(FString::Printf(TEXT("No changes were required to %s"), *SourceFile)));
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT("Error opening source asset %s"), *SourceFile)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(
|
||||
FString::Printf(TEXT("No source files associated with asset %s"), *Script->GetName())));
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
bool bAnyChanges = false;
|
||||
// For each speaker line, set line and choice edge, use source line no to append text ID
|
||||
for (const auto& N : Nodes)
|
||||
{
|
||||
if (N->GetNodeType() == ESUDSScriptNodeType::Text)
|
||||
{
|
||||
if (const auto* TN = Cast<USUDSScriptNodeText>(N))
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(TN->GetText(), TN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::SetVariable)
|
||||
{
|
||||
if (const auto* SN = Cast<USUDSScriptNodeSet>(N))
|
||||
{
|
||||
if (SN->GetExpression().IsTextLiteral())
|
||||
{
|
||||
FText Literal = SN->GetExpression().GetTextLiteralValue();
|
||||
|
||||
bAnyChanges = WriteBackTextID(Literal, SN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Choice)
|
||||
{
|
||||
// Edges
|
||||
for (auto& Edge : N->GetEdges())
|
||||
{
|
||||
bAnyChanges = WriteBackTextID(Edge.GetText(), Edge.GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
else if (N->GetNodeType() == ESUDSScriptNodeType::Gosub)
|
||||
{
|
||||
if (const auto* GN = Cast<USUDSScriptNodeGosub>(N))
|
||||
// Need to write back Gosub IDs so that saved return stacks work
|
||||
bAnyChanges = WriteBackGosubID(GN->GetGosubID(), GN->GetSourceLineNo(), Lines, NameForErrors, Logger) || bAnyChanges;
|
||||
}
|
||||
}
|
||||
|
||||
return bAnyChanges;
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger)
|
||||
{
|
||||
if (AssetText.IsEmpty())
|
||||
return false;
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back TextID to '%s', source line number %d is invalid (Text: '%s')"),
|
||||
*NameForErrors,
|
||||
LineNo,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
const FString TextID = SUDS_GET_TEXT_KEY(AssetText);
|
||||
FString ExistingTextID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundExisting = FSUDSScriptImporter::RetrieveTextIDFromLine(SourceLineView, ExistingTextID, ExistingNum);
|
||||
|
||||
// Existing TextID - replace if already there
|
||||
if (!bFoundExisting || ExistingTextID != TextID)
|
||||
{
|
||||
if (TextIDCheckMatch(AssetText, SourceLine))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *TextID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set TextID on line %d of %s but source file did not contain expected text '%s'"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors,
|
||||
*AssetText.ToString())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::TextIDCheckMatch(const FText& AssetText, const FString& SourceLine)
|
||||
{
|
||||
// Text from our asset must be present in the text in the source line
|
||||
// However, in case this is a multi-line string, take the first line only
|
||||
FString AssetStr = AssetText.ToString();
|
||||
{
|
||||
FString L, R;
|
||||
if (AssetStr.Split("\n", &L, &R))
|
||||
{
|
||||
AssetStr = L.TrimStartAndEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Bear in mind that this line might be a text line, a choice or a set line
|
||||
// therefore we only check if the source line contains the asset text
|
||||
|
||||
return SourceLine.Contains(AssetStr);
|
||||
|
||||
}
|
||||
|
||||
bool FSUDSEditorScriptTools::WriteBackGosubID(const FString& GosubID,
|
||||
int LineNo,
|
||||
TArray<FString>& Lines,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger& Logger)
|
||||
{
|
||||
|
||||
// Line numbers are 1-based
|
||||
const int Idx = LineNo - 1;
|
||||
|
||||
if (!Lines.IsValidIndex(Idx))
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(
|
||||
TEXT("Cannot write back GosubID to '%s', source line number %d is invalid"),
|
||||
*NameForErrors,
|
||||
LineNo)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString& SourceLine = Lines[Idx];
|
||||
FString ExistingID;
|
||||
int ExistingNum;
|
||||
FStringView SourceLineView(SourceLine);
|
||||
const bool bFoundID = FSUDSScriptImporter::RetrieveGosubIDFromLine(SourceLineView, ExistingID, ExistingNum);
|
||||
|
||||
if (!bFoundID || ExistingID != GosubID)
|
||||
{
|
||||
// Check it's a gosub line
|
||||
if (SourceLine.Contains(TEXT("gosub")) || SourceLine.Contains(TEXT("go sub")))
|
||||
{
|
||||
FString Prefix(SourceLineView);
|
||||
FString UpdatedLine = FString::Printf(TEXT("%s %s"), *Prefix, *GosubID);
|
||||
Lines[Idx] = UpdatedLine;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.AddMessage(EMessageSeverity::Error,
|
||||
FText::FromString(FString::Printf(TEXT(
|
||||
"Tried to set GosubID on line %d of %s but source file did not have a gosubu on that line"
|
||||
),
|
||||
LineNo,
|
||||
*NameForErrors)));
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
// Same, no need to change
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
bool USUDSEditorSettings::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (AlwaysAutoGenerateVoiceOverAssetsOnImport)
|
||||
return true;
|
||||
|
||||
for (auto Dir : DirectoriesToAutoGenerateVoiceOverAssetsOnImport)
|
||||
{
|
||||
if (FPaths::IsUnderDirectory(PackagePath, Dir.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueVoiceAssetLocation, DialogueVoiceAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const
|
||||
{
|
||||
return GetOutputDir(DialogueWaveAssetLocation, DialogueWaveAssetSharedDir.Path, PackagePath, ScriptName);
|
||||
}
|
||||
|
||||
FString USUDSEditorSettings::GetOutputDir(ESUDSAssetLocation Location,
|
||||
const FString& SharedPath,
|
||||
const FString& PackagePath,
|
||||
const FString& ScriptName)
|
||||
{
|
||||
switch(Location)
|
||||
{
|
||||
default:
|
||||
case ESUDSAssetLocation::SharedDirectory:
|
||||
return SharedPath;
|
||||
case ESUDSAssetLocation::SharedDirectorySubdir:
|
||||
return FPaths::Combine(SharedPath, ScriptName);
|
||||
case ESUDSAssetLocation::ScriptDirectory:
|
||||
return PackagePath;
|
||||
case ESUDSAssetLocation::ScriptDirectorySubdir:
|
||||
return FPaths::Combine(PackagePath, ScriptName);
|
||||
}
|
||||
}
|
||||
1590
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
1590
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
343
Plugins/SUDS/Source/SUDSEditor/Private/SUDSEditorToolkit.h
Normal file
@@ -0,0 +1,343 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "Framework/Commands/Commands.h"
|
||||
#include "Framework/Text/BaseTextLayoutMarshaller.h"
|
||||
#include "Toolkits/AssetEditorToolkit.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Widgets/Views/STableRow.h"
|
||||
|
||||
class SMultiLineEditableTextBox;
|
||||
struct FSUDSValue;
|
||||
class USUDSDialogue;
|
||||
class USUDSScript;
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SUDS"
|
||||
|
||||
class FSUDSToolbarCommands
|
||||
: public TCommands<FSUDSToolbarCommands>
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSToolbarCommands()
|
||||
: TCommands<FSUDSToolbarCommands>(
|
||||
"SUDS",
|
||||
LOCTEXT("SUDS", "Steves Unreal Dialogue System"),
|
||||
NAME_None,
|
||||
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0
|
||||
FAppStyle::GetAppStyleSetName()
|
||||
#else
|
||||
FEditorStyle::GetStyleSetName()
|
||||
#endif
|
||||
)
|
||||
{ }
|
||||
|
||||
public:
|
||||
|
||||
// TCommands interface
|
||||
|
||||
virtual void RegisterCommands() override
|
||||
{
|
||||
UI_COMMAND(StartDialogue, "Start Dialogue", "Start/restart dialogue", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(WriteBackTextIDs, "Write String Keys", "Write string keys back to script source to stabilise for localisation / voice asset links", EUserInterfaceActionType::Button, FInputChord());
|
||||
UI_COMMAND(GenerateVOAssets, "Generate Voice Assets", "Generate DialogueVoice and DialogueWave assets for VO", EUserInterfaceActionType::Button, FInputChord());
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
TSharedPtr<FUICommandInfo> StartDialogue;
|
||||
TSharedPtr<FUICommandInfo> WriteBackTextIDs;
|
||||
TSharedPtr<FUICommandInfo> GenerateVOAssets;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorOutputRow
|
||||
{
|
||||
public:
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
FSlateColor BgColour;
|
||||
|
||||
FSUDSEditorOutputRow(const FText& InPrefix,
|
||||
const FText& InLine,
|
||||
const FSlateColor& InPrefixColour,
|
||||
const FSlateColor& InLineColour,
|
||||
const FSlateColor& InBgColour) :
|
||||
Prefix(InPrefix),
|
||||
Line(InLine),
|
||||
PrefixColour(InPrefixColour),
|
||||
LineColour(InLineColour),
|
||||
BgColour(InBgColour)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorOutputItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorOutputItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FText, Prefix)
|
||||
SLATE_ARGUMENT(FText, Line)
|
||||
SLATE_ARGUMENT(FSlateColor, PrefixColour)
|
||||
SLATE_ARGUMENT(FSlateColor, LineColour)
|
||||
SLATE_ARGUMENT(FSlateColor, BgColour)
|
||||
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FText Prefix;
|
||||
FText Line;
|
||||
FSlateColor PrefixColour;
|
||||
FSlateColor LineColour;
|
||||
};
|
||||
|
||||
|
||||
class FSUDSEditorVariableRow
|
||||
{
|
||||
public:
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bIsManualOverride;
|
||||
|
||||
FSUDSEditorVariableRow(const FName& InName, const FSUDSValue& InValue, bool bIsManual) : Name(InName),
|
||||
Value(InValue),
|
||||
bIsManualOverride(bIsManual)
|
||||
{
|
||||
}
|
||||
|
||||
friend bool operator<(const FSUDSEditorVariableRow& Lhs, const FSUDSEditorVariableRow& RHS)
|
||||
{
|
||||
return Lhs.Name.LexicalLess(RHS.Name);
|
||||
}
|
||||
|
||||
friend bool operator<(const TSharedPtr<FSUDSEditorVariableRow>& Lhs, const TSharedPtr<FSUDSEditorVariableRow>& RHS)
|
||||
{
|
||||
return *Lhs < *RHS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
class SSUDSEditorVariableItem : public SMultiColumnTableRow< TSharedPtr<FString> >
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSEditorVariableItem)
|
||||
{}
|
||||
SLATE_ARGUMENT(float, InitialWidth)
|
||||
SLATE_ARGUMENT(FName, VariableName)
|
||||
SLATE_ARGUMENT(FSUDSValue, VariableValue)
|
||||
SLATE_ARGUMENT(bool, bIsManualOverride)
|
||||
SLATE_ARGUMENT(class FSUDSEditorToolkit*, Parent)
|
||||
SLATE_END_ARGS()
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Construct this widget. Called by the SNew() Slate macro.
|
||||
*
|
||||
* @param InArgs Declaration used by the SNew() macro to construct this widget.
|
||||
* @oaram InOwnerTableView The owner table into which this row is being placed.
|
||||
*/
|
||||
void Construct( const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView );
|
||||
|
||||
public:
|
||||
/**
|
||||
* Generates the widget for the specified column.
|
||||
*
|
||||
* @param ColumnName The name of the column to generate the widget for.
|
||||
* @return The widget.
|
||||
*/
|
||||
virtual TSharedRef<SWidget> GenerateWidgetForColumn( const FName& ColumnName ) override;
|
||||
protected:
|
||||
float InitialWidth = 70;
|
||||
FName VariableName;
|
||||
FSUDSValue VariableValue;
|
||||
bool bIsManualOverride = false;
|
||||
class FSUDSEditorToolkit* Parent = nullptr;
|
||||
|
||||
TSharedRef<class SWidget> GetGenderMenu();
|
||||
void OnGenderSelected(ETextGender TextGender);
|
||||
virtual FVector2D ComputeDesiredSize(float) const override;
|
||||
|
||||
};
|
||||
|
||||
struct FSUDSTraceLogMessage
|
||||
{
|
||||
TSharedRef<FString> Message;
|
||||
FName Category;
|
||||
FSlateColor Colour;
|
||||
|
||||
FSUDSTraceLogMessage(FName InCategory, const FString& InMessage, const FSlateColor& InColour)
|
||||
: Message(MakeShared<FString>(InMessage))
|
||||
, Category(InCategory)
|
||||
, Colour(InColour)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FSUDSTraceLogMarshaller : public FBaseTextLayoutMarshaller
|
||||
{
|
||||
public:
|
||||
|
||||
FSUDSTraceLogMarshaller();
|
||||
|
||||
// ITextLayoutMarshaller
|
||||
virtual void SetText(const FString& SourceString, FTextLayout& TargetTextLayout) override;
|
||||
virtual void GetText(FString& TargetString, const FTextLayout& SourceTextLayout) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
protected:
|
||||
TArray< TSharedPtr<FSUDSTraceLogMessage> > Messages;
|
||||
};
|
||||
|
||||
// Trace log widget, really just so we can tick and automatically scroll to the end
|
||||
class SSUDSTraceLog : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SSUDSTraceLog)
|
||||
{}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
SSUDSTraceLog() : bIsUserScrolled(false) {}
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
|
||||
|
||||
void AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour);
|
||||
void ClearMessages();
|
||||
void ScrollToEnd();
|
||||
|
||||
protected:
|
||||
bool bIsUserScrolled;
|
||||
TSharedPtr<FSUDSTraceLogMarshaller> TraceLogMarshaller;
|
||||
TSharedPtr<SMultiLineEditableTextBox> TraceLogTextBox;
|
||||
|
||||
void OnUserScrolled(float X);
|
||||
|
||||
};
|
||||
|
||||
class FSUDSEditorToolkit : public FAssetEditorToolkit
|
||||
{
|
||||
public:
|
||||
void InitEditor(const TArray<UObject*>& InObjects);
|
||||
|
||||
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
|
||||
|
||||
virtual FText GetBaseToolkitName() const override;
|
||||
virtual FName GetToolkitFName() const override;
|
||||
virtual FLinearColor GetWorldCentricTabColorScale() const override;
|
||||
virtual FString GetWorldCentricTabPrefix() const override;
|
||||
void UserEditVariable(const FName& Name, FSUDSValue Value);
|
||||
void DeleteVariable(const FName& Name);
|
||||
|
||||
protected:
|
||||
virtual void OnClose() override;
|
||||
|
||||
private:
|
||||
USUDSScript* Script = nullptr;
|
||||
USUDSDialogue* Dialogue = nullptr;
|
||||
float VarColumnWidth = 120;
|
||||
float PrefixColumnWidth = 100;
|
||||
FName StartLabel = NAME_None;
|
||||
bool bResetVarsOnStart = true;
|
||||
FDelegateHandle ReimportDelegateHandle;
|
||||
// FSUDSEditorDialogueRow needs to held by a TSharedPtr for SListView
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorOutputRow>>> OutputListView;
|
||||
TArray<TSharedPtr<FSUDSEditorOutputRow>> OutputRows;
|
||||
TSharedPtr<SVerticalBox> ChoicesBox;
|
||||
TSharedPtr<SListView<TSharedPtr<FSUDSEditorVariableRow>>> VariablesListView;
|
||||
TArray<TSharedPtr<FSUDSEditorVariableRow>> VariableRows;
|
||||
TSharedPtr<SSUDSTraceLog> TraceLog;
|
||||
TMap<FName, FSUDSValue> ManualOverrideVariables;
|
||||
|
||||
static FName DialogueOutputTabName;
|
||||
static FName DetailsTabName;
|
||||
static FName VariablesTabName;
|
||||
static FName LogTabName;
|
||||
|
||||
|
||||
const FSlateColor SpeakerColour = FLinearColor(1.0f, 1.0f, 0.6f, 1.0f);
|
||||
const FSlateColor ChoiceColour = FLinearColor(0.4f, 1.0f, 0.4f, 1.0f);
|
||||
const FSlateColor EventColour = FLinearColor(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
const FSlateColor VarSetColour = FLinearColor(0.8f, 0.6f, 0.9f, 1.0f);
|
||||
const FSlateColor VarEditColour = FLinearColor(0.6f, 0.3f, 1.0f, 1.0f);
|
||||
const FSlateColor StartColour = FLinearColor(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
const FSlateColor FinishColour = FLinearColor(1.0f, 0.3f, 0.3f, 1.0f);
|
||||
const FSlateColor SelectColour = FLinearColor(1.0f, 0.0f, 0.5f, 1.0f);
|
||||
const FSlateColor RowBgColour1 = FLinearColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
const FSlateColor RowBgColour2 = FLinearColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
|
||||
void ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr<SDockTab> Tab);
|
||||
TSharedRef<class SWidget> GetStartLabelMenu();
|
||||
FText GetSelectedStartLabel() const;
|
||||
void OnStartLabelSelected(FName Label);
|
||||
ECheckBoxState GetResetVarsCheckState() const;
|
||||
void OnResetVarsCheckStateChanged(ECheckBoxState NewState);
|
||||
FReply AddVariableClicked();
|
||||
void UpdateVariables();
|
||||
void EnsureTabsVisible();
|
||||
void StartDialogue();
|
||||
void DestroyDialogue();
|
||||
void UpdateOutput();
|
||||
void UpdateChoiceButtons();
|
||||
void AddOutputRow(const FText& Prefix, const FText& Line, const FSlateColor& PrefixColour, const FSlateColor& LineColour);
|
||||
void AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message);
|
||||
|
||||
void AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix);
|
||||
|
||||
void OnDialogueChoice(USUDSDialogue* Dialogue, int ChoiceIndex, int LineNo);
|
||||
void OnDialogueEvent(USUDSDialogue* Dialogue, FName EventName, const TArray<FSUDSValue>& Args, int LineNo);
|
||||
void OnDialogueFinished(USUDSDialogue* Dialogue);
|
||||
void OnDialogueProceeding(USUDSDialogue* Dialogue);
|
||||
void OnDialogueStarting(USUDSDialogue* Dialogue, FName LabelName);
|
||||
void OnDialogueSpeakerLine(USUDSDialogue* Dialogue, int LineNo);
|
||||
void OnDialogueSetVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue, const FString& ExpressionStr, int LineNo);
|
||||
void OnDialogueUserEditedVar(USUDSDialogue* Dialogue, FName VariableName, const FSUDSValue& ToValue);
|
||||
void OnDialogueSelectEval(USUDSDialogue* Dialogue, const FString& ExpressionStr, bool bSuccess, int LineNo);
|
||||
void OnDialogueRandomEval(USUDSDialogue* Dialogue, const int RandomOutcome, int LineNo);
|
||||
|
||||
TSharedRef<ITableRow> OnGenerateRowForOutput(
|
||||
TSharedPtr<FSUDSEditorOutputRow> FsudsEditorDialogueRow,
|
||||
const TSharedRef<STableViewBase>& TableViewBase);
|
||||
TSharedRef<ITableRow> OnGenerateRowForVariable(TSharedPtr<FSUDSEditorVariableRow> Row,
|
||||
const TSharedRef<STableViewBase>& Table);
|
||||
FSlateColor GetColourForCategory(const FName& Category);
|
||||
void OnPostReimport(UObject* Object, bool bSuccess);
|
||||
void Clear();
|
||||
void WriteBackTextIDs();
|
||||
void GenerateVOAssets();
|
||||
|
||||
};
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,381 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
|
||||
#include "ObjectTools.h"
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Internationalization/Regex.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
#include "Sound/SoundWave.h"
|
||||
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// First check for problems
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
bool bError = false;
|
||||
if ((Settings->DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueVoiceAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueVoiceAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueVoiceAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Voice assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueVoiceAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if ((Settings->DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectory || Settings->
|
||||
DialogueWaveAssetLocation == ESUDSAssetLocation::SharedDirectorySubdir) &&
|
||||
(Settings->DialogueWaveAssetSharedDir.Path.IsEmpty() ||
|
||||
!FPackageName::IsValidPath(Settings->DialogueWaveAssetSharedDir.Path)))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave assets are set to generate to a shared dir, but the dir '%s' is not valid."),
|
||||
*Settings->DialogueWaveAssetSharedDir.Path);
|
||||
bError = true;
|
||||
}
|
||||
if (bError) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
TMap<FString, UDialogueVoice*> UnsavedVoices;
|
||||
GenerateVoiceAssets(Script, Flags, Logger, UnsavedVoices);
|
||||
GenerateWaveAssets(Script, Flags, UnsavedVoices, Logger);
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateVoiceAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger, TMap<FString, UDialogueVoice*> &OutCreatedVoices)
|
||||
{
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
ParentDir = GetVoiceOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate voice assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Speaker : Script->GetSpeakers())
|
||||
{
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
FString PackageName;
|
||||
FString AssetName;
|
||||
if (GetSpeakerVoiceAssetNames(Script, Speaker, PackageName, AssetName))
|
||||
{
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Voice! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we still link the existing voice
|
||||
Script->SetSpeakerVoice(Speaker, Cast<UDialogueVoice>(Assets[0].GetAsset()));
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
// Either way nothing more to do
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here then Dialogue Voice didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for voice asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating voice asset %s"), *PackageName);
|
||||
|
||||
UDialogueVoice* NewVoiceAsset = NewObject<UDialogueVoice>(Package, FName(AssetName), Flags);
|
||||
OutCreatedVoices.Add(Speaker, NewVoiceAsset);
|
||||
// there's nothing else to create here, voice is mostly a placeholder with the rest set up later by user
|
||||
FAssetRegistryModule::AssetCreated(NewVoiceAsset);
|
||||
|
||||
Script->SetSpeakerVoice(Speaker, NewVoiceAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
NewVoiceAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoicePackageName(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName)
|
||||
{
|
||||
FString NotUsed;
|
||||
return GetSpeakerVoiceAssetNames(Script, SpeakerID, OutPackageName, NotUsed);
|
||||
}
|
||||
|
||||
bool FSUDSEditorVoiceOverTools::GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
FString Prefix = Settings->DialogueVoiceAssetPrefix;
|
||||
FString ParentDir = GetVoiceOutputDir(Script);
|
||||
|
||||
OutAssetName = FString::Printf(TEXT("%s%s"), *Prefix, *ObjectTools::SanitizeObjectName(SpeakerID));
|
||||
OutPackageName = UPackageTools::SanitizePackageName(ParentDir / OutAssetName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UDialogueVoice* FSUDSEditorVoiceOverTools::FindSpeakerVoice(USUDSScript* Script,
|
||||
const FString& SpeakerID)
|
||||
{
|
||||
FString PackageName;
|
||||
if (GetSpeakerVoicePackageName(Script, SpeakerID, PackageName))
|
||||
{
|
||||
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UDialogueVoice::StaticClass()))
|
||||
{
|
||||
return Cast<UDialogueVoice>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FSUDSEditorVoiceOverTools::GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*> UnsavedVoices, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
// We need to identify specific lines of dialogue to specify a wave asset to go with them, which means we
|
||||
// need to use the Text ID, just like we do for translations. This means that really, you shouldn't start to
|
||||
// assign sounds to wave assets until you've written the text IDs back to the script
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
|
||||
FString Prefix;
|
||||
FString ParentDir;
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
Prefix = FString::Printf(TEXT("%s%s_"), *Settings->DialogueWaveAssetPrefix, *GetScriptNameAsPrefix(Script));
|
||||
ParentDir = GetWaveOutputDir(Script);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Unable to generate wave assets, settings not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto Line = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
// We use the TextID to identify a specific line, just like for translation
|
||||
FString TextID = Line->GetTextID();
|
||||
|
||||
// Remove the '@' characters from the text ID for creating the asset name, they just turn into excessive '_'s
|
||||
const FRegexPattern TextIDPattern(TEXT("\\@([0-9a-fA-F]+)\\@"));
|
||||
FRegexMatcher TextIDRegex(TextIDPattern, TextID);
|
||||
if (TextIDRegex.FindNext())
|
||||
{
|
||||
TextID = TextIDRegex.GetCaptureGroup(1);
|
||||
}
|
||||
|
||||
// All Dialogue Voice assets will be created in their own package
|
||||
const FString SanitizedName = FString::Printf(TEXT("%s%s"),
|
||||
*Prefix,
|
||||
*ObjectTools::SanitizeObjectName(TextID));
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(ParentDir / SanitizedName);
|
||||
|
||||
UDialogueWave* WaveAsset = nullptr;
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (!Assets[0].GetAsset()->IsA(UDialogueWave::StaticClass()))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a Dialogue Wave! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
WaveAsset = Cast<UDialogueWave>(Assets[0].GetAsset());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UDialogueVoice* SpeakerVoice = nullptr;
|
||||
// Finding speaker assets via FAssetRegistryModule does not work on unsaved assets, so we need to check
|
||||
// the unsaved voices that have been created in the previous step first
|
||||
if (auto pSV = UnsavedVoices.Find(Line->GetSpeakerID()))
|
||||
{
|
||||
SpeakerVoice = *pSV;
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
// Now try assets
|
||||
SpeakerVoice = FindSpeakerVoice(Script, Line->GetSpeakerID());
|
||||
}
|
||||
if (!SpeakerVoice)
|
||||
{
|
||||
FString SpeakerPackageName;
|
||||
GetSpeakerVoicePackageName(Script, Line->GetSpeakerID(), SpeakerPackageName);
|
||||
Logger->Logf(ELogVerbosity::Error, TEXT("Error: speaker voice asset for %s is missing, expected to be at %s"), *Line->GetSpeakerID(), *SpeakerPackageName);
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for wave asset %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
FString NativeLangText = Line->GetText().ToString();
|
||||
USoundWave* SoundWave = nullptr;
|
||||
if (!WaveAsset)
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating wave asset %s"), *PackageName);
|
||||
WaveAsset = NewObject<UDialogueWave>(Package, FName(SanitizedName), Flags);
|
||||
FAssetRegistryModule::AssetCreated(WaveAsset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We will always update the existing asset, but will warn if the text is changing
|
||||
if (WaveAsset->ContextMappings.Num() > 0)
|
||||
{
|
||||
auto Mapping = WaveAsset->ContextMappings[0];
|
||||
if (IsValid(Mapping.SoundWave))
|
||||
{
|
||||
SoundWave = Mapping.SoundWave;
|
||||
// This is OK if the context is what we were going to create anyway, we'll just leave it alone
|
||||
if (Mapping.Context.Speaker != SpeakerVoice ||
|
||||
WaveAsset->SpokenText != NativeLangText)
|
||||
{
|
||||
// No good, user has assigned sound to this dialogue wave, but we need to change it to
|
||||
// a different speaker / line
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Dialogue Wave %s has an assigned Sound Wave but the speaker or text has changed. You should update the sound wave!"),
|
||||
*PackageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set spoken text - native language only. Dialogue Wave handles the localisation of this separately,
|
||||
// we can't link it to our String Table unfortunately.
|
||||
WaveAsset->SpokenText = NativeLangText;
|
||||
// Set dialogue context
|
||||
// We need a Speaker Voice, we'll leave the sound and targets blank
|
||||
WaveAsset->UpdateContext(WaveAsset->ContextMappings[0], SoundWave, SpeakerVoice, TArray<UDialogueVoice*>());
|
||||
|
||||
// Now assign the wave to the line
|
||||
Line->SetWave(WaveAsset);
|
||||
Script->MarkPackageDirty();
|
||||
|
||||
Package->FullyLoad();
|
||||
WaveAsset->MarkPackageDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetVoiceOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetVoiceOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetWaveOutputDir(USUDSScript* Script)
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
const FString PackagePath = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString ScriptPrefix = GetScriptNameAsPrefix(Script);
|
||||
return Settings->GetWaveOutputDir(PackagePath, ScriptPrefix);
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
FString FSUDSEditorVoiceOverTools::GetScriptNameAsPrefix(USUDSScript* Script)
|
||||
{
|
||||
FString Name = Script->GetName();
|
||||
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
if (Settings->StripScriptPrefixesWhenGeneratingNames)
|
||||
{
|
||||
int32 Index = INDEX_NONE;
|
||||
if (Name.FindChar('_', Index))
|
||||
{
|
||||
if (Index < Name.Len() - 1)
|
||||
{
|
||||
Name = Name.RightChop(Index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Name;
|
||||
}
|
||||
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
93
Plugins/SUDS/Source/SUDSEditor/Private/SUDSMessageLogger.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "IMessageLogListing.h"
|
||||
#include "MessageLogModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
FSUDSMessageLogger::~FSUDSMessageLogger()
|
||||
{
|
||||
if (bWriteToMessageLog)
|
||||
{
|
||||
//Always clear the old message after an import or re-import
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
// Should NOT clear messages here, otherwise when FSUDSMessageLogger is used multiple times in the import process,
|
||||
// each one is clearing messages from previous stages of the import.
|
||||
//LogListing->ClearMessages();
|
||||
|
||||
if(ErrorMessages.Num() > 0)
|
||||
{
|
||||
LogListing->AddMessages(MoveTemp(ErrorMessages));
|
||||
LogListing->NotifyIfAnyMessages(NSLOCTEXT("SUDS", "ImportErrors", "There were issues with the import."), EMessageSeverity::Warning);
|
||||
MessageLogModule.OpenMessageLog(LogTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasErrors() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumErrors() const
|
||||
{
|
||||
int Errs = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Error)
|
||||
{
|
||||
++Errs;
|
||||
}
|
||||
}
|
||||
return Errs;
|
||||
}
|
||||
|
||||
bool FSUDSMessageLogger::HasWarnings() const
|
||||
{
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int FSUDSMessageLogger::NumWarnings() const
|
||||
{
|
||||
int Count = 0;
|
||||
for (const TSharedRef<FTokenizedMessage>& Msg : ErrorMessages)
|
||||
{
|
||||
if (Msg->GetSeverity() == EMessageSeverity::Warning)
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::AddMessage(EMessageSeverity::Type Severity, const FText& Text)
|
||||
{
|
||||
ErrorMessages.Add(FTokenizedMessage::Create(Severity, Text));
|
||||
}
|
||||
|
||||
void FSUDSMessageLogger::ClearMessages()
|
||||
{
|
||||
const TCHAR* LogTitle = TEXT("SUDS");
|
||||
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
||||
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
||||
LogListing->SetLabel(FText::FromString("SUDS"));
|
||||
LogListing->ClearMessages();
|
||||
}
|
||||
|
||||
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
134
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptActions.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptActions.h"
|
||||
|
||||
#include "SUDSEditorScriptTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorToolkit.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Misc/MessageDialog.h"
|
||||
#include "Widgets/Views/SListView.h"
|
||||
|
||||
FText FSUDSScriptActions::GetName() const
|
||||
{
|
||||
return INVTEXT("SUDS Script");
|
||||
}
|
||||
|
||||
FString FSUDSScriptActions::GetObjectDisplayName(UObject* Object) const
|
||||
{
|
||||
return Object->GetName();
|
||||
}
|
||||
|
||||
UClass* FSUDSScriptActions::GetSupportedClass() const
|
||||
{
|
||||
return USUDSScript::StaticClass();
|
||||
}
|
||||
|
||||
FColor FSUDSScriptActions::GetTypeColor() const
|
||||
{
|
||||
return FColor::Orange;
|
||||
}
|
||||
|
||||
uint32 FSUDSScriptActions::GetCategories()
|
||||
{
|
||||
return EAssetTypeCategories::Misc;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const
|
||||
{
|
||||
for (auto& Asset : TypeAssets)
|
||||
{
|
||||
const auto Script = CastChecked<USUDSScript>(Asset);
|
||||
if (Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutSourceFilePaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FSUDSScriptActions::HasActions(const TArray<UObject*>& InObjects) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor)
|
||||
{
|
||||
MakeShared<FSUDSEditorToolkit>()->InitEditor(InObjects);
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section)
|
||||
{
|
||||
const auto Scripts = GetTypedWeakObjectPtrs<USUDSScript>(InObjects);
|
||||
|
||||
Section.AddMenuEntry(
|
||||
"WriteBackTextIDs",
|
||||
NSLOCTEXT("SUDS", "WriteBackTextIDs", "Write Back String Keys"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"WriteBackTextIDsTooltip",
|
||||
"Write string table keys back to source script to make them constant for localisation."),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Details"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::WriteBackTextIDs, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
Section.AddMenuEntry(
|
||||
"GenerateVOAssets",
|
||||
NSLOCTEXT("SUDS", "GenerateVOAssets", "Generate Voice Assets"),
|
||||
NSLOCTEXT("SUDS",
|
||||
"GenerateVOAssetsTooltip",
|
||||
"Generate Dialogue Voice / Dialogue Wave assets for the selected scripts"),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Export"),
|
||||
FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FSUDSScriptActions::GenerateVOAssets, Scripts),
|
||||
FCanExecuteAction()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void FSUDSScriptActions::WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to write string keys back to the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto Script : Scripts)
|
||||
{
|
||||
if (Script.IsValid())
|
||||
{
|
||||
FSUDSEditorScriptTools::WriteBackTextIDs(Script.Get(), Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void FSUDSScriptActions::GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts)
|
||||
{
|
||||
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
||||
FText::FromString(
|
||||
"Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for the selected scripts?"))
|
||||
== EAppReturnType::Yes)
|
||||
{
|
||||
EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
for (auto WeakScript : Scripts)
|
||||
{
|
||||
if (auto Script = WeakScript.Get())
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
262
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptFactory.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "PackageTools.h"
|
||||
#include "SUDSEditorSettings.h"
|
||||
#include "SUDSEditorVoiceOverTools.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "Internationalization/StringTable.h"
|
||||
#include "Editor.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "Internationalization/StringTableCore.h"
|
||||
|
||||
|
||||
USUDSScriptFactory::USUDSScriptFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
bEditorImport = true;
|
||||
bText = true;
|
||||
Formats.Add(TEXT("sud;SUDS Script File"));
|
||||
}
|
||||
|
||||
UObject* USUDSScriptFactory::FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn)
|
||||
{
|
||||
Flags |= RF_Transactional;
|
||||
FSUDSMessageLogger::ClearMessages();
|
||||
FSUDSMessageLogger Logger;
|
||||
|
||||
USUDSScript* Result = nullptr;
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type);
|
||||
|
||||
const FString FactoryCurrentFilename = UFactory::GetCurrentFilename();
|
||||
FString CurrentSourcePath;
|
||||
FString FilenameNoExtension;
|
||||
FString UnusedExtension;
|
||||
FPaths::Split(FactoryCurrentFilename, CurrentSourcePath, FilenameNoExtension, UnusedExtension);
|
||||
const FString LongPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
|
||||
|
||||
const FString NameForErrors(InName.ToString());
|
||||
|
||||
// Now parse this using utility
|
||||
if(Importer.ImportFromBuffer(Buffer, BufferEnd - Buffer, NameForErrors, &Logger, false))
|
||||
{
|
||||
|
||||
// Populate with data
|
||||
Result = NewObject<USUDSScript>(InParent, InName, Flags);
|
||||
UStringTable* StringTable = CreateStringTable(InParent, InName, Result, Flags, &Logger);
|
||||
Importer.PopulateAsset(Result, StringTable);
|
||||
|
||||
// Register source info
|
||||
const FMD5Hash Hash = FSUDSScriptImporter::CalculateHash(Buffer, BufferEnd - Buffer);
|
||||
Result->AssetImportData->Update(FactoryCurrentFilename, Hash);
|
||||
|
||||
// VO assets at import time?
|
||||
if (ShouldGenerateVoiceAssets(LongPackagePath))
|
||||
{
|
||||
FSUDSEditorVoiceOverTools::GenerateAssets(Result, Flags, &Logger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, Result);
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool USUDSScriptFactory::ShouldGenerateVoiceAssets(const FString& PackagePath) const
|
||||
{
|
||||
if (auto Settings = GetDefault<USUDSEditorSettings>())
|
||||
{
|
||||
return Settings->ShouldGenerateVoiceAssets(PackagePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UStringTable* USUDSScriptFactory::CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger)
|
||||
{
|
||||
auto Settings = GetDefault<USUDSEditorSettings>();
|
||||
const bool bCreateSeparatePackage = Settings && Settings->bCreateStringTablesAsSeparatePackages;
|
||||
|
||||
const FName StringTableName = FName(InName.ToString() + "Strings");
|
||||
UStringTable* Table = nullptr;
|
||||
auto Registry = &FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
||||
const FString PackageDir = FPackageName::GetLongPackagePath(Script->GetOuter()->GetOutermost()->GetPathName());
|
||||
const FString PackageName = UPackageTools::SanitizePackageName(PackageDir / StringTableName.ToString());
|
||||
|
||||
if (bCreateSeparatePackage)
|
||||
{
|
||||
|
||||
// Create string table as its own package (.uasset)
|
||||
|
||||
{
|
||||
// Destroy any string table packed together with the script
|
||||
const FString ScriptPackageName = Script->GetPackage()->GetPathName();
|
||||
TArray<FAssetData> ScriptAssets;
|
||||
Registry->GetAssetsByPackageName(*ScriptPackageName, ScriptAssets);
|
||||
TArray<UObject*> StringTableAssets;
|
||||
for (auto& Asset : ScriptAssets)
|
||||
{
|
||||
if (Asset.GetClass() == UStringTable::StaticClass())
|
||||
{
|
||||
StringTableAssets.Add(Asset.GetAsset());
|
||||
}
|
||||
}
|
||||
if (StringTableAssets.Num() > 0)
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects(StringTableAssets, false );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
// Package already exists, so try and import over the top of it, if it doesn't already have a source file path
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
if (Assets[0].GetAsset()->IsA(UStringTable::StaticClass()))
|
||||
{
|
||||
Table = Cast<UStringTable>(Assets[0].GetAsset());
|
||||
Table->GetMutableStringTable()->ClearSourceStrings();
|
||||
Table->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"Asset %s already exists but is not a String Table! Cannot replace, please move aside and re-import script."),
|
||||
*PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Table)
|
||||
{
|
||||
// String Table didn't exist (although package might have)
|
||||
// It's safe to call CreatePackage either way, it'll return the existing one if needed
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!ensure(Package))
|
||||
{
|
||||
Logger->Logf(ELogVerbosity::Error,
|
||||
TEXT("Failed to create/retrieve package for string table %s"),
|
||||
*PackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger->Logf(ELogVerbosity::Display, TEXT("Creating string table %s"), *PackageName);
|
||||
|
||||
// This constructor registers the string table with FStringTableRegistry
|
||||
Table = NewObject<UStringTable>(Package, StringTableName, Flags);
|
||||
Package->FullyLoad();
|
||||
Table->MarkPackageDirty();
|
||||
FAssetRegistryModule::AssetCreated(Table);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Destroy separate strings package if it exists (could flip this option off after importing)
|
||||
if (FPackageName::DoesPackageExist(*PackageName))
|
||||
{
|
||||
TArray<FAssetData> Assets;
|
||||
if (Registry->GetAssetsByPackageName(*PackageName, Assets))
|
||||
{
|
||||
if (Assets.Num() > 0)
|
||||
{
|
||||
// Note: we need to force deletion because old string table will be referenced
|
||||
// Unfortunately there is no ObjectTools::ForceDeleteAssets, only the inner
|
||||
// ForceDeleteObjects, so we need to do it ourselves
|
||||
ForceDeleteAssets(Assets);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default route, create string table inside script package
|
||||
Table = NewObject<UStringTable>(ScriptParent, StringTableName, Flags);
|
||||
|
||||
}
|
||||
|
||||
return Table;
|
||||
|
||||
}
|
||||
|
||||
void USUDSScriptFactory::ForceDeleteAssets(const TArray<FAssetData>& AssetsToDelete)
|
||||
{
|
||||
// Copied from ObjectTools::DeleteAssets, except that we're using ForceDeleteObjects so we
|
||||
// don't have to display any confusing messages about moving the string table into the shared asset
|
||||
|
||||
TArray<TWeakObjectPtr<UPackage>> PackageFilesToDelete;
|
||||
TArray<UObject*> ObjectsToDelete;
|
||||
for ( int i = 0; i < AssetsToDelete.Num(); i++ )
|
||||
{
|
||||
const FAssetData& AssetData = AssetsToDelete[i];
|
||||
UObject *ObjectToDelete = AssetData.GetAsset({ ULevel::LoadAllExternalObjectsTag });
|
||||
// Assets can be loaded even when their underlying type/class no longer exists...
|
||||
if ( ObjectToDelete!=nullptr )
|
||||
{
|
||||
ObjectsToDelete.Add( ObjectToDelete );
|
||||
}
|
||||
else if ( AssetData.IsUAsset() )
|
||||
{
|
||||
// ... In this cases there is no underlying asset or type so remove the package itself directly after confirming it's valid to do so.
|
||||
FString PackageFilename;
|
||||
if( !FPackageName::DoesPackageExist( AssetData.PackageName.ToString(), &PackageFilename ) )
|
||||
{
|
||||
// Could not determine filename for package so we can not delete
|
||||
continue;
|
||||
}
|
||||
|
||||
UPackage* Package = FindPackage(nullptr, *AssetData.PackageName.ToString());
|
||||
if ( Package )
|
||||
{
|
||||
PackageFilesToDelete.Add(Package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 NumObjectsToDelete = ObjectsToDelete.Num();
|
||||
if ( NumObjectsToDelete > 0 )
|
||||
{
|
||||
ObjectTools::ForceDeleteObjects( ObjectsToDelete, false );
|
||||
}
|
||||
|
||||
const int32 NumPackagesToDelete = PackageFilesToDelete.Num();
|
||||
if (NumPackagesToDelete > 0)
|
||||
{
|
||||
TArray<UPackage*> PackagePointers;
|
||||
for ( const auto& PkgIt : PackageFilesToDelete )
|
||||
{
|
||||
UPackage* Package = PkgIt.Get();
|
||||
if ( Package )
|
||||
{
|
||||
PackagePointers.Add(Package);
|
||||
}
|
||||
}
|
||||
|
||||
if ( PackagePointers.Num() > 0 )
|
||||
{
|
||||
const bool bPerformReferenceCheck = true;
|
||||
ObjectTools::CleanupAfterSuccessfulDelete(PackagePointers, bPerformReferenceCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2765
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
2765
Plugins/SUDS/Source/SUDSEditor/Private/SUDSScriptImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#include "SUDSScriptReimportFactory.h"
|
||||
|
||||
#include "SUDSEditor.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "EditorFramework/AssetImportData.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Sound/DialogueWave.h"
|
||||
|
||||
USUDSScriptReimportFactory::USUDSScriptReimportFactory()
|
||||
{
|
||||
SupportedClass = USUDSScript::StaticClass();
|
||||
bCreateNew = false;
|
||||
// We need to have a unique priority vs the original factory, so go after
|
||||
ImportPriority = DefaultImportPriority - 1;
|
||||
}
|
||||
|
||||
bool USUDSScriptReimportFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && Script->AssetImportData)
|
||||
{
|
||||
Script->AssetImportData->ExtractFilenames(OutFilenames);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void USUDSScriptReimportFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (Script && ensure(NewReimportPaths.Num() == 1))
|
||||
{
|
||||
Script->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EReimportResult::Type USUDSScriptReimportFactory::Reimport(UObject* Obj)
|
||||
{
|
||||
USUDSScript* Script = Cast<USUDSScript>(Obj);
|
||||
if (!Script)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// Make sure file is valid and exists
|
||||
const FString Filename = Script->AssetImportData->GetFirstFilename();
|
||||
if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
|
||||
{
|
||||
return EReimportResult::Failed;
|
||||
}
|
||||
|
||||
// When a new script is created, it actually lives at the same address as the incoming one. UE must re-use objects
|
||||
// when you put them back at the same outer & asset name?
|
||||
// This means if we want to preserve anything from the previously imported object, such as generated VO asset links,
|
||||
// we need to copy those out now.
|
||||
TMap<FString, UDialogueVoice*> PrevSpeakerVoices = Script->GetSpeakerVoices();
|
||||
// Store the TextID -> DialogueWave, but also store the line text as well so we can detect whether it matches & warn if not
|
||||
TMap<FString, TPair<FString, UDialogueWave*> > PrevWaves;
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto W = TN->GetWave())
|
||||
{
|
||||
PrevWaves.Add(TN->GetTextID(), TPair<FString, UDialogueWave*>(TN->GetText().ToString(), W));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the import again
|
||||
EReimportResult::Type Result = EReimportResult::Failed;
|
||||
bool OutCanceled = false;
|
||||
|
||||
if (ImportObject(Script->GetClass(), Script->GetOuter(), *Script->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Log, TEXT("Imported successfully"));
|
||||
|
||||
FSUDSMessageLogger Logger;
|
||||
// Now, try to restore the speaker voice / line wave links from before
|
||||
for (auto SpeakerID : Script->GetSpeakers())
|
||||
{
|
||||
if (auto pVoice = PrevSpeakerVoices.Find(SpeakerID))
|
||||
{
|
||||
Script->SetSpeakerVoice(SpeakerID, *pVoice);
|
||||
}
|
||||
}
|
||||
for (auto Node : Script->GetNodes())
|
||||
{
|
||||
if (auto TN = Cast<USUDSScriptNodeText>(Node))
|
||||
{
|
||||
if (auto pWavePair = PrevWaves.Find(TN->GetTextID()))
|
||||
{
|
||||
// Set the wave link either way
|
||||
TN->SetWave(pWavePair->Value);
|
||||
|
||||
// Check that the text is the same; if it's not, then lines have potentially changed
|
||||
// we still live with the assignment we have (might be a minor edit) but user should be aware
|
||||
if (TN->GetText().ToString() != pWavePair->Key)
|
||||
{
|
||||
Logger.Logf(ELogVerbosity::Error,
|
||||
TEXT(
|
||||
"TextID %s is linked to Dialogue Wave %s, but text has changed. Check whether this line is linked to the correct wave, and consider Writing String Keys back to script before making more script changes in future."),
|
||||
*TN->GetTextID(),
|
||||
*pWavePair->Value->GetName());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Script->AssetImportData->Update(Filename);
|
||||
|
||||
// Try to find the outer package so we can dirty it up
|
||||
if (Script->GetOuter())
|
||||
{
|
||||
Script->GetOuter()->MarkPackageDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
Script->MarkPackageDirty();
|
||||
}
|
||||
Result = EReimportResult::Succeeded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (OutCanceled)
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import canceled"));
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSUDSEditor, Warning, TEXT("-- import failed"));
|
||||
}
|
||||
|
||||
Result = EReimportResult::Failed;
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 USUDSScriptReimportFactory::GetPriority() const
|
||||
{
|
||||
return ImportPriority;
|
||||
}
|
||||
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
24
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditor.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
class FSUDSScriptActions;
|
||||
class FSlateStyleSet;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSEditor, Verbose, All);
|
||||
|
||||
class FSUDSEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
protected:
|
||||
TSharedPtr<FSUDSScriptActions> ScriptActions;
|
||||
TSharedPtr<FSlateStyleSet> StyleSet;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class USUDSScript;
|
||||
class USUDSScriptNode;
|
||||
struct FSUDSMessageLogger;
|
||||
|
||||
class FSUDSEditorScriptTools
|
||||
{
|
||||
public:
|
||||
|
||||
static void WriteBackTextIDs(USUDSScript* Script, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextIDsFromNodes(const TArray<USUDSScriptNode*> Nodes, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackTextID(const FText& AssetText, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool WriteBackGosubID(const FString& GosubID, int LineNo, TArray<FString>& Lines, const FString& NameForErrors, FSUDSMessageLogger& Logger);
|
||||
static bool TextIDCheckMatch(const FText& AssetText, const FString& SourceLine);
|
||||
|
||||
|
||||
};
|
||||
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
73
Plugins/SUDS/Source/SUDSEditor/Public/SUDSEditorSettings.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "SUDSEditorSettings.generated.h"
|
||||
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class ESUDSAssetLocation : uint8
|
||||
{
|
||||
/// Use a single flat shared directory
|
||||
SharedDirectory,
|
||||
/// Use a shared base directory, but create subfolders based on the script asset name
|
||||
SharedDirectorySubdir,
|
||||
/// Place asset alongside the script that originated it
|
||||
ScriptDirectory,
|
||||
/// Place asset in a subfolder of the folder containing the script that generated it, named the same as the script
|
||||
ScriptDirectorySubdir
|
||||
};
|
||||
|
||||
class USUDSScript;
|
||||
/**
|
||||
* Settings for editor-specific aspects of SUDS (no effect at runtime)
|
||||
*/
|
||||
UCLASS(config = Editor, defaultconfig, meta=(DisplayName="SUDS Editor"))
|
||||
class SUDSEDITOR_API USUDSEditorSettings : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Voice assets for speakers in scripts when generated", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueVoiceAssetLocation = ESUDSAssetLocation::SharedDirectory;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Voice assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueVoiceAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Where to place Dialogue Wave assets for speaker lines in scripts", RelativeToGameContentDir, LongPackageName))
|
||||
ESUDSAssetLocation DialogueWaveAssetLocation = ESUDSAssetLocation::ScriptDirectorySubdir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Shared directory for Dialogue Wave assets, if using a shared directory", RelativeToGameContentDir, LongPackageName))
|
||||
FDirectoryPath DialogueWaveAssetSharedDir;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Voice assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueVoiceAssetPrefix = "DV_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "Prefix to give Dialogue Wave assets in front of their SpeakerID", RelativeToGameContentDir, LongPackageName))
|
||||
FString DialogueWaveAssetPrefix = "DW_";
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", meta = (Tooltip = "When generating subdirectories and wave asset names from script names, whether to strip characters before the first '_' to avoid including script prefix", RelativeToGameContentDir, LongPackageName))
|
||||
bool StripScriptPrefixesWhenGeneratingNames = true;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Whether to auto-generate Dialogue Voice/Wave assets for ALL dialogue scripts on import. Note: you can always generate VO assets manually."))
|
||||
bool AlwaysAutoGenerateVoiceOverAssetsOnImport = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Voice", AdvancedDisplay, meta = (Tooltip = "Auto-generate Dialogue Voice/Wave assets for scripts in these directories (and subdirectories) on import. Note: you can always generate VO assets manually.", RelativeToGameContentDir, LongPackageName))
|
||||
TArray<FDirectoryPath> DirectoriesToAutoGenerateVoiceOverAssetsOnImport;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "Whether to generate a spoken line for choices (default false)."))
|
||||
bool AlwaysGenerateSpeakerLinesFromChoices = false;
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Choices", meta = (Tooltip = "The SpeakerID to use for speaker lines generated from choices"))
|
||||
FString SpeakerIdForGeneratedLinesFromChoices = "Player";
|
||||
|
||||
UPROPERTY(config, EditAnywhere, Category = "Assets", AdvancedDisplay, meta = (Tooltip = "Whether to create string tables as a separate package (.uasset) from the SUDS Script, which will cause them to appear separately in the Content Browser (requires script re-import)"))
|
||||
bool bCreateStringTablesAsSeparatePackages = true;
|
||||
|
||||
USUDSEditorSettings() {}
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FString GetVoiceOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
FString GetWaveOutputDir(const FString& PackagePath, const FString& ScriptName) const;
|
||||
static FString GetOutputDir(ESUDSAssetLocation Location, const FString& SharedPath, const FString& PackagePath, const FString& ScriptName);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
#include "Sound/DialogueVoice.h"
|
||||
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
|
||||
class SUDSEDITOR_API FSUDSEditorVoiceOverTools
|
||||
{
|
||||
public:
|
||||
static void GenerateAssets(USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
protected:
|
||||
static void GenerateVoiceAssets(USUDSScript* Script,
|
||||
EObjectFlags Flags,
|
||||
FSUDSMessageLogger *Logger,
|
||||
TMap<FString, UDialogueVoice*> &OutCreatedVoices);
|
||||
static void GenerateWaveAssets(USUDSScript* Script, EObjectFlags Flags, TMap<FString, UDialogueVoice*>, FSUDSMessageLogger* Logger);
|
||||
static bool GetSpeakerVoicePackageName(USUDSScript* Script, const FString& SpeakerID, FString& OutPackageName);
|
||||
static bool GetSpeakerVoiceAssetNames(USUDSScript* Script,
|
||||
const FString& SpeakerID,
|
||||
FString& OutPackageName,
|
||||
FString& OutAssetName);
|
||||
static UDialogueVoice* FindSpeakerVoice(USUDSScript* Script, const FString& SpeakerID);
|
||||
static FString GetScriptNameAsPrefix(USUDSScript* Script);
|
||||
|
||||
static FString GetVoiceOutputDir(USUDSScript* Script);
|
||||
static FString GetWaveOutputDir(USUDSScript* Script);
|
||||
|
||||
};
|
||||
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
60
Plugins/SUDS/Source/SUDSEditor/Public/SUDSMessageLogger.h
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/TokenizedMessage.h"
|
||||
|
||||
class FTokenizedMessage;
|
||||
|
||||
struct SUDSEDITOR_API FSUDSMessageLogger
|
||||
{
|
||||
protected:
|
||||
TArray<TSharedRef<FTokenizedMessage>> ErrorMessages;
|
||||
|
||||
bool bWriteToMessageLog = true;
|
||||
public:
|
||||
FSUDSMessageLogger() {}
|
||||
FSUDSMessageLogger(bool bWriteToMsg) : bWriteToMessageLog(bWriteToMsg) {}
|
||||
~FSUDSMessageLogger();
|
||||
|
||||
void SetWriteToMessageLog(bool bWrite) { bWriteToMessageLog = bWrite; }
|
||||
bool HasErrors() const;
|
||||
int NumErrors() const;
|
||||
bool HasWarnings() const;
|
||||
int NumWarnings() const;
|
||||
|
||||
bool GetWriteToMessageLog() const { return bWriteToMessageLog; }
|
||||
void AddMessage(EMessageSeverity::Type Severity, const FText& Text);
|
||||
|
||||
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 6
|
||||
template <typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, UE::Core::TCheckedFormatString<FString::FmtCharType, Types...> Fmt, Types... Args)
|
||||
#else
|
||||
template <typename FmtType, typename... Types>
|
||||
FORCEINLINE void Logf(ELogVerbosity::Type Verbosity, const FmtType& Fmt, Types... Args)
|
||||
#endif
|
||||
{
|
||||
EMessageSeverity::Type Sev = EMessageSeverity::Info;
|
||||
switch(Verbosity)
|
||||
{
|
||||
case ELogVerbosity::Fatal:
|
||||
case ELogVerbosity::Error:
|
||||
Sev = EMessageSeverity::Error;
|
||||
break;
|
||||
case ELogVerbosity::Warning:
|
||||
Sev = EMessageSeverity::Warning;
|
||||
break;
|
||||
default: ;
|
||||
}
|
||||
AddMessage(Sev, FText::FromString(FString::Printf(Fmt, Args...)));
|
||||
}
|
||||
|
||||
const TArray<TSharedRef<FTokenizedMessage>>& GetErrorMessages() const { return ErrorMessages; }
|
||||
|
||||
/// Clear messages in preparation for an import
|
||||
static void ClearMessages();
|
||||
|
||||
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptActions.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetTypeActions_Base.h"
|
||||
#include "SUDSScriptNodeGosub.h"
|
||||
#include "SUDSScriptNodeText.h"
|
||||
|
||||
class USUDSScriptNode;
|
||||
class USUDSScript;
|
||||
struct FSUDSMessageLogger;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class FSUDSScriptActions : public FAssetTypeActions_Base
|
||||
{
|
||||
public:
|
||||
virtual FText GetName() const override;
|
||||
virtual FString GetObjectDisplayName(UObject* Object) const override;
|
||||
virtual UClass* GetSupportedClass() const override;
|
||||
virtual FColor GetTypeColor() const override;
|
||||
virtual uint32 GetCategories() override;
|
||||
virtual void GetResolvedSourceFilePaths(const TArray<UObject*>& TypeAssets,
|
||||
TArray<FString>& OutSourceFilePaths) const override;
|
||||
virtual bool IsImportedAsset() const override { return true; }
|
||||
virtual void GetActions(const TArray<UObject*>& InObjects, FToolMenuSection& Section) override;
|
||||
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
|
||||
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects,
|
||||
TSharedPtr<IToolkitHost> EditWithinLevelEditor) override;
|
||||
|
||||
protected:
|
||||
void WriteBackTextIDs(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
void GenerateVOAssets(TArray<TWeakObjectPtr<USUDSScript>> Scripts);
|
||||
|
||||
};
|
||||
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
37
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptFactory.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "Factories/Factory.h"
|
||||
#include "SUDSScriptFactory.generated.h"
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UCLASS()
|
||||
class SUDSEDITOR_API USUDSScriptFactory : public UFactory
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USUDSScriptFactory();
|
||||
protected:
|
||||
virtual UObject* FactoryCreateText(UClass* InClass,
|
||||
UObject* InParent,
|
||||
FName InName,
|
||||
EObjectFlags Flags,
|
||||
UObject* Context,
|
||||
const TCHAR* Type,
|
||||
const TCHAR*& Buffer,
|
||||
const TCHAR* BufferEnd,
|
||||
FFeedbackContext* Warn) override;
|
||||
|
||||
bool ShouldGenerateVoiceAssets(const FString& PackagePath) const;
|
||||
FSUDSScriptImporter Importer;
|
||||
|
||||
void ForceDeleteAssets(const TArray<FAssetData>& Assets);
|
||||
UStringTable* CreateStringTable(UObject* ScriptParent, FName InName, USUDSScript* Script, EObjectFlags Flags, FSUDSMessageLogger* Logger);
|
||||
};
|
||||
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
520
Plugins/SUDS/Source/SUDSEditor/Public/SUDSScriptImporter.h
Normal file
@@ -0,0 +1,520 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSExpression.h"
|
||||
|
||||
struct FSUDSMessageLogger;
|
||||
class USUDSScript;
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSUDSImporter, Verbose, All);
|
||||
|
||||
struct SUDSEDITOR_API FSUDSParsedEdge
|
||||
{
|
||||
public:
|
||||
/// Text associated with this edge (if a player choice option)
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// The line this edge was created on
|
||||
int SourceLineNo;
|
||||
/// Condition expression that applies to this edge (for select nodes)
|
||||
FSUDSExpression ConditionExpression;
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate choice lines
|
||||
/// Could be used to disable a choice if you don't have the stats to take it, or anything else
|
||||
/// where you need a bit of extra information about a line that doesn't suit setting a dialogue variable for.
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
int SourceNodeIdx = -1;
|
||||
int TargetNodeIdx = -1;
|
||||
|
||||
FSUDSParsedEdge(int LineNo) : SourceLineNo(LineNo){}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta)
|
||||
: Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedEdge(int FromNodeIdx, int ToNodeIdx, int LineNo)
|
||||
: SourceLineNo(LineNo),
|
||||
SourceNodeIdx(FromNodeIdx),
|
||||
TargetNodeIdx(ToNodeIdx)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
*this = FSUDSParsedEdge(-1);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
enum class ESUDSParsedNodeType : uint8
|
||||
{
|
||||
/// Text node, displaying a line of dialogue
|
||||
Text,
|
||||
/// Choice node, displaying a series of user choices which navigate to other nodes
|
||||
Choice,
|
||||
/// Select node, automatically selecting one which navigates to another node based on state (also Random)
|
||||
Select,
|
||||
/// Goto node, redirects execution somewhere else
|
||||
/// Gotos are only nodes in the parsing structure, because they need to be discoverable as a fallthrough destination
|
||||
/// When converting to runtime use they just become edges
|
||||
Goto,
|
||||
/// Gosub node, acts like goto except stores return location
|
||||
Gosub,
|
||||
/// Return node, returns from a gosub
|
||||
Return,
|
||||
/// Set variable node
|
||||
SetVariable,
|
||||
/// Event node
|
||||
Event
|
||||
};
|
||||
|
||||
/// Intermediate parsed node from script text
|
||||
/// This will be converted into a final asset later
|
||||
struct SUDSEDITOR_API FSUDSParsedNode
|
||||
{
|
||||
public:
|
||||
ESUDSParsedNodeType NodeType;
|
||||
int OriginalIndent;
|
||||
/// Identifier is speaker ID, goto label, variable name etc
|
||||
FString Identifier;
|
||||
/// Text in native language
|
||||
FString Text;
|
||||
/// Identifier of the text, for the string table
|
||||
FString TextID;
|
||||
/// Metadata associated with text, for translator comments
|
||||
TMap<FName, FString> TextMetadata;
|
||||
/// Expression, for nodes that use it (e.g. set)
|
||||
FSUDSExpression Expression;
|
||||
/// Event arguments, for event nodes
|
||||
TArray<FSUDSExpression> EventArgs;
|
||||
/// Labels which lead to this node
|
||||
TArray<FString> Labels;
|
||||
/// Edges leading to other nodes
|
||||
TArray<FSUDSParsedEdge> Edges;
|
||||
/// The line this node was created on
|
||||
int SourceLineNo;
|
||||
/// Whether this is a valid fall-through target
|
||||
bool AllowFallthrough = true;
|
||||
|
||||
/// Custom metadata for a node which the user can set. Can be used to annotate speaker lines
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
// Path hierarchy of choice nodes leading to this node, of the form "/C002/C006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ChoicePath;
|
||||
// Path hierarchy of select nodes leading to this node, of the form "/S002/S006" etc, not including this node index
|
||||
// This helps us identify valid fallthroughs
|
||||
FString ConditionalPath;
|
||||
|
||||
/// Although multiple edges can lead here, this index is for the auto-connected parent (may be nothing)
|
||||
int ParentNodeIdx = -1;
|
||||
|
||||
FSUDSParsedNode(ESUDSParsedNodeType InNodeType, int Indent, int LineNo) : NodeType(InNodeType),
|
||||
OriginalIndent(Indent),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& Label,
|
||||
const FString& GosubID,
|
||||
int Indent,
|
||||
int LineNo) : NodeType(ESUDSParsedNodeType::Gosub),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(Label),
|
||||
TextID(GosubID),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& InSpeaker, const FString& InText, const FString& InTextID, const TMap<FName, FString>& Metadata, const TMap<FName, FSUDSExpression>& UserMeta, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Text),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(InSpeaker),
|
||||
Text(InText),
|
||||
TextID(InTextID),
|
||||
TextMetadata(Metadata),
|
||||
SourceLineNo(LineNo),
|
||||
UserMetadata(UserMeta)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& GotoLabel, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::Goto), OriginalIndent(Indent), Identifier(GotoLabel), SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName, const FSUDSExpression& InExpr, int Indent, int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
|
||||
FSUDSParsedNode(const FString& VariableName,
|
||||
const FSUDSExpression& InExpr,
|
||||
const FString& InTextID,
|
||||
int Indent,
|
||||
int LineNo)
|
||||
: NodeType(ESUDSParsedNodeType::SetVariable),
|
||||
OriginalIndent(Indent),
|
||||
Identifier(VariableName),
|
||||
TextID(InTextID),
|
||||
Expression(InExpr),
|
||||
SourceLineNo(LineNo)
|
||||
{
|
||||
}
|
||||
};
|
||||
class SUDSEDITOR_API FSUDSScriptImporter
|
||||
{
|
||||
public:
|
||||
bool ImportFromBuffer(const TCHAR* Buffer, int32 Len, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void PopulateAsset(USUDSScript* Asset, UStringTable* StringTable);
|
||||
static FMD5Hash CalculateHash(const TCHAR* Buffer, int32 Len);
|
||||
static const FString EndGotoLabel;
|
||||
protected:
|
||||
static const FString TreePathSeparator;
|
||||
|
||||
enum class EConditionalStage : uint8
|
||||
{
|
||||
IfStage,
|
||||
ElseIfStage,
|
||||
ElseStage,
|
||||
RandomStage,
|
||||
RandomOptionStage
|
||||
};
|
||||
enum class EConditionalParent : uint8
|
||||
{
|
||||
Select,
|
||||
ElseIfStage,
|
||||
ElseStage
|
||||
};
|
||||
|
||||
// Struct for tracking if/elseif blocks
|
||||
struct ConditionalContext
|
||||
{
|
||||
/// Index of parent select node where else will be added
|
||||
int SelectNodeIdx = -1;
|
||||
/// Previous block index (parent for nesting)
|
||||
int PreviousBlockIdx = -1;
|
||||
/// Track whether we're in if/elseif/else
|
||||
EConditionalStage Stage;
|
||||
/// String identifying the current condition; for elseif or else contains original "if" context
|
||||
FString ConditionPathElement;
|
||||
|
||||
ConditionalContext(int InSelectNodeIdx, int InPrevBlockIdx, EConditionalStage InStage, const FString& InCondStr) :
|
||||
SelectNodeIdx(InSelectNodeIdx),
|
||||
PreviousBlockIdx(InPrevBlockIdx),
|
||||
Stage(InStage),
|
||||
ConditionPathElement(InCondStr)
|
||||
{
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// Struct for tracking indents
|
||||
struct IndentContext
|
||||
{
|
||||
public:
|
||||
// The index of the Node which is the parent of this context
|
||||
// This potentially changes every time a sequential text node is encountered in the same context, so it's
|
||||
// always pointing to the last node encountered at this level, for connection
|
||||
int LastNodeIdx = -1;
|
||||
|
||||
/// The outermost indent level where this context lives
|
||||
/// You can indent things that don't create a new context, e.g.
|
||||
/// 1. Indent a text line under another text line: this is the same as no indent, just a continuation
|
||||
/// 2. Indent choices or conditions under a text line
|
||||
/// This is just good for readability, but does not create a new context, it's just a linear sequence
|
||||
/// Therefore the ThresholdIndent tracks the outermost indent relating to the current linear sequence, to know
|
||||
/// when you do in fact need to pop the current context off the stack.
|
||||
int ThresholdIndent = 0;
|
||||
|
||||
int LastTextNodeIdx = -1;
|
||||
|
||||
/// The path entry for this indent, to be combined with all previous levels to provide full path context
|
||||
FString PathEntry;
|
||||
|
||||
IndentContext(int NodeIdx, int Indent, const FString& Path) : LastNodeIdx(NodeIdx), ThresholdIndent(Indent), LastTextNodeIdx(-1), PathEntry(Path) {}
|
||||
|
||||
};
|
||||
|
||||
/// A tree of nodes. Contained to separate header nodes from body nodes
|
||||
struct ParsedTree
|
||||
{
|
||||
public:
|
||||
/// The indent context stack representing where we are in the indentation tree while parsing
|
||||
/// There must always be 1 level (root)
|
||||
TArray<IndentContext> IndentLevelStack;
|
||||
/// When encountering conditions and choice lines, we are building up details for an edge to another node, but
|
||||
/// we currently don't know the target node. We keep these pending details here
|
||||
int EdgeInProgressNodeIdx = -1;
|
||||
int EdgeInProgressEdgeIdx = -1;
|
||||
/// List of all nodes, appended to as parsing progresses
|
||||
/// Ordering is important, these nodes must be in the order encountered in the file
|
||||
TArray<FSUDSParsedNode> Nodes;
|
||||
/// Record of goto labels to node index, built up during parsing (forward refs are OK so not complete until end of parsing)
|
||||
TMap<FString, int> GotoLabelList;
|
||||
/// Goto labels which have been encountered but we haven't found a destination yet
|
||||
TArray<FString> PendingGotoLabels;
|
||||
/// Goto labels that lead directly to another goto and thus are just aliases
|
||||
TMap<FString, FString> AliasedGotoLabels;
|
||||
|
||||
/// Conditional blocks
|
||||
/// "if" creates a new context, uses current as parent
|
||||
/// "elseif" and "else" also create new contexts, but copies parent from current (sibling)
|
||||
/// "endif" ends the context
|
||||
/// You cannot have conditionals that were started in an indent context ending outside it
|
||||
TArray<ConditionalContext> ConditionalBlocks;
|
||||
/// Index of the current conditional block, if any
|
||||
int CurrentConditionalBlockIdx = -1;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
IndentLevelStack.Reset();
|
||||
EdgeInProgressNodeIdx = EdgeInProgressEdgeIdx -1;
|
||||
Nodes.Reset();
|
||||
GotoLabelList.Reset();
|
||||
PendingGotoLabels.Reset();
|
||||
AliasedGotoLabels.Reset();
|
||||
ConditionalBlocks.Reset();
|
||||
CurrentConditionalBlockIdx = -1;
|
||||
}
|
||||
};
|
||||
|
||||
ParsedTree HeaderTree;
|
||||
ParsedTree BodyTree;
|
||||
|
||||
struct ParsedMetadata
|
||||
{
|
||||
public:
|
||||
FName Key;
|
||||
FString Value;
|
||||
int IndentLevel;
|
||||
|
||||
ParsedMetadata(const FName& InKey, const FString& InValue, int InIndentLevel)
|
||||
: Key(InKey),
|
||||
Value(InValue),
|
||||
IndentLevel(InIndentLevel)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// Text Metadata applied to speaker lines / choices until reset
|
||||
/// For each key there's a stack of metadata, with most indented at the top
|
||||
TMap<FName, TArray<ParsedMetadata>> PersistentMetadata;
|
||||
/// Text Metadata applied just to the next speaker line or choice
|
||||
TMap<FName, ParsedMetadata> TransientMetadata;
|
||||
/// User metadata for the next speaker line or choice
|
||||
TMap<FName, FSUDSExpression> UserMetadata;
|
||||
|
||||
/// List of speakers, detected during parsing of lines of text
|
||||
TArray<FString> ReferencedSpeakers;
|
||||
|
||||
const int TabIndentValue = 4;
|
||||
bool bHeaderDone = false;
|
||||
bool bTooLateForHeader = false;
|
||||
bool bHeaderInProgress = false;
|
||||
TOptional<bool> bOverrideGenerateSpeakerLineForChoice;
|
||||
TOptional<FString> OverrideChoiceSpeakerID;
|
||||
bool bTextInProgress = false;
|
||||
int ChoiceUniqueId = 0;
|
||||
/// For generating text IDs
|
||||
int TextIDHighestNumber = 0;
|
||||
/// For generating gosub IDs
|
||||
int GosubIDHighestNumber = 0;
|
||||
/// Parse a single line
|
||||
bool ParseLine(const FStringView& Line, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseHeaderLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseBodyLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseCommentMetadataLine(const FStringView& Line, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool IsLastNodeOfType(const ParsedTree& Tree, ESUDSParsedNodeType Type);
|
||||
bool ParseChoiceLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
FSUDSParsedEdge* GetEdgeInProgress(ParsedTree& Tree);
|
||||
void EnsureChoiceNodeExistsAboveSelect(ParsedTree& Tree, int IndentLevel, int LineNo);
|
||||
static bool IsConditionalLine(const FStringView& Line);
|
||||
bool ParseConditionalLine(const FStringView& Line, ParsedTree& Tree, int IndentLevel, int LineNo, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ParseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseElseIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
const FString& ConditionStr,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
|
||||
bool ParseElseLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndIfLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
static bool IsRandomLine(const FStringView& Line);
|
||||
bool ParseRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseBeginRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseRandomOptionLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEndRandomLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLabelLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGotoLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseGosubLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseReturnLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseSetLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseEventLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseTextLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool ParseImportSettingLine(const FStringView& Line,
|
||||
ParsedTree& Tree,
|
||||
int IndentLevel,
|
||||
int LineNo,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
TMap<FName, FString> GetTextMetadataForNextEntry(int CurrentLineIndent);
|
||||
TMap<FName, FSUDSExpression> ConsumeUserMetadata();
|
||||
bool IsCommentLine(const FStringView& TrimmedLine);
|
||||
FStringView TrimLine(const FStringView& Line, int& OutIndentLevel) const;
|
||||
int FindChoiceAfterTextNode(const FSUDSScriptImporter::ParsedTree& Tree, int TextNodeIdx, const FString& ConditionalPath);
|
||||
int RecurseChoiceNodeFindDeepest(const ParsedTree& Tree, int FromChoiceIdx, const FString& ConditionalPath);
|
||||
int RecurseSelectNodeFindDeepestChoice(const ParsedTree& Tree, int FromSelectNode, const FString& ConditionalPath);
|
||||
int FindEdge(const FSUDSScriptImporter::ParsedTree& Tree, int ParentNodeIdx, int TargetNodeIndex);
|
||||
FString MakeIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseIfConditionPathElement(int SelectNodeIdx, const FString& ConditionStr);
|
||||
FString MakeElseConditionPathElement(int SelectNodeIdx);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel);
|
||||
int FindLastChoiceNode(const ParsedTree& Tree, int IndentLevel, int FromIndex, const FString& ConditionPath);
|
||||
void PopIndent(ParsedTree& Tree);
|
||||
void PushIndent(ParsedTree& Tree, int NodeIdx, int Indent, const FString& Path);
|
||||
FString GetCurrentTreePath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetCurrentTreeConditionalPath(const FSUDSScriptImporter::ParsedTree& Tree);
|
||||
FString GetTreeConditionalPath(const ParsedTree& Tree, int NodeIndex);
|
||||
void SetFallthroughForNewNode(FSUDSScriptImporter::ParsedTree& Tree, FSUDSParsedNode& NewNode);
|
||||
int AppendNode(ParsedTree& Tree, const FSUDSParsedNode& InNode);
|
||||
bool SelectNodeIsMissingElsePath(const FSUDSScriptImporter::ParsedTree& Tree, const FSUDSParsedNode& Node);
|
||||
bool PostImportSanityCheck(const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool ChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode,
|
||||
const FString& NameForErrors,
|
||||
FSUDSMessageLogger* Logger,
|
||||
bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& OrigChoiceNode, const FSUDSParsedNode& CurrChoiceNode, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
bool RecurseChoiceNodeCheckPaths(const FSUDSParsedNode& ChoiceNode, const FSUDSParsedEdge& Edge, const FString& NameForErrors, FSUDSMessageLogger*
|
||||
Logger,
|
||||
bool bSilent);
|
||||
void ConnectRemainingNodes(ParsedTree& Tree, const FString& NameForErrors, FSUDSMessageLogger* Logger, bool bSilent);
|
||||
void GenerateTextIDs(ParsedTree& BodyTree);
|
||||
int FindFallthroughNodeIndex(ParsedTree& Tree, int StartNodeIndex, const FString& FromChoicePath, const FString& FromConditionalPath);
|
||||
bool RetrieveAndRemoveTextID(FStringView& InOutLine, FString& OutTextID);
|
||||
bool RetrieveAndRemoveGosubID(FStringView& InOutLine, FString& OutTextID);
|
||||
FString GenerateTextID();
|
||||
const FSUDSParsedNode* GetNode(const ParsedTree& Tree, int Index = 0);
|
||||
int GetGotoTargetNodeIndex(const ParsedTree& Tree, const FString& InLabel);
|
||||
void PopulateAssetFromTree(USUDSScript* Asset,
|
||||
const ParsedTree& Tree,
|
||||
TArray<TObjectPtr<class USUDSScriptNode>>* pOutNodes,
|
||||
TMap<FName, int>* pOutLabels,
|
||||
UStringTable* StringTable);
|
||||
|
||||
public:
|
||||
const FSUDSParsedNode* GetNode(int Index = 0);
|
||||
const FSUDSParsedNode* GetHeaderNode(int Index = 0);
|
||||
/// Resolve a goto label to a target index (after import), or -1 if not resolvable
|
||||
int GetGotoTargetNodeIndex(const FString& Label);
|
||||
static bool RetrieveTextIDFromLine(FStringView& InOutLine, FString& OutTextID, int& OutNumber);
|
||||
static bool RetrieveGosubIDFromLine(FStringView& InOutLine, FString& OutID, int& OutNumber);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "EditorReimportHandler.h"
|
||||
#include "SUDSScriptFactory.h"
|
||||
|
||||
#include "SUDSScriptReimportFactory.generated.h"
|
||||
|
||||
// Reimports a USUDSScriptFactory asset
|
||||
// Necessary to get the import pop-up
|
||||
UCLASS()
|
||||
class USUDSScriptReimportFactory : public USUDSScriptFactory, public FReimportHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
USUDSScriptReimportFactory();
|
||||
|
||||
//~ Begin FReimportHandler Interface
|
||||
virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
|
||||
virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
|
||||
virtual EReimportResult::Type Reimport(UObject* Obj) override;
|
||||
virtual int32 GetPriority() const override;
|
||||
//~ End FReimportHandler Interface
|
||||
};
|
||||
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
62
Plugins/SUDS/Source/SUDSEditor/SUDSEditor.Build.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright Steve Streeting 2022
|
||||
// Released under the MIT license https://opensource.org/license/MIT/
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class SUDSEditor : ModuleRules
|
||||
{
|
||||
public SUDSEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add public include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateIncludePaths.AddRange(
|
||||
new string[] {
|
||||
// ... add other private include paths required here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PublicDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"SUDS"
|
||||
// ... add other public dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"InputCore", // needed by come Slate widgets
|
||||
"Projects", // So that we can use the IPluginManager, required for our custom style
|
||||
"ToolMenus",
|
||||
"MessageLog",
|
||||
"UnrealEd",
|
||||
"EditorStyle"
|
||||
// ... add private dependencies that you statically link with here ...
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
DynamicallyLoadedModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
// ... add any modules that your module loads dynamically here ...
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
21
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "SudsTestModule.h"
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSudsTestModule)
|
||||
|
||||
void FSudsTestModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Started"))
|
||||
}
|
||||
|
||||
void FSudsTestModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
UE_LOG(LogSudsTestModule, Log, TEXT("SUDSTest Module Stopped"))
|
||||
}
|
||||
|
||||
|
||||
IMPLEMENT_MODULE(FSudsTestModule, SUDSTest)
|
||||
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
14
Plugins/SUDS/Source/SUDSTest/Private/SudsTestModule.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "Modules/ModuleInterface.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogSudsTestModule, All, All)
|
||||
|
||||
class FSudsTestModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
274
Plugins/SUDS/Source/SUDSTest/Private/TestChoiceSpeakerLines.cpp
Normal file
@@ -0,0 +1,274 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString ChoicesAsSpeakerLineEnableInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: I see
|
||||
* Choice 1a
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerLineInScript,
|
||||
"SUDSTest.TestChoiceSpeakerLineInScript",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerLineInScript::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineEnableInScriptInput), ChoicesAsSpeakerLineEnableInScriptInput.Len(), "ChoicesAsSpeakerLineEnableInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
// Test choice which disables speaker line
|
||||
Dlg->Choose(1);
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "Player", "I had to say this separately");
|
||||
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const FString ChoicesAsSpeakerLineSetSpeakerIdInScriptInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
[importsetting SpeakerIDForGeneratedLinesFromChoices `TheDude`]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
NPC: You said that choice
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerSetSpeakerIdInScriptInput,
|
||||
"SUDSTest.TestChoiceSpeakerSetSpeakerIdInScriptInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerSetSpeakerIdInScriptInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineSetSpeakerIdInScriptInput), ChoicesAsSpeakerLineSetSpeakerIdInScriptInput.Len(), "ChoicesAsSpeakerLineSetSpeakerIdInScriptInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
const FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "TheDude", "Choice 1");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "You said that choice");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineChainChoiceInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello
|
||||
* Choice 1
|
||||
* Choice 1a
|
||||
NPC: Hey this worked
|
||||
* Choice 1b
|
||||
* Choice 2
|
||||
NPC: Also that one
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoiceSpeakerChainedChoiceInput,
|
||||
"SUDSTest.TestChoiceSpeakerChainedChoiceInput",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoiceSpeakerChainedChoiceInput::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineChainChoiceInput), ChoicesAsSpeakerLineChainChoiceInput.Len(), "ChoicesAsSpeakerLineChainChoiceInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line, with custom speaker ID
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
TestEqual("Choice 2nd level text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice 2nd leveltext", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 1b"));
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1a");
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "Hey this worked");
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
const FString ChoicesAsSpeakerLineWithStringKeysInput = R"RAWSUD(
|
||||
===
|
||||
[importsetting GenerateSpeakerLinesFromChoices true]
|
||||
===
|
||||
Player: Hello @0012@
|
||||
* Choice 1 @0016@
|
||||
NPC: I see @0017@
|
||||
* Choice 1a @0018@
|
||||
NPC: Sure
|
||||
*- Not a speaker line!
|
||||
Player: I had to say this separately
|
||||
* Choice 2
|
||||
NPC: Totally
|
||||
|
||||
Player: The end
|
||||
)RAWSUD";
|
||||
|
||||
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestChoicesAsSpeakerLineWithStringKeys,
|
||||
"SUDSTest.TestChoicesAsSpeakerLineWithStringKeys",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
|
||||
bool FTestChoicesAsSpeakerLineWithStringKeys::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(ChoicesAsSpeakerLineWithStringKeysInput), ChoicesAsSpeakerLineWithStringKeysInput.Len(), "ChoicesAsSpeakerLineWithStringKeysInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Hello");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0012@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Choice 2"));
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0016@");
|
||||
FText ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
// Confirm that we got the choice as a speaker line
|
||||
TestDialogueText(this, "Choice as speaker line 1", Dlg, "Player", "Choice 1");
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Dlg->Continue();
|
||||
TestDialogueText(this, "Regular speaker line", Dlg, "NPC", "I see");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetText()), "@0017@");
|
||||
TestEqual("String key", SUDS_GET_TEXT_KEY(Dlg->GetChoiceText(0)), "@0018@");
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(0).ToString(), TEXT("Choice 1a"));
|
||||
TestEqual("Choice text", Dlg->GetChoiceText(1).ToString(), TEXT("Not a speaker line!"));
|
||||
|
||||
ChoiceText0 = Dlg->GetChoiceText(0);
|
||||
Dlg->Choose(0);
|
||||
TestDialogueText(this, "Duplicated speaker line", Dlg, "Player", "Choice 1a");
|
||||
|
||||
// Confirm that text ID is the same
|
||||
TestTrue("Text should be identical", ChoiceText0.IdenticalTo(Dlg->GetText(), ETextIdenticalModeFlags::DeepCompare));
|
||||
TestEqual("TextIDs should match", SUDS_GET_TEXT_KEY(ChoiceText0), SUDS_GET_TEXT_KEY(Dlg->GetText()));
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
1045
Plugins/SUDS/Source/SUDSTest/Private/TestConditionals.cpp
Normal file
File diff suppressed because it is too large
Load Diff
20
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp
Normal file
20
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "TestEventSub.h"
|
||||
|
||||
#include "SUDSDialogue.h"
|
||||
|
||||
void UTestEventSub::Init(USUDSDialogue* Dlg)
|
||||
{
|
||||
Dlg->OnEvent.AddDynamic(this, &UTestEventSub::OnEvent);
|
||||
Dlg->OnVariableChanged.AddDynamic(this, &UTestEventSub::OnVariableChanged);
|
||||
|
||||
}
|
||||
|
||||
void UTestEventSub::OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args)
|
||||
{
|
||||
EventRecords.Add(FEventRecord { EventName, Args });
|
||||
}
|
||||
|
||||
void UTestEventSub::OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript)
|
||||
{
|
||||
SetVarRecords.Add(FSetVarRecord { VarName, Value, bFromScript });
|
||||
}
|
||||
39
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h
Normal file
39
Plugins/SUDS/Source/SUDSTest/Private/TestEventSub.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SUDSValue.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "TestEventSub.generated.h"
|
||||
|
||||
class USUDSDialogue;
|
||||
UCLASS()
|
||||
class SUDSTEST_API UTestEventSub : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
void Init(USUDSDialogue* Dlg);
|
||||
|
||||
struct FEventRecord
|
||||
{
|
||||
FName Name;
|
||||
TArray<FSUDSValue> Args;
|
||||
};
|
||||
struct FSetVarRecord
|
||||
{
|
||||
FName Name;
|
||||
FSUDSValue Value;
|
||||
bool bFromScript;
|
||||
};
|
||||
|
||||
TArray<FEventRecord> EventRecords;
|
||||
TArray<FSetVarRecord> SetVarRecords;
|
||||
|
||||
UFUNCTION()
|
||||
void OnEvent(USUDSDialogue* Dlg, FName EventName, const TArray<FSUDSValue>& Args);
|
||||
|
||||
UFUNCTION()
|
||||
void OnVariableChanged(USUDSDialogue* Dlg, FName VarName, const FSUDSValue& Value, bool bFromScript);
|
||||
|
||||
|
||||
};
|
||||
278
Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp
Normal file
278
Plugins/SUDS/Source/SUDSTest/Private/TestEvents.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "SUDSDialogue.h"
|
||||
#include "SUDSLibrary.h"
|
||||
#include "SUDSMessageLogger.h"
|
||||
#include "SUDSScript.h"
|
||||
#include "SUDSScriptImporter.h"
|
||||
#include "TestEventSub.h"
|
||||
#include "TestParticipant.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
UE_DISABLE_OPTIMIZATION
|
||||
|
||||
const FString EventParsingInput = R"RAWSUD(
|
||||
Player: Ow do?
|
||||
[set IntVar 2]
|
||||
[set FloatVar 66.67]
|
||||
[set StringVar "Ey up"]
|
||||
[event SummatHappened "2 penneth", 99.99, masculine, {IntVar}, 42, false]
|
||||
NPC: Alreet chook
|
||||
[event Well.Blow.Me.Down {FloatVar}, true, {StringVar}]
|
||||
[event Calculated {FloatVar} + 10, true or false, {StringVar}]
|
||||
[event Actor.Emote `Player`, `Question`]
|
||||
Player: Tara
|
||||
)RAWSUD";
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FTestEvents,
|
||||
"SUDSTest.TestEvents",
|
||||
EAutomationTestFlags::EditorContext |
|
||||
EAutomationTestFlags::ClientContext |
|
||||
EAutomationTestFlags::ProductFilter)
|
||||
|
||||
|
||||
bool FTestEvents::RunTest(const FString& Parameters)
|
||||
{
|
||||
FSUDSMessageLogger Logger(false);
|
||||
FSUDSScriptImporter Importer;
|
||||
TestTrue("Import should succeed", Importer.ImportFromBuffer(GetData(EventParsingInput), EventParsingInput.Len(), "EventParsingInput", &Logger, true));
|
||||
|
||||
auto Script = NewObject<USUDSScript>(GetTransientPackage(), "Test");
|
||||
const ScopedStringTableHolder StringTableHolder;
|
||||
Importer.PopulateAsset(Script, StringTableHolder.StringTable);
|
||||
|
||||
// Script shouldn't be the owner of the dialogue but it's the only UObject we've got right now so why not
|
||||
auto Dlg = USUDSLibrary::CreateDialogue(Script, Script);
|
||||
|
||||
// Subscriber
|
||||
auto EvtSub = NewObject<UTestEventSub>();
|
||||
EvtSub->Init(Dlg);
|
||||
|
||||
// Participant
|
||||
auto Participant = NewObject<UTestParticipant>();
|
||||
Participant->TestNumber = 0;
|
||||
Dlg->AddParticipant(Participant);
|
||||
|
||||
Dlg->Start();
|
||||
|
||||
TestDialogueText(this, "Line 1", Dlg, "Player", "Ow do?");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
// Should have had an event
|
||||
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 1))
|
||||
{
|
||||
TestEqual("Event name", EvtSub->EventRecords[0].Name.ToString(), "SummatHappened");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[0].Args.Num(), 6))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
|
||||
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
|
||||
TestEqual("Event sub arg 3 type", EvtSub->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event sub arg 3 value", EvtSub->EventRecords[0].Args[3].GetIntValue(), 2);
|
||||
TestEqual("Event sub arg 4 type", EvtSub->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event sub arg 4 value", EvtSub->EventRecords[0].Args[4].GetIntValue(), 42);
|
||||
TestEqual("Event sub arg 5 type", EvtSub->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 5 value", EvtSub->EventRecords[0].Args[5].GetBooleanValue(), false);
|
||||
}
|
||||
}
|
||||
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 1))
|
||||
{
|
||||
TestEqual("Event name", Participant->EventRecords[0].Name.ToString(), "SummatHappened");
|
||||
if (TestEqual("Participant arg count", Participant->EventRecords[0].Args.Num(), 6))
|
||||
{
|
||||
TestEqual("Participant arg 0 type", Participant->EventRecords[0].Args[0].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant arg 0 value", Participant->EventRecords[0].Args[0].GetTextValue().ToString(), "2 penneth");
|
||||
TestEqual("Participant arg 1 type", Participant->EventRecords[0].Args[1].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant arg 1 value", Participant->EventRecords[0].Args[1].GetFloatValue(), 99.99f);
|
||||
TestEqual("Participant arg 2 type", Participant->EventRecords[0].Args[2].GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Participant arg 2 value", Participant->EventRecords[0].Args[2].GetGenderValue(), ETextGender::Masculine);
|
||||
TestEqual("Participant arg 3 type", Participant->EventRecords[0].Args[3].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant arg 3 value", Participant->EventRecords[0].Args[3].GetIntValue(), 2);
|
||||
TestEqual("Participant arg 4 type", Participant->EventRecords[0].Args[4].GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant arg 4 value", Participant->EventRecords[0].Args[4].GetIntValue(), 42);
|
||||
TestEqual("Participant arg 5 type", Participant->EventRecords[0].Args[5].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant arg 5 value", Participant->EventRecords[0].Args[5].GetBooleanValue(), false);
|
||||
|
||||
}
|
||||
}
|
||||
if (TestEqual("Event var sub should have received", EvtSub->SetVarRecords.Num(), 10))
|
||||
{
|
||||
// First 7 are from the participant setting variables at startup
|
||||
TestEqual("Event var sub arg 0 name", EvtSub->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
|
||||
TestEqual("Event var sub arg 0 type", EvtSub->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 0 value", EvtSub->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
|
||||
TestFalse("Event var sub arg 0 fromscript", EvtSub->SetVarRecords[0].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 1 name", EvtSub->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
|
||||
TestEqual("Event var sub arg 1 type", EvtSub->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 1 value", EvtSub->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
|
||||
TestFalse("Event var sub arg 1 fromscript", EvtSub->SetVarRecords[1].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 2 name", EvtSub->SetVarRecords[2].Name.ToString(), "NumCats");
|
||||
TestEqual("Event var sub arg 2 type", EvtSub->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event var sub arg 2 value", EvtSub->SetVarRecords[2].Value.GetIntValue(), 3);
|
||||
TestFalse("Event var sub arg 2 fromscript", EvtSub->SetVarRecords[2].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 3 name", EvtSub->SetVarRecords[3].Name.ToString(), "FriendName");
|
||||
TestEqual("Event var sub arg 3 type", EvtSub->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 3 value", EvtSub->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
|
||||
TestFalse("Event var sub arg 3 fromscript", EvtSub->SetVarRecords[3].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 4 name", EvtSub->SetVarRecords[4].Name.ToString(), "Gender");
|
||||
TestEqual("Event var sub arg 4 type", EvtSub->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Event var sub arg 4 value", EvtSub->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
|
||||
TestFalse("Event var sub arg 4 fromscript", EvtSub->SetVarRecords[4].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 5 name", EvtSub->SetVarRecords[5].Name.ToString(), "FloatVal");
|
||||
TestEqual("Event var sub arg 5 type", EvtSub->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event var sub arg 5 value", EvtSub->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
|
||||
TestFalse("Event var sub arg 5 fromscript", EvtSub->SetVarRecords[5].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 6 name", EvtSub->SetVarRecords[6].Name.ToString(), "BoolVal");
|
||||
TestEqual("Event var sub arg 6 type", EvtSub->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event var sub arg 6 value", EvtSub->SetVarRecords[6].Value.GetBooleanValue(), true);
|
||||
TestFalse("Event var sub arg 6 fromscript", EvtSub->SetVarRecords[6].bFromScript);
|
||||
|
||||
// Next 3 are from script
|
||||
TestEqual("Event var sub arg 7 name", EvtSub->SetVarRecords[7].Name.ToString(), "IntVar");
|
||||
TestEqual("Event var sub arg 7 type", EvtSub->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Event var sub arg 7 value", EvtSub->SetVarRecords[7].Value.GetIntValue(), 2);
|
||||
TestTrue("Event var sub arg 7 fromscript", EvtSub->SetVarRecords[7].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 8 name", EvtSub->SetVarRecords[8].Name.ToString(), "FloatVar");
|
||||
TestEqual("Event var sub arg 8 type", EvtSub->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event var sub arg 8 value", EvtSub->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
|
||||
TestTrue("Event var sub arg 8 fromscript", EvtSub->SetVarRecords[8].bFromScript);
|
||||
|
||||
TestEqual("Event var sub arg 9 name", EvtSub->SetVarRecords[9].Name.ToString(), "StringVar");
|
||||
TestEqual("Event var sub arg 9 type", EvtSub->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event var sub arg 9 value", EvtSub->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
|
||||
TestTrue("Event var sub arg 9 fromscript", EvtSub->SetVarRecords[9].bFromScript);
|
||||
}
|
||||
if (TestEqual("Participant var should have received", Participant->SetVarRecords.Num(), 10))
|
||||
{
|
||||
// First 7 are from the participant setting variables at startup
|
||||
TestEqual("Participant var arg 0 name", Participant->SetVarRecords[0].Name.ToString(), "SpeakerName.Player");
|
||||
TestEqual("Participant var arg 0 type", Participant->SetVarRecords[0].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 0 value", Participant->SetVarRecords[0].Value.GetTextValue().ToString(), "Protagonist");
|
||||
TestFalse("Participant var arg 0 fromscript", Participant->SetVarRecords[0].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 1 name", Participant->SetVarRecords[1].Name.ToString(), "SpeakerName.NPC");
|
||||
TestEqual("Participant var arg 1 type", Participant->SetVarRecords[1].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 1 value", Participant->SetVarRecords[1].Value.GetTextValue().ToString(), "An NPC");
|
||||
TestFalse("Participant var arg 1 fromscript", Participant->SetVarRecords[1].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 2 name", Participant->SetVarRecords[2].Name.ToString(), "NumCats");
|
||||
TestEqual("Participant var arg 2 type", Participant->SetVarRecords[2].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant var arg 2 value", Participant->SetVarRecords[2].Value.GetIntValue(), 3);
|
||||
TestFalse("Participant var arg 2 fromscript", Participant->SetVarRecords[2].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 3 name", Participant->SetVarRecords[3].Name.ToString(), "FriendName");
|
||||
TestEqual("Participant var arg 3 type", Participant->SetVarRecords[3].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 3 value", Participant->SetVarRecords[3].Value.GetTextValue().ToString(), "Susan");
|
||||
TestFalse("Participant var arg 3 fromscript", Participant->SetVarRecords[3].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 4 name", Participant->SetVarRecords[4].Name.ToString(), "Gender");
|
||||
TestEqual("Participant var arg 4 type", Participant->SetVarRecords[4].Value.GetType(), ESUDSValueType::Gender);
|
||||
TestEqual("Participant var arg 4 value", Participant->SetVarRecords[4].Value.GetGenderValue(), ETextGender::Feminine);
|
||||
TestFalse("Participant var arg 4 fromscript", Participant->SetVarRecords[4].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 5 name", Participant->SetVarRecords[5].Name.ToString(), "FloatVal");
|
||||
TestEqual("Participant var arg 5 type", Participant->SetVarRecords[5].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant var arg 5 value", Participant->SetVarRecords[5].Value.GetFloatValue(), 12.567f);
|
||||
TestFalse("Participant var arg 5 fromscript", Participant->SetVarRecords[5].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 6 name", Participant->SetVarRecords[6].Name.ToString(), "BoolVal");
|
||||
TestEqual("Participant var arg 6 type", Participant->SetVarRecords[6].Value.GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant var arg 6 value", Participant->SetVarRecords[6].Value.GetBooleanValue(), true);
|
||||
TestFalse("Participant var arg 6 fromscript", Participant->SetVarRecords[6].bFromScript);
|
||||
|
||||
// Next 3 are from script
|
||||
TestEqual("Participant var arg 7 name", Participant->SetVarRecords[7].Name.ToString(), "IntVar");
|
||||
TestEqual("Participant var arg 7 type", Participant->SetVarRecords[7].Value.GetType(), ESUDSValueType::Int);
|
||||
TestEqual("Participant var arg 7 value", Participant->SetVarRecords[7].Value.GetIntValue(), 2);
|
||||
TestTrue("Participant var arg 7 fromscript", Participant->SetVarRecords[7].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 8 name", Participant->SetVarRecords[8].Name.ToString(), "FloatVar");
|
||||
TestEqual("Participant var arg 8 type", Participant->SetVarRecords[8].Value.GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant var arg 8 value", Participant->SetVarRecords[8].Value.GetFloatValue(), 66.67f);
|
||||
TestTrue("Participant var arg 8 fromscript", Participant->SetVarRecords[8].bFromScript);
|
||||
|
||||
TestEqual("Participant var arg 9 name", Participant->SetVarRecords[9].Name.ToString(), "StringVar");
|
||||
TestEqual("Participant var arg 9 type", Participant->SetVarRecords[9].Value.GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant var arg 9 value", Participant->SetVarRecords[9].Value.GetTextValue().ToString(), "Ey up");
|
||||
TestTrue("Participant var arg 9 fromscript", Participant->SetVarRecords[9].bFromScript);
|
||||
}
|
||||
|
||||
TestDialogueText(this, "Line 2", Dlg, "NPC", "Alreet chook");
|
||||
TestTrue("Continue", Dlg->Continue());
|
||||
|
||||
if (TestEqual("Event sub should have received", EvtSub->EventRecords.Num(), 4))
|
||||
{
|
||||
TestEqual("Event name", EvtSub->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[1].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
|
||||
TestEqual("Event sub arg 1 type", EvtSub->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 1 value", EvtSub->EventRecords[1].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[2].Name.ToString(), "Calculated");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[2].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
|
||||
}
|
||||
}
|
||||
if (TestEqual("Participant should have received", Participant->EventRecords.Num(), 4))
|
||||
{
|
||||
TestEqual("Event name", Participant->EventRecords[1].Name.ToString(), "Well.Blow.Me.Down");
|
||||
if (TestEqual("Participant arg count", Participant->EventRecords[1].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Participant arg 0 type", Participant->EventRecords[1].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Participant arg 0 value", Participant->EventRecords[1].Args[0].GetFloatValue(), 66.67f);
|
||||
TestEqual("Participant arg 1 type", Participant->EventRecords[1].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Participant arg 1 value", Participant->EventRecords[1].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Participant arg 2 type", Participant->EventRecords[1].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Participant arg 2 value", Participant->EventRecords[1].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", Participant->EventRecords[2].Name.ToString(), "Calculated");
|
||||
if (TestEqual("Event sub arg count", Participant->EventRecords[2].Args.Num(), 3))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", Participant->EventRecords[2].Args[0].GetType(), ESUDSValueType::Float);
|
||||
TestEqual("Event sub arg 0 value", Participant->EventRecords[2].Args[0].GetFloatValue(), 76.67f);
|
||||
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[1].GetType(), ESUDSValueType::Boolean);
|
||||
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[1].GetBooleanValue(), true);
|
||||
TestEqual("Event sub arg 2 type", Participant->EventRecords[2].Args[2].GetType(), ESUDSValueType::Text);
|
||||
TestEqual("Event sub arg 2 value", Participant->EventRecords[2].Args[2].GetTextValue().ToString(), "Ey up");
|
||||
}
|
||||
TestEqual("Event name", EvtSub->EventRecords[3].Name.ToString(), "Actor.Emote");
|
||||
if (TestEqual("Event sub arg count", EvtSub->EventRecords[3].Args.Num(), 2))
|
||||
{
|
||||
TestEqual("Event sub arg 0 type", EvtSub->EventRecords[3].Args[0].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 0 value", EvtSub->EventRecords[3].Args[0].GetNameValue(), FName("Player"));
|
||||
TestEqual("Event sub arg 2 type", EvtSub->EventRecords[3].Args[1].GetType(), ESUDSValueType::Name);
|
||||
TestEqual("Event sub arg 2 value", EvtSub->EventRecords[3].Args[1].GetNameValue(), FName("Question"));
|
||||
}
|
||||
}
|
||||
|
||||
Script->MarkAsGarbage();
|
||||
return true;
|
||||
}
|
||||
|
||||
UE_ENABLE_OPTIMIZATION
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user