Add new plugins, refactor exp, introduce stat forge to replace GAS

This commit is contained in:
2026-07-15 22:48:04 +02:00
parent 3c084d9669
commit da0a0b643f
267 changed files with 33330 additions and 48 deletions

View File

@@ -171,7 +171,6 @@ bMoviesAreSkippable=False
[/Script/ProjectEleri.EleriGameSettings] [/Script/ProjectEleri.EleriGameSettings]
SpecialItemsMap=(((TagName="Items.Gold"), "ItemDataAsset:DA_Gold"),((TagName="Items.Crystals.Air"), "ItemDataAsset:DA_CrystalAir"),((TagName="Items.Crystals.Earth"), "ItemDataAsset:DA_CrystalEarth"),((TagName="Items.Crystals.Fire"), "ItemDataAsset:DA_CrystalFire"),((TagName="Items.Crystals.Water"), "ItemDataAsset:DA_CrystalWater")) SpecialItemsMap=(((TagName="Items.Gold"), "ItemDataAsset:DA_Gold"),((TagName="Items.Crystals.Air"), "ItemDataAsset:DA_CrystalAir"),((TagName="Items.Crystals.Earth"), "ItemDataAsset:DA_CrystalEarth"),((TagName="Items.Crystals.Fire"), "ItemDataAsset:DA_CrystalFire"),((TagName="Items.Crystals.Water"), "ItemDataAsset:DA_CrystalWater"))
CameraZoomLevels=(((TagName="Camera.BoomOffset.Default"), (X=-1200.000000,Y=0.000000,Z=50.000000)),((TagName="Camera.BoomOffset.Cave"), (X=-800.000000,Y=0.000000,Z=40.000000)),((TagName="Camera.BoomOffset.Flying"), (X=-1500.000000,Y=0.000000,Z=60.000000))) CameraZoomLevels=(((TagName="Camera.BoomOffset.Default"), (X=-1200.000000,Y=0.000000,Z=50.000000)),((TagName="Camera.BoomOffset.Cave"), (X=-800.000000,Y=0.000000,Z=40.000000)),((TagName="Camera.BoomOffset.Flying"), (X=-1500.000000,Y=0.000000,Z=60.000000)))
ExpIncreaseEffect=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/GameplayAbilities/GameplayEffects/GE_AddExp_Template.GE_AddExp_Template_C'
+ProfessionExpArray=100.000000 +ProfessionExpArray=100.000000
+ProfessionExpArray=500.000000 +ProfessionExpArray=500.000000
+ProfessionExpArray=1500.000000 +ProfessionExpArray=1500.000000
@@ -192,4 +191,5 @@ ExpIncreaseEffect=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/Gamepl
+ProfessionExpArray=180000.000000 +ProfessionExpArray=180000.000000
+ProfessionExpArray=208000.000000 +ProfessionExpArray=208000.000000
+ProfessionExpArray=240000.000000 +ProfessionExpArray=240000.000000
ExpIncreaseEffectClass=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/GameEffects/GSE_AddExp_Template.GSE_AddExp_Template_C'

Binary file not shown.

Binary file not shown.

View 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"
}
]
}

Binary file not shown.

View File

@@ -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 ...
}
);
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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;
}
);
}

View File

@@ -0,0 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Widgets/BaseInteractionWidget.h"

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
}
};

View File

@@ -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;
};

View File

@@ -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);
};

View 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"
]
}
]
}

Binary file not shown.

View File

@@ -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"
});
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
/* Copyright UNmisterIZE(Sebastien Durocher) 2025 All Rights Reserved.
Project: NoiseMapGenerator Plugin
File: NoiseMapGeneratorSettings.cpp
*/
#include "NoiseMapGeneratorSettings.h"

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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;
};

View 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"
]
}
]
}

Binary file not shown.

View File

@@ -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 ...
}
);
}
}

View File

@@ -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)

View File

@@ -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();
}

View File

@@ -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;
};

View File

@@ -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();
};

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
{
"cSpell.words": [
"speakerline",
"squarebrackets"
]
}

View 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
""",
)

View 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
View 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.

Binary file not shown.

Binary file not shown.

33
Plugins/SUDS/SUDS.uplugin Normal file
View 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"
}
]
}

View 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)

View File

@@ -0,0 +1,4 @@
#include "SUDSCommon.h"
const FName FSUDSConstants::RandomItemSelectIndexVarName(SUDS_RANDOMITEM_VAR);

File diff suppressed because it is too large Load Diff

View 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;
}

View 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
}
}

View 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;
}
}

View File

@@ -0,0 +1,5 @@
// Copyright Steve Streeting 2022
// Released under the MIT license https://opensource.org/license/MIT/
#include "SUDSParticipant.h"

View 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

View 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();
}

View 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);
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View 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;
}

View 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;
};

View 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

View 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
};

View 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();
}
};

View 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);
};

View 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;
};

View 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
};

View 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;
};

View 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;
};

View 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; }
};

View 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; }
};

View 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; }
};

View 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; }
};

View 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;
}

View 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
};
};

View 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 ...
}
);
}
}

View 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);

View 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;
}

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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;
}

View 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();
}

View 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);
}
}
}
}

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View 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;
};

View File

@@ -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);
};

View 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);
};

View File

@@ -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);
};

View 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();
};

View 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);
};

View 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);
};

View 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);
};

View File

@@ -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
};

View 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 ...
}
);
}
}

View 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)

View 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;
};

View 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

File diff suppressed because it is too large Load Diff

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